From 8335be43a31f310b3b3781fa5f04687c5b44c06d Mon Sep 17 00:00:00 2001 From: Michael Zanetti Date: Mon, 17 Sep 2018 16:01:32 +0200 Subject: [PATCH 01/11] some rework on how we handle the last connected host this isn't good enough, actually worse than the current master but it has some groundwork needed for when we actually improve it --- .../discovery/discoverydevice.cpp | 42 ++++++++++++++ libnymea-app-core/discovery/discoverydevice.h | 4 ++ .../discovery/discoverymodel.cpp | 2 + libnymea-app-core/discovery/discoverymodel.h | 2 + .../discovery/nymeadiscovery.cpp | 55 +++++++++++++++++++ libnymea-app-core/discovery/nymeadiscovery.h | 7 +++ nymea-app/ui/Nymea.qml | 7 +++ nymea-app/ui/connection/ConnectPage.qml | 41 +++++++++----- 8 files changed, 145 insertions(+), 15 deletions(-) diff --git a/libnymea-app-core/discovery/discoverydevice.cpp b/libnymea-app-core/discovery/discoverydevice.cpp index f6d5a45c..d04a265d 100644 --- a/libnymea-app-core/discovery/discoverydevice.cpp +++ b/libnymea-app-core/discovery/discoverydevice.cpp @@ -121,6 +121,7 @@ void Connections::addConnection(Connection *connection) emit dataChanged(index(idx), index(idx), {RoleOnline}); }); endInsertRows(); + emit connectionAdded(connection); emit countChanged(); } @@ -134,6 +135,7 @@ void Connections::removeConnection(Connection *connection) beginRemoveRows(QModelIndex(), idx, idx); m_connections.takeAt(idx)->deleteLater(); endRemoveRows(); + emit connectionRemoved(connection); emit countChanged(); } @@ -157,6 +159,46 @@ Connection* Connections::get(int index) const return nullptr; } +Connection* Connections::bestMatch() const +{ + QList bearerPreference = {Connection::BearerTypeEthernet, Connection::BearerTypeWifi, Connection::BearerTypeCloud, Connection::BearerTypeBluetooth, Connection::BearerTypeUnknown}; + Connection *best = nullptr; + foreach (Connection *c, m_connections) { + if (!best) { + best = c; + continue; + } + uint oldBearerPriority = static_cast(bearerPreference.indexOf(best->bearerType())); + uint newBearerPriority = static_cast(bearerPreference.indexOf(c->bearerType())); + if (newBearerPriority < oldBearerPriority) { + // New one has better bearer, switch + best = c; + continue; + } + if (oldBearerPriority < newBearerPriority) { + // Discard new one as the existing is on a better bearer + continue; + } + + // Same bearer, prefer secure over insecure + if (!best->secure() && c->secure()) { + // New one is secure, old one not. switch + best = c; + continue; + } + if (best->secure() && !c->secure()) { + // Old one is secure, new one isn't, skip new one + continue; + } + + // both options are now on the same bearer and either secure or insecure, prefer nymearpc over websocket for less overhead + if (best->url().scheme().startsWith("ws") && c->url().scheme().startsWith("nymea")) { + best = c; + } + } + return best; +} + QHash Connections::roleNames() const { QHash roles; diff --git a/libnymea-app-core/discovery/discoverydevice.h b/libnymea-app-core/discovery/discoverydevice.h index 4895eaba..8394a48a 100644 --- a/libnymea-app-core/discovery/discoverydevice.h +++ b/libnymea-app-core/discovery/discoverydevice.h @@ -90,7 +90,11 @@ public: Q_INVOKABLE Connection* find(const QUrl &url) const; Q_INVOKABLE Connection* get(int index) const; + Connection *bestMatch() const; + signals: + void connectionAdded(Connection *connection); + void connectionRemoved(Connection *connection); void countChanged(); protected: diff --git a/libnymea-app-core/discovery/discoverymodel.cpp b/libnymea-app-core/discovery/discoverymodel.cpp index d730a316..25c7825d 100644 --- a/libnymea-app-core/discovery/discoverymodel.cpp +++ b/libnymea-app-core/discovery/discoverymodel.cpp @@ -61,6 +61,7 @@ void DiscoveryModel::addDevice(DiscoveryDevice *device) beginInsertRows(QModelIndex(), m_devices.count(), m_devices.count()); m_devices.append(device); endInsertRows(); + emit deviceAdded(device); emit countChanged(); } @@ -74,6 +75,7 @@ void DiscoveryModel::removeDevice(DiscoveryDevice *device) beginRemoveRows(QModelIndex(), idx, idx); m_devices.takeAt(idx); endRemoveRows(); + emit deviceRemoved(device); emit countChanged(); } diff --git a/libnymea-app-core/discovery/discoverymodel.h b/libnymea-app-core/discovery/discoverymodel.h index 47de7faa..bb677bd1 100644 --- a/libnymea-app-core/discovery/discoverymodel.h +++ b/libnymea-app-core/discovery/discoverymodel.h @@ -54,6 +54,8 @@ public: void clearModel(); signals: + void deviceAdded(DiscoveryDevice* device); + void deviceRemoved(DiscoveryDevice* device); void countChanged(); protected: diff --git a/libnymea-app-core/discovery/nymeadiscovery.cpp b/libnymea-app-core/discovery/nymeadiscovery.cpp index d2aed28f..26b90d31 100644 --- a/libnymea-app-core/discovery/nymeadiscovery.cpp +++ b/libnymea-app-core/discovery/nymeadiscovery.cpp @@ -7,10 +7,32 @@ #include #include #include +#include +#include NymeaDiscovery::NymeaDiscovery(QObject *parent) : QObject(parent) { m_discoveryModel = new DiscoveryModel(this); + connect(m_discoveryModel, &DiscoveryModel::deviceAdded, this, [this](DiscoveryDevice *device) { + if (device->uuid() != m_pendingHostResolution) { + return; + } + Connection *c = device->connections()->bestMatch(); + if (!c) { + qDebug() << "Host found but there isn't a valid candidate yet?"; + connect(device->connections(), &Connections::connectionAdded, this, [this, device](Connection *connection) { + if (device->uuid() == m_pendingHostResolution) { + qDebug() << "Host" << m_pendingHostResolution << "resolved to" << connection->url().toString(); + m_pendingHostResolution = QUuid(); + emit serverUuidResolved(connection->url().toString()); + } + }); + return; + } + qDebug() << "Host" << m_pendingHostResolution << "appeared! Best match is" << c->url(); + m_pendingHostResolution = QUuid(); + emit serverUuidResolved(c->url().toString()); + }); m_upnp = new UpnpDiscovery(m_discoveryModel, this); m_zeroConf = new ZeroconfDiscovery(m_discoveryModel, this); @@ -25,6 +47,20 @@ NymeaDiscovery::NymeaDiscovery(QObject *parent) : QObject(parent) m_awsClient->fetchDevices(); } }); + + + QNetworkConfigurationManager manager; + QList configs = manager.allConfigurations(QNetworkConfiguration::Active); + + foreach (const QNetworkConfiguration &config, configs) { + if (config.purpose() != QNetworkConfiguration::PublicPurpose) { + continue; + } + if (config.bearerType() != QNetworkConfiguration::BearerWLAN && config.bearerType() != QNetworkConfiguration::BearerEthernet) { + continue; + } + qDebug() << "Have Network configuration:" << config.name() << config.bearerTypeName() << config.purpose() << config.type(); + } } bool NymeaDiscovery::discovering() const @@ -102,6 +138,25 @@ void NymeaDiscovery::setAwsClient(AWSClient *awsClient) } } +void NymeaDiscovery::resolveServerUuid(const QUuid &uuid) +{ + // Do we already know this host? + DiscoveryDevice *dev = m_discoveryModel->find(uuid); + if (!dev) { + qDebug() << "Host" << uuid << "not known yet..."; + m_pendingHostResolution = uuid; + return; + } + Connection *c = dev->connections()->bestMatch(); + if (!c) { + qDebug() << "Host" << uuid << "is known but doesn't have a usable connection option yet."; + m_pendingHostResolution = uuid; + return; + } + qDebug() << "Host" << uuid << "is known. Best match is" << c->url(); + emit serverUuidResolved(c->url().toString()); +} + void NymeaDiscovery::syncCloudDevices() { for (int i = 0; i < m_awsClient->awsDevices()->rowCount(); i++) { diff --git a/libnymea-app-core/discovery/nymeadiscovery.h b/libnymea-app-core/discovery/nymeadiscovery.h index fd451aa4..0a88b994 100644 --- a/libnymea-app-core/discovery/nymeadiscovery.h +++ b/libnymea-app-core/discovery/nymeadiscovery.h @@ -3,6 +3,7 @@ #include #include +#include #include "connection/awsclient.h" @@ -31,10 +32,14 @@ public: AWSClient* awsClient() const; void setAwsClient(AWSClient *awsClient); + Q_INVOKABLE void resolveServerUuid(const QUuid &uuid); + signals: void discoveringChanged(); void awsClientChanged(); + void serverUuidResolved(const QString &url); + private slots: void syncCloudDevices(); @@ -49,6 +54,8 @@ private: QTimer m_cloudPollTimer; + QUuid m_pendingHostResolution; + }; #endif // NYMEADISCOVERY_H diff --git a/nymea-app/ui/Nymea.qml b/nymea-app/ui/Nymea.qml index 227dfc9d..8ca953f6 100644 --- a/nymea-app/ui/Nymea.qml +++ b/nymea-app/ui/Nymea.qml @@ -52,6 +52,13 @@ ApplicationWindow { anchors.fill: parent } + NymeaDiscovery { + id: discovery + objectName: "discovery" + awsClient: AWSClient + discovering: pageStack.currentItem.objectName === "discoveryPage" + } + onClosing: { rootItem.handleCloseEvent(close) } diff --git a/nymea-app/ui/connection/ConnectPage.qml b/nymea-app/ui/connection/ConnectPage.qml index b8b5fde0..cd3c855a 100644 --- a/nymea-app/ui/connection/ConnectPage.qml +++ b/nymea-app/ui/connection/ConnectPage.qml @@ -11,16 +11,27 @@ Page { readonly property bool haveHosts: discovery.discoveryModel.count > 0 Component.onCompleted: { - print("completed connectPage for tab", connectionTabIndex, "last connected host:", tabSettings.lastConnectedHost) - if (tabSettings.lastConnectedHost.length > 0 && engine.connection.connect(tabSettings.lastConnectedHost)) { - var page = pageStack.push(Qt.resolvedUrl("ConnectingPage.qml")) - page.cancel.connect(function() { - engine.connection.disconnect(); - pageStack.pop(root, StackView.Immediate); - pageStack.push(discoveryPage) - }) - } else { + print("completed connectPage. last connected host:", settings.lastConnectedHost) + if (settings.lastConnectedHost.length > 0) { + discovery.resolveServerUuid(settings.lastConnectedHost) + } + +// if (settings.lastConnectedHost.length > 0 && Engine.connection.connect(tabSettings.lastConnectedHost)) { +// var page = pageStack.push(Qt.resolvedUrl("ConnectingPage.qml")) +// page.cancel.connect(function() { +// Engine.connection.disconnect(); +// pageStack.pop(root, StackView.Immediate); +// pageStack.push(discoveryPage) +// }) +// } else { pageStack.push(discoveryPage) +// } + } + + Connections { + target: discovery + onServerUuidResolved: { + connectToHost(url); } } @@ -34,12 +45,12 @@ Page { engine.connection.connect(url) } - NymeaDiscovery { - id: discovery - objectName: "discovery" - awsClient: AWSClient - discovering: pageStack.currentItem.objectName === "discoveryPage" - } +// NymeaDiscovery { +// id: discovery +// objectName: "discovery" +// awsClient: AWSClient +// discovering: pageStack.currentItem.objectName === "discoveryPage" +// } Connections { target: engine.connection From 8091cbb8c6940f70dc40fa3151719d52f5e29505 Mon Sep 17 00:00:00 2001 From: Michael Zanetti Date: Wed, 30 Jan 2019 00:31:07 +0100 Subject: [PATCH 02/11] improve startup and connection --- .../discovery/nymeadiscovery.cpp | 23 ++++++------ libnymea-app-core/discovery/nymeadiscovery.h | 4 +-- nymea-app/main.cpp | 3 -- nymea-app/platformhelper.h | 2 ++ .../android/platformhelperandroid.cpp | 10 ++++++ .../android/platformhelperandroid.h | 2 ++ .../generic/platformhelpergeneric.cpp | 5 +++ .../generic/platformhelpergeneric.h | 2 ++ nymea-app/ui/Nymea.qml | 3 +- nymea-app/ui/RootItem.qml | 31 +++++++++++----- nymea-app/ui/connection/ConnectPage.qml | 35 ++++++++++++++----- packaging/android/AndroidManifest.xml | 2 +- .../{drawable-mdpi => drawable}/splash.xml | 4 +-- 13 files changed, 88 insertions(+), 38 deletions(-) rename packaging/android/res/{drawable-mdpi => drawable}/splash.xml (73%) diff --git a/libnymea-app-core/discovery/nymeadiscovery.cpp b/libnymea-app-core/discovery/nymeadiscovery.cpp index 26b90d31..c395cc22 100644 --- a/libnymea-app-core/discovery/nymeadiscovery.cpp +++ b/libnymea-app-core/discovery/nymeadiscovery.cpp @@ -14,24 +14,24 @@ NymeaDiscovery::NymeaDiscovery(QObject *parent) : QObject(parent) { m_discoveryModel = new DiscoveryModel(this); connect(m_discoveryModel, &DiscoveryModel::deviceAdded, this, [this](DiscoveryDevice *device) { - if (device->uuid() != m_pendingHostResolution) { + if (!m_pendingHostResolutions.contains(device->uuid())) { return; } Connection *c = device->connections()->bestMatch(); if (!c) { qDebug() << "Host found but there isn't a valid candidate yet?"; connect(device->connections(), &Connections::connectionAdded, this, [this, device](Connection *connection) { - if (device->uuid() == m_pendingHostResolution) { - qDebug() << "Host" << m_pendingHostResolution << "resolved to" << connection->url().toString(); - m_pendingHostResolution = QUuid(); - emit serverUuidResolved(connection->url().toString()); + if (m_pendingHostResolutions.contains(device->uuid())) { + qDebug() << "Host" << device->uuid() << "resolved to" << connection->url().toString(); + m_pendingHostResolutions.removeAll(device->uuid()); + emit serverUuidResolved(device->uuid(), connection->url().toString()); } }); return; } - qDebug() << "Host" << m_pendingHostResolution << "appeared! Best match is" << c->url(); - m_pendingHostResolution = QUuid(); - emit serverUuidResolved(c->url().toString()); + qDebug() << "Host" << device->uuid() << "appeared! Best match is" << c->url(); + m_pendingHostResolutions.removeAll(device->uuid()); + emit serverUuidResolved(device->uuid(), c->url().toString()); }); m_upnp = new UpnpDiscovery(m_discoveryModel, this); @@ -133,6 +133,7 @@ void NymeaDiscovery::setAwsClient(AWSClient *awsClient) } if (m_awsClient) { + m_awsClient->fetchDevices(); connect(m_awsClient, &AWSClient::devicesFetched, this, &NymeaDiscovery::syncCloudDevices); syncCloudDevices(); } @@ -144,17 +145,17 @@ void NymeaDiscovery::resolveServerUuid(const QUuid &uuid) DiscoveryDevice *dev = m_discoveryModel->find(uuid); if (!dev) { qDebug() << "Host" << uuid << "not known yet..."; - m_pendingHostResolution = uuid; + m_pendingHostResolutions.append(uuid); return; } Connection *c = dev->connections()->bestMatch(); if (!c) { qDebug() << "Host" << uuid << "is known but doesn't have a usable connection option yet."; - m_pendingHostResolution = uuid; + m_pendingHostResolutions.append(uuid); return; } qDebug() << "Host" << uuid << "is known. Best match is" << c->url(); - emit serverUuidResolved(c->url().toString()); + emit serverUuidResolved(uuid, c->url().toString()); } void NymeaDiscovery::syncCloudDevices() diff --git a/libnymea-app-core/discovery/nymeadiscovery.h b/libnymea-app-core/discovery/nymeadiscovery.h index 0a88b994..46aaabac 100644 --- a/libnymea-app-core/discovery/nymeadiscovery.h +++ b/libnymea-app-core/discovery/nymeadiscovery.h @@ -38,7 +38,7 @@ signals: void discoveringChanged(); void awsClientChanged(); - void serverUuidResolved(const QString &url); + void serverUuidResolved(const QUuid &uuid, const QString &url); private slots: void syncCloudDevices(); @@ -54,7 +54,7 @@ private: QTimer m_cloudPollTimer; - QUuid m_pendingHostResolution; + QList m_pendingHostResolutions; }; diff --git a/nymea-app/main.cpp b/nymea-app/main.cpp index a3083e31..39ba3c66 100644 --- a/nymea-app/main.cpp +++ b/nymea-app/main.cpp @@ -119,8 +119,5 @@ int main(int argc, char *argv[]) engine->load(QUrl(QLatin1String("qrc:/ui/Nymea.qml"))); -#ifdef Q_OS_ANDROID - QtAndroid::hideSplashScreen(250); -#endif return application.exec(); } diff --git a/nymea-app/platformhelper.h b/nymea-app/platformhelper.h index a5a4a22e..d5bbbef4 100644 --- a/nymea-app/platformhelper.h +++ b/nymea-app/platformhelper.h @@ -25,6 +25,8 @@ public: Q_INVOKABLE virtual void requestPermissions() = 0; + Q_INVOKABLE virtual void hideSplashScreen() = 0; + virtual bool hasPermissions() const = 0; virtual QString machineHostname() const = 0; virtual QString deviceSerial() const = 0; diff --git a/nymea-app/platformintegration/android/platformhelperandroid.cpp b/nymea-app/platformintegration/android/platformhelperandroid.cpp index cde0ece6..0c7ee1e4 100644 --- a/nymea-app/platformintegration/android/platformhelperandroid.cpp +++ b/nymea-app/platformintegration/android/platformhelperandroid.cpp @@ -16,6 +16,16 @@ void PlatformHelperAndroid::requestPermissions() // Not using any fancy permissions in android yet... } +void PlatformHelperAndroid::hideSplashScreen() +{ + // Android's splash will flicker when fading out twice + static bool alreadyHiding = false; + if (!alreadyHiding) { + QtAndroid::hideSplashScreen(250); + alreadyHiding = true; + } +} + bool PlatformHelperAndroid::hasPermissions() const { // Not using any fancy permissions in android yet... diff --git a/nymea-app/platformintegration/android/platformhelperandroid.h b/nymea-app/platformintegration/android/platformhelperandroid.h index 06dc5053..90231bc4 100644 --- a/nymea-app/platformintegration/android/platformhelperandroid.h +++ b/nymea-app/platformintegration/android/platformhelperandroid.h @@ -13,6 +13,8 @@ public: Q_INVOKABLE void requestPermissions() override; + Q_INVOKABLE void hideSplashScreen() override; + bool hasPermissions() const override; QString machineHostname() const override; QString deviceSerial() const override; diff --git a/nymea-app/platformintegration/generic/platformhelpergeneric.cpp b/nymea-app/platformintegration/generic/platformhelpergeneric.cpp index 9c9c7145..4bb802e6 100644 --- a/nymea-app/platformintegration/generic/platformhelpergeneric.cpp +++ b/nymea-app/platformintegration/generic/platformhelpergeneric.cpp @@ -10,6 +10,11 @@ void PlatformHelperGeneric::requestPermissions() emit permissionsRequestFinished(); } +void PlatformHelperGeneric::hideSplashScreen() +{ + +} + bool PlatformHelperGeneric::hasPermissions() const { return true; diff --git a/nymea-app/platformintegration/generic/platformhelpergeneric.h b/nymea-app/platformintegration/generic/platformhelpergeneric.h index 82bb4461..315ff037 100644 --- a/nymea-app/platformintegration/generic/platformhelpergeneric.h +++ b/nymea-app/platformintegration/generic/platformhelpergeneric.h @@ -12,6 +12,8 @@ public: Q_INVOKABLE virtual void requestPermissions() override; + Q_INVOKABLE virtual void hideSplashScreen() override; + virtual bool hasPermissions() const override; virtual QString machineHostname() const override; virtual QString deviceSerial() const override; diff --git a/nymea-app/ui/Nymea.qml b/nymea-app/ui/Nymea.qml index 8ca953f6..747ece62 100644 --- a/nymea-app/ui/Nymea.qml +++ b/nymea-app/ui/Nymea.qml @@ -38,7 +38,6 @@ ApplicationWindow { property alias windowWidth: app.width property alias windowHeight: app.height property bool returnToHome: false - property bool darkTheme: false property string graphStyle: "bars" property string style: "light" property bool showHiddenOptions: false @@ -56,7 +55,7 @@ ApplicationWindow { id: discovery objectName: "discovery" awsClient: AWSClient - discovering: pageStack.currentItem.objectName === "discoveryPage" +// discovering: pageStack.currentItem.objectName === "discoveryPage" } onClosing: { diff --git a/nymea-app/ui/RootItem.qml b/nymea-app/ui/RootItem.qml index a3b7463f..59a60795 100644 --- a/nymea-app/ui/RootItem.qml +++ b/nymea-app/ui/RootItem.qml @@ -35,6 +35,14 @@ Item { tabbar.currentIndex = swipeView.currentIndex } function removeTab(index) { + if (swipeView.currentIndex === index) { + if (swipeView.currentIndex > 0) { + swipeView.currentIndex--; + } else { + swipeView.currentIndex++; + } + } + remove(index); settings.tabCount--; tabbar.currentIndex = swipeView.currentIndex @@ -89,7 +97,7 @@ Item { } Component.onCompleted: { - pageStack.push(Qt.resolvedUrl("connection/ConnectPage.qml")) + pageStack.push(Qt.resolvedUrl("connection/ConnectPage.qml"), StackView.Immediate) setupPushNotifications(); } @@ -98,6 +106,7 @@ Item { pageStack.clear() if (!engine.connection.connected) { pageStack.push(Qt.resolvedUrl("connection/ConnectPage.qml")) + PlatformHelper.hideSplashScreen(); return; } @@ -118,11 +127,10 @@ Item { init(); }) } - } else if (engine.jsonRpcClient.connected) { - pageStack.push(Qt.resolvedUrl("MainPage.qml")) } else { - pageStack.push(Qt.resolvedUrl("connection/ConnectPage.qml")) + pageStack.push(Qt.resolvedUrl("MainPage.qml")) } + PlatformHelper.hideSplashScreen(); } function handleCloseEvent(close) { @@ -135,7 +143,7 @@ Item { pageStack.pop(); } } - } + } function setupPushNotifications(askForPermissions) { if (askForPermissions === undefined) { @@ -166,7 +174,7 @@ Item { onConnectedChanged: { print("json client connected changed", engine.jsonRpcClient.connected) if (engine.jsonRpcClient.connected) { - tabSettings.lastConnectedHost = engine.connection.url + tabSettings.lastConnectedHost = engine.jsonRpcClient.serverUuid } init(); } @@ -271,15 +279,23 @@ Item { id: tabbar Layout.fillWidth: true Material.elevation: 2 + position: TabBar.Footer Repeater { - model: mainRepeater.count + model: tabModel.count delegate: TabButton { id: hostTabButton property var engine: mainRepeater.itemAt(index)._engine property string serverName: engine.nymeaConfiguration.serverName Material.elevation: index + width: Math.max(150, tabbar.width / tabbar.count) + + Rectangle { + anchors.fill: parent + color: Material.foreground + opacity: 0.06 + } contentItem: RowLayout { Label { @@ -324,7 +340,6 @@ Item { } } } - } } } diff --git a/nymea-app/ui/connection/ConnectPage.qml b/nymea-app/ui/connection/ConnectPage.qml index cd3c855a..7334f351 100644 --- a/nymea-app/ui/connection/ConnectPage.qml +++ b/nymea-app/ui/connection/ConnectPage.qml @@ -11,9 +11,11 @@ Page { readonly property bool haveHosts: discovery.discoveryModel.count > 0 Component.onCompleted: { - print("completed connectPage. last connected host:", settings.lastConnectedHost) - if (settings.lastConnectedHost.length > 0) { - discovery.resolveServerUuid(settings.lastConnectedHost) + print("completed connectPage. last connected host:", tabSettings.lastConnectedHost) + if (tabSettings.lastConnectedHost.length > 0) { + discovery.resolveServerUuid(tabSettings.lastConnectedHost) + } else { + PlatformHelper.hideSplashScreen(); } // if (settings.lastConnectedHost.length > 0 && Engine.connection.connect(tabSettings.lastConnectedHost)) { @@ -24,19 +26,23 @@ Page { // pageStack.push(discoveryPage) // }) // } else { - pageStack.push(discoveryPage) + pageStack.push(discoveryPage, StackView.Immediate) // } } Connections { target: discovery onServerUuidResolved: { - connectToHost(url); + print("** resolved", uuid, tabSettings.lastConnectedHost) + if (uuid == tabSettings.lastConnectedHost) { + print("yesss") + connectToHost(url, true); + } } } - function connectToHost(url) { - var page = pageStack.push(Qt.resolvedUrl("ConnectingPage.qml")) + function connectToHost(url, noAnimations) { + var page = pageStack.push(Qt.resolvedUrl("ConnectingPage.qml"), noAnimations ? StackView.Immediate : StackView.PushTransition) page.cancel.connect(function() { engine.connection.disconnect() pageStack.pop(root, StackView.Immediate); @@ -124,10 +130,21 @@ Page { } Timer { - id: startupTimer - interval: 5000 + id: splashHideTimeout + interval: 3000 repeat: false running: true + onTriggered: { + PlatformHelper.hideSplashScreen() + startupTimer.start() + } + } + + Timer { + id: startupTimer + interval: 10000 + repeat: false + running: false } diff --git a/packaging/android/AndroidManifest.xml b/packaging/android/AndroidManifest.xml index f59d1f71..a73cc177 100644 --- a/packaging/android/AndroidManifest.xml +++ b/packaging/android/AndroidManifest.xml @@ -2,7 +2,7 @@ - + diff --git a/packaging/android/res/drawable-mdpi/splash.xml b/packaging/android/res/drawable/splash.xml similarity index 73% rename from packaging/android/res/drawable-mdpi/splash.xml rename to packaging/android/res/drawable/splash.xml index 8f4fc965..4d0cdae0 100644 --- a/packaging/android/res/drawable-mdpi/splash.xml +++ b/packaging/android/res/drawable/splash.xml @@ -2,11 +2,11 @@ - + - From 4d7400d1c7602174b64712cc1045b33901e6c050 Mon Sep 17 00:00:00 2001 From: Michael Zanetti Date: Thu, 31 Jan 2019 12:03:36 +0100 Subject: [PATCH 03/11] fix splash --- packaging/android/res/drawable-mdpi/icon.png | Bin 0 -> 13691 bytes .../android/res/drawable-mdpi/round_icon.png | Bin 0 -> 16079 bytes .../android/res/values/splashscreentheme.xml | 6 ++++++ 3 files changed, 6 insertions(+) create mode 100644 packaging/android/res/drawable-mdpi/icon.png create mode 100644 packaging/android/res/drawable-mdpi/round_icon.png create mode 100644 packaging/android/res/values/splashscreentheme.xml diff --git a/packaging/android/res/drawable-mdpi/icon.png b/packaging/android/res/drawable-mdpi/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..c9e36c7245e83b60ebfa551935fe6fe42cccf09b GIT binary patch literal 13691 zcmYj2bySmYxMS2PVSscwmG18D?h*tfm6Yz$(jncTgh zyK{EF?K|(&@z(RiXlp7F;8Nj&Kp+AYWqDl?2o3m&27+J#4>R{KcEAI}Lsmr}0(=4? zHqpR;95-bX4-n|d_y4|V>1dw|oUZQn zxkutuAP@scMP5eVH}9~^&y#jGXy>}^!tj6r1jdj7uX4J;UZ#*DYG1!l$`D9AOr*7K z=gTj-RiEEDDkw?1RcBg~$e$J|kSsW|3EcK%R2jJaXgyGGGZ1H^((I)2>rD(P%Mt7c z!HfD@*UI*DrQZRyUtf29z+RPD4MhccV(2fmd+m2C-F07d^mK$WK-kC-zn(HnmyN4FaK?fhsux0}u!8Bi$u_dX z>eMHZMj`MniY|x&0EYkDlhP5 zjL|wV?o!|V!2Y_P=vWQM6ON$hVPH=a3z==2=`l&@V`BxG?u%Amz|p%Q{DuuqGRD39 zV2c@3xQgSF_Rlc38H@?jEA$m?Z~d?8GDNt()F?o(YgJN1D4?`sj9^GrrMzDlL)s?p zNwn_Y>PWJtRmPB+8}C)j?G`dzGoS<{uSi7>Jk03D^!osi2&LwTYIkxAS;WykwL zS=?+N5^2XEqJRuNg>d39hLIS`)qB+n@*R)!P7EaF)GgnEA1aK2ox>U#bRL5^ z06m9FG+V@55M^K|AjxSLI8;POjyQ112KDgvdWN`!o&H zH4KZ&FG3|te;N3^Nya{50hGR`_&lnMu>1o9ns3967Zk$^hq=yKiv;=S)9t82Wg@`f z=;%@P&wIL%&U?(Y{%yS@k{-~VR-AE4&USvUhnhL2%oi*=>T$r0+R5kYc7IpMU@?9` z-Aop`xug1YjTDGjN67|e&}y+yPKfo(82z@+bog^z>EVf!-#2xE;9woS|AXEF60bvD ze39VI_?hBpkCvs$gqokEPif{=Gh>ta!YuO22M>84XOT3&@qHrT#i;(#-Owh{quch@ z%c>Lx{XFbOY%G92$)9<=kms}Gq$OSc*A%scG!|j=5Lts=ubG=cKhUepI$nYB3J%Su zICm0WX9{CU1nlk!7{%|LB0E7&ulizt;qNHbCi6~*&Z;|~CRmSi7v!04n&QYB%wfSm zbawlfx#}9{zgu21GcOYPVo2wJkbfrC8n@fTMFxGe?(h>{M^NF&{yu?7&k`G!Fe>M; z)Myf9reB>Prp1H)k{i?joTh6I;vr)#&L8_w0_^4wukI(dtnYRY*bM#?Bb{*&PkQ4& z*j8Q>&#lc*oxW=v1)>+9_^~6lqLn~~o!E6Pj%=qVi?cE!p4Sxc)5>yXvONi+2r30} zkdVft|8mP3vhzqnDmX#vervW>R6utP;2&TqgdGj*z@j7kkoMlAN5}D_U5*s|P^{An z;r61K;THpsn<2)O|C7mD>SU*$;O`K=e9e={*&c~V41qwKg&} z#sW~8K}$ktiE9B>3ldN2njoO~7v-qCV%O8Ok(Wg8Kv0mSXg<60?Oxlg-)Y~g5DqZ? zvOm{thfk}!!%okENyZ-qX>=}g{|{0@dzE?g9aPD3L}b_i6k{qx{c-Y-k?-}?LWuKB zl5{n{;*UCngA;1I@2wT}_>3t+t+6_{>C=veYxyU&BM6;Ev*JV`vIy;= zYsc3=!Q2^nn?0R(h%irDqTPiye{}kV2L|Z3;M*j?G?hX<oa#g_(?d6AUZPXpm(R8tRU1<)j3Y5hndQ(?Gb>>))vz#|s1S zF-P?BysC5n(4d}?Zpky5w(?dD&LG1~0tKzk|AN3D=2vu41f*0w-n7i6t+lRPgsyWR z)Cr&fquyoN*;ABnpeuIo-RKd+Q4Bj0SQdo-{kXVXA_~d!maq3J^)f+wOmt zd}b6quTSn&<3fU}<|dcNNraZfY9lox(?fw6P1QUr3_caIrGhULxWTQ;a6cOaV?HZI zYR3iyc%STUql;{G^79M|B}xojP#!xW-$hGj96H|zI_;pZLm`+zI8hF7CRw0{7yU#y z;l276vRR54TZIHg40aTO4%5L$yFayFh)Q~f_K2OrtZNixzcPWTB6}tFFV7w{8oAtH zRW$M-nun$HNBWHBZQDE3aKmO0C=>?D>354)5K&fm?0Khzd%?M-v>6%_p3A@o&! z37FFEJ76Y>gTIT$CGu7;pMh81Hs>iHF7tN=v+OvljRRIP4`^6q4%k^A2K8pdbrujB;dIwaMu!~fhq%XxF5p!(Dh3ua@Q-ik>Q`VE(|cF>9KN^W$)r{vkSWdG!wPsabRQ`8&FW zz~qAMC!n%voNk}Lwu!IdneolI?{rjLi(bx;NG;tuHqBI{MvDz;U?8-T5&RXu9lzyv zf6=DRMR~NlAG1T+OOfPd0ZE)WTD`?4sdny(#~rl z$iMQ65dIE%#WUK-md>vN-398OtLCtoqCzhmNZWoqAimZ%o?Ii1Ody%(0!^&^MQsu~n zg}k%XNW$@Nl@!R{zx`AUSxPwkhp4iW8#WcJGF4MAK=g zYl3u(Zn^d!R`xtUmTNtHoaLe)2n$0>Gm&k^;rT-=-f0e`wK zpYf)&G8GwdKud6W|E;; zylbLOYdMs{xxLt>qf;8jnyC!T7TtH(G1B@9*WqGPBKxEpso%&^Gxhe_ICJkSJVzJ3 z@&wdUZb@4_o!7#1pXk!Zj0bunJOy#T z|MJk4=G{lWjqohRk#lTuE~s1Z2$?*7J6Yb*Fd$eo_4|iW^Fn;HByIHZ!hmr5<)G1X z+6?=V^rw;i4z$1=`vI`o)=E^`3F>P65kU*_EcTE(;~$_xP0Wb3mIkQx79Qpr zjh`fJ)N(u9ffbOF1#u484DDftHu=Z0d z?9Iy$3Kcy38+`h3w8>_cLx;XIPMT4d%gy{q6A7w*8_Uo8m+#??zACB9-`&r(eiBi< z+-~?5e>|8;y&1RzoLJ&~1~}i_oUn@-xo&QV(XoHFs^=v_JK;&!JfSaqq}HO54uJb= zsE}mMkXDxW1W5#R2sQe6)t2A7v5SoQXmyXI^s3n$tjP~|?71a?nH8-WFBn&)Tf)-=0+#ttQZ)30~Z5TQX<@jg5xdVEGNUAd_t zftw1J3(DHS2)^ToFVXJ%dVYH?O5JH5?TVkb^~d8wo4CZ^`iQL=4GO0I2>VBL*D=+Y zzR~<#lj+ueLH7fpAf9kSRqP-2lZ}Cv(=w(T4G*CZY0!8qXNMb*JW9Z!Pm;10-5QdxIo8()Tf z=YJh{GF=|Y$F1wy9BET{l?0Z*M1wGjLLcuMx%kBSbTWorAI}nK-SX|;XcXbcA{KOr zWGA7nmL87e_Z*dfn%)r{o-!`eb`2$sclT!3J7%%YwK}R`{MFB!{7xzyMdmpjC~sx$ zD3rS(xV5|WQd1c=injQb4Wk$=z*^I9^AX%-=@7^<#e?Mp->h z8OQ_G?&in*9#5o^B09f8P%?^dR6dZ+qt}aNfey1Y$?=?LJ&^Na6}Hf6?Sa$RYnv}8 z6uw+F(k$>5R)yPZO{5y(tFlniJ^kK|SZKIRHg+`)t>1?RrRHOYfGmBu_}9^xYujk%A>H8@Lb=J-WU{=d|7tj^=EFlabkx%9&8kn~V> zMk93*m}Pn9bMR{>DK2oZ68ycqG|QK`A0CQ_Jnq|^xRrr%2r@x?{oWL&LU(hWqeaW7 z_i1d{F=!*Qu+()UBhB-^ww6{=k-Q%5?_1;ajy{IcRx^m!_Tw|7E_kb^%hP1Ci5@nq>?(X49Y>?NVq^Cp+88T^ee{F|Gw9%op4~h#CiZjDq1|8l z?>D&QNZ+hMfPGvvbOM1L`#f`bYZUL8H%DEEzQWq#80$&ZJ`*XgQ}`7G3v&Z}BWC1n zR0Kha1G~*mzb*a`ocXHkb9W6*gI#0!Nrv2`wd1ub!K3EEs4$X0?t7Iqe;BomCiO zsj?v4w`%d5X6{yhu{pi97|3WYSj>9x6&~un5EE_<#@_h(Hqk67^BcOou!t|tG-W3V zI|fyHULP)^D2+B+cdXROQnl?!hxwIR9;9_FJK4L&e!6L&&b@SdXhypC9JIoBYJUD* z6hekpZBPfHm@r!zJlT}*=HD`u%kwP194dUmaoP{kX%dm*C(@?&-I#iL>3KD=(*D)q z)`E?;wdkj+e0L-i+Q-=*DEMJTWznXB1tpFhE*VrVEaN?bCrU%azaQ2_0FGRe!rS z7${OUnE?MLlA$8C&EByvbVrrz8ScfT?~zqA(`WbDbe$2V&@07(T~}u|Ryt6Q_r&!&j=qs8t+6I!Os zW@&_*uKETIy5XC@!x%$idacF08{y7clb1c1|C+of^m`hZFjPp2@MRUFm0Fg^jF$Zw zcV@K8?0he#-aj!HPt=V)>K7Dj_G+{;5Xzlze_QE_W$7h{S4nN!TItFJGGbB^mhjA; z4$l+`z6XEa;}<3Fy&Zev{|!3;>{9q_#gjQds(<zx%6n&C z>TULomi|<{_k!XZcWn2M03)x4ZbZ1p^O^$64~gxfD-wR=&u%G=+E4AFghlxLCp%pC zwDwV4Vmr%{((r(uXT}X5;MJqYmL>(bJ)sv{>!Va0 zKiammj~+Fl)7&>c`(@A_XVohRIDW-TJR{4_$qx7G(3EmwF4l@U^JUuKXKqwDqAME4 zRYI0CO$xiulq*Q*VY7j~6=DVFvO0rVltZ-hc--7PijLx%%3F?m-LD+QZn*jjm7>7o zp-L@FWAodW;LkH{5mWuHg^tGp1jc0QiNzeV0<#_S@FNuugksaa)^e_*OisBI?Z3`G z` z2G$-aIW_9?pl#_n$m!ft-Mu!`*&tc9CK57_x zigxSk0$b%|Q^q(O*FoERJ!K>A>Qim*`Uv~PEy=g_on^5;5S)}`K@F<@GdBaxVzM^z zx5r~ohlie8O-CNaqsIGjU)xrdayR5b< zX;e8Ax*FlTNBR!`$cJX{oHzAh;$%6=Rug{~$xd(#1!17`%?+UyC9UDVA}v>VT_AJc z(FX2;fr&j&O$^*6lq_^Y63URPc*=jMHf+>^N?X4c#L3y;KZ|lnalIvnMux^fVa3`- zy&Xw4TAFTeJtMjtXZ;-4@P(C4$RsKapOeDC+*+TCxqEz8<(m4b!_6Ob5AUmnkBuui zTd^GQJe)|`vj0(5Szz3`Ix1l_aS`C2Z_%4%WmLW{hKvYt7R4V>FY|a>+^%rpQclCt zRd$9hJOkPj3C)G&*lDZ+cBou$>6PE5&iG1jc=4VvL7*X+6Ts&Gc;#A{U`+5=sabF> zd6C4d`#^z5KoE_`8+?y<4ZtyPjeEE}a_`wmS-S5&x!Uygj-TXgpge<($l+~g@lJSf z?|*jKh~xE58X%eO^tj-?oxLJCbsBzBW@|+e+kNR0VJQzHQw9R2$LIFLf!~&g^D*{T zc-~cVmz}0oDkVLe?`=Rr!(|6fI-1v{1Lxol!3}0yA0h^-B{Fobce75g=n`+1|2+}mk|yuU5WA#=U(TsAswCiBn>6~6 ztgCrsruuBC#YrRDadN5FMV?<_-G?cxEuJ9qoUXAk%_-@(zaxehmpf8?h1c=f8_r86 zosNvv*kO$is?WgM&jed)x2|ZRpq|j3>eu9E=Q|#n;W^lQ_B>rLLKgyr)-yiv*VT9BmiY!>y?jOi z4Z&@w=*k^myvG2t)@*Kf^zgCB>~bKdr z!_+}_%*JJQgNQH68MEVst2dhzgw9JEgNXYaURkH3-7=H-9(F)K>W^PJA4c!#?KP&q zuJ<2z7X#SM+|5rog)$2AU?Sr0l*VxU!)G0YtOn`M_K(ial^*Xuxw-6q9k}k1Od!XeV>4$=MBBNAFCj$$q?8a9cC_XjF69!R)L&WjIG${L&Y6lC4Kj+MD}l2qef zS0;wbsJNKEiC@CgF&GY`T1oV87}GaX^0A{M`+g~HI31d4`FKz-SIEeUM4K5xjJ>l3^85ay_R)Lv>SqExDvar$Y7m&RmuKWA9>3?FHJp*r{;t{o zF)eQEm^GZX?#te{`zyZ7km0h6_0}&n__cn(15OU&)eVBaC)gJ z_Oi^7=N8klC(lME`758eM6U(y4RoN^yop4$`6>=C+?z6ZLf|QHbc-NK;(+_{zq?hF z<2Er4HgEy|;@f#2E$Kh3yMB@5>WPkHg!l`tPN~BuH6Re_!GE;?TU{#>@j`HX+KUAF zU%~$RBK>!XxRThu?6oNZ#R<>Bfj4uJCGYle~Hb zY$<-V2$0mo1+1UPJqOEZU=!ekf3~MV)4qNh@Wf?)h{>fRHG$R~@>>a`2rEnVB?^%7 zdat5|8zE4B8GJOCsm+W}sJPmqQO zUe6=)`C>yiz11lZ+LvdX@lVbYgo67Q(04-H5Lfs81w9#$#i4Hes^9(^$`)&{=C=Lc zh9IX$B^mO5v!v_$4J~!c2{t{7+l$u@4xEvGk?Cz&^$*BI%%kEp# z2V-nAJ$`R~y_Ll}=B0>RvF4MQC7;a4;>fQ>X&>v;hJi((9o@V;p3wjw#neQZ) zIF%3Ig6@oi5qH@z@Y0!1yi^#j^Gpe^BrL6j9Dzdz01knC8yicEmPzcmkkqn!tYcTvR(WJta+A0t)^(RGJfk8~mT4Q&((J4$_3>TMv$|Kin0Oy81HhkW6}vmW&CNRe4-m@|)vEsuoro2}V%)^1Qm)ZU~@nIaPoap>LqYZz!^ zY2tN7t;`n?3^YvCQXkF%HnH}3)CwVb!I$%sCC+B*to>YaMUq;8qDN9uG#ks#!joSa z<4yu&LCl{Or2)ZH0Nm@-VEf~yJaPlGHGcUJYIB}t%9r1il_Qw%b#-Rzn0XrB6?QzB zo?<4nA!|g}?R*&ZHIS+A?mun$E(N>yNW7K&Oc+a2qA~ibNrllGP1rnE(-ZkyT=}J* zI{y4CFYGvJKQ=<@vS<4jvlIZIDOJ52yuN0qAw{c}DZ-%@R`2>>q~M!=ieEfiA^Aq) zSIj{D%Y*P=c2{m)i_ZKbA_{`!wPBVR+!oNNa(;W+`cP2BKq`nA_y)Kgw;6iF_?FDv z^EO%!oqaQ)Ir((EZ%z$htUb<$2#?-G#bz!Q03C)QP8sC(y0gbr(eL;gAJm_|U zFK9T~$)`J1ft^1*HmR|qwm>4toL~M;_bS5(8I5OosfO~4#y;SR=kP9zCfhv+lXVuK z*}h6zsYC?}^qb*~s{tQJWWmNZCiz*3-ivP}RmTL4sknQ3Xhzr=W}mpYUs zf;ZDf&n+UFvXC^9c$wKEvnSED@b%(9u>f;Tk#d+RKaWtEQ~x|s@;TLMH=(ZFm7rd` zwQ9AfCzB!nz2IqtSxW(yXTObIAge2i6Uy8!Q&HO1a$NQo&eN9%1q=rqQG=(Sl0{`> zRZrsbPQDks`d#nmHE_@?<3kv#TZFG$)+d%SC|NLeD7X6*ZKqBeL-dnIM9}LvZR-DA zbbdWSp_ZjuXYa*)!vwSbAZ%ImoAWEf#%6@Tte!TvJ9kkgaw>VqX6JT0l0hT|$=le7 zlh*>rf%<-d+k16Gta|^$GQ;1rofucMm8NQyZ8+wo#j6ltpCi#_&TQ)&9$p~SGs{*d zhIiIqD`0Gznx*M)_IM!B192cZGUZx=BVqxsctI9hG>j^rt|(E%$g`DLCt2()j-0u< z`J{o?VpK$cKFo`&c=@uNq&pfOyqXk{JdshRX>!>S1!$lwLiWc8x7K_xUy7`~`tfd; zTkJC3k3>Wzx;^na30RF~#A&?yIR=Xo2)E-ttvvVSv)HA<+B9CeEQVz^F+qAqY9gaR zYM*=5sonE)Rhg3a9~T5YiEuth^vb}wLi6d)*cm>mmN0V%c7BVEi$iUGniS@ueWA+L4xshTUED`Zf{Bsj*|= zj7C=Fq;~s14gG8JeA;7NxF^g&p6~ol1PCeLtLWIU7Oo6N9zLjg|O^fC_ zTWIxGvR*F?K8yyC8!DS226N+OH~k#Z9}>}PTLG?~i5zn*?Z^LyrG%@pz;wh=U(onA z-f6bVuNDYU<4~WKBWRN0rM9XsNEKN+*ti|Ilcj#b3-UBSdd{C56*0^g@RK^`t!69> zVB#e)YT#9$$wheFKhD7{vvOAE+IrmS^}nzJ+RgH{>F(H{-J(jR9V}h2tpdq{;PP!Ie*|Rhqe@Gjz}& zZS;bm`8PGzkqc6>FVJ`42s4<~iU-h2zmv-=qxE+&%0+BSjMls&Dfj@H@y{Bcot#|9 zImC0^+KCgA*QoBxugO~$Y3m-6wTA&$w|3c|Z#|K1S|U~6A;Y}D#5`ng@*0`=Jr|*y z?Agqyr^w7i+|uM(Uu`aUZ|3qJfZ-2nWc0{k84T@PN->N2VFKnHxwA}N-CUiZd=Pt- zE>VKK&a;3-c=+3s?E9;+D{F{aG!PNxFPC-y%^xI$h-b(DMD512F&I;js;5|Lt+1x) zHVN{-`Tl9Q=?A>^Hi&oEJ{y?xNB(qv9jR{id@D17{62?Y(PThD=ytEkJ;c$q^h@Yh zaXeAsa*&ugfd<|^MIZ4q0HDq1K5^)2nH9=3m~Y~Zj0`R98v_sYo-SLVmfHR5#q&r7 zB_Ko^+q)^BllnAY;97DXf*4Yi`FIMDwD=;e(s{?^ z61yADkBJKwb$gB8H!{6Hf8oR7XZGg-HQId9pheK)Z@samg)?)s=_U%=OKc3h@GvFp zaSzn|-nG8T7pOeQ1$60oSxjL6T3YbEVE|XYm8Wk`JSDgD+|?~m;J}lw$zF2PM+^hI zg-`sc6XoujD=A_)_bE5Yu(NR(Qemef%~NIa-ClIWtLm!L2PiTuPVZNBNevWPu=FdI z9Xcavpmi+xxWGMI9^S?*>y@R|BoSJxjPOL4-_d8VeJ{U&^+wF6BdOJ@hf2Sje_WI4 z`t(*hz>v^gXATc-BGe2Xdx0}Riv-0+M%pR-KHqwFe6(w-?<2qy`srN8HOKd2bj^nI zdlKM=T*clZlz2KyAl81o=hw@ZwOKQo);G>ZMj(*{FJX zUg7=4g|Q8wNbWSqKN$u^$u~PrIAETH_QiBOiV7VSZwWmK&aISf^foRPj1wk`Y&*3 z3$p2qI?R6u69XHwRwKz^lMnJ6Sg&KA4E8Va|MI(nvM*ftyfn?Yr0U}25578foZjGN zZ|w)t$g{gfbovMpPdvN#)O9}RNpGHm&-ZZnMt$Ai$y|_qje{U@fmg<(_iCenxc zM>LW!x44icYKC8&Fc2Fsoh^h%z%CSO$b4oRjbu}_>pCo8%)+TE21VPl2w-I<6OlmF zYA0@9y+WK$< z4)*CW)~*vUtXk0df(gJ2L!W8bSD6pM$i0EO}z2oZ9g)Um$mxar$EO1 z^c@%q3Vp~Blni?OpmS8*SK}sKRs1A=qDl^aFVj1A!+Xh0l#zDO(;7cxyhdZz63TSB zqNL5hJ+rt=^Yj%OvoQ1A^jy0#f_z(s_zcY57V#ANE&|@D)g(|C%7*%;a5oLPD1!DS-SON)NB{_>#`G?Y)SZ*<YfOedG043&r?e%Q3=vf6LZ5lXXok@`ALRsx0WZ%*15 zP|ti->bSUcZ82nw&2XJwU6VA;IEHvgJ3(225$nkqH)*;?OEH*qA-eBCsHr!zp)6kPROE8#s7{CMqB)x#=5va`lAXA zDncxSJF!N(lbRSHYNIdedh{2h)J&kkrN3BKeaKDOWQfQj;f9H)-)P!VX}T-&{+JlW zGuOwe6}1_;ZB>lYWYu00C%AQvH9&Vj@!w8N8hzE5Ky!g+cwn0DH1atzz@sXQ0rGo` z_++h(LTbc1tx)$b?1Y(Hd^P~6rN@;+-lpk#1oMnLX4N*j4Sq)c3OpjmD1I{nN&-4P zvd2fy>2_+<{4r4!+!8CifJ{WwNT4^xVx%QR1QRWM!gS?{IDsd76T1x2kR67v)3biU zr$qm${K@JEp6FrH!^a)o}QLOrffhsNf5muu9Cvh2~*fTP>fS2*~tirP^ zG17FPhbmoL+_2TPVPw9hmYKpAWfEl4%*|({1scb1W(y)i3KHZoOKJD(y|79c2ANky>O~;`T)M-|$e8`~Ryun{SVXrDW zcHyF~kdL?#|JSurA5sf`RAmt`KK#HD3$mFsbp<~Vp`2}gzL#;cwsKv(9yq7{EksXd z0BUfkz72)PboKxLRvEbA^lz^Ij}U1xd+LT2PyvRE$xu6_4|a~_-LgCz?tNgwo}mv~ z6uu=A#}P+!GFkr%krfce3B*P<@cFv@te`}5+wjG@0uiDgW8Y*p$%b)D)mZv$NTA;- zvv>u^?CVKu;D*pwdx_PJUc)QTJMbJlA6&hHl`_93_Qk>?yH2*5FB$S#EJ3#faN+>2 kgw#fkpB#{|x{|v`kNcs5Q02ZXpiv5>qM#{XBWoG{y?vwb9smF$KLP=m=*WYyN2v|+fa)pt zRvQ!f3c|FCMSjO}dt=}U0KDk??+r}ld_jf$k-|$+-%HEY#>>~j!y4f0>&s*B;^=8< z;bzU_>S3FGB0&iN&;#Bo$ZGrL9RKnSGM@7CkVY~{=~*Yc*$SewltHHGbZ6^r1P zO8e=Pd&#mmT80$)y2Kr3%gyqXP3%qJq`x}K$8K{za$NNP_dz4(O(mx}8;y<$_T}L7 zX=5SIKf-?lw_P}P{kJzK39OC)!_7G&8Icf4TsFQJxF8g46j;H|ay}>tpro*EtSFsP zxJeEyCDLvL&*K`~^pb`{-El+kerp#wsBXq~TSQ5c9`)QllYp^iO4Xu4d>}py%^853 zsM{~RB{UusA;8n(ESKvySV&YZsRWt?RtG)g59QqGr)Y4PXjr)ruzB9?D+~`dBjcG^ zq2t05ZpJ3z(mffL4;|93|C3achAcx zS3@{GJQDnI!~(SdRC>U_9*liD=)Ou3Z<`O>3%gg?cB)bL0 z13Fo*ePM&BPiTHa32Z4Ci~#0u<;SpU&W9Jj^$zd22$C>^C=ofa2{Uc{R|()Sh~|tb zRfsZmubDRTti}NG(|AM|(PzuXDiz5Os0A|G-t~B)TyDI@nbLZ(Y!c?(X7NLLhI++$ ze4tOPytHlFCLbChuW z*r7j$df!R)8>?ivW4o@*VB}pu(~=rx9WtvMSJ_5@c4KdJXe{seEAqd$U7#*z2$_S) zXNOvIR2dyttrmQJgTprRUBN-LRZfU>nYUtjcgHEHw46liu|>>ConlLPY=9$5$+Da- zaS|(YZtRdWth{M(?asBz+XUBpI25x(Ng*0J;JcTuBx*X%12n;k6wehXN+U)L@{%6O zJ<#$19pu8hBG$}t^>F*!_=$L$;NT|9=%bHv5wv7C+ zvHiGzCL7}Brc|~-xr?=Vbo~&lbDSf2yKHy$_R3^zwpgYrT zo~}fSuZS&Ol2CF?O(Uf$LOOiMRV;*m6cYL8w?b>Yz_j!uc5>Ol0CG_MgZBWW|Mnz@ z>+FmP&n-!J`}uvT?r_S9K*z5j2`Mg5pJ{{iYxsf+%8jN)O#}Q}FQf#6JZ42o1~3q_ ztNDc)TL-vr(>Nr`%e|YwYMWq%mAhyl7=G)Vbja88*|MG+p*CGC13R0a<2>}bHP*Za z71uYB^Aa2&_37Q%(<`GWQ9Yp zqb-WCJJ`2%Cs&u_ts6Ni)O&dS1n|M3Pz z%n2a1xIoq^&o=lk+(NcaZ6l_9QS&fJzN}VdN9x42XUfXc-a-H3oQQ`Ws_AYp2dE^JM)Wwmo-@enX zid=8Rj_%TepU(upEgCF38M~vJ(pvF|4J;lHa9#BiIPOmC;U};?{#-dSXBM1r@B3Iv zAM?%P2?DPuzZ-0|O|ZoED42jc(}xIW7l*=*eeu z>LGDjTOX}|{wOrnMBwN={xFp#i($bSp6RNlV?R(n?)I&-LatTw{Cobgs?~^_X%+nE~#}&%$B54rzfF4qUCk zAwNIDuu93p*2d85+=8QqxvqO%o!jBsbi{p0*MyyfS#QLTw;NRDhSzVMe(>2Mw>Nhb zFYvJ3bxH5RX8RGG+&!^lZ^6=WKQhwa_%RlUh;I`mT5|zP3!p|4dLum zG!)YA>mhoK7oGhkwEKK~&Ra`Jt?|{9nX8i%^LCJC#-Oebxlk0gjSc(4_oTpebE-`( z+{BSg?Vr>Yd{BX_mXua;5Pi3W2g{#sBgEQ`M5=Kd^y(r-O`%xwW-@a=C+nPz`%(7EV~X&w%jR*xIqF z4GUvoLd{^3#30h?K-6Hrt%2;;wmC_AnYTNTaM6+>2y&skEVtZsspg{ zhLnzp@kRV}8%XGZ&fJ^(kdGvHg7T?IUcG*6do;{))^*;G5viAOCS2(f6gM9*fE-Wt z4C$xiduDgEQ-9o&^rQVp-bIC0c{4k30Lt$;L9b}&8fRDpKb}1fhO_z~D7-^G(f1^A zT?@hho{59&V#8Q0mDAvth@S4ksfU@bH)o6jRxQ8%+RHmj(nv}dX!pt&t80ktDs*T7 zED)`S@vk?w!Mk~5zKd0i$KxvVSy&*?px&M8LWs$b4x0MdnmNh~AQdKlNLKl# z_SnFLF0K)9Dz9q3^N40L(j(aqAJpwEf!Q zAuV)Z(x#M`xbq*;ZK^|>feuh(Mttl3Zk$3b8UH+a@~NGbOSCP&7qOBaOrz{SswiCF zFhlV-asFlJesRc2-FN6vee7ffgpc)LlYoGR530t^ z=kHw(mUQpqXrkZyE1GA=YVW|~9I#;+BvM+H=jYbu9+B#j7l3|BX_;y40{sS5P)FcJ zrBkf<5iueeMg0OXtoK(EDsyXy5{Dq|Nz1$H*#Aal&$sfiO>wr1=FcL!~d?uUsc{hd8<0@Ed*md8j`S*t9HLFO3P)_xo~h781Etl=+nF z0S)uyb~-uO<0e}(o_<{to#kn2UKER~*H%!DDWh_AiWbsLX-zVs&D3WmbrvO&C_RJg zmTjT(Q2ITMKQ@fHN0n2^bMnsdi`jWuEwHoc{^f+azaEON94eu4M@xPnZ|isi<^gV0 z*F{{=_rE*Z#z-`(?4hOC*sqIca2Q0)t4pVMOqLAi*G`ksz2scae8F@^Cp)@%by#I{ zA3rVk?bpXaf4kGIgcc#JvK&mdi2g**=CeibdRMm(83K`8m0?AxT;DfA1X_>6;bvuC zqCeoa(zjeqi;5Io9Hk;4fE5W)!|+k|#59mZk5~PeyXNAEq0`qKqv&&)_5;_C6nRuY zgzl$iV=tog>*=ewSse7J$y`dnLz-&wB(93=k*is zx!>!amuF*rXq}CFSV>8_tV?&@nNPLx;E1LBs@>Tq(Ps4@-BCTkQlD>hIG*GGOAB=8 z`>E$W>CMnfUf-Tq(RtVe@!5Wr&OWQ)&@{6=4+2yxNm;w?LjV)MO+xPpS8GDVITv$f zzYGrvuB~=`T-n1X(h)Br-EqS65^T75a&|4KFMFj_6-`M5ttJg>B+S%P0UYcfpR!Ox z&wp1)Z~UmosJp(I`r~IlHW$VnQr$gxhCoAxO3%uMJF3<;Uq;`bt)Q+Q`S238j-JV+ zV=F-kAcZ|Y`)gF~jiwnr_xnXok8OuiX6&1Lg;k@t4o(Q5yzTu4f#>u^{>1IDMO(JG zq}jmMpzry|NAcLQ++S2OBZzJa?1tcpjKF9{K1r3jNKsLcT7|j6D5QzoWYU{c2n@d! zl?(|#Jqe6wEVBKgl{(4}V&if(8L2iqd;OrhBDhG34G#B)cyugN=qUg`l!vmy*El}= zGW$Is)YKC4cAT?a7}qIzrC$c*VVH(QUVLTDLuCD6H-{>Y&1H1t>!5y-{u_km*9$Ow zNOUr8__B1@T-#EXIGEHql=QVdGeIp~4;9dbibxz?Q*g~TFyl+tKs*OSo{N@m(@c=r zrRs(V-(K=-wHn)Xwwqs|A)eQpS2z0!F8J51Oq=Yw0OsnviXcuG;SAp;?VRkz~J_WE`3VM5J>d|d| z#`4LRmK#d)+ag11+W2o{p07)*q~qNOiY!{owRQKTq@2y|c>vj8Tg6=j)CrFE&-wUU zwI#(P&8R`Wov4(7z6nVFiH_(1hq8Jz0g8;PKSu z&fRxhb7tFCOno=klEa=Q*XHukmh=aXGL-;|0fj|7fZArJ0zHXv7ju94?@j!_ly)B= zV&hg;ViYgaF~86K$+d+9={WLsx$jVU7Ei`xIM*Yy<~-5HY@(t)Y_)t<;;aW*A@3Vc z>r)op#+wxFF0suTSOBnc2IbD{*P65-0#fr0*4D<+*Bn9Ix%X%5SfonGBXboi{tt^qf#->s&p zNc|d7aXD&G$cb?vN%N(%a@7?^?7SwebQ@F&Us{Qc@~s#Mg2&C&Ihz@c3Z-D-(nqc9 z=Y5ZA7{ncfX!B)ZOeEB-LO6 znQo>JG7)lti2C|cMZkiB+=a<<;FPG|%Htf%!#Nn?iK1;W3 zm>AqJYOk{@z=?A0UbaGjlykEY}&@pQDJf1A+|QisE^@3qv#)}RkC^WteLagC;_ z?N`j)&#o+0Wqu`TI_b{rnB)^HrcD*MOm&Fc?H6Q<6Chv~w2zUXl3cQrSyTfWPF43T z_zZ2k`{!`<{f|R}Z#pWFIQwjfX>ee|%?cs-veBT+5AAf}yv?5xgdKWJbesQUSL<9_ ztK&8_LyYQva-ganqcwCdnLq16fFg8LE_{YYa(TKdwNiO*LpfVL)cl{|+VYJcE2|Ry zlP&fuYL+q5E*S=y)Fv6uUi*Q3s7G8+|4Q&>V|5Q94zy(hS|2UvMjamKXyo)FF=u-x zT;90Oiu=Z;^H+zwS>slX$RUb{`uWgD4@{Uhwt8CDq_H`Kf86Y08;4dpEFjcR_3bd_ zEU8m*mm|qiz+Aoeu)`#LMFd-7ZTg zXT+F1HHDPGh0&V#SK74Lvb1Eof~W#(cl(=nCkFM2d8DTVn-{h(&bAoEMPG>?)kX$T zu{bdeW$ZnmxQ-zOM`nnZO+20n)O)J=qwDoiAK@c#h7-f)+=+p^wP%?PBB{^PY|P1D z&|tcfs6W|X<+t_}#pG>aVC1>G*w~*3{#MD1MK{zKO@H_zP8ng`=!Mt(voms{Ah26A zMG61ckU+_e03lxZ8!X8Bbm1a|;CuKP-0StWN1U9i5t&hdkmx`{U!t_TY8KlVff560 zm~#3AG#HGp4`EL1NM!<)2h_?b`f!wylnkIFwzcrnoF2*xIsvsZ0t;D3;~~iMK8eqDyGGyHmYa4dLAvsW(xJOg zp(i4whiO$zpZ0zZv?jpN!J-;YckjEx=Tb95CjKnVj&Ba=@~9;4%xwFH{1~T+U3dGBQ4)J_D%;Nn`m}E=RtDJ#Cq3lu9!iL%!XdO z1D##fqD+e{(qyj5zdQ8VWfj=vnKoIpYju&==JivL?!m5<&xI$?Z*%)g_n+H)j6map z2&%6UpB%ck{d)H(SP7vvk3YSKoXZU_zs0=YRaylJrhW>U_`9;)eUS7+2KX54OYGf+ zN)qP^*}VQU$pS)7_0wV#OIfh+mpy-P$K-qO5nOd>z9Q=1EpHc3TnpAp+X#Y!kz%I5nNal$0k*PgIKZtKKbU1pdCnMfa*j8Il@Oa~HV z)r(|ub;D?)>;yAar6B8(dyJsf*CFpv|5Dw}fPr8;Y$2A!Mayks9&G4eA;aJC$fW1df6m7=n^{rjBN#iwI*g|?g7^}Dtzknx;)eC`sa;sg7L*@RNp#n<-qak9G04^0 zEqb~xZ+o9(*wM$X{LYzSyC4bI&XVYL*rvv{L+K?VYE6{J*TlR-S*~>v*G(&wB=E_V z7=sSt3s7TEQQK{n&p5W&SW8Rg?~r#W@eO5={hkw$js}Heg{y(%CTM4rYe8<6JxL3e z6Y9l97N!!xVP-kYqsW4T33|1?3E=$z-Lr*oQ^eYW@NdTy(c+ugGEDQ{OYO*vK(jC_ zEeWMe`;Cgl!1=Zyt=VQo9M0bD>gXha7L=T{$*@m*zU>2v>#~LHzJjueQPW6#{UHQ@82;*XY$6-F_bul!lw`cE@NKRqiUtj^W@GVN zE{1nx>~C79jr_bP(&I#K<3H`;qiK~z)t>h6aTh<1FW1}=y66XEY5n1ApQPQG@f@6& z_KPSib5B0w40C3j+OpHi9{jfg0r>T3cwZyDVt&MheSKX|>B|F#>DY8a0GV>1+;EF= zWZ%dS*S#8teQlkj$5zEl9PY_UOJK@}rrz_y=i!)-xlqq|3>mjH=kWtPswR|t9zhdL z5PgmJwf@D&LtW?HT6tbK3x|Zk7hOY^F=pSHfAmE=27wRVMJxq`IVmFwwO?+=U-c;0o*Z)Au zxTR%3S)cytTu)@Vb%SFuqSESO6KBNNS8DuoAr38_3@+5{*E!`Zm*LUDb4d^|ZnDPx zHjBEk+UOgo67t27b@nGQBS4QMr2)NlRbyR1U9ztmr)2YH^(E}`DMY#8^NusrV6iD< z$JvC-(MZKt8RNKncl$L1HZ_IV=w$M#^%|Tax`(0WCxs%cwW`aar|gV;!*2SD%jhA2 zz_k60((vQ>w&6Zmy`@Cx1K<5bz6BTYk3Va$%(%B9r_tH_4lZigF@c(&5><_AGD%EE zJ*GQC!1F8+UXm}x89XwYs)+%w! zwO}zSs>RAsxHs! zRa@+Np@iguro+q%_q>o(EYGXg(^ssgc#Mo=Uz=?>dF#h6Z5Mi{HY?0;3u+m{7UOes zc5xg&Rx*{KF6HgL>sxVl+u0X#@I{n;)l>3TQ)Yp;^73*I;7$5YF8Ja9hSTr#~im}Af^*hO@IJ>nTuyn_rt|XC$rYL9Mva@1UmILZBV0S-|Lv)b`4hG z3}xkb?tB9HJ&kE6B~IbG)n^4?QMq5ElM{yP|d-?-Ad#P|E<4GSmI&YnDq#x3vu?SW2tD zoP|i`5H>ld@f!Ak3JY|@kbgVd!*6fL9Zl73Egi2}o>d+n6~0QlRA!ywYGlt3@%gZ& z*wpMpK0P%hLBoopWNkXMKWADDA6UoB)P?|S30)B_g(X+dQ;H0f^;k-b!$FdsbjEX& z)Y5N$)0t~c^1D9MVcI854A_We9fVTBCH8rlJPJWI9Z2!A`c<8S;CI4PgwoLg#VlRT zB*@J%>^S+WURuq`DRREgFB+4rDyTkACMXg{CDdyqt(Q?eauHPZK>=nvcz?q#uP*vn z2krSct(}$L9VjU#Ib30@?tWDs0?M;oV6a)zPpfIGNJmqU6*Bw;nCIhd(&T3S>yg0ez}{p|~}|3+`EWl)#eyk(aN zf$M;zr00H~-1KSEN7T6wmrJnFjMHH^{*f~Rtkf$R`29RJoNzUJwxN8)vv<7{8P*Nk z4NB`!6L{)tq`?N$wuL!QN~ydt%n;Al76`EcmrnCp7%5x+9r~r#b@a_`yQlTL(+_oQ zG0A}NUwdlq#|MpqcPM9ZifTESBW));s3;eXGiJ2JRz*ms&mfgioAu@1`{)i1^FmQK z_+=xZoJ`g=J}Vik5NAvddl8js9|>C;^X)kYhdbaB-3956{B~}h;%#T6rlWP6J;5nk z?xWxja#~Xp%Q9^7xiVZnCjicRUHzSHp~v{ivG`wO2FhVu!0igv+B>6~g{$JpKifCU z6~cL=SrTU&M;U~)v@Bv$Qbm~Gt307H+|gLhasJf7>_wA2*gh&0Ud|j})?14LMWv;B zXsDXkJ!!gF(vrPbel_r_Z1EzPO^_Af`|#=a`(_O((k)jsNc>1yLabnU2;Ocb8{>B@ zB`OZEVw#ZqNbFe!ew_ScA(t<(Gbg#7e)6bJ7RC6{Rxh&jI_I03{0TPf_Q@Prd?2Ft4BvGh!#UEK)eGgrOZ%G4(vhc2B7 zzY2J`m_dS`2}YDXRPV(An#_CgYbUej&%Va~l8)`^AK0?uvBUO3DVcF_!L`N4k94P@ zaT$xqalX#k12cO7-Ay)GVoO`LJ3B4|;u~jsUzoX+C4Bv)AO%xWrmLZ1w?PJh7<3)S zE<9#4$%R%Ni)egmFl_fAzc7rvXA0hYQEcMbHFe?Z>H2AWvokE8GR@P!wbNu)HnTh<;^^tO$3!88{4QP_LpSuO*cgf5SS zNm^#bLaj1%oCH@kq6)G0TNs}2g|BO{w6AGLVp!^;FFoDXO%v_=sn5rD2{^16nbf%| zofQHY*d`VSe{vfH>tlcouLMcY|KLTv)OcgX2=IRGt1RB^+LBXWq{(GpQdx*1zI(*_ zsZo4=AJpPYv(FVXq8L-z1 zd|gsP07f4e%(C@EuL1R&BtBcpbP&a z#uO;yR3)-^0#|JGor|BpjaLE=5%@JLS|S>jqOlhAg72xX{}Mq=riNGS%z|wy9lGh9 z7yQqV0B_)wVW?|?C@}vDd!WFnCOFxEGW=> z=XPNNE}JrftvSaw3b?CbP}Or}>6b{uWF)9{L5cZjaWIu(|?y(-&99 zPf?edk;u10s!n7@h;5u;ndWIGsOL-m^YI2aK3z{7ni>~$ z@kC*a|K0yJvU`i)&-f|KX`-7tBqF1yPdi~>qltUujmxv7j|CSqF;h;!Nt{$YK2ep! ztH)<-&+o`&8z-!%BC7ahN_zJY;4rheYejiKJ?6AQWv17?(Sv0QCVcG7$; zdC}fG@dB^=c)V1E7_K|94_Q|6x)w3l{EvDi0d0aVw90WU!B<$vxs?Pio3I60NUgp{ zf1bbv&&%fts3GaM1E-$+SVseeZ`hPYFaM3UY3}z8i8!wy@#djr4=bPkX#1B#0PW#+;C)={9$Wt#YdnJf5(~Mfm%7#0 z_1;(Sv#2QlwxJ7%W4L3vLZpsn_dw|U5xp~=7fUD{B%#g;1qhoM7m9^VGBt# zK}t&pFNigQ2R!62Q&zdW?CZYMl@T>=3dSPKDlHYt>{0IjE|3y)ZGNq4hDOCn%24aB z)L@Zv6|DNx=&xkE=Wj_lde8@OQok_pEipk?o=TqlKay~Sbu^}zaKlFPn9F&p0`w@0 zj2S>GVWJ`?@2|g{aSzbfRD>{0H)*1tEeLw6x$i_XbDbWkBoSMUQIGMzGv|yfio5D$ zXTVHUwH(~MKqGX0rchYFep2SRP$d7N57+*@AHNv~nW7kh)ul~rd*5Nnz3s6$%E)zU zi^(XcVC|WmQ3ls`Ovp}VuPt%7q2s(CsK##MnEJ|~J|%s|Wr8Y*F8q_fH&my9Et^Xh zU|InCHHf>z_z$PR|6$5rWScM?(L|eQY1~x&F==-<#{}qGA;XQc)WmOd9mP^ICGk`i zCw=l(?rrgBd=CaA3~D>rTapAFKGvxym zS$;*6<`t?e^vZgOI{JC&1f+$Q4axvNgk$lT%}s31>i#!C%WvJyl^3=ipJ)=ni339{ zpuW)-+v6WQcP-HrHS1@De@VyH!-om~QoS?R)Ve?VEJ1XUx*x~xv^3$T6_Z))iuUv9 zNJR{z{+N3mA5|hG?4Q@PHDYsks%|4qO5nBxTScR1w8ajo6?>{`b8hK3wtT&HL&PH;JRO3o(EgnvP#PRvvO5RF;|Yqp}!&x-&GWL9WMRVidq~;DrVm zR+XK~iBKbMYb()TT#Uwy+*JA+x1${2w?m`6FmL_h@h2&^87Hz61`NQV;2MR<7s z8ifEL=Ug?_)czFMMMbrYd-V(AK=yBc$cuWlcw)?pwfM#ajqXVqAYM%kAJC|N$BqxW z1SMu(YMJ;z-xvt(uxGdHuA7afrZa@j@{U}Ta_1Tl5R+47La1v69)VSBQ@EbD2c}s@ zSp=hlF-^v!f76=YOGg(1M(iDTsH<-w69bXa93E5U<2$1&8$GSl76q+25sQ37->ykR zyn9KjOj_H5cBsx7aF@-h#zy8qZWDM9KCR~h+k;z+Gflb`4~#R8zp|4v9-B0s<%P-_u+ z5y&aGHrP9Vo!a)NIy|;%Edo$y`&WhIkFyfqB6<@fbbW?7d-YHGcu|425(k*fA0$uG0K0Pcxmnqb{q1- zmhyDKM|Ml6R-4PCV?Q%Ntk>e&A)`%P2;vF1sKR%~Q|+B_gYbF+C_7`(lxX&|Owf(l zP9R2}!f2Tkx-b~$sh04+#v+C7?^!w#I=0@nd0!*wox5Pmf1=S@C6c;~)w+o+_ZEVm z-fW}_jm7uqDQqh+pI6QIIO9BlJmcMT{ClG}g*Jb43CCSMO=*GU3K6X!0WK=tNIJUU z)dtXr`dGxXPT3eI3jk7AZOoa5ds|yx2`x51X#~DKr{e&ad--vMFbjyGO{(CY#oME$ zkVM^YiM#4V#=ES1=pKAo8CLG0wPr0U`P_P9Yw6@9);sShibf}X*I|#6lKiHF+2kb$ z2LR$Bv(fo3%>wQbxMtHg;_mQi!Y^CE$$_7GP0VRm6bP^#h!GLo&MlIrmZKs1;YBDsH2ZTVUUka4PfKc)PRETXlAJdHh_r?l5#8Nkz1d612J^bjRt zDXo1ET=vykd+}Ia5)E{kj2yRov+Q(Xx^2N_R8Z zfmdGtIIVtbWmj! zUZY9rCf4D;IBmOR(LR6P%N?iVlO!2lT-KW#3j25wIJtiIq1+jO`t(NOb-AYQD`B^1 zuA2Jz9d5hDkBLtDbv*uN4!rcFiLGj&tObfvOpF-yoz)^39}zFetW?)RhX??U!wPCK z+@V<4Ygz@J=|t?%6DQfYKUw=?t0~Cklt)IwaFLS~<*~1UYA37jI+>Wbn9I zpk5-@`bbff0f018QciZBhdSsx!ychl^c35C1m zXD*xwH)xmurTI`IEVObUq04yslHM15`>jXM(I6(OFSPaCCn$WETHRB4mPkDaXm-8( zgsawFR%(F$1t*F(Zb;9;ASMf2rUUd$B4T(Lz=et|$eVTT@(3PNcLhBFu@RuBDG2U zf*YQ%XjM8(Qq~Fp#12iOJ$1}p>9ajdys9p&6&(!E6OEKrfTMxlVwg!xlZdu!{oXT8 zwFAhkt5-;Fu#$Po0X84d;2M>jwFEU>P<)eT^yv}73FAbd*+=QY>9x$<2q{ON$GCB< zPF1uU1ZRrqMKvRY1MV0m>R66#Jl&Ym-fWytn$`P2LYei^J~-(790O4wI|j1pf(0sC zO{gSi2!H@@Lt&wyv2}M6zro##e`d4!dducoTIXWT7rX);m4)njX*h}Q;mx{^nXa8< z>_7m)XA@AD?80?D|5#uC^0iHt;i7FKnca)x;ycj(_#-k>O_*0%45CG0KV-#ryg!Wp zF0h9RkWuJ#5F#jh0URBqTe}tN%l9wWcpt84XNNr@a>z^jC)E%W@gR8X>7>yXMu4yc z``64~4GrWF0|4M#&;MHPB}qY#G|=1Mm9BEjT2Us))#l1X`ke+PPsQSBfms(KMdo=$ z@M;LZZBEGkm;n$_vn~(oZLAuUGru2s)v!|pF~44d zH2!_ys2(xYI!CIKXRF3VpSfOyW_tA(8ik2O>zWQ`h<)`t1NUFU}I zmz2m&*#D;saJYC_JWaCIn=@(G)@*OiIi}OcaaDahJkgE^0dchZrS=N_3(5~GHg31T92LIv zBq^2S{5R8xfc}si#cXKo=2H4-OB8-ZBZ(ke<$`&-Z%HFHzO8KX zh!Zyi44(t(tclqdf@^{vE|%6tXPHNHdAds0@*R()nTd_s^t8MFHd@Q9U%$)SHo`i1 z2S5Ulb$B+ge#a^5%!stB`N~kp$HzMNYH!^en}s^cD4mN9C_*LtBr5#JR_H+Ozw33v zslwy)avZ`6CI{(E-5Jw;uEyX4>Y`|}`fII`ia56D>o3Ez6|D4-1Tb80b^sD1^w))8 z)QUzH5mI!|!An=hEA^1(t>hHx^HWH-yMOoZ%Z1QCK|67pf z!mH)F4%gQu=1}P&9Z60{!#R$#FR&|!N6Rb;dsG|gf7f%nNx~lu=d3#|P1lyx&7XLIAqwlHU{S5Z_>^}mIDoL&1gws?8~F|<>9`*Ce2~8?q)ED+i`enE@*l#LGC?sQ# z8z5ZXX0kB~TQr>O=PMB<4Ly8D&RT|k(T2AETEPd?xC)NsF7@--`fHdk$-NBGm-+9O z0N0?dZnR(4i44P~|AqX=#;P}s2A@EHg7ONICww>c%EJ$(>}wTQ;FU(JZZF_p@Mp`k zp+mG)V9tN^N}atN*ge;0VJ_{h7&I;bmcq7dIF#fG&69jsZbLR)Sz)^qNj;n8n&*)b z(!LUkQU@9RFH6!-cD>hg$7;0|7U2qP0e>Duts?NFb8DKtdYO_a8@@)=vPj-VHU&ab zGJWRZC7aZ0A10d1)#b4Bo7SHzVz}EJ!@*$&8Z$FO51lYqU^kMJ`H2i1Rc!!w2aD<> z8JpG#fbF>Q>jk(Hs0$t8XhJ8i>$JYM72`g8EMzj%hba?YdeHCAfMBM-cw-{Zjma4_ z6VXNZIru+@DU#XxM%z0Rk1qBelN3pN#YxeO;?;*YBb&r$Q_GYR04cHMO~6w<1daK) z(;1%i#wS>bd(tXxwYT*F1tY*(PKSHlhia7*?o)uYbCzvnxGUB&{zFV`#?Fd03{wV8 zV}==aPrQmhwiH}f-sch?igrQ-_GB{7L^5_Qw66%MPDl{e=9ZO*TCCJMh%_|(_@S-F zs2bhl)swJ*x7KB1M)0b^+y!n9EkZs%P@*)j&fB<4O;K7ZR25NHl2_TC&ebMEZ|6 zjN~EfNKeG~Q8~@?xKSiM7nripDu(^SZs(c@N&q{U{x1z#Ojd^~Y#jDO$O&xgWuf}; zY)CUGfKCm3!P()^&6D%d(2V4>TjGHg8-NjT0nj-{zuS7|Z7zf&ojdTi literal 0 HcmV?d00001 diff --git a/packaging/android/res/values/splashscreentheme.xml b/packaging/android/res/values/splashscreentheme.xml new file mode 100644 index 00000000..915db62c --- /dev/null +++ b/packaging/android/res/values/splashscreentheme.xml @@ -0,0 +1,6 @@ + + + + From 22dd3fe27d15a4db21f73e7dac885222106b56af Mon Sep 17 00:00:00 2001 From: Michael Zanetti Date: Tue, 5 Feb 2019 13:52:08 +0100 Subject: [PATCH 04/11] intermediate commit. Working pretty well now. No cleanup done. some broken menu entries related to connect. TBC --- libnymea-app-core/connection/awsclient.cpp | 16 +- .../connection/bluetoothtransport.cpp | 6 + .../connection/bluetoothtransport.h | 3 + .../connection/cloudtransport.cpp | 6 + libnymea-app-core/connection/cloudtransport.h | 3 + .../discovery/bluetoothservicediscovery.cpp | 20 +- .../discovery/bluetoothservicediscovery.h | 6 +- .../connection/discovery/nymeadiscovery.cpp | 227 +++++++++++++ .../discovery/nymeadiscovery.h | 21 +- .../discovery/upnpdiscovery.cpp | 10 +- .../discovery/upnpdiscovery.h | 10 +- .../discovery/zeroconfdiscovery.cpp | 52 +-- .../discovery/zeroconfdiscovery.h | 6 +- .../connection/nymeaconnection.cpp | 309 ++++++++++++++---- .../connection/nymeaconnection.h | 41 ++- .../nymeahost.cpp} | 90 +++-- .../nymeahost.h} | 36 +- libnymea-app-core/connection/nymeahosts.cpp | 204 ++++++++++++ .../nymeahosts.h} | 72 +++- .../connection/nymeatransportinterface.h | 1 + .../connection/tcpsockettransport.cpp | 9 +- .../connection/tcpsockettransport.h | 1 + .../connection/websockettransport.cpp | 6 + .../connection/websockettransport.h | 2 + .../discovery/discoverymodel.cpp | 115 ------- .../discovery/nymeadiscovery.cpp | 204 ------------ libnymea-app-core/discovery/nymeahost.cpp | 68 ---- libnymea-app-core/discovery/nymeahost.h | 54 --- libnymea-app-core/discovery/nymeahosts.cpp | 150 --------- libnymea-app-core/discovery/nymeahosts.h | 59 ---- libnymea-app-core/libnymea-app-core.h | 13 +- libnymea-app-core/libnymea-app-core.pro | 28 +- nymea-app/main.cpp | 6 + .../android/platformhelperandroid.cpp | 4 +- nymea-app/ui/Nymea.qml | 1 + nymea-app/ui/RootItem.qml | 31 +- nymea-app/ui/SettingsPage.qml | 2 +- nymea-app/ui/connection/ConnectPage.qml | 97 +++--- nymea-app/ui/connection/ConnectingPage.qml | 2 +- .../wifisetup/WirelessSetupPage.qml | 12 +- 40 files changed, 1053 insertions(+), 950 deletions(-) rename libnymea-app-core/{ => connection}/discovery/bluetoothservicediscovery.cpp (87%) rename libnymea-app-core/{ => connection}/discovery/bluetoothservicediscovery.h (85%) create mode 100644 libnymea-app-core/connection/discovery/nymeadiscovery.cpp rename libnymea-app-core/{ => connection}/discovery/nymeadiscovery.h (78%) rename libnymea-app-core/{ => connection}/discovery/upnpdiscovery.cpp (97%) rename libnymea-app-core/{ => connection}/discovery/upnpdiscovery.h (91%) rename libnymea-app-core/{ => connection}/discovery/zeroconfdiscovery.cpp (81%) rename libnymea-app-core/{ => connection}/discovery/zeroconfdiscovery.h (78%) rename libnymea-app-core/{discovery/discoverydevice.cpp => connection/nymeahost.cpp} (77%) rename libnymea-app-core/{discovery/discoverydevice.h => connection/nymeahost.h} (84%) create mode 100644 libnymea-app-core/connection/nymeahosts.cpp rename libnymea-app-core/{discovery/discoverymodel.h => connection/nymeahosts.h} (51%) delete mode 100644 libnymea-app-core/discovery/discoverymodel.cpp delete mode 100644 libnymea-app-core/discovery/nymeadiscovery.cpp delete mode 100644 libnymea-app-core/discovery/nymeahost.cpp delete mode 100644 libnymea-app-core/discovery/nymeahost.h delete mode 100644 libnymea-app-core/discovery/nymeahosts.cpp delete mode 100644 libnymea-app-core/discovery/nymeahosts.h diff --git a/libnymea-app-core/connection/awsclient.cpp b/libnymea-app-core/connection/awsclient.cpp index e89dbc29..39268c80 100644 --- a/libnymea-app-core/connection/awsclient.cpp +++ b/libnymea-app-core/connection/awsclient.cpp @@ -866,25 +866,25 @@ bool AWSClient::postToMQTT(const QString &boxId, const QString ×tamp, std:: request.setUrl("https://" + m_configs.value(m_usedConfig).mqttEndpoint + path1); qDebug() << "Posting to MQTT:" << request.url().toString(); - qDebug() << "HEADERS:"; - foreach (const QByteArray &headerName, request.rawHeaderList()) { - qDebug() << headerName << ":" << request.rawHeader(headerName); - } - qDebug() << "Payload:" << payload; +// qDebug() << "HEADERS:"; +// foreach (const QByteArray &headerName, request.rawHeaderList()) { +// qDebug() << headerName << ":" << request.rawHeader(headerName); +// } +// qDebug() << "Payload:" << payload; QNetworkReply *reply = m_nam->post(request, payload); connect(reply, &QNetworkReply::finished, this, [reply, callback]() { reply->deleteLater(); QByteArray data = reply->readAll(); - qDebug() << "post reply" << data; +// qDebug() << "MQTT post reply" << data; if (reply->error() != QNetworkReply::NoError) { - qWarning() << "Network reply error" << reply->error() << reply->errorString(); + qWarning() << "MQTT Network reply error" << reply->error() << reply->errorString(); callback(false); return; } QJsonParseError error; QJsonDocument jsonDoc = QJsonDocument::fromJson(data, &error); if (error.error != QJsonParseError::NoError) { - qWarning() << "Failed to parse reply" << error.error << error.errorString() << data; + qWarning() << "Failed to parse MQTT reply" << error.error << error.errorString() << data; callback(false); return; } diff --git a/libnymea-app-core/connection/bluetoothtransport.cpp b/libnymea-app-core/connection/bluetoothtransport.cpp index af12a988..68e9969b 100644 --- a/libnymea-app-core/connection/bluetoothtransport.cpp +++ b/libnymea-app-core/connection/bluetoothtransport.cpp @@ -43,6 +43,7 @@ bool BluetoothTransport::connect(const QUrl &url) qWarning() << "BluetoothInterface: Cannot connect. Invalid scheme in url" << url.toString(); return false; } + m_url = url; QUrlQuery query(url); QString macAddressString = query.queryItemValue("mac"); @@ -54,6 +55,11 @@ bool BluetoothTransport::connect(const QUrl &url) return true; } +QUrl BluetoothTransport::url() const +{ + return m_url; +} + void BluetoothTransport::disconnect() { m_socket->close(); diff --git a/libnymea-app-core/connection/bluetoothtransport.h b/libnymea-app-core/connection/bluetoothtransport.h index 6eb3ddc3..32a032ea 100644 --- a/libnymea-app-core/connection/bluetoothtransport.h +++ b/libnymea-app-core/connection/bluetoothtransport.h @@ -24,6 +24,7 @@ #define BLUETOOTHTRANSPORT_H #include +#include #include #include "nymeatransportinterface.h" @@ -42,11 +43,13 @@ public: explicit BluetoothTransport(QObject *parent = nullptr); bool connect(const QUrl &url) override; + QUrl url() const override; void disconnect() override; ConnectionState connectionState() const override; void sendData(const QByteArray &data) override; private: + QUrl m_url; QBluetoothSocket *m_socket = nullptr; QBluetoothServiceInfo m_service; diff --git a/libnymea-app-core/connection/cloudtransport.cpp b/libnymea-app-core/connection/cloudtransport.cpp index e9575792..03818ad8 100644 --- a/libnymea-app-core/connection/cloudtransport.cpp +++ b/libnymea-app-core/connection/cloudtransport.cpp @@ -49,6 +49,7 @@ bool CloudTransport::connect(const QUrl &url) } qDebug() << "Connecting to" << url; + m_url = url; m_timestamp = QDateTime::currentDateTime(); bool postResult = m_awsClient->postToMQTT(url.host(), QString::number(m_timestamp.toMSecsSinceEpoch()), [this](bool success) { @@ -68,6 +69,11 @@ bool CloudTransport::connect(const QUrl &url) return true; } +QUrl CloudTransport::url() const +{ + return m_url; +} + void CloudTransport::disconnect() { qDebug() << "CloudTransport: Disconnecting from server."; diff --git a/libnymea-app-core/connection/cloudtransport.h b/libnymea-app-core/connection/cloudtransport.h index 90ecb445..00d09ea9 100644 --- a/libnymea-app-core/connection/cloudtransport.h +++ b/libnymea-app-core/connection/cloudtransport.h @@ -4,6 +4,7 @@ #include "nymeatransportinterface.h" #include +#include class AWSClient; namespace remoteproxyclient { @@ -25,12 +26,14 @@ public: explicit CloudTransport(AWSClient *awsClient, QObject *parent = nullptr); bool connect(const QUrl &url) override; + QUrl url() const override; void disconnect() override; ConnectionState connectionState() const override; void sendData(const QByteArray &data) override; void ignoreSslErrors(const QList &errors) override; private: + QUrl m_url; AWSClient *m_awsClient = nullptr; remoteproxyclient::RemoteProxyConnection *m_remoteproxyConnection = nullptr; QDateTime m_timestamp; diff --git a/libnymea-app-core/discovery/bluetoothservicediscovery.cpp b/libnymea-app-core/connection/discovery/bluetoothservicediscovery.cpp similarity index 87% rename from libnymea-app-core/discovery/bluetoothservicediscovery.cpp rename to libnymea-app-core/connection/discovery/bluetoothservicediscovery.cpp index 39ebbfc2..9485a9f5 100644 --- a/libnymea-app-core/discovery/bluetoothservicediscovery.cpp +++ b/libnymea-app-core/connection/discovery/bluetoothservicediscovery.cpp @@ -1,13 +1,13 @@ #include "bluetoothservicediscovery.h" -#include "discoverymodel.h" -#include "discoverydevice.h" +#include "../nymeahosts.h" +#include "../nymeahost.h" #include -BluetoothServiceDiscovery::BluetoothServiceDiscovery(DiscoveryModel *discoveryModel, QObject *parent) : +BluetoothServiceDiscovery::BluetoothServiceDiscovery(NymeaHosts *nymeaHosts, QObject *parent) : QObject(parent), - m_discoveryModel(discoveryModel) + m_nymeaHosts(nymeaHosts) { m_nymeaServiceUuid = QBluetoothUuid(QUuid("997936b5-d2cd-4c57-b41b-c6048320cd2b")); @@ -29,7 +29,7 @@ bool BluetoothServiceDiscovery::available() const if (!m_localDevice) return false; - return m_localDevice->isValid() && !m_localDevice->hostMode() != QBluetoothLocalDevice::HostPoweredOff; + return m_localDevice->isValid() && m_localDevice->hostMode() != QBluetoothLocalDevice::HostPoweredOff; } void BluetoothServiceDiscovery::discover() @@ -101,15 +101,15 @@ void BluetoothServiceDiscovery::onServiceDiscovered(const QBluetoothServiceInfo if (serviceInfo.serviceClassUuids().first() == QBluetoothUuid(QUuid("997936b5-d2cd-4c57-b41b-c6048320cd2b"))) { qDebug() << "BluetoothServiceDiscovery: Found nymea rfcom service!"; -// DiscoveryDevice* device = m_discoveryModel->find(serviceInfo.device().address()); -// if (!device) { -// device = new DiscoveryDevice(DiscoveryDevice::DeviceTypeBluetooth, this); +// NymeaHost* host = m_nymeaHosts->find(serviceInfo.device().address()); +// if (!host) { +// host = new DiscoveryDevice(DiscoveryDevice::DeviceTypeBluetooth, this); // qDebug() << "BluetoothServiceDiscovery: Adding new bluetooth host to model"; -// device->setName(QString("%1 (%2)").arg(serviceInfo.serviceName()).arg(serviceInfo.device().name())); +// host->setName(QString("%1 (%2)").arg(serviceInfo.serviceName()).arg(serviceInfo.device().name())); //// device->setBluetoothAddress(serviceInfo.device().address()); // PortConfig pc; -// m_discoveryModel->addDevice(device); +// m_nymeaHosts->addHost(device); // } } } diff --git a/libnymea-app-core/discovery/bluetoothservicediscovery.h b/libnymea-app-core/connection/discovery/bluetoothservicediscovery.h similarity index 85% rename from libnymea-app-core/discovery/bluetoothservicediscovery.h rename to libnymea-app-core/connection/discovery/bluetoothservicediscovery.h index 1da95907..40c18737 100644 --- a/libnymea-app-core/discovery/bluetoothservicediscovery.h +++ b/libnymea-app-core/connection/discovery/bluetoothservicediscovery.h @@ -6,13 +6,13 @@ #include #include -class DiscoveryModel; +class NymeaHosts; class BluetoothServiceDiscovery : public QObject { Q_OBJECT public: - explicit BluetoothServiceDiscovery(DiscoveryModel *discoveryModel, QObject *parent = nullptr); + explicit BluetoothServiceDiscovery(NymeaHosts *nymeaHosts, QObject *parent = nullptr); bool discovering() const; bool available() const; @@ -21,7 +21,7 @@ public: Q_INVOKABLE void stopDiscovery(); private: - DiscoveryModel *m_discoveryModel = nullptr; + NymeaHosts *m_nymeaHosts = nullptr; QBluetoothLocalDevice *m_localDevice = nullptr; QBluetoothServiceDiscoveryAgent *m_serviceDiscovery = nullptr; QBluetoothUuid m_nymeaServiceUuid; diff --git a/libnymea-app-core/connection/discovery/nymeadiscovery.cpp b/libnymea-app-core/connection/discovery/nymeadiscovery.cpp new file mode 100644 index 00000000..5dc0566f --- /dev/null +++ b/libnymea-app-core/connection/discovery/nymeadiscovery.cpp @@ -0,0 +1,227 @@ +#include "nymeadiscovery.h" +#include "upnpdiscovery.h" +#include "zeroconfdiscovery.h" +#include "bluetoothservicediscovery.h" +#include "connection/awsclient.h" +#include "../nymeahost.h" + +#include +#include +#include +#include +#include +#include + +NymeaDiscovery::NymeaDiscovery(QObject *parent) : QObject(parent) +{ + m_nymeaHosts = new NymeaHosts(this); + + loadFromDisk(); + + m_upnp = new UpnpDiscovery(m_nymeaHosts, this); + m_zeroConf = new ZeroconfDiscovery(m_nymeaHosts, this); + +#ifndef Q_OS_IOS + m_bluetooth = new BluetoothServiceDiscovery(m_nymeaHosts, this); +#endif + + m_cloudPollTimer.setInterval(5000); + connect(&m_cloudPollTimer, &QTimer::timeout, this, [this](){ + if (m_awsClient && m_awsClient->isLoggedIn()) { + m_awsClient->fetchDevices(); + } + }); + +} + +NymeaDiscovery::~NymeaDiscovery() +{ +} + +bool NymeaDiscovery::discovering() const +{ + return m_discovering; +} + +void NymeaDiscovery::setDiscovering(bool discovering) +{ + if (m_discovering == discovering) + return; + + m_discovering = discovering; + // If we have zeroconf skip upnp. ZeroConf will not do an active discovery and if it's available it'll always have good data + if (!m_zeroConf->available()) { + if (discovering) { + m_upnp->discover(); + } else { + m_upnp->stopDiscovery(); + } + } + if (discovering) { + // If there's no Zeroconf, use UPnP instead + if (!m_zeroConf->available()) { + m_upnp->discover(); + } + + // Always start Bluetooth discovery if HW is available + if (m_bluetooth) { + m_bluetooth->discover(); + } + + // start polling cloud + m_cloudPollTimer.start(); + // If we're logged in, poll right away + if (m_awsClient && m_awsClient->isLoggedIn()) { + m_awsClient->fetchDevices(); + } + } else { + if (!m_zeroConf->available()) { + m_upnp->stopDiscovery(); + } + + if (m_bluetooth) { + m_bluetooth->stopDiscovery(); + } + + m_cloudPollTimer.stop(); + } + + emit discoveringChanged(); +} + +NymeaHosts *NymeaDiscovery::nymeaHosts() const +{ + return m_nymeaHosts; +} + +AWSClient *NymeaDiscovery::awsClient() const +{ + return m_awsClient; +} + +void NymeaDiscovery::setAwsClient(AWSClient *awsClient) +{ + if (m_awsClient != awsClient) { + m_awsClient = awsClient; + emit awsClientChanged(); + } + + if (m_awsClient) { + m_awsClient->fetchDevices(); + connect(m_awsClient, &AWSClient::devicesFetched, this, &NymeaDiscovery::syncCloudDevices); + } +} + +void NymeaDiscovery::cacheHost(NymeaHost *host) +{ + QSettings settings; + settings.beginGroup("HostCache"); + settings.remove(host->uuid().toString()); + settings.beginGroup(host->uuid().toString()); + settings.setValue("name", host->name()); + QList connections; + Connection *remoteConnection = host->connections()->bestMatch(Connection::BearerTypeCloud); + if (remoteConnection) { + connections.append(remoteConnection); + } + Connection *lanConnection = host->connections()->bestMatch(Connection::BearerTypeWifi | Connection::BearerTypeEthernet); + if (lanConnection) { + connections.append(lanConnection); + } + Connection *btConnection = host->connections()->bestMatch(Connection::BearerTypeBluetooth); + if (btConnection) { + connections.append(btConnection); + } + int i = 0; + foreach (Connection *connection, connections) { + settings.beginGroup(QString::number(i++)); + settings.setValue("url", connection->url()); + settings.setValue("bearerType", connection->bearerType()); + settings.value("secure", connection->secure()); + settings.setValue("displayName", connection->displayName()); + settings.endGroup(); + } + settings.endGroup(); +} + +void NymeaDiscovery::syncCloudDevices() +{ + for (int i = 0; i < m_awsClient->awsDevices()->rowCount(); i++) { + AWSDevice *d = m_awsClient->awsDevices()->get(i); + NymeaHost *host = m_nymeaHosts->find(d->id()); + if (!host) { + host = new NymeaHost(); + host->setUuid(d->id()); + host->setName(d->name()); + qDebug() << "CloudDiscovery: Adding new host:" << host->name() << host->uuid().toString(); + m_nymeaHosts->addHost(host); + } + QUrl url; + url.setScheme("cloud"); + url.setHost(d->id()); + Connection *conn = host->connections()->find(url); + if (!conn) { + conn = new Connection(url, Connection::BearerTypeCloud, true, d->id()); + qDebug() << "CloudDiscovery: Adding new connection to host:" << host->name() << conn->url().toString(); + host->connections()->addConnection(conn); + } + conn->setOnline(d->online()); + } + + QList hostsToRemove; + for (int i = 0; i < m_nymeaHosts->rowCount(); i++) { + NymeaHost *host = m_nymeaHosts->get(i); + for (int j = 0; j < host->connections()->rowCount(); j++) { + if (host->connections()->get(j)->bearerType() == Connection::BearerTypeCloud) { + if (m_awsClient->awsDevices()->getDevice(host->uuid().toString()) == nullptr) { + host->connections()->removeConnection(j); + break; + } + } + } + if (host->connections()->rowCount() == 0) { + hostsToRemove.append(host); + } + } + while (!hostsToRemove.isEmpty()) { + m_nymeaHosts->removeHost(hostsToRemove.takeFirst()); + } +} + +void NymeaDiscovery::loadFromDisk() +{ + QSettings settings; + settings.beginGroup("HostCache"); + foreach (const QString &serverUuid, settings.childGroups()) { + settings.beginGroup(serverUuid); + NymeaHost* host = m_nymeaHosts->find(QUuid(serverUuid)); + if (!host) { + host = new NymeaHost(m_nymeaHosts); + host->setName(settings.value("name").toString()); + host->setUuid(QUuid(serverUuid)); + m_nymeaHosts->addHost(host); + } + qDebug() << "Loaded Host from cache" << host->name() << host->uuid(); + foreach (const QString &group, settings.childGroups()) { + settings.beginGroup(group); + QString url = settings.value("url").toString(); + Connection* connection = host->connections()->find(url); + if (!connection) { + Connection::BearerType bearerType = static_cast(settings.value("bearerType").toInt()); + bool secure = settings.value("secure").toBool(); + QString displayName = settings.value("displayName").toString(); + connection = new Connection(url, bearerType, secure, displayName, host); + host->connections()->addConnection(connection); + qDebug() << "|- Connection:" << group << connection->url() << connection->bearerType() << "secure:" << connection->secure(); + } + settings.endGroup(); + } + settings.endGroup(); + } + +} + +void NymeaDiscovery::updateActiveBearers() +{ +} + diff --git a/libnymea-app-core/discovery/nymeadiscovery.h b/libnymea-app-core/connection/discovery/nymeadiscovery.h similarity index 78% rename from libnymea-app-core/discovery/nymeadiscovery.h rename to libnymea-app-core/connection/discovery/nymeadiscovery.h index 46aaabac..b486b2b7 100644 --- a/libnymea-app-core/discovery/nymeadiscovery.h +++ b/libnymea-app-core/connection/discovery/nymeadiscovery.h @@ -6,8 +6,9 @@ #include #include "connection/awsclient.h" +#include "connection/nymeahost.h" -class DiscoveryModel; +class NymeaHosts; class UpnpDiscovery; class ZeroconfDiscovery; class BluetoothServiceDiscovery; @@ -17,22 +18,23 @@ class NymeaDiscovery : public QObject { Q_OBJECT Q_PROPERTY(bool discovering READ discovering WRITE setDiscovering NOTIFY discoveringChanged) - Q_PROPERTY(DiscoveryModel *discoveryModel READ discoveryModel CONSTANT) - Q_PROPERTY(AWSClient* awsClient READ awsClient WRITE setAwsClient NOTIFY awsClientChanged) + Q_PROPERTY(NymeaHosts* nymeaHosts READ nymeaHosts CONSTANT) + public: explicit NymeaDiscovery(QObject *parent = nullptr); + ~NymeaDiscovery(); bool discovering() const; void setDiscovering(bool discovering); - DiscoveryModel *discoveryModel() const; + NymeaHosts *nymeaHosts() const; AWSClient* awsClient() const; void setAwsClient(AWSClient *awsClient); - Q_INVOKABLE void resolveServerUuid(const QUuid &uuid); + Q_INVOKABLE void cacheHost(NymeaHost* host); signals: void discoveringChanged(); @@ -43,14 +45,19 @@ signals: private slots: void syncCloudDevices(); + void loadFromDisk(); + + void updateActiveBearers(); + private: bool m_discovering = false; - DiscoveryModel *m_discoveryModel = nullptr; + NymeaHosts *m_nymeaHosts = nullptr; + + AWSClient *m_awsClient = nullptr; UpnpDiscovery *m_upnp = nullptr; ZeroconfDiscovery *m_zeroConf = nullptr; BluetoothServiceDiscovery *m_bluetooth = nullptr; - AWSClient *m_awsClient = nullptr; QTimer m_cloudPollTimer; diff --git a/libnymea-app-core/discovery/upnpdiscovery.cpp b/libnymea-app-core/connection/discovery/upnpdiscovery.cpp similarity index 97% rename from libnymea-app-core/discovery/upnpdiscovery.cpp rename to libnymea-app-core/connection/discovery/upnpdiscovery.cpp index 87a9e861..2853d363 100644 --- a/libnymea-app-core/discovery/upnpdiscovery.cpp +++ b/libnymea-app-core/connection/discovery/upnpdiscovery.cpp @@ -25,9 +25,9 @@ #include #include -UpnpDiscovery::UpnpDiscovery(DiscoveryModel *discoveryModel, QObject *parent) : +UpnpDiscovery::UpnpDiscovery(NymeaHosts *nymeaHosts, QObject *parent) : QObject(parent), - m_discoveryModel(discoveryModel) + m_nymeaHosts(nymeaHosts) { m_networkAccessManager = new QNetworkAccessManager(this); connect(m_networkAccessManager, &QNetworkAccessManager::finished, this, &UpnpDiscovery::networkReplyFinished); @@ -240,12 +240,12 @@ void UpnpDiscovery::networkReplyFinished(QNetworkReply *reply) // qDebug() << "discovered device" << uuid << name << discoveredAddress << version << connections << data; - DiscoveryDevice* device = m_discoveryModel->find(uuid); + NymeaHost* device = m_nymeaHosts->find(uuid); if (!device) { - device = new DiscoveryDevice(m_discoveryModel); + device = new NymeaHost(m_nymeaHosts); device->setUuid(uuid); qDebug() << "UPnP: Adding new host to model"; - m_discoveryModel->addDevice(device); + m_nymeaHosts->addHost(device); } device->setName(name); device->setVersion(version); diff --git a/libnymea-app-core/discovery/upnpdiscovery.h b/libnymea-app-core/connection/discovery/upnpdiscovery.h similarity index 91% rename from libnymea-app-core/discovery/upnpdiscovery.h rename to libnymea-app-core/connection/discovery/upnpdiscovery.h index b5cab329..21e9ebfa 100644 --- a/libnymea-app-core/discovery/upnpdiscovery.h +++ b/libnymea-app-core/connection/discovery/upnpdiscovery.h @@ -27,14 +27,14 @@ #include #include -#include "discoverydevice.h" -#include "discoverymodel.h" +#include "../nymeahost.h" +#include "../nymeahosts.h" class UpnpDiscovery : public QObject { Q_OBJECT public: - explicit UpnpDiscovery(DiscoveryModel *discoveryModel, QObject *parent = 0); + explicit UpnpDiscovery(NymeaHosts *nymeaHosts, QObject *parent = nullptr); bool discovering() const; @@ -49,7 +49,7 @@ private: QTimer m_repeatTimer; - DiscoveryModel *m_discoveryModel; + NymeaHosts *m_nymeaHosts; QHash m_runningReplies; QList m_foundDevices; @@ -57,7 +57,7 @@ private: signals: void discoveringChanged(); void availableChanged(); - void discoveryModelChanged(); + void nymeaHostsChanged(); private slots: void writeDiscoveryPacket(); diff --git a/libnymea-app-core/discovery/zeroconfdiscovery.cpp b/libnymea-app-core/connection/discovery/zeroconfdiscovery.cpp similarity index 81% rename from libnymea-app-core/discovery/zeroconfdiscovery.cpp rename to libnymea-app-core/connection/discovery/zeroconfdiscovery.cpp index 2f3053e4..fff26f0c 100644 --- a/libnymea-app-core/discovery/zeroconfdiscovery.cpp +++ b/libnymea-app-core/connection/discovery/zeroconfdiscovery.cpp @@ -2,11 +2,11 @@ #include -#include "discoverydevice.h" +#include "../nymeahost.h" -ZeroconfDiscovery::ZeroconfDiscovery(DiscoveryModel *discoveryModel, QObject *parent) : +ZeroconfDiscovery::ZeroconfDiscovery(NymeaHosts *nymeaHosts, QObject *parent) : QObject(parent), - m_discoveryModel(discoveryModel) + m_nymeaHosts(nymeaHosts) { #ifdef WITH_ZEROCONF // NOTE: There seem to be too many issues in QtZeroConf and IPv6. @@ -70,6 +70,16 @@ void ZeroconfDiscovery::serviceEntryAdded(const QZeroConfService &entry) return; } + // Workaround a bug in deeper layers (I believe it's avahi, but could be QtZeroConf too): + // Sometimes the ip() field contains an IPv6 address. In that case the entry is likely garbage as + // it does not mean the host necessarily exports the services on IPv6. + bool isIPv4; + entry.ip().toIPv4Address(&isIPv4); + if (!isIPv4) { + qDebug() << "Skipping invalid Avahi entry: IPv4:" << entry.ip(); + return; + } + // qDebug() << "zeroconf service discovered" << entry.type() << entry.name() << " IP:" << entry.ip() << "IPv6:" << entry.ipv6() << entry.txt(); QString uuid; @@ -91,18 +101,18 @@ void ZeroconfDiscovery::serviceEntryAdded(const QZeroConfService &entry) version = txtRecord.second; } } - qDebug() << "avahi service entry added" << serverName << uuid << sslEnabled; +// qDebug() << "avahi service entry added" << serverName << uuid << sslEnabled; - DiscoveryDevice* device = m_discoveryModel->find(uuid); - if (!device) { - device = new DiscoveryDevice(m_discoveryModel); - device->setUuid(uuid); + NymeaHost* host = m_nymeaHosts->find(uuid); + if (!host) { + host = new NymeaHost(m_nymeaHosts); + host->setUuid(uuid); qDebug() << "ZeroConf: Adding new host:" << serverName << uuid; - m_discoveryModel->addDevice(device); + m_nymeaHosts->addHost(host); } - device->setName(serverName); - device->setVersion(version); + host->setName(serverName); + host->setVersion(version); QUrl url; // NOTE: On linux this is "_jsonrpc._tcp" while on apple systems this is "_jsonrpc._tcp." if (entry.type().startsWith("_jsonrpc._tcp")) { @@ -112,12 +122,12 @@ void ZeroconfDiscovery::serviceEntryAdded(const QZeroConfService &entry) } url.setHost(!entry.ip().isNull() ? entry.ip().toString() : entry.ipv6().toString()); url.setPort(entry.port()); - if (!device->connections()->find(url)){ - qDebug() << "Zeroconf: Adding new connection to host:" << device->name() << url.toString(); + if (!host->connections()->find(url)){ + qDebug() << "Zeroconf: Adding new connection to host:" << host->name() << url.toString(); QString displayName = QString("%1:%2").arg(url.host()).arg(url.port()); Connection *connection = new Connection(url, Connection::BearerTypeWifi, sslEnabled, displayName); connection->setOnline(true); - device->connections()->addConnection(connection); + host->connections()->addConnection(connection); } } @@ -149,8 +159,8 @@ void ZeroconfDiscovery::serviceEntryRemoved(const QZeroConfService &entry) // qDebug() << "Zeroconf: Service entry removed" << entry.name(); - DiscoveryDevice* device = m_discoveryModel->find(uuid); - if (!device) { + NymeaHost* host = m_nymeaHosts->find(uuid); + if (!host) { // Nothing to do... return; } @@ -163,19 +173,19 @@ void ZeroconfDiscovery::serviceEntryRemoved(const QZeroConfService &entry) } url.setHost(!entry.ip().isNull() ? entry.ip().toString() : entry.ipv6().toString()); url.setPort(entry.port()); - Connection *connection = device->connections()->find(url); + Connection *connection = host->connections()->find(url); if (!connection){ // Connection url not found... return; } // Ok, now we need to remove it - device->connections()->removeConnection(connection); + host->connections()->removeConnection(connection); // And if there aren't any connections left, remove the entire device - if (device->connections()->rowCount() == 0) { - qDebug() << "Zeroconf: Removing connection from host:" << device->name() << url.toString(); - m_discoveryModel->removeDevice(device); + if (host->connections()->rowCount() == 0) { + qDebug() << "Zeroconf: Removing connection from host:" << host->name() << url.toString(); + m_nymeaHosts->removeHost(host); } } #endif diff --git a/libnymea-app-core/discovery/zeroconfdiscovery.h b/libnymea-app-core/connection/discovery/zeroconfdiscovery.h similarity index 78% rename from libnymea-app-core/discovery/zeroconfdiscovery.h rename to libnymea-app-core/connection/discovery/zeroconfdiscovery.h index 39608c33..906a1748 100644 --- a/libnymea-app-core/discovery/zeroconfdiscovery.h +++ b/libnymea-app-core/connection/discovery/zeroconfdiscovery.h @@ -5,7 +5,7 @@ #include "qzeroconf.h" #endif -#include "discoverymodel.h" +#include "../nymeahosts.h" #include @@ -14,14 +14,14 @@ class ZeroconfDiscovery : public QObject Q_OBJECT public: - explicit ZeroconfDiscovery(DiscoveryModel *discoveryModel, QObject *parent = nullptr); + explicit ZeroconfDiscovery(NymeaHosts *nymeaHosts, QObject *parent = nullptr); ~ZeroconfDiscovery(); bool available() const; bool discovering() const; private: - DiscoveryModel *m_discoveryModel; + NymeaHosts *m_nymeaHosts; #ifdef WITH_ZEROCONF QZeroConf *m_zeroconfJsonRPC = nullptr; diff --git a/libnymea-app-core/connection/nymeaconnection.cpp b/libnymea-app-core/connection/nymeaconnection.cpp index 36f57efc..9be30e90 100644 --- a/libnymea-app-core/connection/nymeaconnection.cpp +++ b/libnymea-app-core/connection/nymeaconnection.cpp @@ -1,4 +1,5 @@ #include "nymeaconnection.h" +#include "nymeahost.h" #include #include @@ -14,51 +15,18 @@ NymeaConnection::NymeaConnection(QObject *parent) : QObject(parent) { -} + m_networkConfigManager = new QNetworkConfigurationManager(this); -bool NymeaConnection::connect(const QString &url) -{ - if (connected()) { - qWarning() << "Already connected. Cannot connect multiple times"; - return false; - } + QObject::connect(m_networkConfigManager, &QNetworkConfigurationManager::configurationAdded, this, [this](const QNetworkConfiguration &config){ +// qDebug() << "Network configuration added:" << config.name() << config.bearerTypeName() << config.purpose(); + updateActiveBearers(); + }); + QObject::connect(m_networkConfigManager, &QNetworkConfigurationManager::configurationRemoved, this, [this](const QNetworkConfiguration &config){ +// qDebug() << "Network configuration removed:" << config.name() << config.bearerTypeName() << config.purpose(); + updateActiveBearers(); + }); - m_currentUrl = QUrl(url); - emit currentUrlChanged(); - if (!m_transports.contains(m_currentUrl.scheme())) { - qWarning() << "Cannot connect to urls of scheme" << m_currentUrl.scheme() << "Supported schemes are" << m_transports.keys(); - return false; - } - - // Create a new transport - m_currentTransport = m_transports.value(m_currentUrl.scheme())->createTransport(); - QObject::connect(m_currentTransport, &NymeaTransportInterface::sslErrors, this, &NymeaConnection::onSslErrors); - QObject::connect(m_currentTransport, &NymeaTransportInterface::error, this, &NymeaConnection::onError); - QObject::connect(m_currentTransport, &NymeaTransportInterface::connected, this, &NymeaConnection::onConnected); - QObject::connect(m_currentTransport, &NymeaTransportInterface::disconnected, this, &NymeaConnection::onDisconnected); - QObject::connect(m_currentTransport, &NymeaTransportInterface::dataReady, this, &NymeaConnection::dataAvailable); - - // Load any certificate we might have for this url - QByteArray pem; - if (loadPem(m_currentUrl, pem)) { - qDebug() << "Loaded SSL certificate for" << m_currentUrl.host(); - QList expectedSslErrors; - expectedSslErrors.append(QSslError::HostNameMismatch); - expectedSslErrors.append(QSslError(QSslError::SelfSignedCertificate, QSslCertificate(pem))); - m_currentTransport->ignoreSslErrors(expectedSslErrors); - } - - qDebug() << "Connecting to:" << m_currentUrl; - return m_currentTransport->connect(m_currentUrl); -} - -void NymeaConnection::disconnect() -{ - if (!m_currentTransport || m_currentTransport->connectionState() == NymeaTransportInterface::ConnectionStateDisconnected) { - qWarning() << "not connected, cannot disconnect"; - return; - } - m_currentTransport->disconnect(); + updateActiveBearers(); } void NymeaConnection::acceptCertificate(const QString &url, const QByteArray &pem) @@ -84,30 +52,56 @@ bool NymeaConnection::isTrusted(const QString &url) return false; } +Connection::BearerTypes NymeaConnection::availableBearerTypes() const +{ + return m_availableBearerTypes; +} + bool NymeaConnection::connected() { - return m_currentTransport && m_currentTransport->connectionState() == NymeaTransportInterface::ConnectionStateConnected; + return m_currentHost && m_currentTransport && m_currentTransport->connectionState() == NymeaTransportInterface::ConnectionStateConnected; } -QString NymeaConnection::url() const +NymeaHost *NymeaConnection::currentHost() const { - return m_currentUrl.toString(); + return m_currentHost; } -QString NymeaConnection::hostAddress() const +void NymeaConnection::setCurrentHost(NymeaHost *host) { - return m_currentUrl.host(); + if (m_currentHost == host) { + return; + } + + if (m_currentTransport) { + m_currentTransport = nullptr; + emit currentConnectionChanged(); + emit connectedChanged(false); + } + + while (!m_transportCandidates.isEmpty()) { + NymeaTransportInterface *transport = m_transportCandidates.keys().first(); + m_transportCandidates.remove(transport); + transport->deleteLater(); + } + if (m_currentHost) { + m_currentHost = nullptr; + } + + m_currentHost = host; + emit currentHostChanged(); + + if (m_currentHost) { + connectInternal(m_currentHost); + } } -int NymeaConnection::port() const +Connection *NymeaConnection::currentConnection() const { - return m_currentUrl.port(); -} - -QString NymeaConnection::bluetoothAddress() const -{ - QUrlQuery query(m_currentUrl); - return query.queryItemValue("mac"); + if (!m_currentHost || !m_currentTransport) { + return nullptr; + } + return m_transportCandidates.value(m_currentTransport); } void NymeaConnection::sendData(const QByteArray &data) @@ -122,15 +116,16 @@ void NymeaConnection::sendData(const QByteArray &data) void NymeaConnection::onSslErrors(const QList &errors) { - qDebug() << "Connection: SSL errors:" << errors; + NymeaTransportInterface *transport = qobject_cast(sender()); + + qDebug() << "SSL errors for url:" << transport->url(); QList ignoredErrors; foreach (const QSslError &error, errors) { + qDebug() << error.errorString(); if (error.error() == QSslError::HostNameMismatch) { qDebug() << "Ignoring host mismatch on certificate."; ignoredErrors.append(error); } else if (error.error() == QSslError::SelfSignedCertificate || error.error() == QSslError::CertificateUntrusted) { - qDebug() << "have a self signed certificate." << error.certificate(); - // Check our cert DB QByteArray pem; @@ -140,7 +135,7 @@ void NymeaConnection::onSslErrors(const QList &errors) // However, we want to emit verifyConnectionCertificate in any case here. QSettings settings; settings.beginGroup("acceptedCertificates"); - QByteArray storedFingerPrint = settings.value(m_currentUrl.host()).toByteArray(); + QByteArray storedFingerPrint = settings.value(transport->url().host()).toByteArray(); settings.endGroup(); QByteArray certificateFingerprint; @@ -158,15 +153,18 @@ void NymeaConnection::onSslErrors(const QList &errors) ignoredErrors.append(error); // Update the config to use the new system: - storePem(m_currentUrl, error.certificate().toPem()); + storePem(transport->url(), error.certificate().toPem()); // Check new style PEM storage - } else if (loadPem(m_currentUrl, pem) && pem == error.certificate().toPem()) { + } else if (loadPem(transport->url(), pem) && pem == error.certificate().toPem()) { qDebug() << "Found a SSL certificate for this host. Ignoring error."; ignoredErrors.append(error); // Ok... nothing found... Pop up the message } else { + qDebug() << "Host presents an unknown self signed certificate:" << error.certificate(); + qDebug() << "Asking user for confirmation."; + QStringList info; info << tr("Common Name:") << error.certificate().issuerInfo(QSslCertificate::CommonName); info << tr("Oragnisation:") < &errors) // info << tr("Name Qualifier:")<< error.certificate().issuerInfo(QSslCertificate::DistinguishedNameQualifier); // info << tr("Email:")<< error.certificate().issuerInfo(QSslCertificate::EmailAddress); - emit verifyConnectionCertificate(m_currentUrl.toString(), info, certificateFingerprint, error.certificate().toPem()); + emit verifyConnectionCertificate(transport->url().toString(), info, certificateFingerprint, error.certificate().toPem()); } } else { // Reject the connection on all other errors... @@ -187,38 +185,141 @@ void NymeaConnection::onSslErrors(const QList &errors) if (ignoredErrors == errors) { // Note, due to a workaround in the WebSocketTransport we must not call this // unless we've handled all the errors or the websocket will ignore unhandled errors too... - m_currentTransport->ignoreSslErrors(ignoredErrors); + transport->ignoreSslErrors(ignoredErrors); } } void NymeaConnection::onError(QAbstractSocket::SocketError error) { QMetaEnum errorEnum = QMetaEnum::fromType(); - emit connectionError(errorEnum.valueToKey(error)); + QString errorString = errorEnum.valueToKey(error); + + NymeaTransportInterface* transport = qobject_cast(sender()); + + if (transport == m_currentTransport) { + qDebug() << "Current transport failed:" << error; + // The current transport failed, forward the error + emit connectionError(errorString); + return; + } + + if (!m_currentTransport) { + // We're trying to connect and one of the transports failed... + qDebug() << "A transport error happened for" << transport->url() << error; + if (m_transportCandidates.contains(transport)) { + m_transportCandidates.remove(transport); + transport->deleteLater(); + } + if (m_transportCandidates.isEmpty()) { + emit connectionError(errorString); + } + } } void NymeaConnection::onConnected() { - if (m_currentTransport != sender()) { - qWarning() << "NymeaConnection: An inactive transport is emitting signals... ignoring."; + NymeaTransportInterface* newTransport = qobject_cast(sender()); + if (!m_currentTransport) { + m_currentTransport = newTransport; + qDebug() << "NymeaConnection: Connected to" << m_currentHost->name() << "via" << m_currentTransport->url(); + emit connectedChanged(true); + return; + } + + if (m_currentTransport != newTransport) { + qDebug() << "Alternative connection established:" << newTransport->url(); + Connection *existingConnection = m_transportCandidates.value(m_currentTransport); + Connection *alternativeConnection = m_transportCandidates.value(newTransport); + if (alternativeConnection->priority() > existingConnection->priority()) { + qDebug() << "New connection has higher priority! Roaming from" << existingConnection->url() << existingConnection->priority() << "to" << alternativeConnection->url() << alternativeConnection->priority(); +// m_transportCandidates.remove(m_currentTransport); +// m_currentTransport->deleteLater(); + m_currentTransport = newTransport; + } else { + qDebug() << "Connection" << alternativeConnection->url() << alternativeConnection->priority() << "has lower priority than existing" << existingConnection->url() << existingConnection->priority(); + m_transportCandidates.remove(newTransport); + newTransport->deleteLater(); + } return; } - qDebug() << "NymeaConnection: connected."; - emit connectedChanged(true); } void NymeaConnection::onDisconnected() { - if (m_currentTransport != sender()) { - qWarning() << "NymeaConnection: An inactive transport is emitting signals... ignoring."; + NymeaTransportInterface* t = qobject_cast(sender()); + if (m_currentTransport != t) { + qWarning() << "NymeaConnection: An inactive transport for url" << t->url() << "disconnected... Cleaning up..."; + if (m_transportCandidates.contains(t)) { + m_transportCandidates.remove(t); + } + t->deleteLater(); return; } + m_transportCandidates.remove(m_currentTransport); m_currentTransport->deleteLater(); m_currentTransport = nullptr; + emit currentConnectionChanged(); + qDebug() << "NymeaConnection: disconnected."; emit connectedChanged(false); + + connectInternal(m_currentHost); } +void NymeaConnection::updateActiveBearers() +{ + Connection::BearerTypes availableBearerTypes; + QList configs = m_networkConfigManager->allConfigurations(QNetworkConfiguration::Active); +// qDebug() << "Network configuations:" << configs.count(); + foreach (const QNetworkConfiguration &config, configs) { +// qDebug() << "Candidate network config:" << config.name() << config.bearerTypeFamily() << config.bearerTypeName(); + availableBearerTypes.setFlag(qBearerTypeToNymeaBearerType(config.bearerType())); + } +// qDebug() << "Available bearers:" << availableBearerTypes; + if (m_availableBearerTypes != availableBearerTypes) { + qDebug() << "Available Bearer Types changed:" << availableBearerTypes; + + m_availableBearerTypes = availableBearerTypes; + emit availableBearerTypesChanged(); + } + + if (!m_currentHost) { + // No host set... Nothing to do... + return; + } + if (!m_currentTransport) { + // There's a host but no connection. Try connecting now... + connectInternal(m_currentHost); + } + +} + +Connection::BearerType NymeaConnection::qBearerTypeToNymeaBearerType(QNetworkConfiguration::BearerType type) const +{ + switch (type) { + case QNetworkConfiguration::BearerWLAN: + return Connection::BearerTypeWifi; + case QNetworkConfiguration::BearerEthernet: + return Connection::BearerTypeEthernet; + case QNetworkConfiguration::Bearer2G: + case QNetworkConfiguration::BearerCDMA2000: + case QNetworkConfiguration::BearerWCDMA: + case QNetworkConfiguration::BearerHSPA: + case QNetworkConfiguration::BearerWiMAX: + case QNetworkConfiguration::BearerEVDO: + case QNetworkConfiguration::BearerLTE: + case QNetworkConfiguration::Bearer3G: + case QNetworkConfiguration::Bearer4G: + return Connection::BearerTypeCloud; + case QNetworkConfiguration::BearerBluetooth: + return Connection::BearerTypeBluetooth; + default: + qWarning() << "Unhandled Bearer Type Family:" << type; + } + return Connection::BearerTypeNone; +} + + bool NymeaConnection::storePem(const QUrl &host, const QByteArray &pem) { QDir dir(QStandardPaths::writableLocation(QStandardPaths::DataLocation) + "/sslcerts/"); @@ -249,6 +350,70 @@ bool NymeaConnection::loadPem(const QUrl &host, QByteArray &pem) void NymeaConnection::registerTransport(NymeaTransportInterfaceFactory *transportFactory) { foreach (const QString &scheme, transportFactory->supportedSchemes()) { - m_transports[scheme] = transportFactory; + m_transportFactories[scheme] = transportFactory; } } + +void NymeaConnection::connect(NymeaHost *nymeaHost) +{ + setCurrentHost(nymeaHost); +} + +void NymeaConnection::connectInternal(NymeaHost *host) +{ + if (m_availableBearerTypes.testFlag(Connection::BearerTypeWifi) || m_availableBearerTypes.testFlag(Connection::BearerTypeEthernet)) { + Connection* lanConnection = host->connections()->bestMatch(Connection::BearerTypeWifi | Connection::BearerTypeEthernet); + if (lanConnection) { + qDebug() << "Best candidate LAN connection:" << lanConnection->url(); + connectInternal(lanConnection); + } + } + + if (m_availableBearerTypes.testFlag(Connection::BearerTypeCloud)) { + Connection* wanConnection = host->connections()->bestMatch(Connection::BearerTypeCloud); + if (wanConnection) { + qDebug() << "Best candidate WAN connection:" << wanConnection->url(); + connectInternal(wanConnection); + } + } +} + +bool NymeaConnection::connectInternal(Connection *connection) +{ + if (!m_transportFactories.contains(connection->url().scheme())) { + qWarning() << "Cannot connect to urls of scheme" << connection->url().scheme() << "Supported schemes are" << m_transportFactories.keys(); + return false; + } + + if (m_transportCandidates.values().contains(connection)) { + qDebug() << "Already have a connection (or connection attempt) for" << connection->url(); + return false; + } + + // Create a new transport + NymeaTransportInterface* newTransport = m_transportFactories.value(connection->url().scheme())->createTransport(); + QObject::connect(newTransport, &NymeaTransportInterface::sslErrors, this, &NymeaConnection::onSslErrors); + QObject::connect(newTransport, &NymeaTransportInterface::error, this, &NymeaConnection::onError); + QObject::connect(newTransport, &NymeaTransportInterface::connected, this, &NymeaConnection::onConnected); + QObject::connect(newTransport, &NymeaTransportInterface::disconnected, this, &NymeaConnection::onDisconnected); + QObject::connect(newTransport, &NymeaTransportInterface::dataReady, this, &NymeaConnection::dataAvailable); + + // Load any certificate we might have for this url + QByteArray pem; + if (loadPem(connection->url(), pem)) { + qDebug() << "Loaded SSL certificate for" << connection->url().host(); + QList expectedSslErrors; + expectedSslErrors.append(QSslError::HostNameMismatch); + expectedSslErrors.append(QSslError(QSslError::SelfSignedCertificate, QSslCertificate(pem))); + newTransport->ignoreSslErrors(expectedSslErrors); + } + + m_transportCandidates.insert(newTransport, connection); + qDebug() << "Connecting to:" << connection->url(); + return newTransport->connect(connection->url()); +} + +void NymeaConnection::disconnect() +{ + setCurrentHost(nullptr); +} diff --git a/libnymea-app-core/connection/nymeaconnection.h b/libnymea-app-core/connection/nymeaconnection.h index 6bb0cda3..91771d68 100644 --- a/libnymea-app-core/connection/nymeaconnection.h +++ b/libnymea-app-core/connection/nymeaconnection.h @@ -6,6 +6,10 @@ #include #include #include +#include + + +#include "nymeahost.h" class NymeaTransportInterface; class NymeaTransportInterfaceFactory; @@ -14,34 +18,37 @@ class NymeaConnection : public QObject { Q_OBJECT Q_PROPERTY(bool connected READ connected NOTIFY connectedChanged) - Q_PROPERTY(QString url READ url NOTIFY currentUrlChanged) - Q_PROPERTY(QString hostAddress READ hostAddress NOTIFY currentUrlChanged) - Q_PROPERTY(int port READ port NOTIFY currentUrlChanged) - Q_PROPERTY(QString bluetoothAddress READ bluetoothAddress NOTIFY currentUrlChanged) + Q_PROPERTY(NymeaHost* currentHost READ currentHost WRITE setCurrentHost NOTIFY currentHostChanged) + Q_PROPERTY(Connection* currentConnection READ currentConnection NOTIFY currentConnectionChanged) + Q_PROPERTY(Connection::BearerTypes availableBearerTypes READ availableBearerTypes NOTIFY availableBearerTypesChanged) public: explicit NymeaConnection(QObject *parent = nullptr); void registerTransport(NymeaTransportInterfaceFactory *transportFactory); - Q_INVOKABLE bool connect(const QString &url); + Q_INVOKABLE void connect(NymeaHost* nymeaHost); Q_INVOKABLE void disconnect(); Q_INVOKABLE void acceptCertificate(const QString &url, const QByteArray &pem); Q_INVOKABLE bool isTrusted(const QString &url); + Connection::BearerTypes availableBearerTypes() const; + bool connected(); - QString url() const; - QString hostAddress() const; - int port() const; - QString bluetoothAddress() const; + NymeaHost* currentHost() const; + void setCurrentHost(NymeaHost *host); + + Connection* currentConnection() const; void sendData(const QByteArray &data); signals: - void currentUrlChanged(); + void availableBearerTypesChanged(); void verifyConnectionCertificate(const QString &url, const QStringList &issuerInfo, const QByteArray &fingerprint, const QByteArray &pem); + void currentHostChanged(); void connectedChanged(bool connected); + void currentConnectionChanged(); void connectionError(const QString &error); void dataAvailable(const QByteArray &data); @@ -51,14 +58,24 @@ private slots: void onConnected(); void onDisconnected(); + void updateActiveBearers(); private: bool storePem(const QUrl &host, const QByteArray &pem); bool loadPem(const QUrl &host, QByteArray &pem); + void connectInternal(NymeaHost *host); + bool connectInternal(Connection *connection); + + Connection::BearerType qBearerTypeToNymeaBearerType(QNetworkConfiguration::BearerType type) const; + private: - QHash m_transports; + QNetworkConfigurationManager *m_networkConfigManager = nullptr; + Connection::BearerTypes m_availableBearerTypes = Connection::BearerTypeNone; + + QHash m_transportFactories; + QHash m_transportCandidates; NymeaTransportInterface *m_currentTransport = nullptr; - QUrl m_currentUrl; + NymeaHost *m_currentHost = nullptr; }; #endif // NYMEACONNECTION_H diff --git a/libnymea-app-core/discovery/discoverydevice.cpp b/libnymea-app-core/connection/nymeahost.cpp similarity index 77% rename from libnymea-app-core/discovery/discoverydevice.cpp rename to libnymea-app-core/connection/nymeahost.cpp index d04a265d..1b920e6a 100644 --- a/libnymea-app-core/discovery/discoverydevice.cpp +++ b/libnymea-app-core/connection/nymeahost.cpp @@ -18,32 +18,41 @@ * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ -#include "discoverydevice.h" +#include "nymeahost.h" #include -DiscoveryDevice::DiscoveryDevice(QObject *parent): +NymeaHost::NymeaHost(QObject *parent): QObject(parent), m_connections(new Connections(this)) { + connect(m_connections, &Connections::dataChanged, this, [this](const QModelIndex &, const QModelIndex &, const QVector){ + emit connectionChanged(); + }); + connect(m_connections, &Connections::connectionAdded, this, [this](Connection*){ + emit connectionChanged(); + }); + connect(m_connections, &Connections::connectionRemoved, this, [this](Connection*){ + emit connectionChanged(); + }); } -QUuid DiscoveryDevice::uuid() const +QUuid NymeaHost::uuid() const { return m_uuid; } -void DiscoveryDevice::setUuid(const QUuid &uuid) +void NymeaHost::setUuid(const QUuid &uuid) { m_uuid = uuid; } -QString DiscoveryDevice::name() const +QString NymeaHost::name() const { return m_name; } -void DiscoveryDevice::setName(const QString &name) +void NymeaHost::setName(const QString &name) { if (m_name != name) { m_name = name; @@ -51,12 +60,12 @@ void DiscoveryDevice::setName(const QString &name) } } -QString DiscoveryDevice::version() const +QString NymeaHost::version() const { return m_version; } -void DiscoveryDevice::setVersion(const QString &version) +void NymeaHost::setVersion(const QString &version) { if (m_version != version) { m_version = version; @@ -64,7 +73,7 @@ void DiscoveryDevice::setVersion(const QString &version) } } -Connections* DiscoveryDevice::connections() const +Connections* NymeaHost::connections() const { return m_connections; } @@ -159,40 +168,21 @@ Connection* Connections::get(int index) const return nullptr; } -Connection* Connections::bestMatch() const +Connection *Connections::bestMatch(Connection::BearerTypes bearerTypes) const { - QList bearerPreference = {Connection::BearerTypeEthernet, Connection::BearerTypeWifi, Connection::BearerTypeCloud, Connection::BearerTypeBluetooth, Connection::BearerTypeUnknown}; + QList bearerPreference = {Connection::BearerTypeEthernet, Connection::BearerTypeWifi, Connection::BearerTypeCloud, Connection::BearerTypeBluetooth, Connection::BearerTypeNone}; Connection *best = nullptr; +// qDebug() << "Bestmatch" << m_connections.count(); foreach (Connection *c, m_connections) { +// qDebug() << "have connection:" << bearerTypes << c->url() << bearerTypes.testFlag(c->bearerType()); + if (!bearerTypes.testFlag(c->bearerType())) { + continue; + } if (!best) { best = c; continue; } - uint oldBearerPriority = static_cast(bearerPreference.indexOf(best->bearerType())); - uint newBearerPriority = static_cast(bearerPreference.indexOf(c->bearerType())); - if (newBearerPriority < oldBearerPriority) { - // New one has better bearer, switch - best = c; - continue; - } - if (oldBearerPriority < newBearerPriority) { - // Discard new one as the existing is on a better bearer - continue; - } - - // Same bearer, prefer secure over insecure - if (!best->secure() && c->secure()) { - // New one is secure, old one not. switch - best = c; - continue; - } - if (best->secure() && !c->secure()) { - // Old one is secure, new one isn't, skip new one - continue; - } - - // both options are now on the same bearer and either secure or insecure, prefer nymearpc over websocket for less overhead - if (best->url().scheme().startsWith("ws") && c->url().scheme().startsWith("nymea")) { + if (c->priority() > best->priority()) { best = c; } } @@ -252,3 +242,31 @@ void Connection::setOnline(bool online) emit onlineChanged(); } } + +int Connection::priority() const +{ + int prio = 0; + switch(m_bearerType) { + case BearerTypeEthernet: + prio += 400; + break; + case BearerTypeWifi: + prio += 300; + break; + case BearerTypeBluetooth: + prio += 200; + break; + case BearerTypeCloud: + prio += 100; + break; + default: + prio += 0; + } + if (m_secure) { + prio += 10; + } +// if (m_url.scheme().startsWith("nymea")) { +// prio += 5; +// } + return prio; +} diff --git a/libnymea-app-core/discovery/discoverydevice.h b/libnymea-app-core/connection/nymeahost.h similarity index 84% rename from libnymea-app-core/discovery/discoverydevice.h rename to libnymea-app-core/connection/nymeahost.h index 8394a48a..52659e53 100644 --- a/libnymea-app-core/discovery/discoverydevice.h +++ b/libnymea-app-core/connection/nymeahost.h @@ -18,8 +18,8 @@ * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ -#ifndef DISCOVERYDEVICE_H -#define DISCOVERYDEVICE_H +#ifndef NYMEAHOST_H +#define NYMEAHOST_H #include #include @@ -36,15 +36,19 @@ class Connection: public QObject { Q_PROPERTY(bool secure READ secure CONSTANT) Q_PROPERTY(QString displayName READ displayName CONSTANT) Q_PROPERTY(bool online READ online NOTIFY onlineChanged) + Q_PROPERTY(int priority READ priority NOTIFY priorityChanged) + public: enum BearerType { - BearerTypeUnknown, - BearerTypeWifi, - BearerTypeEthernet, - BearerTypeBluetooth, - BearerTypeCloud + BearerTypeNone = 0x00, + BearerTypeWifi = 0x01, + BearerTypeEthernet = 0x02, + BearerTypeBluetooth = 0x04, + BearerTypeCloud = 0x08, + BearerTypeAll = 0xFF }; Q_ENUM(BearerType) + Q_DECLARE_FLAGS(BearerTypes, BearerType) Connection(const QUrl &url, BearerType bearerType, bool secure, const QString &displayName, QObject *parent = nullptr); @@ -54,13 +58,15 @@ public: QString displayName() const; bool online() const; void setOnline(bool online); + int priority() const; signals: void onlineChanged(); + void priorityChanged(); private: QUrl m_url; - BearerType m_bearerType = BearerTypeUnknown; + BearerType m_bearerType = BearerTypeNone; bool m_secure = false; QString m_displayName; bool m_online = false; @@ -89,23 +95,22 @@ public: Q_INVOKABLE Connection* find(const QUrl &url) const; Q_INVOKABLE Connection* get(int index) const; - - Connection *bestMatch() const; + Q_INVOKABLE Connection* bestMatch(Connection::BearerTypes bearerTypes = Connection::BearerTypeAll) const; signals: + void countChanged(); void connectionAdded(Connection *connection); void connectionRemoved(Connection *connection); - void countChanged(); protected: QHash roleNames() const override; private: QList m_connections; - }; +Q_DECLARE_OPERATORS_FOR_FLAGS(Connection::BearerTypes) -class DiscoveryDevice: public QObject +class NymeaHost: public QObject { Q_OBJECT Q_PROPERTY(QUuid uuid READ uuid CONSTANT) @@ -114,7 +119,7 @@ class DiscoveryDevice: public QObject Q_PROPERTY(Connections* connections READ connections CONSTANT) public: - explicit DiscoveryDevice(QObject *parent = nullptr); + explicit NymeaHost(QObject *parent = nullptr); QUuid uuid() const; void setUuid(const QUuid &uuid); @@ -130,6 +135,7 @@ public: signals: void nameChanged(); void versionChanged(); + void connectionChanged(); private: QUuid m_uuid; @@ -138,4 +144,4 @@ private: Connections *m_connections = nullptr; }; -#endif // DISCOVERYDEVICE_H +#endif // NYMEAHOST_H diff --git a/libnymea-app-core/connection/nymeahosts.cpp b/libnymea-app-core/connection/nymeahosts.cpp new file mode 100644 index 00000000..2fe4d4d0 --- /dev/null +++ b/libnymea-app-core/connection/nymeahosts.cpp @@ -0,0 +1,204 @@ +/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * + * * + * Copyright (C) 2015 Simon Stuerz * + * * + * This file is part of nymea:app. * + * * + * nymea:app is free software: you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * + * the Free Software Foundation, version 3 of the License. * + * * + * nymea:app is distributed in the hope that it will be useful, * + * but WITHOUT ANY WARRANTY; without even the implied warranty of * + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * + * GNU General Public License for more details. * + * * + * You should have received a copy of the GNU General Public License * + * along with nymea:app. If not, see . * + * * + * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ + +#include "nymeahosts.h" +#include "connection/discovery/nymeadiscovery.h" +#include "nymeahost.h" +#include "connection/nymeaconnection.h" + +NymeaHosts::NymeaHosts(QObject *parent) : + QAbstractListModel(parent) +{ +} + +int NymeaHosts::rowCount(const QModelIndex &parent) const +{ + Q_UNUSED(parent) + return m_hosts.count(); +} + +QVariant NymeaHosts::data(const QModelIndex &index, int role) const +{ + if (index.row() < 0 || index.row() >= m_hosts.count()) + return QVariant(); + + NymeaHost *host = m_hosts.at(index.row()); + switch (role) { + case UuidRole: + return host->uuid(); + case NameRole: + return host->name(); + case VersionRole: + return host->version(); + } + return QVariant(); +} + +void NymeaHosts::addHost(NymeaHost *host) +{ + for (int i = 0; i < m_hosts.count(); i++) { + if (m_hosts.at(i)->uuid() == host->uuid()) { + qWarning() << "Host already added. Update existing host instead."; + return; + } + } + host->setParent(this); + connect(host, &NymeaHost::connectionChanged, this, &NymeaHosts::hostChanged); + + beginInsertRows(QModelIndex(), m_hosts.count(), m_hosts.count()); + m_hosts.append(host); + endInsertRows(); + emit hostAdded(host); + emit countChanged(); +} + +void NymeaHosts::removeHost(NymeaHost *host) +{ + int idx = m_hosts.indexOf(host); + if (idx == -1) { + qWarning() << "Cannot remove NymeaHost" << host << "as its nit in the model"; + return; + } + beginRemoveRows(QModelIndex(), idx, idx); + m_hosts.takeAt(idx); + endRemoveRows(); + emit hostRemoved(host); + emit countChanged(); +} + +NymeaHost *NymeaHosts::get(int index) const +{ + if (index < 0 || index >= m_hosts.count()) { + return nullptr; + } + return m_hosts.at(index); +} + +NymeaHost *NymeaHosts::find(const QUuid &uuid) +{ + foreach (NymeaHost *dev, m_hosts) { + if (dev->uuid() == uuid) { + return dev; + } + } + return nullptr; +} + +void NymeaHosts::clearModel() +{ + beginResetModel(); + m_hosts.clear(); + endResetModel(); + emit countChanged(); +} + +QHash NymeaHosts::roleNames() const +{ + QHash roles; + roles[UuidRole] = "uuid"; + roles[NameRole] = "name"; + roles[VersionRole] = "version"; + return roles; +} + +NymeaHostsFilterModel::NymeaHostsFilterModel(QObject *parent): + QSortFilterProxyModel(parent) +{ + +} + +NymeaDiscovery *NymeaHostsFilterModel::discovery() const +{ + return m_nymeaDiscovery; +} + +void NymeaHostsFilterModel::setDiscovery(NymeaDiscovery *discovery) +{ + if (m_nymeaDiscovery != discovery) { + m_nymeaDiscovery = discovery; + setSourceModel(discovery->nymeaHosts()); + emit discoveryChanged(); + + connect(discovery->nymeaHosts(), &NymeaHosts::hostChanged, this, [this](){ +// qDebug() << "Host Changed!"; + invalidateFilter(); + emit countChanged(); + }); + + emit countChanged(); + } +} + +NymeaConnection *NymeaHostsFilterModel::nymeaConnection() const +{ + return m_nymeaConnection; +} + +void NymeaHostsFilterModel::setNymeaConnection(NymeaConnection *nymeaConnection) +{ + if (m_nymeaConnection != nymeaConnection) { + m_nymeaConnection = nymeaConnection; + emit nymeaConnectionChanged(); + + connect(m_nymeaConnection, &NymeaConnection::availableBearerTypesChanged, this, [this](){ + qDebug() << "Bearer Types Changed!"; + invalidateFilter(); + emit countChanged(); + }); + + invalidateFilter(); + emit countChanged(); + } +} + +bool NymeaHostsFilterModel::showUnreachableBearers() const +{ + return m_showUneachableBearers; +} + +void NymeaHostsFilterModel::setShowUnreachableBearers(bool showUnreachableBearers) +{ + if (m_showUneachableBearers != showUnreachableBearers) { + m_showUneachableBearers = showUnreachableBearers; + emit showUnreachableBearersChanged(); + invalidateFilter(); + emit countChanged(); + } +} + +bool NymeaHostsFilterModel::filterAcceptsRow(int sourceRow, const QModelIndex &sourceParent) const +{ + Q_UNUSED(sourceParent) + NymeaHost *host = m_nymeaDiscovery->nymeaHosts()->get(sourceRow); + if (m_nymeaConnection && !m_showUneachableBearers) { + bool hasReachableConnection = false; + for (int i = 0; i < host->connections()->rowCount(); i++) { +// qDebug() << "checking host for available bearer" << host->name() << host->connections()->get(i)->url(); + if (m_nymeaConnection->availableBearerTypes().testFlag(host->connections()->get(i)->bearerType())) { + hasReachableConnection = true; + break; + } + } + if (!hasReachableConnection) { + return false; + } + } + return true; +} diff --git a/libnymea-app-core/discovery/discoverymodel.h b/libnymea-app-core/connection/nymeahosts.h similarity index 51% rename from libnymea-app-core/discovery/discoverymodel.h rename to libnymea-app-core/connection/nymeahosts.h index bb677bd1..664f64d9 100644 --- a/libnymea-app-core/discovery/discoverymodel.h +++ b/libnymea-app-core/connection/nymeahosts.h @@ -18,51 +18,91 @@ * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ -#ifndef DISCOVERYMODEL_H -#define DISCOVERYMODEL_H +#ifndef NYMEAHOSTS_H +#define NYMEAHOSTS_H #include #include #include +#include -class DiscoveryDevice; +class NymeaHost; +class NymeaDiscovery; +class NymeaConnection; -class DiscoveryModel : public QAbstractListModel +class NymeaHosts : public QAbstractListModel { Q_OBJECT Q_PROPERTY(int count READ rowCount NOTIFY countChanged) public: - enum DeviceRole { - DeviceTypeRole, + enum HostRole { UuidRole, NameRole, VersionRole }; - Q_ENUM(DeviceRole) + Q_ENUM(HostRole) - explicit DiscoveryModel(QObject *parent = nullptr); + explicit NymeaHosts(QObject *parent = nullptr); int rowCount(const QModelIndex & parent = QModelIndex()) const; QVariant data(const QModelIndex & index, int role = Qt::DisplayRole) const; - void addDevice(DiscoveryDevice *device); - void removeDevice(DiscoveryDevice *device); + void addHost(NymeaHost *host); + void removeHost(NymeaHost *host); - Q_INVOKABLE DiscoveryDevice *get(int index) const; - Q_INVOKABLE DiscoveryDevice *find(const QUuid &uuid); + Q_INVOKABLE NymeaHost *get(int index) const; + Q_INVOKABLE NymeaHost *find(const QUuid &uuid); void clearModel(); signals: - void deviceAdded(DiscoveryDevice* device); - void deviceRemoved(DiscoveryDevice* device); + void hostAdded(NymeaHost* host); + void hostRemoved(NymeaHost* host); void countChanged(); + void hostChanged(); protected: QHash roleNames() const; private: - QList m_devices; + QList m_hosts; }; -#endif // DISCOVERYMODEL_H +class NymeaHostsFilterModel: public QSortFilterProxyModel +{ + Q_OBJECT + Q_PROPERTY(int count READ rowCount NOTIFY countChanged) + Q_PROPERTY(NymeaDiscovery* discovery READ discovery WRITE setDiscovery NOTIFY discoveryChanged) + Q_PROPERTY(NymeaConnection* nymeaConnection READ nymeaConnection WRITE setNymeaConnection NOTIFY nymeaConnectionChanged) + Q_PROPERTY(bool showUnreachableBearers READ showUnreachableBearers WRITE setShowUnreachableBearers NOTIFY showUnreachableBearersChanged) + +public: + NymeaHostsFilterModel(QObject *parent = nullptr); + + NymeaDiscovery* discovery() const; + void setDiscovery(NymeaDiscovery *discovery); + + NymeaConnection* nymeaConnection() const; + void setNymeaConnection(NymeaConnection* nymeaConnection); + + bool showUnreachableBearers() const; + void setShowUnreachableBearers(bool showUnreachableBearers); + +signals: + void countChanged(); + void discoveryChanged(); + void nymeaConnectionChanged(); + void showUnreachableBearersChanged(); + +protected: + bool filterAcceptsRow(int sourceRow, const QModelIndex &sourceParent) const override; + +private: + NymeaDiscovery *m_nymeaDiscovery = nullptr; + NymeaConnection *m_nymeaConnection = nullptr; + + bool m_showUneachableBearers = false; + +}; + +#endif // NYMEAHOSTS_H diff --git a/libnymea-app-core/connection/nymeatransportinterface.h b/libnymea-app-core/connection/nymeatransportinterface.h index 5ccb6aec..ca9b9e77 100644 --- a/libnymea-app-core/connection/nymeatransportinterface.h +++ b/libnymea-app-core/connection/nymeatransportinterface.h @@ -53,6 +53,7 @@ public: virtual ~NymeaTransportInterface() = default; virtual bool connect(const QUrl &url) = 0; + virtual QUrl url() const = 0; virtual void disconnect() = 0; virtual ConnectionState connectionState() const = 0; virtual void sendData(const QByteArray &data) = 0; diff --git a/libnymea-app-core/connection/tcpsockettransport.cpp b/libnymea-app-core/connection/tcpsockettransport.cpp index 87d94151..3f2c17ed 100644 --- a/libnymea-app-core/connection/tcpsockettransport.cpp +++ b/libnymea-app-core/connection/tcpsockettransport.cpp @@ -5,7 +5,6 @@ TcpSocketTransport::TcpSocketTransport(QObject *parent) : NymeaTransportInterface(parent) { QObject::connect(&m_socket, &QSslSocket::connected, this, &TcpSocketTransport::onConnected); - QObject::connect(&m_socket, &QSslSocket::disconnected, this, &TcpSocketTransport::disconnected); QObject::connect(&m_socket, &QSslSocket::encrypted, this, &TcpSocketTransport::onEncrypted); typedef void (QSslSocket:: *sslErrorsSignal)(const QList &); QObject::connect(&m_socket, static_cast(&QSslSocket::sslErrors), this, &TcpSocketTransport::sslErrors); @@ -58,6 +57,11 @@ bool TcpSocketTransport::connect(const QUrl &url) return false; } +QUrl TcpSocketTransport::url() const +{ + return m_url; +} + NymeaTransportInterface::ConnectionState TcpSocketTransport::connectionState() const { switch (m_socket.state()) { @@ -91,6 +95,9 @@ void TcpSocketTransport::socketReadyRead() void TcpSocketTransport::onSocketStateChanged(const QAbstractSocket::SocketState &state) { qDebug() << "Socket state changed -->" << state; + if (state == QAbstractSocket::UnconnectedState) { + emit disconnected(); + } } NymeaTransportInterface *TcpSocketTransportFactory::createTransport(QObject *parent) const diff --git a/libnymea-app-core/connection/tcpsockettransport.h b/libnymea-app-core/connection/tcpsockettransport.h index 2d2cb822..185071c6 100644 --- a/libnymea-app-core/connection/tcpsockettransport.h +++ b/libnymea-app-core/connection/tcpsockettransport.h @@ -21,6 +21,7 @@ public: explicit TcpSocketTransport(QObject *parent = nullptr); bool connect(const QUrl &url) override; + QUrl url() const override; ConnectionState connectionState() const override; void disconnect() override; void sendData(const QByteArray &data) override; diff --git a/libnymea-app-core/connection/websockettransport.cpp b/libnymea-app-core/connection/websockettransport.cpp index ff2c56f7..34d6f215 100644 --- a/libnymea-app-core/connection/websockettransport.cpp +++ b/libnymea-app-core/connection/websockettransport.cpp @@ -42,10 +42,16 @@ WebsocketTransport::WebsocketTransport(QObject *parent) : bool WebsocketTransport::connect(const QUrl &url) { + m_url = url; m_socket->open(QUrl(url)); return true; } +QUrl WebsocketTransport::url() const +{ + return m_url; +} + NymeaTransportInterface::ConnectionState WebsocketTransport::connectionState() const { switch (m_socket->state()) { diff --git a/libnymea-app-core/connection/websockettransport.h b/libnymea-app-core/connection/websockettransport.h index be2d4223..7256a002 100644 --- a/libnymea-app-core/connection/websockettransport.h +++ b/libnymea-app-core/connection/websockettransport.h @@ -40,12 +40,14 @@ public: explicit WebsocketTransport(QObject *parent = nullptr); bool connect(const QUrl &url) override; + QUrl url() const override; ConnectionState connectionState() const override; void disconnect() override; void sendData(const QByteArray &data) override; void ignoreSslErrors(const QList &errors) override; private: + QUrl m_url; QWebSocket *m_socket; private slots: diff --git a/libnymea-app-core/discovery/discoverymodel.cpp b/libnymea-app-core/discovery/discoverymodel.cpp deleted file mode 100644 index 25c7825d..00000000 --- a/libnymea-app-core/discovery/discoverymodel.cpp +++ /dev/null @@ -1,115 +0,0 @@ -/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * - * * - * Copyright (C) 2015 Simon Stuerz * - * * - * This file is part of nymea:app. * - * * - * nymea:app is free software: you can redistribute it and/or modify * - * it under the terms of the GNU General Public License as published by * - * the Free Software Foundation, version 3 of the License. * - * * - * nymea:app is distributed in the hope that it will be useful, * - * but WITHOUT ANY WARRANTY; without even the implied warranty of * - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * - * GNU General Public License for more details. * - * * - * You should have received a copy of the GNU General Public License * - * along with nymea:app. If not, see . * - * * - * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ - -#include "discoverymodel.h" -#include "discoverydevice.h" - -DiscoveryModel::DiscoveryModel(QObject *parent) : - QAbstractListModel(parent) -{ -} - -int DiscoveryModel::rowCount(const QModelIndex &parent) const -{ - Q_UNUSED(parent) - return m_devices.count(); -} - -QVariant DiscoveryModel::data(const QModelIndex &index, int role) const -{ - if (index.row() < 0 || index.row() >= m_devices.count()) - return QVariant(); - - DiscoveryDevice *device = m_devices.at(index.row()); - switch (role) { - case UuidRole: - return device->uuid(); - case NameRole: - return device->name(); - case VersionRole: - return device->version(); - } - return QVariant(); -} - -void DiscoveryModel::addDevice(DiscoveryDevice *device) -{ - for (int i = 0; i < m_devices.count(); i++) { - if (m_devices.at(i)->uuid() == device->uuid()) { - qWarning() << "Device already added. Update existing device instead."; - return; - } - } - device->setParent(this); - beginInsertRows(QModelIndex(), m_devices.count(), m_devices.count()); - m_devices.append(device); - endInsertRows(); - emit deviceAdded(device); - emit countChanged(); -} - -void DiscoveryModel::removeDevice(DiscoveryDevice *device) -{ - int idx = m_devices.indexOf(device); - if (idx == -1) { - qWarning() << "Cannot remove DiscoveryDevice" << device << "as its nit in the model"; - return; - } - beginRemoveRows(QModelIndex(), idx, idx); - m_devices.takeAt(idx); - endRemoveRows(); - emit deviceRemoved(device); - emit countChanged(); -} - -DiscoveryDevice *DiscoveryModel::get(int index) const -{ - if (index < 0 || index >= m_devices.count()) { - return nullptr; - } - return m_devices.at(index); -} - -DiscoveryDevice *DiscoveryModel::find(const QUuid &uuid) -{ - foreach (DiscoveryDevice *dev, m_devices) { - if (dev->uuid() == uuid) { - return dev; - } - } - return nullptr; -} - -void DiscoveryModel::clearModel() -{ - beginResetModel(); - m_devices.clear(); - endResetModel(); - emit countChanged(); -} - -QHash DiscoveryModel::roleNames() const -{ - QHash roles; - roles[UuidRole] = "uuid"; - roles[NameRole] = "name"; - roles[VersionRole] = "version"; - return roles; -} diff --git a/libnymea-app-core/discovery/nymeadiscovery.cpp b/libnymea-app-core/discovery/nymeadiscovery.cpp deleted file mode 100644 index c395cc22..00000000 --- a/libnymea-app-core/discovery/nymeadiscovery.cpp +++ /dev/null @@ -1,204 +0,0 @@ -#include "nymeadiscovery.h" -#include "upnpdiscovery.h" -#include "zeroconfdiscovery.h" -#include "bluetoothservicediscovery.h" -#include "connection/awsclient.h" - -#include -#include -#include -#include -#include - -NymeaDiscovery::NymeaDiscovery(QObject *parent) : QObject(parent) -{ - m_discoveryModel = new DiscoveryModel(this); - connect(m_discoveryModel, &DiscoveryModel::deviceAdded, this, [this](DiscoveryDevice *device) { - if (!m_pendingHostResolutions.contains(device->uuid())) { - return; - } - Connection *c = device->connections()->bestMatch(); - if (!c) { - qDebug() << "Host found but there isn't a valid candidate yet?"; - connect(device->connections(), &Connections::connectionAdded, this, [this, device](Connection *connection) { - if (m_pendingHostResolutions.contains(device->uuid())) { - qDebug() << "Host" << device->uuid() << "resolved to" << connection->url().toString(); - m_pendingHostResolutions.removeAll(device->uuid()); - emit serverUuidResolved(device->uuid(), connection->url().toString()); - } - }); - return; - } - qDebug() << "Host" << device->uuid() << "appeared! Best match is" << c->url(); - m_pendingHostResolutions.removeAll(device->uuid()); - emit serverUuidResolved(device->uuid(), c->url().toString()); - }); - - m_upnp = new UpnpDiscovery(m_discoveryModel, this); - m_zeroConf = new ZeroconfDiscovery(m_discoveryModel, this); - -#ifndef Q_OS_IOS - m_bluetooth = new BluetoothServiceDiscovery(m_discoveryModel, this); -#endif - - m_cloudPollTimer.setInterval(5000); - connect(&m_cloudPollTimer, &QTimer::timeout, this, [this](){ - if (m_awsClient && m_awsClient->isLoggedIn()) { - m_awsClient->fetchDevices(); - } - }); - - - QNetworkConfigurationManager manager; - QList configs = manager.allConfigurations(QNetworkConfiguration::Active); - - foreach (const QNetworkConfiguration &config, configs) { - if (config.purpose() != QNetworkConfiguration::PublicPurpose) { - continue; - } - if (config.bearerType() != QNetworkConfiguration::BearerWLAN && config.bearerType() != QNetworkConfiguration::BearerEthernet) { - continue; - } - qDebug() << "Have Network configuration:" << config.name() << config.bearerTypeName() << config.purpose() << config.type(); - } -} - -bool NymeaDiscovery::discovering() const -{ - return m_discovering; -} - -void NymeaDiscovery::setDiscovering(bool discovering) -{ - if (m_discovering == discovering) - return; - - m_discovering = discovering; - // If we have zeroconf skip upnp. ZeroConf will not do an active discovery and if it's available it'll always have good data - if (!m_zeroConf->available()) { - if (discovering) { - m_upnp->discover(); - } else { - m_upnp->stopDiscovery(); - } - } - if (discovering) { - // If there's no Zeroconf, use UPnP instead - if (!m_zeroConf->available()) { - m_upnp->discover(); - } - - // Always start Bluetooth discovery if HW is available - if (m_bluetooth) { - m_bluetooth->discover(); - } - - // start polling cloud - m_cloudPollTimer.start(); - // If we're logged in, poll right away - if (m_awsClient && m_awsClient->isLoggedIn()) { - syncCloudDevices(); - m_awsClient->fetchDevices(); - } - } else { - if (!m_zeroConf->available()) { - m_upnp->stopDiscovery(); - } - - if (m_bluetooth) { - m_bluetooth->stopDiscovery(); - } - - m_cloudPollTimer.stop(); - } - - emit discoveringChanged(); -} - -DiscoveryModel *NymeaDiscovery::discoveryModel() const -{ - return m_discoveryModel; -} - -AWSClient *NymeaDiscovery::awsClient() const -{ - return m_awsClient; -} - -void NymeaDiscovery::setAwsClient(AWSClient *awsClient) -{ - if (m_awsClient != awsClient) { - m_awsClient = awsClient; - emit awsClientChanged(); - } - - if (m_awsClient) { - m_awsClient->fetchDevices(); - connect(m_awsClient, &AWSClient::devicesFetched, this, &NymeaDiscovery::syncCloudDevices); - syncCloudDevices(); - } -} - -void NymeaDiscovery::resolveServerUuid(const QUuid &uuid) -{ - // Do we already know this host? - DiscoveryDevice *dev = m_discoveryModel->find(uuid); - if (!dev) { - qDebug() << "Host" << uuid << "not known yet..."; - m_pendingHostResolutions.append(uuid); - return; - } - Connection *c = dev->connections()->bestMatch(); - if (!c) { - qDebug() << "Host" << uuid << "is known but doesn't have a usable connection option yet."; - m_pendingHostResolutions.append(uuid); - return; - } - qDebug() << "Host" << uuid << "is known. Best match is" << c->url(); - emit serverUuidResolved(uuid, c->url().toString()); -} - -void NymeaDiscovery::syncCloudDevices() -{ - for (int i = 0; i < m_awsClient->awsDevices()->rowCount(); i++) { - AWSDevice *d = m_awsClient->awsDevices()->get(i); - DiscoveryDevice *device = m_discoveryModel->find(d->id()); - if (!device) { - device = new DiscoveryDevice(); - device->setUuid(d->id()); - device->setName(d->name()); - qDebug() << "CloudDiscovery: Adding new host:" << device->name() << device->uuid().toString(); - m_discoveryModel->addDevice(device); - } - QUrl url; - url.setScheme("cloud"); - url.setHost(d->id()); - Connection *conn = device->connections()->find(url); - if (!conn) { - conn = new Connection(url, Connection::BearerTypeCloud, true, d->id()); - qDebug() << "CloudDiscovery: Adding new connection to host:" << device->name() << conn->url().toString(); - device->connections()->addConnection(conn); - } - conn->setOnline(d->online()); - } - - QList devicesToRemove; - for (int i = 0; i < m_discoveryModel->rowCount(); i++) { - DiscoveryDevice *device = m_discoveryModel->get(i); - for (int j = 0; j < device->connections()->rowCount(); j++) { - if (device->connections()->get(j)->bearerType() == Connection::BearerTypeCloud) { - if (m_awsClient->awsDevices()->getDevice(device->uuid().toString()) == nullptr) { - device->connections()->removeConnection(j); - break; - } - } - } - if (device->connections()->rowCount() == 0) { - devicesToRemove.append(device); - } - } - while (!devicesToRemove.isEmpty()) { - m_discoveryModel->removeDevice(devicesToRemove.takeFirst()); - } -} - diff --git a/libnymea-app-core/discovery/nymeahost.cpp b/libnymea-app-core/discovery/nymeahost.cpp deleted file mode 100644 index f07614f0..00000000 --- a/libnymea-app-core/discovery/nymeahost.cpp +++ /dev/null @@ -1,68 +0,0 @@ -/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * - * * - * Copyright (C) 2015 Simon Stuerz * - * * - * This file is part of nymea:app. * - * * - * nymea:app is free software: you can redistribute it and/or modify * - * it under the terms of the GNU General Public License as published by * - * the Free Software Foundation, version 3 of the License. * - * * - * nymea:app is distributed in the hope that it will be useful, * - * but WITHOUT ANY WARRANTY; without even the implied warranty of * - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * - * GNU General Public License for more details. * - * * - * You should have received a copy of the GNU General Public License * - * along with nymea:app. If not, see . * - * * - * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ - -#include "nymeahost.h" - -NymeaHost::NymeaHost(QObject *parent) : - QObject(parent) -{ -} - -QString NymeaHost::name() const -{ - return m_name; -} - -void NymeaHost::setName(const QString &name) -{ - m_name = name; -} - -QString NymeaHost::webSocketUrl() const -{ - return m_webSocketUrl; -} - -void NymeaHost::setWebSocketUrl(const QString &webSocketUrl) -{ - m_webSocketUrl = webSocketUrl; -} - -QString NymeaHost::hostAddress() const -{ - return m_hostAddress; -} - -void NymeaHost::setHostAddress(const QString &hostAddress) -{ - m_hostAddress = hostAddress; -} - -QUuid NymeaHost::uuid() const -{ - return m_uuid; -} - -void NymeaHost::setUuid(const QUuid &uuid) -{ - m_uuid = uuid; -} - - diff --git a/libnymea-app-core/discovery/nymeahost.h b/libnymea-app-core/discovery/nymeahost.h deleted file mode 100644 index 8c6efef9..00000000 --- a/libnymea-app-core/discovery/nymeahost.h +++ /dev/null @@ -1,54 +0,0 @@ -/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * - * * - * Copyright (C) 2015 Simon Stuerz * - * * - * This file is part of nymea:app. * - * * - * nymea:app is free software: you can redistribute it and/or modify * - * it under the terms of the GNU General Public License as published by * - * the Free Software Foundation, version 3 of the License. * - * * - * nymea:app is distributed in the hope that it will be useful, * - * but WITHOUT ANY WARRANTY; without even the implied warranty of * - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * - * GNU General Public License for more details. * - * * - * You should have received a copy of the GNU General Public License * - * along with nymea:app. If not, see . * - * * - * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ - -#ifndef NYMEAHOST_H -#define NYMEAHOST_H - -#include -#include -#include - -class NymeaHost : public QObject -{ - Q_OBJECT -public: - explicit NymeaHost(QObject *parent = 0); - - QString name() const; - void setName(const QString &name); - - QString webSocketUrl() const; - void setWebSocketUrl(const QString &webSocketUrl); - - QString hostAddress() const; - void setHostAddress(const QString &hostAddress); - - QUuid uuid() const; - void setUuid(const QUuid &uuid); - -private: - QString m_name; - QString m_webSocketUrl; - QString m_hostAddress; - QUuid m_uuid; - -}; - -#endif // NYMEAHOST_H diff --git a/libnymea-app-core/discovery/nymeahosts.cpp b/libnymea-app-core/discovery/nymeahosts.cpp deleted file mode 100644 index 13dccf20..00000000 --- a/libnymea-app-core/discovery/nymeahosts.cpp +++ /dev/null @@ -1,150 +0,0 @@ -/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * - * * - * Copyright (C) 2015 Simon Stuerz * - * * - * This file is part of nymea:app. * - * * - * nymea:app is free software: you can redistribute it and/or modify * - * it under the terms of the GNU General Public License as published by * - * the Free Software Foundation, version 3 of the License. * - * * - * nymea:app is distributed in the hope that it will be useful, * - * but WITHOUT ANY WARRANTY; without even the implied warranty of * - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * - * GNU General Public License for more details. * - * * - * You should have received a copy of the GNU General Public License * - * along with nymea:app. If not, see . * - * * - * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ - -#include "nymeahosts.h" -#include "nymeahost.h" - -#include -#include -#include - -NymeaHosts::NymeaHosts(QObject *parent) : - QAbstractListModel(parent) -{ - beginResetModel(); - QSettings settings; - qDebug() << "Connections: loading connections " << settings.fileName(); - settings.beginGroup("Connections"); - foreach (const QString &uuid, settings.childGroups()) { - settings.beginGroup(uuid); - NymeaHost *host = new NymeaHost(this); - host->setName(settings.value("name").toString()); - host->setHostAddress(settings.value("hostAddress").toString()); - host->setWebSocketUrl(settings.value("webSocketUrl").toString()); - host->setUuid(QUuid(uuid)); - qDebug() << " " << host->webSocketUrl(); - m_hosts.append(host); - settings.endGroup(); - } - - settings.endGroup(); - endResetModel(); -} - -NymeaHost *NymeaHosts::get(const QString &webSocketUrl) -{ - foreach (NymeaHost *host, m_hosts) { - if (host->webSocketUrl() == webSocketUrl) { - return host; - } - } - return nullptr; -} - -QList NymeaHosts::hosts() -{ - return m_hosts; -} - -int NymeaHosts::rowCount(const QModelIndex &parent) const -{ - Q_UNUSED(parent) - return m_hosts.count(); -} - -QVariant NymeaHosts::data(const QModelIndex &index, int role) const -{ - if (index.row() < 0 || index.row() >= m_hosts.count()) - return QVariant(); - - NymeaHost *host = m_hosts.at(index.row()); - if (role == NameRole) { - return host->name(); - } else if (role == HostAddressRole) { - return host->hostAddress(); - } else if (role == WebSocketUrlRole) { - return host->webSocketUrl(); - } - return QVariant(); -} - -void NymeaHosts::addHost(const QString &name, const QString &hostAddress, const QString &webSocketUrl) -{ - // check if we allready have added this connection - foreach (NymeaHost *host, m_hosts) { - if (host->webSocketUrl() == webSocketUrl) { - return; - } - } - - NymeaHost *host = new NymeaHost(this); - host->setName(name); - host->setHostAddress(hostAddress); - host->setWebSocketUrl(webSocketUrl); - host->setUuid(QUuid::createUuid()); - - qDebug() << "NymeaHosts: add connection" << host->webSocketUrl(); - beginInsertRows(QModelIndex(), m_hosts.count(), m_hosts.count()); - m_hosts.append(host); - endInsertRows(); - - QSettings settings; - settings.beginGroup("Connections"); - settings.beginGroup(host->uuid().toString()); - settings.setValue("name", name); - settings.setValue("hostAddress", hostAddress); - settings.setValue("webSocketUrl", webSocketUrl); - settings.endGroup(); - settings.endGroup(); - qDebug() << "Connections: saved connection" << settings.fileName(); -} - -void NymeaHosts::removeHost(NymeaHost *host) -{ - int index = m_hosts.indexOf(host); - beginRemoveRows(QModelIndex(), index, index); - qDebug() << "Connections: removed connection" << host->webSocketUrl(); - m_hosts.removeAt(index); - - QSettings settings; - settings.beginGroup("Connections"); - settings.remove(host->uuid().toString()); - settings.endGroup(); - host->deleteLater(); - endRemoveRows(); -} - -void NymeaHosts::clearModel() -{ - beginResetModel(); - qDebug() << "NymeaHosts: delete all hosts"; - qDeleteAll(m_hosts); - m_hosts.clear(); - endResetModel(); -} - -QHash NymeaHosts::roleNames() const -{ - QHash roles; - roles[NameRole] = "name"; - roles[HostAddressRole] = "hostAddress"; - roles[WebSocketUrlRole] = "webSocketUrl"; - return roles; -} diff --git a/libnymea-app-core/discovery/nymeahosts.h b/libnymea-app-core/discovery/nymeahosts.h deleted file mode 100644 index d705b417..00000000 --- a/libnymea-app-core/discovery/nymeahosts.h +++ /dev/null @@ -1,59 +0,0 @@ -/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * - * * - * Copyright (C) 2015 Simon Stuerz * - * * - * This file is part of nymea:app. * - * * - * nymea:app is free software: you can redistribute it and/or modify * - * it under the terms of the GNU General Public License as published by * - * the Free Software Foundation, version 3 of the License. * - * * - * nymea:app is distributed in the hope that it will be useful, * - * but WITHOUT ANY WARRANTY; without even the implied warranty of * - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * - * GNU General Public License for more details. * - * * - * You should have received a copy of the GNU General Public License * - * along with nymea:app. If not, see . * - * * - * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ - -#ifndef NYMEAHOSTS_H -#define NYMEAHOSTS_H - -#include - -class NymeaHost; - -class NymeaHosts : public QAbstractListModel -{ - Q_OBJECT -public: - enum ConnectionRole { - NameRole = Qt::DisplayRole, - HostAddressRole, - WebSocketUrlRole - }; - - explicit NymeaHosts(QObject *parent = 0); - - Q_INVOKABLE NymeaHost *get(const QString &webSocketUrl); - QList hosts(); - - int rowCount(const QModelIndex & parent = QModelIndex()) const; - QVariant data(const QModelIndex & index, int role = Qt::DisplayRole) const; - - void addHost(const QString &name, const QString &hostAddress, const QString &webSocketUrl); - Q_INVOKABLE void removeHost(NymeaHost *host); - - void clearModel(); - -protected: - QHash roleNames() const; - -private: - QList m_hosts; - -}; - -#endif // NYMEAHOSTS_H diff --git a/libnymea-app-core/libnymea-app-core.h b/libnymea-app-core/libnymea-app-core.h index d4402af8..b21ffe36 100644 --- a/libnymea-app-core/libnymea-app-core.h +++ b/libnymea-app-core/libnymea-app-core.h @@ -2,14 +2,14 @@ #define LIBNYMEAAPPCORE_H #include "engine.h" +#include "connection/nymeahosts.h" +#include "connection/nymeahost.h" +#include "connection/discovery/nymeadiscovery.h" #include "vendorsproxy.h" #include "deviceclassesproxy.h" #include "devicesproxy.h" #include "pluginsproxy.h" #include "devicediscovery.h" -#include "discovery/nymeadiscovery.h" -#include "discovery/discoverymodel.h" -#include "discovery/discoverydevice.h" #include "interfacesmodel.h" #include "rulemanager.h" #include "models/rulesfiltermodel.h" @@ -159,9 +159,10 @@ void registerQmlTypes() { qmlRegisterUncreatableType(uri, 1, 0, "MqttPolicies", "Get it from NymeaConfiguration"); qmlRegisterType(uri, 1, 0, "NymeaDiscovery"); - qmlRegisterUncreatableType(uri, 1, 0, "DiscoveryModel", "Get it from NymeaDiscovery"); - qmlRegisterUncreatableType(uri, 1, 0, "DiscoveryDevice", "Get it from DiscoveryModel"); - qmlRegisterUncreatableType(uri, 1, 0, "Connection", "Get it from DiscoveryDevice"); + qmlRegisterUncreatableType(uri, 1, 0, "NymeaHosts", "Get it from NymeaDiscovery"); + qmlRegisterType(uri, 1, 0, "NymeaHostsFilterModel"); + qmlRegisterUncreatableType(uri, 1, 0, "NymeaHost", "Get it from NymeaHosts"); + qmlRegisterUncreatableType(uri, 1, 0, "Connection", "Get it from NymeaHost"); qmlRegisterType(uri, 1, 0, "LogsModel"); qmlRegisterType(uri, 1, 0, "LogsModelNg"); diff --git a/libnymea-app-core/libnymea-app-core.pro b/libnymea-app-core/libnymea-app-core.pro index 271b9531..70858764 100644 --- a/libnymea-app-core/libnymea-app-core.pro +++ b/libnymea-app-core/libnymea-app-core.pro @@ -25,19 +25,22 @@ INCLUDEPATH += $$top_srcdir/libnymea-common \ SOURCES += \ engine.cpp \ + connection/nymeahost.cpp \ + connection/nymeahosts.cpp \ connection/nymeaconnection.cpp \ connection/nymeatransportinterface.cpp \ connection/websockettransport.cpp \ connection/tcpsockettransport.cpp \ connection/bluetoothtransport.cpp \ connection/awsclient.cpp \ + connection/discovery/nymeadiscovery.cpp \ + connection/discovery/upnpdiscovery.cpp \ + connection/discovery/zeroconfdiscovery.cpp \ + connection/discovery/bluetoothservicediscovery.cpp \ devicemanager.cpp \ jsonrpc/jsontypes.cpp \ jsonrpc/jsonrpcclient.cpp \ jsonrpc/jsonhandler.cpp \ - discovery/nymeahost.cpp \ - discovery/nymeahosts.cpp \ - discovery/upnpdiscovery.cpp \ devices.cpp \ devicesproxy.cpp \ deviceclasses.cpp \ @@ -46,14 +49,10 @@ SOURCES += \ vendorsproxy.cpp \ pluginsproxy.cpp \ interfacesmodel.cpp \ - discovery/zeroconfdiscovery.cpp \ - discovery/discoverydevice.cpp \ - discovery/discoverymodel.cpp \ rulemanager.cpp \ models/rulesfiltermodel.cpp \ models/logsmodel.cpp \ models/valuelogsproxymodel.cpp \ - discovery/nymeadiscovery.cpp \ logmanager.cpp \ wifisetup/bluetoothdevice.cpp \ wifisetup/bluetoothdeviceinfo.cpp \ @@ -74,7 +73,6 @@ SOURCES += \ ruletemplates/ruleactiontemplate.cpp \ ruletemplates/stateevaluatortemplate.cpp \ ruletemplates/statedescriptortemplate.cpp \ - discovery/bluetoothservicediscovery.cpp \ connection/cloudtransport.cpp \ connection/sigv4utils.cpp \ ruletemplates/ruleactionparamtemplate.cpp \ @@ -88,6 +86,8 @@ SOURCES += \ HEADERS += \ engine.h \ + connection/nymeahost.h \ + connection/nymeahosts.h \ connection/nymeaconnection.h \ connection/nymeatransportinterface.h \ connection/websockettransport.h \ @@ -95,13 +95,14 @@ HEADERS += \ connection/bluetoothtransport.h \ connection/awsclient.h \ connection/sigv4utils.h \ + connection/discovery/nymeadiscovery.h \ + connection/discovery/upnpdiscovery.h \ + connection/discovery/zeroconfdiscovery.h \ + connection/discovery/bluetoothservicediscovery.h \ devicemanager.h \ jsonrpc/jsontypes.h \ jsonrpc/jsonrpcclient.h \ jsonrpc/jsonhandler.h \ - discovery/nymeahost.h \ - discovery/nymeahosts.h \ - discovery/upnpdiscovery.h \ devices.h \ devicesproxy.h \ deviceclasses.h \ @@ -110,14 +111,10 @@ HEADERS += \ vendorsproxy.h \ pluginsproxy.h \ interfacesmodel.h \ - discovery/zeroconfdiscovery.h \ - discovery/discoverydevice.h \ - discovery/discoverymodel.h \ rulemanager.h \ models/rulesfiltermodel.h \ models/logsmodel.h \ models/valuelogsproxymodel.h \ - discovery/nymeadiscovery.h \ logmanager.h \ wifisetup/bluetoothdevice.h \ wifisetup/bluetoothdeviceinfo.h \ @@ -139,7 +136,6 @@ HEADERS += \ ruletemplates/ruleactiontemplate.h \ ruletemplates/stateevaluatortemplate.h \ ruletemplates/statedescriptortemplate.h \ - discovery/bluetoothservicediscovery.h \ connection/cloudtransport.h \ ruletemplates/ruleactionparamtemplate.h \ configuration/serverconfiguration.h \ diff --git a/nymea-app/main.cpp b/nymea-app/main.cpp index 39ba3c66..a3814403 100644 --- a/nymea-app/main.cpp +++ b/nymea-app/main.cpp @@ -57,6 +57,12 @@ QObject *platformHelperProvider(QQmlEngine *engine, QJSEngine *scriptEngine) int main(int argc, char *argv[]) { + QLoggingCategory::setFilterRules("RemoteProxyClientJsonRpcTraffic.debug=false\n" + "RemoteProxyClientJsonRpc.debug=false\n" + "RemoteProxyClientWebSocket.debug=false\n" + "RemoteProxyClientConnection.debug=false\n" + "RemoteProxyClientConnectionTraffic.debug=false\n" + ); QCoreApplication::setAttribute(Qt::AA_EnableHighDpiScaling); QApplication application(argc, argv); application.setApplicationName("nymea-app"); diff --git a/nymea-app/platformintegration/android/platformhelperandroid.cpp b/nymea-app/platformintegration/android/platformhelperandroid.cpp index 0c7ee1e4..1e482976 100644 --- a/nymea-app/platformintegration/android/platformhelperandroid.cpp +++ b/nymea-app/platformintegration/android/platformhelperandroid.cpp @@ -59,10 +59,10 @@ void PlatformHelperAndroid::vibrate(PlatformHelper::HapticsFeedback feedbackType int duration; switch (feedbackType) { case HapticsFeedbackSelection: - duration = 15; + duration = 20; break; case HapticsFeedbackImpact: - duration = 25; + duration = 30; break; case HapticsFeedbackNotification: duration = 500; diff --git a/nymea-app/ui/Nymea.qml b/nymea-app/ui/Nymea.qml index 747ece62..c40b307d 100644 --- a/nymea-app/ui/Nymea.qml +++ b/nymea-app/ui/Nymea.qml @@ -57,6 +57,7 @@ ApplicationWindow { awsClient: AWSClient // discovering: pageStack.currentItem.objectName === "discoveryPage" } + property alias _discovery: discovery onClosing: { rootItem.handleCloseEvent(close) diff --git a/nymea-app/ui/RootItem.qml b/nymea-app/ui/RootItem.qml index 59a60795..93979105 100644 --- a/nymea-app/ui/RootItem.qml +++ b/nymea-app/ui/RootItem.qml @@ -5,6 +5,7 @@ import QtQuick.Layouts 1.3 import Qt.labs.settings 1.0 import Nymea 1.0 import "components" +import "connection" Item { id: root @@ -74,7 +75,7 @@ Item { height: swipeView.height width: swipeView.width objectName: "pageStack" - initialItem: Page {} + initialItem: ConnectPage {} property var tabSettings: Settings { category: "tabSettings" + index @@ -88,7 +89,7 @@ Item { readonly property Engine engine: engineObject readonly property Engine _engine: engineObject // In case a child cannot use "engine" property int connectionTabIndex: index - onConnectionTabIndexChanged: tabSettings.lastConnectedHost = engine.connection.url +// onConnectionTabIndexChanged: tabSettings.lastConnectedHost = engine.connection.url Binding { target: AWSClient @@ -97,20 +98,26 @@ Item { } Component.onCompleted: { - pageStack.push(Qt.resolvedUrl("connection/ConnectPage.qml"), StackView.Immediate) - setupPushNotifications(); +// pageStack.push(Qt.resolvedUrl("connection/ConnectPage.qml"), StackView.Immediate) +// setupPushNotifications(); } function init() { - print("calling init. Auth required:", engine.jsonRpcClient.authenticationRequired, "initial setup required:", engine.jsonRpcClient.initialSetupRequired, "jsonrpc connected:", engine.jsonRpcClient.connected) + print("calling init. Auth required:", engine.jsonRpcClient.authenticationRequired, "initial setup required:", engine.jsonRpcClient.initialSetupRequired, "jsonrpc connected:", engine.jsonRpcClient.connected, "Current host:", engine.connection.currentHost) pageStack.clear() - if (!engine.connection.connected) { + if (!engine.connection.currentHost) { pageStack.push(Qt.resolvedUrl("connection/ConnectPage.qml")) PlatformHelper.hideSplashScreen(); return; } + if (engine.jsonRpcClient.connected) { + pageStack.push(Qt.resolvedUrl("MainPage.qml")) + PlatformHelper.hideSplashScreen(); + return; + } if (engine.jsonRpcClient.authenticationRequired || engine.jsonRpcClient.initialSetupRequired) { + PlatformHelper.hideSplashScreen(); if (engine.jsonRpcClient.pushButtonAuthAvailable) { print("opening push button auth") var page = pageStack.push(Qt.resolvedUrl("PushButtonAuthPage.qml")) @@ -127,10 +134,8 @@ Item { init(); }) } - } else { - pageStack.push(Qt.resolvedUrl("MainPage.qml")) } - PlatformHelper.hideSplashScreen(); + pageStack.push(Qt.resolvedUrl("connection/ConnectingPage.qml")) } function handleCloseEvent(close) { @@ -169,11 +174,19 @@ Item { } } + Connections { + target: engine.connection + onCurrentHostChanged: { + init(); + } + } + Connections { target: engine.jsonRpcClient onConnectedChanged: { print("json client connected changed", engine.jsonRpcClient.connected) if (engine.jsonRpcClient.connected) { + discovery.cacheHost(engine.connection.currentHost) tabSettings.lastConnectedHost = engine.jsonRpcClient.serverUuid } init(); diff --git a/nymea-app/ui/SettingsPage.qml b/nymea-app/ui/SettingsPage.qml index df24ac24..5dcdf8e2 100644 --- a/nymea-app/ui/SettingsPage.qml +++ b/nymea-app/ui/SettingsPage.qml @@ -37,7 +37,7 @@ Page { Label { Layout.fillWidth: true elide: Text.ElideMiddle - text: engine.connection.url + text: engine.connection.currentConnection.url } Button { text: qsTr("Disconnect") diff --git a/nymea-app/ui/connection/ConnectPage.qml b/nymea-app/ui/connection/ConnectPage.qml index 7334f351..55618cc9 100644 --- a/nymea-app/ui/connection/ConnectPage.qml +++ b/nymea-app/ui/connection/ConnectPage.qml @@ -8,12 +8,18 @@ import "../components" Page { id: root - readonly property bool haveHosts: discovery.discoveryModel.count > 0 + readonly property bool haveHosts: hostsProxy.count > 0 Component.onCompleted: { - print("completed connectPage. last connected host:", tabSettings.lastConnectedHost) + print("Ready to connect") if (tabSettings.lastConnectedHost.length > 0) { - discovery.resolveServerUuid(tabSettings.lastConnectedHost) + print("Last connected host was", tabSettings.lastConnectedHost) + var cachedHost = discovery.nymeaHosts.find(tabSettings.lastConnectedHost); + if (cachedHost) { + engine.connection.currentHost = cachedHost + } else { + print("Warning: There is a last connected host but UUID is unknown to discovery...") + } } else { PlatformHelper.hideSplashScreen(); } @@ -30,16 +36,6 @@ Page { // } } - Connections { - target: discovery - onServerUuidResolved: { - print("** resolved", uuid, tabSettings.lastConnectedHost) - if (uuid == tabSettings.lastConnectedHost) { - print("yesss") - connectToHost(url, true); - } - } - } function connectToHost(url, noAnimations) { var page = pageStack.push(Qt.resolvedUrl("ConnectingPage.qml"), noAnimations ? StackView.Immediate : StackView.PushTransition) @@ -51,12 +47,23 @@ Page { engine.connection.connect(url) } -// NymeaDiscovery { -// id: discovery -// objectName: "discovery" -// awsClient: AWSClient -// discovering: pageStack.currentItem.objectName === "discoveryPage" -// } + function connectToHost2(host, noAnimations) { + var page = pageStack.push(Qt.resolvedUrl("ConnectingPage.qml"), noAnimations ? StackView.Immediate : StackView.PushTransition) + page.cancel.connect(function() { + engine.connection.disconnect() + pageStack.pop(root, StackView.Immediate); + pageStack.push(discoveryPage) + }) + print("Connecting to host", host) + engine.connection.connect(host) + } + + NymeaHostsFilterModel { + id: hostsProxy + discovery: _discovery + showUnreachableBearers: false + nymeaConnection: engine.connection + } Connections { target: engine.connection @@ -169,7 +176,7 @@ Page { Label { Layout.fillWidth: true text: root.haveHosts ? - qsTr("There are %1 %2 boxes in your network! Which one would you like to use?").arg(discovery.discoveryModel.count).arg(app.systemName) + qsTr("There are %1 %2 boxes in your network! Which one would you like to use?").arg(discovery.nymeaHosts.count).arg(app.systemName) : startupTimer.running ? qsTr("We haven't found any %1 boxes in your network yet.").arg(app.systemName) : qsTr("There doesn't seem to be a %1 box installed in your network. Please make sure your %1 box is correctly set up and connected.").arg(app.systemName) wrapMode: Text.WordWrap @@ -181,27 +188,27 @@ Page { ListView { Layout.fillWidth: true Layout.fillHeight: true - model: discovery.discoveryModel + model: hostsProxy clip: true delegate: MeaListItemDelegate { - id: discoveryDeviceDelegate + id: nymeaHostDelegate width: parent.width height: app.delegateHeight objectName: "discoveryDelegate" + index - property var discoveryDevice: discovery.discoveryModel.get(index) + property var nymeaHost: discovery.nymeaHosts.get(index) property string defaultConnectionIndex: { var usedConfigIndex = 0; - for (var i = 1; i < discoveryDevice.connections.count; i++) { - var oldConfig = discoveryDevice.connections.get(usedConfigIndex); - var newConfig = discoveryDevice.connections.get(i); + for (var i = 1; i < nymeaHost.connections.count; i++) { + var oldConfig = nymeaHost.connections.get(usedConfigIndex); + var newConfig = nymeaHost.connections.get(i); // Preference of bearerType var bearerPreference = [Connection.BearerTypeEthernet, Connection.BearerTypeWifi, Connection.BearerTypeBluetooth, Connection.BearerTypeCloud] var oldBearerPriority = bearerPreference.indexOf(oldConfig.bearerType); var newBearerPriority = bearerPreference.indexOf(newConfig.bearerType); if (newBearerPriority < oldBearerPriority) { - print(discoveryDevice.name, "switching to preferred index", i, "of bearer type", newConfig.bearerType, "from", oldConfig.bearerType, "new prio:", newBearerPriority, "old:", oldBearerPriority) + print(nymeaHost.name, "switching to preferred index", i, "of bearer type", newConfig.bearerType, "from", oldConfig.bearerType, "new prio:", newBearerPriority, "old:", oldBearerPriority) usedConfigIndex = i; continue; } @@ -227,7 +234,7 @@ Page { } iconName: { - switch (discoveryDevice.connections.get(defaultConnectionIndex).bearerType) { + switch (nymeaHost.connections.get(defaultConnectionIndex).bearerType) { case Connection.BearerTypeWifi: return "../images/network-wifi-symbolic.svg"; case Connection.BearerTypeEthernet: @@ -241,21 +248,21 @@ Page { } text: model.name - subText: discoveryDevice.connections.get(defaultConnectionIndex).url + subText: nymeaHost.connections.get(defaultConnectionIndex).url wrapTexts: false prominentSubText: false progressive: false - property bool isSecure: discoveryDevice.connections.get(defaultConnectionIndex).secure - property bool isTrusted: engine.connection.isTrusted(discoveryDeviceDelegate.discoveryDevice.connections.get(defaultConnectionIndex).url) - property bool isOnline: discoveryDevice.connections.get(defaultConnectionIndex).online + property bool isSecure: nymeaHost.connections.get(defaultConnectionIndex).secure + property bool isTrusted: engine.connection.isTrusted(nymeaHostDelegate.nymeaHost.connections.get(defaultConnectionIndex).url) + property bool isOnline: nymeaHost.connections.get(defaultConnectionIndex).online tertiaryIconName: isSecure ? "../images/network-secure.svg" : "" tertiaryIconColor: isTrusted ? app.accentColor : Material.foreground secondaryIconName: !isOnline ? "../images/cloud-error.svg" : "" secondaryIconColor: "red" - swipe.enabled: discoveryDeviceDelegate.discoveryDevice.deviceType === DiscoveryDevice.DeviceTypeNetwork + swipe.enabled: nymeaHostDelegate.nymeaHost.deviceType === NymeaHost.DeviceTypeNetwork onClicked: { - root.connectToHost(discoveryDeviceDelegate.discoveryDevice.connections.get(defaultConnectionIndex).url) + root.connectToHost2(nymeaHostDelegate.nymeaHost) } swipe.right: MouseArea { @@ -268,9 +275,9 @@ Page { name: "../images/info.svg" } onClicked: { - if (model.deviceType === DiscoveryDevice.DeviceTypeNetwork) { + if (model.deviceType === NymeaHost.DeviceTypeNetwork) { swipe.close() - var popup = infoDialog.createObject(app,{discoveryDevice: discovery.discoveryModel.get(index)}) + var popup = infoDialog.createObject(app,{nymeaHost: discovery.nymeaHosts.get(index)}) popup.open() } } @@ -300,14 +307,14 @@ Page { Layout.leftMargin: app.margins Layout.rightMargin: app.margins wrapMode: Text.WordWrap - visible: discovery.discoveryModel.count === 0 + visible: discovery.nymeaHosts.count === 0 text: qsTr("Do you have a %1 box but it's not connected to your network yet? Use the wireless setup to connect it!").arg(app.systemName) } Button { Layout.fillWidth: true Layout.leftMargin: app.margins Layout.rightMargin: app.margins - visible: discovery.discoveryModel.count === 0 + visible: discovery.nymeaHosts.count === 0 text: qsTr("Start wireless setup") onClicked: pageStack.push(Qt.resolvedUrl("wifisetup/BluetoothDiscoveryPage.qml"), {nymeaDiscovery: discovery}) } @@ -325,7 +332,7 @@ Page { Layout.leftMargin: app.margins Layout.rightMargin: app.margins Layout.bottomMargin: app.margins - visible: discovery.discoveryModel.count === 0 + visible: discovery.nymeaHosts.count === 0 text: qsTr("Demo mode (online)") onClicked: { root.connectToHost("nymea://nymea.nymea.io:2222") @@ -472,7 +479,7 @@ Page { standardButtons: Dialog.Ok - property var discoveryDevice: null + property var nymeaHost: null header: Item { implicitHeight: headerRow.height + app.margins * 2 @@ -508,7 +515,7 @@ Page { text: "Name:" } Label { - text: dialog.discoveryDevice.name + text: dialog.nymeaHost.name Layout.fillWidth: true elide: Text.ElideRight } @@ -516,7 +523,7 @@ Page { text: "UUID:" } Label { - text: dialog.discoveryDevice.uuid + text: dialog.nymeaHost.uuid Layout.fillWidth: true elide: Text.ElideRight } @@ -524,7 +531,7 @@ Page { text: "Version:" } Label { - text: dialog.discoveryDevice.version + text: dialog.nymeaHost.version Layout.fillWidth: true elide: Text.ElideRight } @@ -544,7 +551,7 @@ Page { id: contentColumn width: parent.width Repeater { - model: dialog.discoveryDevice.connections + model: dialog.nymeaHost.connections delegate: MeaListItemDelegate { Layout.fillWidth: true wrapTexts: false @@ -573,7 +580,7 @@ Page { secondaryIconColor: "red" onClicked: { - root.connectToHost(dialog.discoveryDevice.connections.get(index).url) + root.connectToHost2(dialog.nymeaHost.connections.get(index)) dialog.close() } } diff --git a/nymea-app/ui/connection/ConnectingPage.qml b/nymea-app/ui/connection/ConnectingPage.qml index 2f11739d..ca2132d9 100644 --- a/nymea-app/ui/connection/ConnectingPage.qml +++ b/nymea-app/ui/connection/ConnectingPage.qml @@ -28,7 +28,7 @@ Page { } Label { Layout.fillWidth: true - text: engine.connection.url + text: engine.connection.currentHost.uuid font.pixelSize: app.smallFont wrapMode: Text.WrapAtWordBoundaryOrAnywhere horizontalAlignment: Text.AlignHCenter diff --git a/nymea-app/ui/connection/wifisetup/WirelessSetupPage.qml b/nymea-app/ui/connection/wifisetup/WirelessSetupPage.qml index 321d5b0c..f9900ae6 100644 --- a/nymea-app/ui/connection/wifisetup/WirelessSetupPage.qml +++ b/nymea-app/ui/connection/wifisetup/WirelessSetupPage.qml @@ -52,7 +52,7 @@ Page { } Connections { - target: root.nymeaDiscovery.discoveryModel + target: root.nymeadiscovery.nymeaHosts onCountChanged: updateConnectButton(); } @@ -63,14 +63,14 @@ Page { } // FIXME: We should rather look for the UUID here, but nymea-networkmanager doesn't support getting us the nymea uuid (yet) - for (var i = 0; i < root.nymeaDiscovery.discoveryModel.count; i++) { - for (var j = 0; j < root.nymeaDiscovery.discoveryModel.get(i).connections.count; j++) { - if (root.nymeaDiscovery.discoveryModel.get(i).connections.get(j).url.toString().indexOf(root.networkManagerController.manager.currentConnection.hostAddress) >= 0) { - connectButton.url = root.nymeaDiscovery.discoveryModel.get(i).connections.get(j).url + for (var i = 0; i < root.nymeadiscovery.nymeaHosts.count; i++) { + for (var j = 0; j < root.nymeadiscovery.nymeaHosts.get(i).connections.count; j++) { + if (root.nymeadiscovery.nymeaHosts.get(i).connections.get(j).url.toString().indexOf(root.networkManagerController.manager.currentConnection.hostAddress) >= 0) { + connectButton.url = root.nymeadiscovery.nymeaHosts.get(i).connections.get(j).url return; } } - root.nymeaDiscovery.discoveryModel.get(i).connections.countChanged.connect(function() { + root.nymeadiscovery.nymeaHosts.get(i).connections.countChanged.connect(function() { updateConnectButton(); }) } From f75b66c0ff0f5fbd55bb44a1d1aa6999af87c500 Mon Sep 17 00:00:00 2001 From: Michael Zanetti Date: Wed, 6 Feb 2019 03:00:10 +0100 Subject: [PATCH 05/11] Mostly working, cleanup still to be done --- libnymea-app-core/connection/awsclient.cpp | 6 + .../connection/cloudtransport.cpp | 1 + .../discovery/zeroconfdiscovery.cpp | 11 +- .../connection/nymeaconnection.cpp | 72 ++++++- .../connection/nymeaconnection.h | 21 +- libnymea-app-core/connection/nymeahosts.cpp | 2 +- nymea-app/resources.qrc | 1 + nymea-app/ui/RootItem.qml | 46 ++++- nymea-app/ui/connection/CertificateDialog.qml | 110 +++++++++++ nymea-app/ui/connection/ConnectPage.qml | 181 +----------------- nymea-app/ui/connection/ConnectingPage.qml | 45 +++++ 11 files changed, 295 insertions(+), 201 deletions(-) create mode 100644 nymea-app/ui/connection/CertificateDialog.qml diff --git a/libnymea-app-core/connection/awsclient.cpp b/libnymea-app-core/connection/awsclient.cpp index 39268c80..b2a0cdd1 100644 --- a/libnymea-app-core/connection/awsclient.cpp +++ b/libnymea-app-core/connection/awsclient.cpp @@ -7,6 +7,7 @@ #include #include #include +#include #include "sigv4utils.h" @@ -872,6 +873,11 @@ bool AWSClient::postToMQTT(const QString &boxId, const QString ×tamp, std:: // } // qDebug() << "Payload:" << payload; QNetworkReply *reply = m_nam->post(request, payload); + QTimer::singleShot(5000, reply, [reply, callback](){ + reply->deleteLater(); + qWarning() << "Timeout posting to MQTT"; + callback(false); + }); connect(reply, &QNetworkReply::finished, this, [reply, callback]() { reply->deleteLater(); QByteArray data = reply->readAll(); diff --git a/libnymea-app-core/connection/cloudtransport.cpp b/libnymea-app-core/connection/cloudtransport.cpp index 03818ad8..c30cac62 100644 --- a/libnymea-app-core/connection/cloudtransport.cpp +++ b/libnymea-app-core/connection/cloudtransport.cpp @@ -54,6 +54,7 @@ bool CloudTransport::connect(const QUrl &url) m_timestamp = QDateTime::currentDateTime(); bool postResult = m_awsClient->postToMQTT(url.host(), QString::number(m_timestamp.toMSecsSinceEpoch()), [this](bool success) { if (success) { + qDebug() << "MQTT Post done. Connecting to remote proxy"; m_remoteproxyConnection->connectServer(QUrl("wss://remoteproxy.nymea.io")); } else { qDebug() << "Posting to MQTT failed"; diff --git a/libnymea-app-core/connection/discovery/zeroconfdiscovery.cpp b/libnymea-app-core/connection/discovery/zeroconfdiscovery.cpp index fff26f0c..836e75d6 100644 --- a/libnymea-app-core/connection/discovery/zeroconfdiscovery.cpp +++ b/libnymea-app-core/connection/discovery/zeroconfdiscovery.cpp @@ -180,12 +180,13 @@ void ZeroconfDiscovery::serviceEntryRemoved(const QZeroConfService &entry) } // Ok, now we need to remove it - host->connections()->removeConnection(connection); +// host->connections()->removeConnection(connection); + connection->setOnline(false); // And if there aren't any connections left, remove the entire device - if (host->connections()->rowCount() == 0) { - qDebug() << "Zeroconf: Removing connection from host:" << host->name() << url.toString(); - m_nymeaHosts->removeHost(host); - } +// if (host->connections()->rowCount() == 0) { +// qDebug() << "Zeroconf: Removing connection from host:" << host->name() << url.toString(); +// m_nymeaHosts->removeHost(host); +// } } #endif diff --git a/libnymea-app-core/connection/nymeaconnection.cpp b/libnymea-app-core/connection/nymeaconnection.cpp index 9be30e90..717e4ae3 100644 --- a/libnymea-app-core/connection/nymeaconnection.cpp +++ b/libnymea-app-core/connection/nymeaconnection.cpp @@ -10,6 +10,7 @@ #include #include #include +#include #include "nymeatransportinterface.h" @@ -32,6 +33,9 @@ NymeaConnection::NymeaConnection(QObject *parent) : QObject(parent) void NymeaConnection::acceptCertificate(const QString &url, const QByteArray &pem) { storePem(url, pem); + if (m_currentHost) { + connectInternal(m_currentHost); + } } bool NymeaConnection::isTrusted(const QString &url) @@ -62,6 +66,11 @@ bool NymeaConnection::connected() return m_currentHost && m_currentTransport && m_currentTransport->connectionState() == NymeaTransportInterface::ConnectionStateConnected; } +NymeaConnection::ConnectionStatus NymeaConnection::connectionStatus() const +{ + return m_connectionStatus; +} + NymeaHost *NymeaConnection::currentHost() const { return m_currentHost; @@ -91,6 +100,9 @@ void NymeaConnection::setCurrentHost(NymeaHost *host) m_currentHost = host; emit currentHostChanged(); + m_connectionStatus = ConnectionStatusConnecting; + emit connectionStatusChanged(); + if (m_currentHost) { connectInternal(m_currentHost); } @@ -175,6 +187,8 @@ void NymeaConnection::onSslErrors(const QList &errors) // info << tr("Name Qualifier:")<< error.certificate().issuerInfo(QSslCertificate::DistinguishedNameQualifier); // info << tr("Email:")<< error.certificate().issuerInfo(QSslCertificate::EmailAddress); + m_connectionStatus = ConnectionStatusSslUntrusted; + emit connectionStatusChanged(); emit verifyConnectionCertificate(transport->url().toString(), info, certificateFingerprint, error.certificate().toPem()); } } else { @@ -196,22 +210,58 @@ void NymeaConnection::onError(QAbstractSocket::SocketError error) NymeaTransportInterface* transport = qobject_cast(sender()); + ConnectionStatus errorStatus = ConnectionStatusUnknownError; + switch (error) { + case QAbstractSocket::ConnectionRefusedError: + errorStatus = ConnectionStatusConnectionRefused; + break; + case QAbstractSocket::HostNotFoundError: + errorStatus = ConnectionStatusHostNotFound; + break; + case QAbstractSocket::NetworkError: + errorStatus = ConnectionStatusBearerFailed; + break; + case QAbstractSocket::RemoteHostClosedError: + errorStatus = ConnectionStatusRemoteHostClosed; + break; + case QAbstractSocket::SocketTimeoutError: + errorStatus = ConnectionStatusTimeout; + break; + case QAbstractSocket::SslInternalError: + case QAbstractSocket::SslInvalidUserDataError: + errorStatus = ConnectionStatusSslError; + break; + case QAbstractSocket::SslHandshakeFailedError: + errorStatus = ConnectionStatusSslUntrusted; + break; + default: + errorStatus = ConnectionStatusUnknownError; + } + if (transport == m_currentTransport) { qDebug() << "Current transport failed:" << error; // The current transport failed, forward the error - emit connectionError(errorString); + m_connectionStatus = errorStatus; + emit connectionStatusChanged(); return; } if (!m_currentTransport) { // We're trying to connect and one of the transports failed... - qDebug() << "A transport error happened for" << transport->url() << error; + qDebug() << "A transport error happened for" << transport->url() << error << "(Still trying on" << m_transportCandidates.count() << "connections)"; if (m_transportCandidates.contains(transport)) { m_transportCandidates.remove(transport); transport->deleteLater(); } if (m_transportCandidates.isEmpty()) { - emit connectionError(errorString); + m_connectionStatus = errorStatus; + emit connectionStatusChanged(); + + if (m_connectionStatus != ConnectionStatusSslUntrusted) { + QTimer::singleShot(1000, m_currentHost, [this](){ + connectInternal(m_currentHost); + }); + } } } } @@ -263,7 +313,10 @@ void NymeaConnection::onDisconnected() qDebug() << "NymeaConnection: disconnected."; emit connectedChanged(false); - connectInternal(m_currentHost); + // Try to reconnect, only if we're not waiting for SSL certs to be trusted. + if (m_connectionStatus != ConnectionStatusSslUntrusted) { + connectInternal(m_currentHost); + } } void NymeaConnection::updateActiveBearers() @@ -277,18 +330,20 @@ void NymeaConnection::updateActiveBearers() } // qDebug() << "Available bearers:" << availableBearerTypes; if (m_availableBearerTypes != availableBearerTypes) { - qDebug() << "Available Bearer Types changed:" << availableBearerTypes; + qDebug() << "Available Bearer Types changed:" << availableBearerTypes; m_availableBearerTypes = availableBearerTypes; emit availableBearerTypesChanged(); } if (!m_currentHost) { // No host set... Nothing to do... + qDebug() << "No current host... Nothing to do..."; return; } if (!m_currentTransport) { // There's a host but no connection. Try connecting now... + qDebug() << "There's a host but no connection. Trying to connect now..."; connectInternal(m_currentHost); } @@ -338,6 +393,7 @@ bool NymeaConnection::storePem(const QUrl &host, const QByteArray &pem) bool NymeaConnection::loadPem(const QUrl &host, QByteArray &pem) { QDir dir(QStandardPaths::writableLocation(QStandardPaths::DataLocation) + "/sslcerts/"); + qDebug() << "Loading certificates from:" << dir.absoluteFilePath(host.host() + ".pem"); QFile certFile(dir.absoluteFilePath(host.host() + ".pem")); if (!certFile.open(QFile::ReadOnly)) { return false; @@ -361,6 +417,12 @@ void NymeaConnection::connect(NymeaHost *nymeaHost) void NymeaConnection::connectInternal(NymeaHost *host) { + if (m_availableBearerTypes == Connection::BearerTypeNone) { + qDebug() << "No available bearer. Not connecting... (" << m_availableBearerTypes << ")"; + m_connectionStatus = ConnectionStatusNoBearerAvailable; + emit connectionStatusChanged(); + return; + } if (m_availableBearerTypes.testFlag(Connection::BearerTypeWifi) || m_availableBearerTypes.testFlag(Connection::BearerTypeEthernet)) { Connection* lanConnection = host->connections()->bestMatch(Connection::BearerTypeWifi | Connection::BearerTypeEthernet); if (lanConnection) { diff --git a/libnymea-app-core/connection/nymeaconnection.h b/libnymea-app-core/connection/nymeaconnection.h index 91771d68..faeba0a4 100644 --- a/libnymea-app-core/connection/nymeaconnection.h +++ b/libnymea-app-core/connection/nymeaconnection.h @@ -21,8 +21,24 @@ class NymeaConnection : public QObject Q_PROPERTY(NymeaHost* currentHost READ currentHost WRITE setCurrentHost NOTIFY currentHostChanged) Q_PROPERTY(Connection* currentConnection READ currentConnection NOTIFY currentConnectionChanged) Q_PROPERTY(Connection::BearerTypes availableBearerTypes READ availableBearerTypes NOTIFY availableBearerTypesChanged) + Q_PROPERTY(ConnectionStatus connectionStatus READ connectionStatus NOTIFY connectionStatusChanged) public: + enum ConnectionStatus { + ConnectionStatusUnconnected, + ConnectionStatusConnecting, + ConnectionStatusNoBearerAvailable, + ConnectionStatusBearerFailed, + ConnectionStatusHostNotFound, + ConnectionStatusConnectionRefused, + ConnectionStatusRemoteHostClosed, + ConnectionStatusTimeout, + ConnectionStatusSslError, + ConnectionStatusSslUntrusted, + ConnectionStatusUnknownError, + ConnectionStatusConnected + }; + Q_ENUM(ConnectionStatus) explicit NymeaConnection(QObject *parent = nullptr); void registerTransport(NymeaTransportInterfaceFactory *transportFactory); @@ -35,12 +51,14 @@ public: Connection::BearerTypes availableBearerTypes() const; bool connected(); + ConnectionStatus connectionStatus() const; NymeaHost* currentHost() const; void setCurrentHost(NymeaHost *host); Connection* currentConnection() const; + void sendData(const QByteArray &data); signals: @@ -48,8 +66,8 @@ signals: void verifyConnectionCertificate(const QString &url, const QStringList &issuerInfo, const QByteArray &fingerprint, const QByteArray &pem); void currentHostChanged(); void connectedChanged(bool connected); + void connectionStatusChanged(); void currentConnectionChanged(); - void connectionError(const QString &error); void dataAvailable(const QByteArray &data); private slots: @@ -69,6 +87,7 @@ private: Connection::BearerType qBearerTypeToNymeaBearerType(QNetworkConfiguration::BearerType type) const; private: + ConnectionStatus m_connectionStatus = ConnectionStatusUnconnected; QNetworkConfigurationManager *m_networkConfigManager = nullptr; Connection::BearerTypes m_availableBearerTypes = Connection::BearerTypeNone; diff --git a/libnymea-app-core/connection/nymeahosts.cpp b/libnymea-app-core/connection/nymeahosts.cpp index 2fe4d4d0..112e79cd 100644 --- a/libnymea-app-core/connection/nymeahosts.cpp +++ b/libnymea-app-core/connection/nymeahosts.cpp @@ -158,7 +158,7 @@ void NymeaHostsFilterModel::setNymeaConnection(NymeaConnection *nymeaConnection) emit nymeaConnectionChanged(); connect(m_nymeaConnection, &NymeaConnection::availableBearerTypesChanged, this, [this](){ - qDebug() << "Bearer Types Changed!"; +// qDebug() << "Bearer Types Changed!"; invalidateFilter(); emit countChanged(); }); diff --git a/nymea-app/resources.qrc b/nymea-app/resources.qrc index 11cbd132..a994706d 100644 --- a/nymea-app/resources.qrc +++ b/nymea-app/resources.qrc @@ -163,5 +163,6 @@ ui/thingconfiguration/SetupWizard.qml ui/thingconfiguration/EditThingsPage.qml ui/thingconfiguration/ConfigureThingPage.qml + ui/connection/CertificateDialog.qml diff --git a/nymea-app/ui/RootItem.qml b/nymea-app/ui/RootItem.qml index 93979105..2adae4a4 100644 --- a/nymea-app/ui/RootItem.qml +++ b/nymea-app/ui/RootItem.qml @@ -75,7 +75,7 @@ Item { height: swipeView.height width: swipeView.width objectName: "pageStack" - initialItem: ConnectPage {} + initialItem: Page {} property var tabSettings: Settings { category: "tabSettings" + index @@ -98,23 +98,30 @@ Item { } Component.onCompleted: { -// pageStack.push(Qt.resolvedUrl("connection/ConnectPage.qml"), StackView.Immediate) -// setupPushNotifications(); + if (tabSettings.lastConnectedHost.length > 0) { + print("Last connected host was", tabSettings.lastConnectedHost) + var cachedHost = discovery.nymeaHosts.find(tabSettings.lastConnectedHost); + if (cachedHost) { + engine.connection.currentHost = cachedHost + } else { + print("Warning: There is a last connected host but UUID is unknown to discovery...") + } + } else { + PlatformHelper.hideSplashScreen(); + pageStack.push(Qt.resolvedUrl("connection/ConnectPage.qml"), StackView.Immediate) + } } + function init() { print("calling init. Auth required:", engine.jsonRpcClient.authenticationRequired, "initial setup required:", engine.jsonRpcClient.initialSetupRequired, "jsonrpc connected:", engine.jsonRpcClient.connected, "Current host:", engine.connection.currentHost) pageStack.clear() if (!engine.connection.currentHost) { + print("pushing ConnectPage") pageStack.push(Qt.resolvedUrl("connection/ConnectPage.qml")) PlatformHelper.hideSplashScreen(); return; } - if (engine.jsonRpcClient.connected) { - pageStack.push(Qt.resolvedUrl("MainPage.qml")) - PlatformHelper.hideSplashScreen(); - return; - } if (engine.jsonRpcClient.authenticationRequired || engine.jsonRpcClient.initialSetupRequired) { PlatformHelper.hideSplashScreen(); @@ -126,6 +133,7 @@ Item { engine.connection.disconnect(); init(); }) + return; } else { var page = pageStack.push(Qt.resolvedUrl("LoginPage.qml")); page.backPressed.connect(function() { @@ -133,9 +141,22 @@ Item { engine.connection.disconnect() init(); }) + return; } } - pageStack.push(Qt.resolvedUrl("connection/ConnectingPage.qml")) + + if (engine.jsonRpcClient.connected) { + pageStack.push(Qt.resolvedUrl("MainPage.qml")) + PlatformHelper.hideSplashScreen(); + return; + } + + print("pushing ConnectingPage") + PlatformHelper.hideSplashScreen(); + var page = pageStack.push(Qt.resolvedUrl("connection/ConnectingPage.qml")); + page.cancel.connect(function(){ + engine.connection.disconnect(); + }) } function handleCloseEvent(close) { @@ -179,8 +200,15 @@ Item { onCurrentHostChanged: { init(); } + onVerifyConnectionCertificate: { + print("verify cert!") + var certDialogComponent = Qt.createComponent(Qt.resolvedUrl("connection/CertificateDialog.qml")); + var popup = certDialogComponent.createObject(root, {url: url, issuerInfo: issuerInfo, fingerprint: fingerprint, pem: pem}); + popup.open(); + } } + Connections { target: engine.jsonRpcClient onConnectedChanged: { diff --git a/nymea-app/ui/connection/CertificateDialog.qml b/nymea-app/ui/connection/CertificateDialog.qml new file mode 100644 index 00000000..dc1c62dc --- /dev/null +++ b/nymea-app/ui/connection/CertificateDialog.qml @@ -0,0 +1,110 @@ +import QtQuick 2.9 +import QtQuick.Controls 2.2 +import QtQuick.Controls.Material 2.2 +import QtQuick.Layouts 1.3 +import Nymea 1.0 +import "../components" + +Dialog { + id: certDialog + width: Math.min(parent.width * .9, 400) + x: (parent.width - width) / 2 + y: (parent.height - height) / 2 + standardButtons: Dialog.Yes | Dialog.No + + property string url + property var fingerprint + property var issuerInfo + property var pem + + readonly property bool hasOldFingerprint: engine.connection.isTrusted(url) + + ColumnLayout { + id: certLayout + anchors.fill: parent + // spacing: app.margins + + RowLayout { + Layout.fillWidth: true + spacing: app.margins + ColorIcon { + Layout.preferredHeight: app.iconSize * 2 + Layout.preferredWidth: height + name: certDialog.hasOldFingerprint ? "../images/lock-broken.svg" : "../images/info.svg" + color: certDialog.hasOldFingerprint ? "red" : app.accentColor + } + + Label { + id: titleLabel + Layout.fillWidth: true + wrapMode: Text.WordWrap + text: certDialog.hasOldFingerprint ? qsTr("Warning") : qsTr("Hi there!") + color: certDialog.hasOldFingerprint ? "red" : app.accentColor + font.pixelSize: app.largeFont + } + } + + Label { + Layout.fillWidth: true + wrapMode: Text.WordWrap + text: certDialog.hasOldFingerprint ? qsTr("The certificate of this %1 box has changed!").arg(app.systemName) : qsTr("It seems this is the first time you connect to this %1 box.").arg(app.systemName) + } + + Label { + Layout.fillWidth: true + wrapMode: Text.WordWrap + text: certDialog.hasOldFingerprint ? qsTr("Did you change the box's configuration? Verify if this information is correct.") : qsTr("This is the box's certificate. Once you trust it, an encrypted connection will be established.") + } + + ThinDivider {} + Item { + Layout.fillWidth: true + Layout.fillHeight: true + implicitHeight: certGridLayout.implicitHeight + Flickable { + anchors.fill: parent + contentHeight: certGridLayout.implicitHeight + clip: true + + ScrollBar.vertical: ScrollBar { + policy: contentHeight > height ? ScrollBar.AlwaysOn : ScrollBar.AsNeeded + } + + GridLayout { + id: certGridLayout + columns: 2 + width: parent.width + + Repeater { + model: certDialog.issuerInfo + + Label { + Layout.fillWidth: true + wrapMode: Text.WordWrap + text: modelData + } + } + Label { + Layout.fillWidth: true + Layout.columnSpan: 2 + wrapMode: Text.WrapAtWordBoundaryOrAnywhere + text: qsTr("Fingerprint: ") + certDialog.fingerprint + } + } + } + } + + ThinDivider {} + + Label { + Layout.fillWidth: true + wrapMode: Text.WordWrap + text: certDialog.hasOldFingerprint ? qsTr("Do you want to connect nevertheless?") : qsTr("Do you want to trust this device?") + font.bold: true + } + } + + onAccepted: { + engine.connection.acceptCertificate(certDialog.url, certDialog.pem) + } +} diff --git a/nymea-app/ui/connection/ConnectPage.qml b/nymea-app/ui/connection/ConnectPage.qml index 55618cc9..f2eaa5c7 100644 --- a/nymea-app/ui/connection/ConnectPage.qml +++ b/nymea-app/ui/connection/ConnectPage.qml @@ -12,28 +12,8 @@ Page { Component.onCompleted: { print("Ready to connect") - if (tabSettings.lastConnectedHost.length > 0) { - print("Last connected host was", tabSettings.lastConnectedHost) - var cachedHost = discovery.nymeaHosts.find(tabSettings.lastConnectedHost); - if (cachedHost) { - engine.connection.currentHost = cachedHost - } else { - print("Warning: There is a last connected host but UUID is unknown to discovery...") - } - } else { - PlatformHelper.hideSplashScreen(); - } -// if (settings.lastConnectedHost.length > 0 && Engine.connection.connect(tabSettings.lastConnectedHost)) { -// var page = pageStack.push(Qt.resolvedUrl("ConnectingPage.qml")) -// page.cancel.connect(function() { -// Engine.connection.disconnect(); -// pageStack.pop(root, StackView.Immediate); -// pageStack.push(discoveryPage) -// }) -// } else { - pageStack.push(discoveryPage, StackView.Immediate) -// } + pageStack.push(discoveryPage, StackView.Immediate) } @@ -65,55 +45,6 @@ Page { nymeaConnection: engine.connection } - Connections { - target: engine.connection - onVerifyConnectionCertificate: { - print("verify cert!") - var popup = certDialogComponent.createObject(root, {url: url, issuerInfo: issuerInfo, fingerprint: fingerprint, pem: pem}); - popup.open(); - } - onConnectionError: { - var errorMessage; - switch (error) { - case "ConnectionRefusedError": - errorMessage = qsTr("The host has rejected our connection. This probably means that %1 stopped running. Did you unplug your %1 box?").arg(app.systemName); - break; - case "SslInvalidUserDataError": - case "SslHandshakeFailedError": - // silently ignore. They'll be handled by the SSL logic - return; - case "HostNotFoundError": - errorMessage = qsTr("%1:core could not be found on this address. Please make sure you entered the address correctly and that the box is powered on.").arg(app.systemName); - break; - case "NetworkError": - errorMessage = qsTr("It seems you're not connected to the network."); - break; - case "RemoteHostClosedError": - errorMessage = qsTr("%1:core has closed the connection. This probably means it has been turned off or restarted.").arg(app.systemName); - break; - case "SocketTimeoutError": - errorMessage = qsTr("%1:core did not respond. Please make sure your network connection works properly").arg(app.systemName); - break; - default: - errorMessage = qsTr("An unknown error happened. We're very sorry for that. (Error code: %1)").arg(error); - } - - pageStack.pop(root, StackView.Immediate) - pageStack.push(discoveryPage) - - print("opening ErrorDialog with message:", errorMessage, error) - var comp = Qt.createComponent(Qt.resolvedUrl("../components/ErrorDialog.qml")) - var popup = comp.createObject(app, {text: errorMessage}) - popup.open() - } - onConnectedChanged: { - if (!connected) { - pageStack.pop(root, StackView.Immediate) - pageStack.push(discoveryPage) - } - } - } - Component { id: discoveryPage @@ -357,116 +288,6 @@ Page { } } - Component { - id: certDialogComponent - - Dialog { - id: certDialog - width: Math.min(parent.width * .9, 400) - x: (parent.width - width) / 2 - y: (parent.height - height) / 2 - standardButtons: Dialog.Yes | Dialog.No - - property string url - property var fingerprint - property var issuerInfo - property var pem - - readonly property bool hasOldFingerprint: engine.connection.isTrusted(url) - - ColumnLayout { - id: certLayout - anchors.fill: parent - // spacing: app.margins - - RowLayout { - Layout.fillWidth: true - spacing: app.margins - ColorIcon { - Layout.preferredHeight: app.iconSize * 2 - Layout.preferredWidth: height - name: certDialog.hasOldFingerprint ? "../images/lock-broken.svg" : "../images/info.svg" - color: certDialog.hasOldFingerprint ? "red" : app.accentColor - } - - Label { - id: titleLabel - Layout.fillWidth: true - wrapMode: Text.WordWrap - text: certDialog.hasOldFingerprint ? qsTr("Warning") : qsTr("Hi there!") - color: certDialog.hasOldFingerprint ? "red" : app.accentColor - font.pixelSize: app.largeFont - } - } - - Label { - Layout.fillWidth: true - wrapMode: Text.WordWrap - text: certDialog.hasOldFingerprint ? qsTr("The certificate of this %1 box has changed!").arg(app.systemName) : qsTr("It seems this is the first time you connect to this %1 box.").arg(app.systemName) - } - - Label { - Layout.fillWidth: true - wrapMode: Text.WordWrap - text: certDialog.hasOldFingerprint ? qsTr("Did you change the box's configuration? Verify if this information is correct.") : qsTr("This is the box's certificate. Once you trust it, an encrypted connection will be established.") - } - - ThinDivider {} - Item { - Layout.fillWidth: true - Layout.fillHeight: true - implicitHeight: certGridLayout.implicitHeight - Flickable { - anchors.fill: parent - contentHeight: certGridLayout.implicitHeight - clip: true - - ScrollBar.vertical: ScrollBar { - policy: contentHeight > height ? ScrollBar.AlwaysOn : ScrollBar.AsNeeded - } - - GridLayout { - id: certGridLayout - columns: 2 - width: parent.width - - Repeater { - model: certDialog.issuerInfo - - Label { - Layout.fillWidth: true - wrapMode: Text.WordWrap - text: modelData - } - } - Label { - Layout.fillWidth: true - Layout.columnSpan: 2 - wrapMode: Text.WrapAtWordBoundaryOrAnywhere - text: qsTr("Fingerprint: ") + certDialog.fingerprint - } - } - } - } - - ThinDivider {} - - Label { - Layout.fillWidth: true - wrapMode: Text.WordWrap - text: certDialog.hasOldFingerprint ? qsTr("Do you want to connect nevertheless?") : qsTr("Do you want to trust this device?") - font.bold: true - } - } - - onAccepted: { - engine.connection.acceptCertificate(certDialog.url, certDialog.pem) - root.connectToHost(certDialog.url) - } - } - } - - Component { id: infoDialog Dialog { diff --git a/nymea-app/ui/connection/ConnectingPage.qml b/nymea-app/ui/connection/ConnectingPage.qml index ca2132d9..dff23c2f 100644 --- a/nymea-app/ui/connection/ConnectingPage.qml +++ b/nymea-app/ui/connection/ConnectingPage.qml @@ -33,6 +33,51 @@ Page { wrapMode: Text.WrapAtWordBoundaryOrAnywhere horizontalAlignment: Text.AlignHCenter } + Label { + Layout.fillWidth: true + text: { + var errorMessage; + switch (engine.connection.connectionStatus) { + case NymeaConnection.ConnectionStatusUnconnected: + case NymeaConnection.ConnectionStatusConnecting: + case NymeaConnection.ConnectionStatusConnected: + errorMessage = ""; + break; + case NymeaConnection.ConnectionStatusBearerFailed: + errorMessage = qsTr("The network connection failed.") + break; + case NymeaConnection.ConnectionStatusNoBearerAvailable: + errorMessage = qsTr("It seems you're not connected to the network."); + break; + case NymeaConnection.ConnectionStatusHostNotFound: + errorMessage = qsTr("%1:core could not be found on this address. Please make sure you entered the address correctly and that the box is powered on.").arg(app.systemName); + break; + case NymeaConnection.ConnectionStatusConnectionRefused: + errorMessage = qsTr("The host has rejected our connection. This probably means that %1 stopped running. Did you unplug your %1 box?").arg(app.systemName); + break; + case NymeaConnection.ConnectionStatusRemoteHostClosed: + errorMessage = qsTr("%1:core has closed the connection. This probably means it has been turned off or restarted.").arg(app.systemName); + break; + + case NymeaConnection.ConnectionStatusTimeout: + errorMessage = qsTr("%1:core did not respond. Please make sure your network connection works properly").arg(app.systemName); + break; + case NymeaConnection.ConnectionStatusSslError: + errorMessage = qsTr("An unrecovareable SSL Error happened. Please make sure certificates are installed correctly."); + break; + case NymeaConnection.ConnectionStatusSslUntrusted: + errorMessage = qsTr("The SSL Certificate is not trusted."); + break; + case NymeaConnection.ConnectionStatusUnknownError: + default: + errorMessage = qsTr("An unknown error happened. We're very sorry for that. (Error code: %1)").arg(engine.connection.connectionStatus); + } + return errorMessage; + } + font.pixelSize: app.smallFont + wrapMode: Text.WrapAtWordBoundaryOrAnywhere + horizontalAlignment: Text.AlignHCenter + } } Button { From a2272f9699a3c032339be59749659d8533e6c980 Mon Sep 17 00:00:00 2001 From: Michael Zanetti Date: Wed, 6 Feb 2019 12:07:00 +0100 Subject: [PATCH 06/11] bring back demo mode --- libnymea-app-core/connection/nymeaconnection.cpp | 4 ++++ libnymea-app-core/connection/nymeahost.cpp | 4 +--- libnymea-app-core/connection/nymeahosts.cpp | 12 ++++++++++++ libnymea-app-core/connection/nymeahosts.h | 1 + nymea-app/ui/connection/ConnectPage.qml | 6 ++++-- 5 files changed, 22 insertions(+), 5 deletions(-) diff --git a/libnymea-app-core/connection/nymeaconnection.cpp b/libnymea-app-core/connection/nymeaconnection.cpp index 717e4ae3..2c3502d6 100644 --- a/libnymea-app-core/connection/nymeaconnection.cpp +++ b/libnymea-app-core/connection/nymeaconnection.cpp @@ -428,6 +428,8 @@ void NymeaConnection::connectInternal(NymeaHost *host) if (lanConnection) { qDebug() << "Best candidate LAN connection:" << lanConnection->url(); connectInternal(lanConnection); + } else { + qDebug() << "No available LAN connection to" << host->name(); } } @@ -436,6 +438,8 @@ void NymeaConnection::connectInternal(NymeaHost *host) if (wanConnection) { qDebug() << "Best candidate WAN connection:" << wanConnection->url(); connectInternal(wanConnection); + } else { + qDebug() << "No available WAN connection to" << host->name(); } } } diff --git a/libnymea-app-core/connection/nymeahost.cpp b/libnymea-app-core/connection/nymeahost.cpp index 1b920e6a..3c0b4f00 100644 --- a/libnymea-app-core/connection/nymeahost.cpp +++ b/libnymea-app-core/connection/nymeahost.cpp @@ -170,12 +170,10 @@ Connection* Connections::get(int index) const Connection *Connections::bestMatch(Connection::BearerTypes bearerTypes) const { - QList bearerPreference = {Connection::BearerTypeEthernet, Connection::BearerTypeWifi, Connection::BearerTypeCloud, Connection::BearerTypeBluetooth, Connection::BearerTypeNone}; Connection *best = nullptr; -// qDebug() << "Bestmatch" << m_connections.count(); foreach (Connection *c, m_connections) { // qDebug() << "have connection:" << bearerTypes << c->url() << bearerTypes.testFlag(c->bearerType()); - if (!bearerTypes.testFlag(c->bearerType())) { + if ((bearerTypes & c->bearerType()) == Connection::BearerTypeNone) { continue; } if (!best) { diff --git a/libnymea-app-core/connection/nymeahosts.cpp b/libnymea-app-core/connection/nymeahosts.cpp index 112e79cd..05b4fc2b 100644 --- a/libnymea-app-core/connection/nymeahosts.cpp +++ b/libnymea-app-core/connection/nymeahosts.cpp @@ -22,6 +22,7 @@ #include "connection/discovery/nymeadiscovery.h" #include "nymeahost.h" #include "connection/nymeaconnection.h" +#include NymeaHosts::NymeaHosts(QObject *parent) : QAbstractListModel(parent) @@ -83,6 +84,17 @@ void NymeaHosts::removeHost(NymeaHost *host) emit countChanged(); } +NymeaHost *NymeaHosts::createHost(const QString &name, const QUrl &url) +{ + NymeaHost *host = new NymeaHost(this); + host->setUuid(QUuid::createUuid()); + host->setName(name); + Connection *connection = new Connection(url, Connection::BearerTypeAll, false, url.toString(), host); + host->connections()->addConnection(connection); + addHost(host); + return host; +} + NymeaHost *NymeaHosts::get(int index) const { if (index < 0 || index >= m_hosts.count()) { diff --git a/libnymea-app-core/connection/nymeahosts.h b/libnymea-app-core/connection/nymeahosts.h index 664f64d9..01ae5fcb 100644 --- a/libnymea-app-core/connection/nymeahosts.h +++ b/libnymea-app-core/connection/nymeahosts.h @@ -49,6 +49,7 @@ public: void addHost(NymeaHost *host); void removeHost(NymeaHost *host); + Q_INVOKABLE NymeaHost* createHost(const QString &name, const QUrl &url); Q_INVOKABLE NymeaHost *get(int index) const; Q_INVOKABLE NymeaHost *find(const QUuid &uuid); diff --git a/nymea-app/ui/connection/ConnectPage.qml b/nymea-app/ui/connection/ConnectPage.qml index f2eaa5c7..b31c4816 100644 --- a/nymea-app/ui/connection/ConnectPage.qml +++ b/nymea-app/ui/connection/ConnectPage.qml @@ -60,7 +60,8 @@ Page { } onClicked: { if (index === 2) { - root.connectToHost("nymea://nymea.nymea.io:2222") + var host = discovery.nymeaHosts.createHost("Demo server", "nymea://nymea.nymea.io:2222") + engine.connection.connect(host) } else { pageStack.push(model.get(index).page, {nymeaDiscovery: discovery}); } @@ -266,7 +267,8 @@ Page { visible: discovery.nymeaHosts.count === 0 text: qsTr("Demo mode (online)") onClicked: { - root.connectToHost("nymea://nymea.nymea.io:2222") + var host = nymeaHosts.createHost("Demo server", "nymea://nymea.nymea.io:2222") + engine.connection.connect(host) } } From ec08e6c7d5088ef0988e49e19051bf364b0c9fd1 Mon Sep 17 00:00:00 2001 From: Michael Zanetti Date: Wed, 6 Feb 2019 13:45:29 +0100 Subject: [PATCH 07/11] some cleanup --- libnymea-app-core/connection/awsclient.cpp | 8 ++++---- libnymea-app-core/connection/nymeaconnection.cpp | 2 +- libnymea-app-core/connection/nymeahosts.cpp | 10 +++++++--- libnymea-app-core/connection/nymeahosts.h | 6 ++++-- libnymea-app-core/jsonrpc/jsonrpcclient.cpp | 6 ++++++ nymea-app/ui/RootItem.qml | 11 +++++------ nymea-app/ui/connection/ConnectPage.qml | 10 ++++++---- nymea-app/ui/connection/ConnectingPage.qml | 4 ++-- nymea-app/ui/connection/ManualConnectPage.qml | 8 ++------ nymea-app/ui/mainviews/FavoritesView.qml | 4 ++-- 10 files changed, 39 insertions(+), 30 deletions(-) diff --git a/libnymea-app-core/connection/awsclient.cpp b/libnymea-app-core/connection/awsclient.cpp index b2a0cdd1..2d2ed5fd 100644 --- a/libnymea-app-core/connection/awsclient.cpp +++ b/libnymea-app-core/connection/awsclient.cpp @@ -226,10 +226,10 @@ void AWSClient::login(const QString &username, const QString &password, int atte m_idToken = authenticationResult.value("IdToken").toByteArray(); m_refreshToken = authenticationResult.value("RefreshToken").toByteArray(); - qDebug() << "AWS ID token" << m_idToken; +// qDebug() << "AWS ID token" << m_idToken; QList jwtParts = m_idToken.split('.'); if (jwtParts.count() != 3) { - qWarning() << "JWT token doesn't have 3 parts"; + qWarning() << "Error: JWT token doesn't have 3 parts. Cannot retrieve AWS Cognito ID."; return; } // qDebug() << "decoded header:" << QByteArray::fromBase64(jwtParts.at(0)); @@ -237,7 +237,7 @@ void AWSClient::login(const QString &username, const QString &password, int atte QJsonDocument tokenPayloadJsonDoc = QJsonDocument::fromJson(QByteArray::fromBase64(jwtParts.at(1))); m_userId = tokenPayloadJsonDoc.toVariant().toMap().value("cognito:username").toByteArray(); - qDebug() << "Getting cognito ID"; +// qDebug() << "Getting cognito ID"; getId(); }); } @@ -597,7 +597,7 @@ void AWSClient::getId() } m_identityId = jsonDoc.toVariant().toMap().value("IdentityId").toByteArray(); - qDebug() << "Received cognito identity id" << m_identityId;// << qUtf8Printable(data); +// qDebug() << "Received cognito identity id" << m_identityId;// << qUtf8Printable(data); getCredentialsForIdentity(m_identityId); }); diff --git a/libnymea-app-core/connection/nymeaconnection.cpp b/libnymea-app-core/connection/nymeaconnection.cpp index 2c3502d6..f5a8fe2d 100644 --- a/libnymea-app-core/connection/nymeaconnection.cpp +++ b/libnymea-app-core/connection/nymeaconnection.cpp @@ -393,7 +393,7 @@ bool NymeaConnection::storePem(const QUrl &host, const QByteArray &pem) bool NymeaConnection::loadPem(const QUrl &host, QByteArray &pem) { QDir dir(QStandardPaths::writableLocation(QStandardPaths::DataLocation) + "/sslcerts/"); - qDebug() << "Loading certificates from:" << dir.absoluteFilePath(host.host() + ".pem"); +// qDebug() << "Loading certificates from:" << dir.absoluteFilePath(host.host() + ".pem"); QFile certFile(dir.absoluteFilePath(host.host() + ".pem")); if (!certFile.open(QFile::ReadOnly)) { return false; diff --git a/libnymea-app-core/connection/nymeahosts.cpp b/libnymea-app-core/connection/nymeahosts.cpp index 05b4fc2b..fa6b8e8b 100644 --- a/libnymea-app-core/connection/nymeahosts.cpp +++ b/libnymea-app-core/connection/nymeahosts.cpp @@ -84,12 +84,11 @@ void NymeaHosts::removeHost(NymeaHost *host) emit countChanged(); } -NymeaHost *NymeaHosts::createHost(const QString &name, const QUrl &url) +NymeaHost *NymeaHosts::createHost(const QString &name, const QUrl &url, Connection::BearerType bearerType) { NymeaHost *host = new NymeaHost(this); - host->setUuid(QUuid::createUuid()); host->setName(name); - Connection *connection = new Connection(url, Connection::BearerTypeAll, false, url.toString(), host); + Connection *connection = new Connection(url, bearerType, false, url.toString(), host); host->connections()->addConnection(connection); addHost(host); return host; @@ -195,6 +194,11 @@ void NymeaHostsFilterModel::setShowUnreachableBearers(bool showUnreachableBearer } } +NymeaHost *NymeaHostsFilterModel::get(int index) const +{ + return m_nymeaDiscovery->nymeaHosts()->get(mapToSource(this->index(index, 0)).row()); +} + bool NymeaHostsFilterModel::filterAcceptsRow(int sourceRow, const QModelIndex &sourceParent) const { Q_UNUSED(sourceParent) diff --git a/libnymea-app-core/connection/nymeahosts.h b/libnymea-app-core/connection/nymeahosts.h index 01ae5fcb..f92848c3 100644 --- a/libnymea-app-core/connection/nymeahosts.h +++ b/libnymea-app-core/connection/nymeahosts.h @@ -25,8 +25,8 @@ #include #include #include +#include "nymeahost.h" -class NymeaHost; class NymeaDiscovery; class NymeaConnection; @@ -49,7 +49,7 @@ public: void addHost(NymeaHost *host); void removeHost(NymeaHost *host); - Q_INVOKABLE NymeaHost* createHost(const QString &name, const QUrl &url); + Q_INVOKABLE NymeaHost* createHost(const QString &name, const QUrl &url, Connection::BearerType bearerType); Q_INVOKABLE NymeaHost *get(int index) const; Q_INVOKABLE NymeaHost *find(const QUuid &uuid); @@ -89,6 +89,8 @@ public: bool showUnreachableBearers() const; void setShowUnreachableBearers(bool showUnreachableBearers); + Q_INVOKABLE NymeaHost* get(int index) const; + signals: void countChanged(); void discoveryChanged(); diff --git a/libnymea-app-core/jsonrpc/jsonrpcclient.cpp b/libnymea-app-core/jsonrpc/jsonrpcclient.cpp index e499b1c0..059c455b 100644 --- a/libnymea-app-core/jsonrpc/jsonrpcclient.cpp +++ b/libnymea-app-core/jsonrpc/jsonrpcclient.cpp @@ -363,6 +363,7 @@ void JsonRpcClient::dataReceived(const QByteArray &data) if (!protoVersionString.contains('.')) { protoVersionString.prepend("0."); } + m_jsonRpcVersion = QVersionNumber::fromString(protoVersionString); qDebug() << "Handshake reply:" << "Protocol version:" << protoVersionString << "InitRequired:" << m_initialSetupRequired << "AuthRequired:" << m_authenticationRequired << "PushButtonAvailable:" << m_pushButtonAuthAvailable;; @@ -377,6 +378,11 @@ void JsonRpcClient::dataReceived(const QByteArray &data) emit handshakeReceived(); + if (m_connection->currentHost()->uuid().isNull()) { + qDebug() << "Updating Server UUID in connection:" << m_connection->currentHost()->uuid().toString() << "->" << m_serverUuid; + m_connection->currentHost()->setUuid(m_serverUuid); + } + if (m_initialSetupRequired) { emit initialSetupRequiredChanged(); return; diff --git a/nymea-app/ui/RootItem.qml b/nymea-app/ui/RootItem.qml index 2adae4a4..37bd0539 100644 --- a/nymea-app/ui/RootItem.qml +++ b/nymea-app/ui/RootItem.qml @@ -102,14 +102,13 @@ Item { print("Last connected host was", tabSettings.lastConnectedHost) var cachedHost = discovery.nymeaHosts.find(tabSettings.lastConnectedHost); if (cachedHost) { - engine.connection.currentHost = cachedHost - } else { - print("Warning: There is a last connected host but UUID is unknown to discovery...") + engine.connection.connect(cachedHost) + return; } - } else { - PlatformHelper.hideSplashScreen(); - pageStack.push(Qt.resolvedUrl("connection/ConnectPage.qml"), StackView.Immediate) + print("Warning: There is a last connected host but UUID is unknown to discovery...") } + PlatformHelper.hideSplashScreen(); + pageStack.push(Qt.resolvedUrl("connection/ConnectPage.qml"), StackView.Immediate) } diff --git a/nymea-app/ui/connection/ConnectPage.qml b/nymea-app/ui/connection/ConnectPage.qml index b31c4816..b3b627c9 100644 --- a/nymea-app/ui/connection/ConnectPage.qml +++ b/nymea-app/ui/connection/ConnectPage.qml @@ -60,7 +60,7 @@ Page { } onClicked: { if (index === 2) { - var host = discovery.nymeaHosts.createHost("Demo server", "nymea://nymea.nymea.io:2222") + var host = discovery.nymeaHosts.createHost("Demo server", "nymea://nymea.nymea.io:2222", Connection.BearerTypeCloud) engine.connection.connect(host) } else { pageStack.push(model.get(index).page, {nymeaDiscovery: discovery}); @@ -128,7 +128,7 @@ Page { width: parent.width height: app.delegateHeight objectName: "discoveryDelegate" + index - property var nymeaHost: discovery.nymeaHosts.get(index) + property var nymeaHost: hostsProxy.get(index) property string defaultConnectionIndex: { var usedConfigIndex = 0; for (var i = 1; i < nymeaHost.connections.count; i++) { @@ -209,7 +209,9 @@ Page { onClicked: { if (model.deviceType === NymeaHost.DeviceTypeNetwork) { swipe.close() - var popup = infoDialog.createObject(app,{nymeaHost: discovery.nymeaHosts.get(index)}) + var nymeaHost = hostsProxy.get(index); + print("Getting info for", nymeaHost.name) + var popup = infoDialog.createObject(app,{nymeaHost: nymeaHost}) popup.open() } } @@ -267,7 +269,7 @@ Page { visible: discovery.nymeaHosts.count === 0 text: qsTr("Demo mode (online)") onClicked: { - var host = nymeaHosts.createHost("Demo server", "nymea://nymea.nymea.io:2222") + var host = nymeaHosts.createHost("Demo server", "nymea://nymea.nymea.io:2222", Connection.BearerTypeCloud) engine.connection.connect(host) } } diff --git a/nymea-app/ui/connection/ConnectingPage.qml b/nymea-app/ui/connection/ConnectingPage.qml index dff23c2f..7b3035ec 100644 --- a/nymea-app/ui/connection/ConnectingPage.qml +++ b/nymea-app/ui/connection/ConnectingPage.qml @@ -53,7 +53,7 @@ Page { errorMessage = qsTr("%1:core could not be found on this address. Please make sure you entered the address correctly and that the box is powered on.").arg(app.systemName); break; case NymeaConnection.ConnectionStatusConnectionRefused: - errorMessage = qsTr("The host has rejected our connection. This probably means that %1 stopped running. Did you unplug your %1 box?").arg(app.systemName); + errorMessage = qsTr("The host has rejected our connection. This probably means that %1 is not running on this host. Perhaps it's restarting?").arg(app.systemName); break; case NymeaConnection.ConnectionStatusRemoteHostClosed: errorMessage = qsTr("%1:core has closed the connection. This probably means it has been turned off or restarted.").arg(app.systemName); @@ -70,7 +70,7 @@ Page { break; case NymeaConnection.ConnectionStatusUnknownError: default: - errorMessage = qsTr("An unknown error happened. We're very sorry for that. (Error code: %1)").arg(engine.connection.connectionStatus); + errorMessage = qsTr("An unknown error happened. We're very sorry for that.").arg(engine.connection.connectionStatus); } return errorMessage; } diff --git a/nymea-app/ui/connection/ManualConnectPage.qml b/nymea-app/ui/connection/ManualConnectPage.qml index 1ac9f794..adea028e 100644 --- a/nymea-app/ui/connection/ManualConnectPage.qml +++ b/nymea-app/ui/connection/ManualConnectPage.qml @@ -96,12 +96,8 @@ Page { } print("Try to connect ", rpcUrl) - engine.connection.connect(rpcUrl) - var page = pageStack.push(Qt.resolvedUrl("ConnectingPage.qml")) - page.cancel.connect(function() { - engine.connection.disconnect() - pageStack.pop(root) - }) + var host = discovery.nymeaHosts.createHost("Manual connection", rpcUrl, Connection.BearerTypeCloud); + engine.connection.connect(host) } } } diff --git a/nymea-app/ui/mainviews/FavoritesView.qml b/nymea-app/ui/mainviews/FavoritesView.qml index 4ae51480..eb51c485 100644 --- a/nymea-app/ui/mainviews/FavoritesView.qml +++ b/nymea-app/ui/mainviews/FavoritesView.qml @@ -185,7 +185,7 @@ Item { readonly property var powerState: device.states.getState(powerStateType.id) readonly property var brightnessStateType: deviceClass.stateTypes.findByName("brightness"); - readonly property var brightnessState: device.states.getState(brightnessStateType.id) + readonly property var brightnessState: brightnessStateType ? device.states.getState(brightnessStateType.id) : null ThrottledSlider { Layout.fillWidth: true @@ -195,7 +195,7 @@ Item { enabled: opacity > 0 from: 0 to: 100 - value: brightnessState.value + value: brightnessState ? brightnessState.value : 0 onMoved: { var deviceClass = engine.deviceManager.deviceClasses.getDeviceClass(device.deviceClassId); var actionType = deviceClass.actionTypes.findByName("brightness"); From 5f202789c449da5673743bf9604c11a3724dc549 Mon Sep 17 00:00:00 2001 From: Michael Zanetti Date: Wed, 6 Feb 2019 14:07:26 +0100 Subject: [PATCH 08/11] add dummy implementation for iOS --- nymea-app/platformintegration/ios/platformhelperios.cpp | 5 +++++ nymea-app/platformintegration/ios/platformhelperios.h | 2 ++ 2 files changed, 7 insertions(+) diff --git a/nymea-app/platformintegration/ios/platformhelperios.cpp b/nymea-app/platformintegration/ios/platformhelperios.cpp index 3885d710..f213e37d 100644 --- a/nymea-app/platformintegration/ios/platformhelperios.cpp +++ b/nymea-app/platformintegration/ios/platformhelperios.cpp @@ -12,6 +12,11 @@ void PlatformHelperIOS::requestPermissions() emit permissionsRequestFinished(); } +void PlatformHelperIOS::hideSplashScreen() +{ + // Nothing to be done +} + bool PlatformHelperIOS::hasPermissions() const { return true; diff --git a/nymea-app/platformintegration/ios/platformhelperios.h b/nymea-app/platformintegration/ios/platformhelperios.h index b8af806d..b54a2155 100644 --- a/nymea-app/platformintegration/ios/platformhelperios.h +++ b/nymea-app/platformintegration/ios/platformhelperios.h @@ -13,6 +13,8 @@ public: Q_INVOKABLE virtual void requestPermissions() override; + Q_INVOKABLE void hideSplashScreen() override; + virtual bool hasPermissions() const override; virtual QString machineHostname() const override; virtual QString deviceSerial() const override; From 468bdbd4feceb90e4cdae98f9b0b40d26e320be7 Mon Sep 17 00:00:00 2001 From: Michael Zanetti Date: Wed, 6 Feb 2019 15:03:10 +0100 Subject: [PATCH 09/11] handle unknonw bearer types --- libnymea-app-core/connection/nymeaconnection.cpp | 10 +++++++--- libnymea-app-core/connection/nymeahost.h | 1 + libnymea-app-core/connection/nymeahosts.cpp | 2 +- 3 files changed, 9 insertions(+), 4 deletions(-) diff --git a/libnymea-app-core/connection/nymeaconnection.cpp b/libnymea-app-core/connection/nymeaconnection.cpp index f5a8fe2d..ed769e3b 100644 --- a/libnymea-app-core/connection/nymeaconnection.cpp +++ b/libnymea-app-core/connection/nymeaconnection.cpp @@ -325,7 +325,11 @@ void NymeaConnection::updateActiveBearers() QList configs = m_networkConfigManager->allConfigurations(QNetworkConfiguration::Active); // qDebug() << "Network configuations:" << configs.count(); foreach (const QNetworkConfiguration &config, configs) { -// qDebug() << "Candidate network config:" << config.name() << config.bearerTypeFamily() << config.bearerTypeName(); + qDebug() << "Candidate network config:" << config.name() << config.bearerTypeFamily() << config.bearerTypeName(); + + // NOTE: iOS doesn't correctly report bearer types. It'll be Unknown all the time +// availableBearerTypes.setFlag(Connection::BearerTypeUnknown); + availableBearerTypes.setFlag(qBearerTypeToNymeaBearerType(config.bearerType())); } // qDebug() << "Available bearers:" << availableBearerTypes; @@ -368,8 +372,8 @@ Connection::BearerType NymeaConnection::qBearerTypeToNymeaBearerType(QNetworkCon return Connection::BearerTypeCloud; case QNetworkConfiguration::BearerBluetooth: return Connection::BearerTypeBluetooth; - default: - qWarning() << "Unhandled Bearer Type Family:" << type; + case QNetworkConfiguration::BearerUnknown: + return Connection::BearerTypeUnknown; } return Connection::BearerTypeNone; } diff --git a/libnymea-app-core/connection/nymeahost.h b/libnymea-app-core/connection/nymeahost.h index 52659e53..1f62a336 100644 --- a/libnymea-app-core/connection/nymeahost.h +++ b/libnymea-app-core/connection/nymeahost.h @@ -45,6 +45,7 @@ public: BearerTypeEthernet = 0x02, BearerTypeBluetooth = 0x04, BearerTypeCloud = 0x08, + BearerTypeUnknown = 0xFF, BearerTypeAll = 0xFF }; Q_ENUM(BearerType) diff --git a/libnymea-app-core/connection/nymeahosts.cpp b/libnymea-app-core/connection/nymeahosts.cpp index fa6b8e8b..b00f5e2d 100644 --- a/libnymea-app-core/connection/nymeahosts.cpp +++ b/libnymea-app-core/connection/nymeahosts.cpp @@ -206,7 +206,7 @@ bool NymeaHostsFilterModel::filterAcceptsRow(int sourceRow, const QModelIndex &s if (m_nymeaConnection && !m_showUneachableBearers) { bool hasReachableConnection = false; for (int i = 0; i < host->connections()->rowCount(); i++) { -// qDebug() << "checking host for available bearer" << host->name() << host->connections()->get(i)->url(); + qDebug() << "checking host for available bearer" << host->name() << host->connections()->get(i)->url() << "available bearer types:" << m_nymeaConnection->availableBearerTypes() << "hosts bearer types" << host->connections()->get(i)->bearerType(); if (m_nymeaConnection->availableBearerTypes().testFlag(host->connections()->get(i)->bearerType())) { hasReachableConnection = true; break; From 5f99248fd2d5a342cf378cfa46e61e212528b440 Mon Sep 17 00:00:00 2001 From: Michael Zanetti Date: Thu, 7 Feb 2019 13:12:57 +0100 Subject: [PATCH 10/11] cleanup --- .../discovery/zeroconfdiscovery.cpp | 17 ++-- .../connection/nymeaconnection.cpp | 77 +++++++++++++++---- .../connection/nymeaconnection.h | 3 +- libnymea-app-core/connection/nymeahost.cpp | 11 ++- libnymea-app-core/connection/nymeahosts.cpp | 2 +- nymea-app/ui/RootItem.qml | 2 +- nymea-app/ui/appsettings/CloudLoginPage.qml | 10 +-- nymea-app/ui/connection/ConnectPage.qml | 41 +++------- 8 files changed, 95 insertions(+), 68 deletions(-) diff --git a/libnymea-app-core/connection/discovery/zeroconfdiscovery.cpp b/libnymea-app-core/connection/discovery/zeroconfdiscovery.cpp index 836e75d6..baaf3c8e 100644 --- a/libnymea-app-core/connection/discovery/zeroconfdiscovery.cpp +++ b/libnymea-app-core/connection/discovery/zeroconfdiscovery.cpp @@ -122,12 +122,16 @@ void ZeroconfDiscovery::serviceEntryAdded(const QZeroConfService &entry) } url.setHost(!entry.ip().isNull() ? entry.ip().toString() : entry.ipv6().toString()); url.setPort(entry.port()); - if (!host->connections()->find(url)){ + Connection *connection = host->connections()->find(url); + if (!connection) { qDebug() << "Zeroconf: Adding new connection to host:" << host->name() << url.toString(); QString displayName = QString("%1:%2").arg(url.host()).arg(url.port()); - Connection *connection = new Connection(url, Connection::BearerTypeWifi, sslEnabled, displayName); + connection = new Connection(url, Connection::BearerTypeWifi, sslEnabled, displayName); connection->setOnline(true); host->connections()->addConnection(connection); + } else { + qDebug() << "Zeroconf: Setting connection online:" << host->name() << url.toString(); + connection->setOnline(true); } } @@ -179,14 +183,7 @@ void ZeroconfDiscovery::serviceEntryRemoved(const QZeroConfService &entry) return; } - // Ok, now we need to remove it -// host->connections()->removeConnection(connection); + qDebug() << "Zeroconf: Setting connection offline:" << host->name() << url.toString(); connection->setOnline(false); - - // And if there aren't any connections left, remove the entire device -// if (host->connections()->rowCount() == 0) { -// qDebug() << "Zeroconf: Removing connection from host:" << host->name() << url.toString(); -// m_nymeaHosts->removeHost(host); -// } } #endif diff --git a/libnymea-app-core/connection/nymeaconnection.cpp b/libnymea-app-core/connection/nymeaconnection.cpp index ed769e3b..63e8efea 100644 --- a/libnymea-app-core/connection/nymeaconnection.cpp +++ b/libnymea-app-core/connection/nymeaconnection.cpp @@ -278,18 +278,27 @@ void NymeaConnection::onConnected() if (m_currentTransport != newTransport) { qDebug() << "Alternative connection established:" << newTransport->url(); - Connection *existingConnection = m_transportCandidates.value(m_currentTransport); - Connection *alternativeConnection = m_transportCandidates.value(newTransport); - if (alternativeConnection->priority() > existingConnection->priority()) { - qDebug() << "New connection has higher priority! Roaming from" << existingConnection->url() << existingConnection->priority() << "to" << alternativeConnection->url() << alternativeConnection->priority(); + + // In theory, we could roam from one connection to another. + // However, in practice it turns out there are too many issues for this to be reliable + // So lets just tear down any alternative connection that comes up again. + + m_transportCandidates.remove(newTransport); + newTransport->deleteLater(); + + +// Connection *existingConnection = m_transportCandidates.value(m_currentTransport); +// Connection *alternativeConnection = m_transportCandidates.value(newTransport); +// if (alternativeConnection->priority() > existingConnection->priority()) { +// qDebug() << "New connection has higher priority! Roaming from" << existingConnection->url() << existingConnection->priority() << "to" << alternativeConnection->url() << alternativeConnection->priority(); // m_transportCandidates.remove(m_currentTransport); // m_currentTransport->deleteLater(); - m_currentTransport = newTransport; - } else { - qDebug() << "Connection" << alternativeConnection->url() << alternativeConnection->priority() << "has lower priority than existing" << existingConnection->url() << existingConnection->priority(); - m_transportCandidates.remove(newTransport); - newTransport->deleteLater(); - } +// m_currentTransport = newTransport; +// } else { +// qDebug() << "Connection" << alternativeConnection->url() << alternativeConnection->priority() << "has lower priority than existing" << existingConnection->url() << existingConnection->priority(); +// m_transportCandidates.remove(newTransport); +// newTransport->deleteLater(); +// } return; } } @@ -308,10 +317,21 @@ void NymeaConnection::onDisconnected() m_transportCandidates.remove(m_currentTransport); m_currentTransport->deleteLater(); m_currentTransport = nullptr; + + foreach (NymeaTransportInterface *candidate, m_transportCandidates.keys()) { + if (candidate->connectionState() == NymeaTransportInterface::ConnectionStateConnected) { + qDebug() << "Alternative connection is still up. Roaming to:" << candidate->url(); + m_currentTransport = candidate; + break; + } + } + emit currentConnectionChanged(); - qDebug() << "NymeaConnection: disconnected."; - emit connectedChanged(false); + if (!m_currentTransport) { + qDebug() << "NymeaConnection: disconnected."; + emit connectedChanged(false); + } // Try to reconnect, only if we're not waiting for SSL certs to be trusted. if (m_connectionStatus != ConnectionStatusSslUntrusted) { @@ -345,6 +365,16 @@ void NymeaConnection::updateActiveBearers() qDebug() << "No current host... Nothing to do..."; return; } + + // In theory we could try to connect via any different/new bearers now. However, in practice + // I have observed the following issues: + // - When roaming from WiFi to mobile data, we've already lost WiFi at this point + // (Unless aggressive WiFi to mobile handover is enabled on the phone) + // - When roaming from mobile to Wifi, for some reason, any new connection attempts + // fail as long as the mobile data isn't shut down by the OS. + // Those issues prevent roaming from working properly, so let's just not do anything at + // this point if there already is a connected channel, try reconnecting otherwise. + if (!m_currentTransport) { // There's a host but no connection. Try connecting now... qDebug() << "There's a host but no connection. Trying to connect now..."; @@ -414,8 +444,22 @@ void NymeaConnection::registerTransport(NymeaTransportInterfaceFactory *transpor } } -void NymeaConnection::connect(NymeaHost *nymeaHost) +void NymeaConnection::connect(NymeaHost *nymeaHost, Connection *connection) { + if (!nymeaHost) { + return; + } + + m_preferredConnection = nullptr; + if (connection) { + if (nymeaHost->connections()->find(connection->url())) { + qDebug() << "Setting preferred connection to" << connection->url(); + m_preferredConnection = connection; + } else { + qWarning() << "Connection" << connection << "is not a candidate for" << nymeaHost->name() << "Not setting preferred connection."; + } + } + setCurrentHost(nymeaHost); } @@ -427,6 +471,13 @@ void NymeaConnection::connectInternal(NymeaHost *host) emit connectionStatusChanged(); return; } + + if (m_preferredConnection) { + qDebug() << "Preferred connection is set. Using" << m_preferredConnection->url(); + connectInternal(m_preferredConnection); + return; + } + if (m_availableBearerTypes.testFlag(Connection::BearerTypeWifi) || m_availableBearerTypes.testFlag(Connection::BearerTypeEthernet)) { Connection* lanConnection = host->connections()->bestMatch(Connection::BearerTypeWifi | Connection::BearerTypeEthernet); if (lanConnection) { diff --git a/libnymea-app-core/connection/nymeaconnection.h b/libnymea-app-core/connection/nymeaconnection.h index faeba0a4..ad7c69a3 100644 --- a/libnymea-app-core/connection/nymeaconnection.h +++ b/libnymea-app-core/connection/nymeaconnection.h @@ -43,7 +43,7 @@ public: void registerTransport(NymeaTransportInterfaceFactory *transportFactory); - Q_INVOKABLE void connect(NymeaHost* nymeaHost); + Q_INVOKABLE void connect(NymeaHost* nymeaHost, Connection *connection = nullptr); Q_INVOKABLE void disconnect(); Q_INVOKABLE void acceptCertificate(const QString &url, const QByteArray &pem); Q_INVOKABLE bool isTrusted(const QString &url); @@ -95,6 +95,7 @@ private: QHash m_transportCandidates; NymeaTransportInterface *m_currentTransport = nullptr; NymeaHost *m_currentHost = nullptr; + Connection *m_preferredConnection = nullptr; }; #endif // NYMEACONNECTION_H diff --git a/libnymea-app-core/connection/nymeahost.cpp b/libnymea-app-core/connection/nymeahost.cpp index 3c0b4f00..3e59ed3a 100644 --- a/libnymea-app-core/connection/nymeahost.cpp +++ b/libnymea-app-core/connection/nymeahost.cpp @@ -238,12 +238,17 @@ void Connection::setOnline(bool online) if (m_online != online) { m_online = online; emit onlineChanged(); + emit priorityChanged(); } } int Connection::priority() const { int prio = 0; + if (m_online) { + prio += 1000; + } + switch(m_bearerType) { case BearerTypeEthernet: prio += 400; @@ -263,8 +268,8 @@ int Connection::priority() const if (m_secure) { prio += 10; } -// if (m_url.scheme().startsWith("nymea")) { -// prio += 5; -// } + if (m_url.scheme().startsWith("nymea")) { + prio += 1; + } return prio; } diff --git a/libnymea-app-core/connection/nymeahosts.cpp b/libnymea-app-core/connection/nymeahosts.cpp index b00f5e2d..18b6987e 100644 --- a/libnymea-app-core/connection/nymeahosts.cpp +++ b/libnymea-app-core/connection/nymeahosts.cpp @@ -206,7 +206,7 @@ bool NymeaHostsFilterModel::filterAcceptsRow(int sourceRow, const QModelIndex &s if (m_nymeaConnection && !m_showUneachableBearers) { bool hasReachableConnection = false; for (int i = 0; i < host->connections()->rowCount(); i++) { - qDebug() << "checking host for available bearer" << host->name() << host->connections()->get(i)->url() << "available bearer types:" << m_nymeaConnection->availableBearerTypes() << "hosts bearer types" << host->connections()->get(i)->bearerType(); +// qDebug() << "checking host for available bearer" << host->name() << host->connections()->get(i)->url() << "available bearer types:" << m_nymeaConnection->availableBearerTypes() << "hosts bearer types" << host->connections()->get(i)->bearerType(); if (m_nymeaConnection->availableBearerTypes().testFlag(host->connections()->get(i)->bearerType())) { hasReachableConnection = true; break; diff --git a/nymea-app/ui/RootItem.qml b/nymea-app/ui/RootItem.qml index 37bd0539..8d87e6d4 100644 --- a/nymea-app/ui/RootItem.qml +++ b/nymea-app/ui/RootItem.qml @@ -111,6 +111,7 @@ Item { pageStack.push(Qt.resolvedUrl("connection/ConnectPage.qml"), StackView.Immediate) } + Timer { running: true; repeat: false; interval: 3000; onTriggered: PlatformHelper.hideSplashScreen(); } function init() { print("calling init. Auth required:", engine.jsonRpcClient.authenticationRequired, "initial setup required:", engine.jsonRpcClient.initialSetupRequired, "jsonrpc connected:", engine.jsonRpcClient.connected, "Current host:", engine.connection.currentHost) @@ -151,7 +152,6 @@ Item { } print("pushing ConnectingPage") - PlatformHelper.hideSplashScreen(); var page = pageStack.push(Qt.resolvedUrl("connection/ConnectingPage.qml")); page.cancel.connect(function(){ engine.connection.disconnect(); diff --git a/nymea-app/ui/appsettings/CloudLoginPage.qml b/nymea-app/ui/appsettings/CloudLoginPage.qml index 742227ac..e6f827a7 100644 --- a/nymea-app/ui/appsettings/CloudLoginPage.qml +++ b/nymea-app/ui/appsettings/CloudLoginPage.qml @@ -86,14 +86,10 @@ Page { secondaryIconName: !model.online ? "../images/cloud-error.svg" : "" onClicked: { + print("clicked, connected:", engine.connection.connected, model.id) if (!engine.connection.connected) { - var page = pageStack.push(Qt.resolvedUrl("../connection/ConnectingPage.qml")) - page.cancel.connect(function() { - engine.connection.disconnect() - pageStack.pop(root, StackView.Immediate); - pageStack.push(discoveryPage) - }) - engine.connection.connect("cloud://" + model.id) + var host = discovery.nymeaHosts.find(model.id) + engine.connection.connect(host); } } diff --git a/nymea-app/ui/connection/ConnectPage.qml b/nymea-app/ui/connection/ConnectPage.qml index b3b627c9..19f11b58 100644 --- a/nymea-app/ui/connection/ConnectPage.qml +++ b/nymea-app/ui/connection/ConnectPage.qml @@ -130,39 +130,16 @@ Page { objectName: "discoveryDelegate" + index property var nymeaHost: hostsProxy.get(index) property string defaultConnectionIndex: { - var usedConfigIndex = 0; - for (var i = 1; i < nymeaHost.connections.count; i++) { - var oldConfig = nymeaHost.connections.get(usedConfigIndex); - var newConfig = nymeaHost.connections.get(i); - - // Preference of bearerType - var bearerPreference = [Connection.BearerTypeEthernet, Connection.BearerTypeWifi, Connection.BearerTypeBluetooth, Connection.BearerTypeCloud] - var oldBearerPriority = bearerPreference.indexOf(oldConfig.bearerType); - var newBearerPriority = bearerPreference.indexOf(newConfig.bearerType); - if (newBearerPriority < oldBearerPriority) { - print(nymeaHost.name, "switching to preferred index", i, "of bearer type", newConfig.bearerType, "from", oldConfig.bearerType, "new prio:", newBearerPriority, "old:", oldBearerPriority) - usedConfigIndex = i; - continue; - } - if (oldBearerPriority < newBearerPriority) { - continue; // discard new one the one we have is on a better bearer type - } - - // prefer secure over insecure - if (!oldConfig.secure && newConfig.secure) { - usedConfigIndex = i; - continue; - } - if (oldConfig.secure && !newConfig.secure) { - continue; // discard new one as the one we already have is more secure - } - - // both options are now on the same bearer and either secure or insecure, prefer nymearpc over websocket for less overhead - if (oldConfig.url.toString().startsWith("ws") && newConfig.url.toString().startsWith("nymea")) { - usedConfigIndex = i; + var bestIndex = -1 + var bestPriority = 0; + for (var i = 0; i < nymeaHost.connections.count; i++) { + var connection = nymeaHost.connections.get(i); + if (bestIndex === -1 || connection.priority > bestPriority) { + bestIndex = i; + bestPriority = connection.priority; } } - return usedConfigIndex + return bestIndex; } iconName: { @@ -405,8 +382,8 @@ Page { secondaryIconColor: "red" onClicked: { - root.connectToHost2(dialog.nymeaHost.connections.get(index)) dialog.close() + engine.connection.connect(dialog.nymeaHost, dialog.nymeaHost.connections.get(index)) } } } From 0c7b28ecaed04a657084fddd9793c1dc41f04fd5 Mon Sep 17 00:00:00 2001 From: Michael Zanetti Date: Thu, 7 Feb 2019 17:26:30 +0100 Subject: [PATCH 11/11] reconnect on failure --- libnymea-app-core/connection/nymeaconnection.cpp | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/libnymea-app-core/connection/nymeaconnection.cpp b/libnymea-app-core/connection/nymeaconnection.cpp index 63e8efea..d0b6a188 100644 --- a/libnymea-app-core/connection/nymeaconnection.cpp +++ b/libnymea-app-core/connection/nymeaconnection.cpp @@ -248,11 +248,11 @@ void NymeaConnection::onError(QAbstractSocket::SocketError error) if (!m_currentTransport) { // We're trying to connect and one of the transports failed... - qDebug() << "A transport error happened for" << transport->url() << error << "(Still trying on" << m_transportCandidates.count() << "connections)"; if (m_transportCandidates.contains(transport)) { m_transportCandidates.remove(transport); transport->deleteLater(); } + qDebug() << "A transport error happened for" << transport->url() << error << "(Still trying on" << m_transportCandidates.count() << "connections)"; if (m_transportCandidates.isEmpty()) { m_connectionStatus = errorStatus; emit connectionStatusChanged(); @@ -283,6 +283,7 @@ void NymeaConnection::onConnected() // However, in practice it turns out there are too many issues for this to be reliable // So lets just tear down any alternative connection that comes up again. + qDebug() << "Dropping alternative connection again..."; m_transportCandidates.remove(newTransport); newTransport->deleteLater(); @@ -312,6 +313,16 @@ void NymeaConnection::onDisconnected() m_transportCandidates.remove(t); } t->deleteLater(); + + if (!m_currentTransport && m_transportCandidates.isEmpty()) { + qDebug() << "Last connection dropped. Trying to reconnect.."; + QTimer::singleShot(1000, this, [this](){ + if (m_currentHost) { + connectInternal(m_currentHost); + } + }); + } + return; } m_transportCandidates.remove(m_currentTransport);