From 3ea0ec1f9f48cb115fdce147ac122a7af8b2771e Mon Sep 17 00:00:00 2001 From: Michael Zanetti Date: Thu, 1 Oct 2020 16:56:16 +0200 Subject: [PATCH 1/4] Initial stab on NFC support --- androidservice/androidservice.pro | 2 +- .../controlviews/devicecontrolapplication.cpp | 47 ++++++++-- .../controlviews/devicecontrolapplication.h | 14 +++ .../nymeaappservice/nymeaappservice.h | 2 + nymea-app/images.qrc | 1 + nymea-app/main.cpp | 2 + nymea-app/nfchelper.cpp | 86 +++++++++++++++++ nymea-app/nfchelper.h | 42 +++++++++ nymea-app/nymea-app.pro | 6 +- nymea-app/ui/devicepages/DevicePageBase.qml | 18 +++- nymea-app/ui/images/nfc.svg | 94 +++++++++++++++++++ packaging/android/AndroidManifest.xml | 11 +++ .../nymeaapp/NymeaAppControlsActivity.java | 21 +++++ 13 files changed, 337 insertions(+), 9 deletions(-) create mode 100644 nymea-app/nfchelper.cpp create mode 100644 nymea-app/nfchelper.h create mode 100644 nymea-app/ui/images/nfc.svg diff --git a/androidservice/androidservice.pro b/androidservice/androidservice.pro index 41e7d52e..1b0cb54b 100644 --- a/androidservice/androidservice.pro +++ b/androidservice/androidservice.pro @@ -2,7 +2,7 @@ TEMPLATE = lib TARGET = service CONFIG += dll QT += core androidextras -QT += network qml quick quickcontrols2 svg websockets bluetooth charts +QT += network qml quick quickcontrols2 svg websockets bluetooth charts nfc include(../config.pri) include(../android_openssl/openssl.pri) diff --git a/androidservice/controlviews/devicecontrolapplication.cpp b/androidservice/controlviews/devicecontrolapplication.cpp index 1b319967..70a3e5f3 100644 --- a/androidservice/controlviews/devicecontrolapplication.cpp +++ b/androidservice/controlviews/devicecontrolapplication.cpp @@ -13,6 +13,7 @@ #include #include #include +#include QObject *platformHelperProvider(QQmlEngine *engine, QJSEngine *scriptEngine) { @@ -26,26 +27,31 @@ DeviceControlApplication::DeviceControlApplication(int argc, char *argv[]) : QAp setApplicationName("nymea-app"); setOrganizationName("nymea"); + QNearFieldManager *manager = new QNearFieldManager(this); + int ret = manager->registerNdefMessageHandler(this, SLOT(handleNdefMessage(QNdefMessage,QNearFieldTarget*))); + qDebug() << "*** NFC registered" << ret; + QString nymeaId = QtAndroid::androidActivity().callObjectMethod("nymeaId").toString(); QString thingId = QtAndroid::androidActivity().callObjectMethod("thingId").toString(); QSettings settings; - NymeaDiscovery *discovery = new NymeaDiscovery(this); + m_discovery = new NymeaDiscovery(this); AWSClient::instance()->setConfig(settings.value("cloudEnvironment").toString()); - discovery->setAwsClient(AWSClient::instance()); - NymeaHost *host = discovery->nymeaHosts()->find(nymeaId); + m_discovery->setAwsClient(AWSClient::instance()); + NymeaHost *host = m_discovery->nymeaHosts()->find(nymeaId); - if (!host) { + if (nymeaId.isEmpty() && !host) { qWarning() << "No such nymea host:" << nymeaId; // TODO: We could wait here until the discovery finds it... But it really should be cached already... exit(1); } - Engine *m_engine = new Engine(this); + m_engine = new Engine(this); + + m_engine->jsonRpcClient()->connectToHost(host); qDebug() << "Connecting to:" << host; - m_engine->jsonRpcClient()->connectToHost(host); qDebug() << "Creating QML view"; QQmlApplicationEngine *qmlEngine = new QQmlApplicationEngine(this); @@ -64,3 +70,32 @@ DeviceControlApplication::DeviceControlApplication(int argc, char *argv[]) : QAp qmlEngine->load(QUrl(QLatin1String("qrc:/Main.qml"))); } +void DeviceControlApplication::handleNdefMessage(QNdefMessage message, QNearFieldTarget *target) +{ + qDebug() << "************* NFC message!" << message.toByteArray() << target; + foreach (const QNdefRecord &record, message) { + QNdefNfcUriRecord uriRecord(record); + qDebug() << "record" << uriRecord.uri(); + QUrl url = uriRecord.uri(); + QUuid nymeaId = QUuid(url.host().split('.').first()); + QUuid thingId = QUuid(url.host().split('.').last()); + QList> queryItems = QUrlQuery(url.query()).queryItems(); + for (int i = 0; i < queryItems.count(); i++) { + QUuid stateTypeId = queryItems.at(i).first; + QVariant value = queryItems.at(i).second; + + } + + NymeaHost *host = m_discovery->nymeaHosts()->find(nymeaId); + m_engine->jsonRpcClient()->connectToHost(host); + qmlEngine->rootContext()->setContextProperty("controlledThingId", thingId); + + } +} + +void DeviceControlApplication::createView() +{ + +} + + diff --git a/androidservice/controlviews/devicecontrolapplication.h b/androidservice/controlviews/devicecontrolapplication.h index 4c80e850..ef1fb332 100644 --- a/androidservice/controlviews/devicecontrolapplication.h +++ b/androidservice/controlviews/devicecontrolapplication.h @@ -2,6 +2,11 @@ #define DEVICECONTROLAPPLICATION_H #include +#include +#include + +#include "connection/discovery/nymeadiscovery.h" +#include "engine.h" class DeviceControlApplication : public QApplication { @@ -9,6 +14,15 @@ class DeviceControlApplication : public QApplication public: explicit DeviceControlApplication(int argc, char *argv[]); +private slots: + void handleNdefMessage(QNdefMessage message,QNearFieldTarget* target); + + void createView(); + +private: + NymeaDiscovery *m_discovery = nullptr; + Engine *m_engine = nullptr; + }; #endif // DEVICECONTROLAPPLICATION_H diff --git a/androidservice/nymeaappservice/nymeaappservice.h b/androidservice/nymeaappservice/nymeaappservice.h index 6f6e653a..5e823efa 100644 --- a/androidservice/nymeaappservice/nymeaappservice.h +++ b/androidservice/nymeaappservice/nymeaappservice.h @@ -2,6 +2,8 @@ #define NYMEAAPPSERVICE_H #include +#include +#include #include "engine.h" diff --git a/nymea-app/images.qrc b/nymea-app/images.qrc index 265e31b1..0e2c299e 100644 --- a/nymea-app/images.qrc +++ b/nymea-app/images.qrc @@ -236,5 +236,6 @@ ui/images/connections/nm-signal-100-secure.svg ui/images/connections/bluetooth.svg ui/images/connections/network-wired-disabled.svg + ui/images/nfc.svg diff --git a/nymea-app/main.cpp b/nymea-app/main.cpp index 680de44b..f1b0595b 100644 --- a/nymea-app/main.cpp +++ b/nymea-app/main.cpp @@ -52,6 +52,7 @@ #include "pushnotifications.h" #include "applogcontroller.h" #include "ruletemplates/messages.h" +#include "nfchelper.h" QObject *platformHelperProvider(QQmlEngine *engine, QJSEngine *scriptEngine) { @@ -124,6 +125,7 @@ int main(int argc, char *argv[]) QQmlApplicationEngine *engine = new QQmlApplicationEngine(); qmlRegisterSingletonType("Nymea", 1, 0, "PlatformHelper", platformHelperProvider); + qmlRegisterType("Nymea", 1, 0, "NfcHelper"); PushNotifications::instance()->connectClient(); qmlRegisterSingletonType("Nymea", 1, 0, "PushNotifications", PushNotifications::pushNotificationsProvider); diff --git a/nymea-app/nfchelper.cpp b/nymea-app/nfchelper.cpp new file mode 100644 index 00000000..a93929aa --- /dev/null +++ b/nymea-app/nfchelper.cpp @@ -0,0 +1,86 @@ +#include "nfchelper.h" +#include "types/deviceclass.h" +#include "types/statetype.h" + +#include +#include +#include +#include +#include +#include + +NfcHelper::NfcHelper(QObject *parent) : QObject(parent) +{ + m_manager = new QNearFieldManager(this); + connect(m_manager, &QNearFieldManager::targetDetected, this, &NfcHelper::targetDetected); + connect(m_manager, &QNearFieldManager::targetLost, this, &NfcHelper::targetLost); +} + +bool NfcHelper::busy() const +{ + return m_busy; +} + +void NfcHelper::writeThingStates(Engine *engine, Device *thing) +{ + if (m_busy) { + return; + } + + QUrl url; + url.setScheme("nymea"); + url.setHost(engine->jsonRpcClient()->currentHost()->uuid().toString().remove(QRegExp("[{}]")) + "." + thing->id().toString().remove(QRegExp("[{}]"))); + QUrlQuery query; + for (int i = 0; i < thing->thingClass()->stateTypes()->rowCount(); i++) { + StateType *stateType = thing->thingClass()->stateTypes()->get(i); + ActionType *actionType = thing->thingClass()->actionTypes()->getActionType(stateType->id()); + if (!actionType) { + continue; // Read only state + } + QVariant currentValue = thing->states()->getState(stateType->id())->value(); + query.addQueryItem(stateType->id().toString().remove(QRegExp("[{}]")), currentValue.toString()); + } + url.setQuery(query); + qDebug() << "writing message" << url; + + QNdefNfcUriRecord record; + record.setUri(url); + QNdefMessage message; + message.append(record); + + m_currentMessage = message; + m_manager->startTargetDetection(); + m_busy = true; + emit busyChanged(); +} + +void NfcHelper::targetDetected(QNearFieldTarget *target) +{ + connect(target, &QNearFieldTarget::ndefMessagesWritten, this, &NfcHelper::ndefMessageWritten); + connect(target, &QNearFieldTarget::error, this, &NfcHelper::targetError); + + + QNearFieldTarget::RequestId m_request = target->writeNdefMessages(QList() << m_currentMessage); + if (!m_request.isValid()) { + qDebug() << "Error writing tag"; + //targetError(QNearFieldTarget::NdefWriteError, m_request); + } +} + +void NfcHelper::targetLost(QNearFieldTarget *target) +{ + qDebug() << "Target lost" << target; +} + +void NfcHelper::ndefMessageWritten() +{ + qDebug() << "NDEF message written"; + m_manager->stopTargetDetection(); + m_busy = false; + emit busyChanged(); +} + +void NfcHelper::targetError() +{ + qDebug() << "Target error"; +} diff --git a/nymea-app/nfchelper.h b/nymea-app/nfchelper.h new file mode 100644 index 00000000..a2df5a3d --- /dev/null +++ b/nymea-app/nfchelper.h @@ -0,0 +1,42 @@ +#ifndef NFCHELPER_H +#define NFCHELPER_H + +#include +#include +#include + +#include "types/device.h" +#include "engine.h" + +class NfcHelper : public QObject +{ + Q_OBJECT + Q_PROPERTY(bool busy READ busy NOTIFY busyChanged) + +public: + explicit NfcHelper(QObject *parent = nullptr); + + bool busy() const; + +public slots: + void writeThingStates(Engine *engine, Device *thing); + +signals: + void busyChanged(); + +private slots: + void targetDetected(QNearFieldTarget *target); + void targetLost(QNearFieldTarget *target); + + void ndefMessageWritten(); + void targetError(); + +private: + QNearFieldManager *m_manager = nullptr; + bool m_busy = false; + + QNdefMessage m_currentMessage; + +}; + +#endif // NFCHELPER_H diff --git a/nymea-app/nymea-app.pro b/nymea-app/nymea-app.pro index 8bb1b85e..ffaedaa8 100644 --- a/nymea-app/nymea-app.pro +++ b/nymea-app/nymea-app.pro @@ -2,7 +2,7 @@ TEMPLATE=app TARGET=nymea-app include(../config.pri) -QT += network qml quick quickcontrols2 svg websockets bluetooth charts gui-private +QT += network qml quick quickcontrols2 svg websockets bluetooth charts gui-private nfc INCLUDEPATH += $$top_srcdir/libnymea-app LIBS += -L$$top_builddir/libnymea-app/ -lnymea-app @@ -14,6 +14,7 @@ PRE_TARGETDEPS += ../libnymea-app HEADERS += \ mainmenumodel.h \ + nfchelper.h \ platformintegration/generic/raspberrypihelper.h \ stylecontroller.h \ pushnotifications.h \ @@ -24,6 +25,7 @@ HEADERS += \ SOURCES += main.cpp \ mainmenumodel.cpp \ + nfchelper.cpp \ platformintegration/generic/raspberrypihelper.cpp \ stylecontroller.cpp \ pushnotifications.cpp \ @@ -162,3 +164,5 @@ BR=$$BRANDING target.path = /usr/bin INSTALLS += target + +ANDROID_ABIS = armeabi-v7a arm64-v8a diff --git a/nymea-app/ui/devicepages/DevicePageBase.qml b/nymea-app/ui/devicepages/DevicePageBase.qml index 502cb107..794ffec1 100644 --- a/nymea-app/ui/devicepages/DevicePageBase.qml +++ b/nymea-app/ui/devicepages/DevicePageBase.qml @@ -96,7 +96,7 @@ Page { thingMenu.addItem(menuEntryComponent.createObject(thingMenu, {text: qsTr("Logs"), iconSource: "../images/logs.svg", functionName: "openDeviceLogPage"})) } - if (engine.jsonRpcClient.ensureServerVersion(1.6)) { + if (engine.jsonRpcClient.ensureServerVersion("1.6")) { thingMenu.addItem(menuEntryComponent.createObject(thingMenu, { text: Qt.binding(function() { return favoritesProxy.count === 0 ? qsTr("Mark as favorite") : qsTr("Remove from favorites")}), @@ -111,6 +111,14 @@ Page { functionName: "addToGroup" })) } + + thingMenu.addItem(menuEntryComponent.createObject(thingMenu, + { + text: qsTr("Write NFC tag"), + iconSource: "../images/nfc.svg", + functionName: "writeNfcTag" + + })); } function openDeviceMagicPage() { pageStack.push(Qt.resolvedUrl("../magic/DeviceRulesPage.qml"), {device: root.device}) @@ -138,6 +146,14 @@ Page { pageStack.push(Qt.resolvedUrl("DeviceLogPage.qml"), {device: root.device }); } + NfcHelper { + id: nfcHelper + } + + function writeNfcTag() { + nfcHelper.writeThingStates(engine, root.thing) + } + Component { id: menuEntryComponent IconMenuItem { diff --git a/nymea-app/ui/images/nfc.svg b/nymea-app/ui/images/nfc.svg new file mode 100644 index 00000000..dea348bd --- /dev/null +++ b/nymea-app/ui/images/nfc.svg @@ -0,0 +1,94 @@ + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + diff --git a/packaging/android/AndroidManifest.xml b/packaging/android/AndroidManifest.xml index b8380cf5..5fea09f0 100644 --- a/packaging/android/AndroidManifest.xml +++ b/packaging/android/AndroidManifest.xml @@ -64,6 +64,11 @@ + + + + + @@ -95,6 +100,11 @@ + + + + + @@ -145,4 +155,5 @@ Remove the comment if you do not require these default features. --> + diff --git a/packaging/android/src/io/guh/nymeaapp/NymeaAppControlsActivity.java b/packaging/android/src/io/guh/nymeaapp/NymeaAppControlsActivity.java index ffa9f070..466f66e0 100644 --- a/packaging/android/src/io/guh/nymeaapp/NymeaAppControlsActivity.java +++ b/packaging/android/src/io/guh/nymeaapp/NymeaAppControlsActivity.java @@ -8,6 +8,9 @@ import android.telephony.TelephonyManager; import android.provider.Settings.Secure; import android.os.Vibrator; import android.os.Process; +import android.nfc.NfcAdapter; +import android.nfc.NdefMessage; +import android.os.Parcelable; // An activity spawned by android device controls on demand. @@ -48,4 +51,22 @@ public class NymeaAppControlsActivity extends org.qtproject.qt5.android.bindings v.vibrate(duration); } + @Override + protected void onNewIntent(Intent intent) { + Log.d(TAG, "*************** New intent"); + super.onNewIntent(intent); + if (NfcAdapter.ACTION_NDEF_DISCOVERED.equals(intent.getAction())) { + Parcelable[] rawMessages = + intent.getParcelableArrayExtra(NfcAdapter.EXTRA_NDEF_MESSAGES); + if (rawMessages != null) { + NdefMessage[] messages = new NdefMessage[rawMessages.length]; + for (int i = 0; i < rawMessages.length; i++) { + messages[i] = (NdefMessage) rawMessages[i]; + Log.d(TAG, messages[i].toString()); + } + // Process the messages array. + } + } + } + } From 2508e4dd8a5f06e586b45096a5e07bbcdcccd1b1 Mon Sep 17 00:00:00 2001 From: Michael Zanetti Date: Fri, 2 Oct 2020 23:50:59 +0200 Subject: [PATCH 2/4] More work on NFC support --- androidservice/androidservice.pro | 2 + .../controlviews/devicecontrolapplication.cpp | 17 +- .../controlviews/devicecontrolapplication.h | 3 +- nymea-app/images.qrc | 1 + nymea-app/nymea-app.pro | 8 +- nymea-app/resources.qrc | 1 + nymea-app/ui/devicepages/DevicePageBase.qml | 6 +- nymea-app/ui/images/smartphone.svg | 166 +++++++++++++++ nymea-app/ui/magic/WriteNfcTagPage.qml | 197 ++++++++++++++++++ 9 files changed, 386 insertions(+), 15 deletions(-) create mode 100644 nymea-app/ui/images/smartphone.svg create mode 100644 nymea-app/ui/magic/WriteNfcTagPage.qml diff --git a/androidservice/androidservice.pro b/androidservice/androidservice.pro index 1b0cb54b..08ab083e 100644 --- a/androidservice/androidservice.pro +++ b/androidservice/androidservice.pro @@ -29,6 +29,7 @@ SOURCES += \ nymeaappservice/androidbinder.cpp \ ../nymea-app/stylecontroller.cpp \ ../nymea-app/platformhelper.cpp \ + ../nymea-app/nfchelper.cpp \ ../nymea-app/platformintegration/android/platformhelperandroid.cpp \ service_main.cpp @@ -38,6 +39,7 @@ HEADERS += \ nymeaappservice/androidbinder.h \ ../nymea-app/stylecontroller.h \ ../nymea-app/platformhelper.h \ + ../nymea-app/nfchelper.h \ ../nymea-app/platformintegration/android/platformhelperandroid.h \ DISTFILES += \ diff --git a/androidservice/controlviews/devicecontrolapplication.cpp b/androidservice/controlviews/devicecontrolapplication.cpp index 70a3e5f3..39a724a1 100644 --- a/androidservice/controlviews/devicecontrolapplication.cpp +++ b/androidservice/controlviews/devicecontrolapplication.cpp @@ -6,6 +6,7 @@ #include "libnymea-app-core.h" #include "../nymea-app/stylecontroller.h" #include "../nymea-app/platformhelper.h" +#include "../nymea-app/nfchelper.h" #include "../nymea-app/platformintegration/android/platformhelperandroid.h" #include @@ -54,20 +55,21 @@ DeviceControlApplication::DeviceControlApplication(int argc, char *argv[]) : QAp qDebug() << "Connecting to:" << host; qDebug() << "Creating QML view"; - QQmlApplicationEngine *qmlEngine = new QQmlApplicationEngine(this); + m_qmlEngine = new QQmlApplicationEngine(this); registerQmlTypes(); qmlRegisterSingletonType("Nymea", 1, 0, "PlatformHelper", platformHelperProvider); qmlRegisterSingletonType(QUrl("qrc:///ui/utils/NymeaUtils.qml"), "Nymea", 1, 0, "NymeaUtils" ); + qmlRegisterType("Nymea", 1, 0, "NfcHelper"); StyleController styleController; - qmlEngine->rootContext()->setContextProperty("styleController", &styleController); - qmlEngine->rootContext()->setContextProperty("engine", m_engine); - qmlEngine->rootContext()->setContextProperty("_engine", m_engine); - qmlEngine->rootContext()->setContextProperty("controlledThingId", thingId); + m_qmlEngine->rootContext()->setContextProperty("styleController", &styleController); + m_qmlEngine->rootContext()->setContextProperty("engine", m_engine); + m_qmlEngine->rootContext()->setContextProperty("_engine", m_engine); + m_qmlEngine->rootContext()->setContextProperty("controlledThingId", thingId); - qmlEngine->load(QUrl(QLatin1String("qrc:/Main.qml"))); + m_qmlEngine->load(QUrl(QLatin1String("qrc:/Main.qml"))); } void DeviceControlApplication::handleNdefMessage(QNdefMessage message, QNearFieldTarget *target) @@ -88,8 +90,7 @@ void DeviceControlApplication::handleNdefMessage(QNdefMessage message, QNearFiel NymeaHost *host = m_discovery->nymeaHosts()->find(nymeaId); m_engine->jsonRpcClient()->connectToHost(host); - qmlEngine->rootContext()->setContextProperty("controlledThingId", thingId); - + m_qmlEngine->rootContext()->setContextProperty("controlledThingId", thingId); } } diff --git a/androidservice/controlviews/devicecontrolapplication.h b/androidservice/controlviews/devicecontrolapplication.h index ef1fb332..aa557ee8 100644 --- a/androidservice/controlviews/devicecontrolapplication.h +++ b/androidservice/controlviews/devicecontrolapplication.h @@ -4,6 +4,7 @@ #include #include #include +#include #include "connection/discovery/nymeadiscovery.h" #include "engine.h" @@ -22,7 +23,7 @@ private slots: private: NymeaDiscovery *m_discovery = nullptr; Engine *m_engine = nullptr; - + QQmlApplicationEngine *m_qmlEngine = nullptr; }; #endif // DEVICECONTROLAPPLICATION_H diff --git a/nymea-app/images.qrc b/nymea-app/images.qrc index 0e2c299e..dd6ab995 100644 --- a/nymea-app/images.qrc +++ b/nymea-app/images.qrc @@ -237,5 +237,6 @@ ui/images/connections/bluetooth.svg ui/images/connections/network-wired-disabled.svg ui/images/nfc.svg + ui/images/smartphone.svg diff --git a/nymea-app/nymea-app.pro b/nymea-app/nymea-app.pro index ffaedaa8..e847d96d 100644 --- a/nymea-app/nymea-app.pro +++ b/nymea-app/nymea-app.pro @@ -165,4 +165,10 @@ BR=$$BRANDING target.path = /usr/bin INSTALLS += target -ANDROID_ABIS = armeabi-v7a arm64-v8a +ANDROID_ABIS = armeabi-v7a + +contains(ANDROID_TARGET_ARCH,) { + ANDROID_ABIS = \ + armeabi-v7a \ + arm64-v8a +} diff --git a/nymea-app/resources.qrc b/nymea-app/resources.qrc index 8a09163c..3867539f 100644 --- a/nymea-app/resources.qrc +++ b/nymea-app/resources.qrc @@ -223,5 +223,6 @@ ui/components/BatteryStatusIcon.qml ui/components/SetupStatusIcon.qml ui/components/UpdateStatusIcon.qml + ui/magic/WriteNfcTagPage.qml diff --git a/nymea-app/ui/devicepages/DevicePageBase.qml b/nymea-app/ui/devicepages/DevicePageBase.qml index 794ffec1..2acfaa2b 100644 --- a/nymea-app/ui/devicepages/DevicePageBase.qml +++ b/nymea-app/ui/devicepages/DevicePageBase.qml @@ -146,12 +146,8 @@ Page { pageStack.push(Qt.resolvedUrl("DeviceLogPage.qml"), {device: root.device }); } - NfcHelper { - id: nfcHelper - } - function writeNfcTag() { - nfcHelper.writeThingStates(engine, root.thing) + pageStack.push(Qt.resolvedUrl("../magic/WriteNfcTagPage.qml"), {thing: root.thing}) } Component { diff --git a/nymea-app/ui/images/smartphone.svg b/nymea-app/ui/images/smartphone.svg new file mode 100644 index 00000000..1cf0b449 --- /dev/null +++ b/nymea-app/ui/images/smartphone.svg @@ -0,0 +1,166 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + diff --git a/nymea-app/ui/magic/WriteNfcTagPage.qml b/nymea-app/ui/magic/WriteNfcTagPage.qml new file mode 100644 index 00000000..d5cfe4a1 --- /dev/null +++ b/nymea-app/ui/magic/WriteNfcTagPage.qml @@ -0,0 +1,197 @@ +/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * +* +* Copyright 2013 - 2020, nymea GmbH +* Contact: contact@nymea.io +* +* This file is part of nymea. +* This project including source code and documentation is protected by +* copyright law, and remains the property of nymea GmbH. All rights, including +* reproduction, publication, editing and translation, are reserved. The use of +* this project is subject to the terms of a license agreement to be concluded +* with nymea GmbH in accordance with the terms of use of nymea GmbH, available +* under https://nymea.io/license +* +* GNU General Public License Usage +* Alternatively, this project may be redistributed and/or modified under the +* terms of the GNU General Public License as published by the Free Software +* Foundation, GNU version 3. This project 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 +* this project. If not, see . +* +* For any further details and any questions please contact us under +* contact@nymea.io or see our FAQ/Licensing Information on +* https://nymea.io/license/faq +* +* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ + +import QtQuick 2.5 +import QtQuick.Controls 2.1 +import QtQuick.Layouts 1.1 +import Nymea 1.0 +import "../components" + +Page { + id: root + property Thing thing: null + readonly property ThingClass thingClass: thing.thingClass + + header: NymeaHeader { + text: qsTr("Write NFC tag") + onBackPressed: { + root.backPressed(); + } + } + + + NfcHelper { + id: nfcHelper + } +// nfcHelper.writeThingStates(engine, root.thing) + + GridLayout { + anchors.fill: parent + columns: app.landscape ? 2 : 1 + + Item { + Layout.preferredWidth: Math.min(root.width / parent.columns, root.height) + Layout.preferredHeight: app.iconSize * 8 + + SequentialAnimation { + loops: Animation.Infinite + running: true + + PropertyAction { target: phoneIcon; property: "anchors.horizontalCenterOffset"; value: app.iconSize * 2 } + PropertyAction { target: phoneIcon; property: "scale"; value: 1.3 } + NumberAnimation { + target: phoneIcon + property: "opacity" + duration: 500 + to: 1 + } + + PauseAnimation { duration: 500 } + ParallelAnimation { + NumberAnimation { + target: phoneIcon + property: "anchors.horizontalCenterOffset" + from: app.iconSize * 2 + to: -app.iconSize * 2 + duration: 1500 + easing.type: Easing.OutQuad + } + + NumberAnimation { + target: phoneIcon + property: "scale" + duration: 1500 + from: 1.3 + to: 1 + easing.type: Easing.InOutQuad + } + } + + ParallelAnimation { + loops: 2 + NumberAnimation { + duration: 250 + target: vibrateCircle + property: "scale" + from: .8 + to: 1.5 + } + NumberAnimation { + duration: 250 + target: vibrateCircle + property: "opacity" + from: 1 + to: 0 + } + } + PauseAnimation { + duration: 500 + } + + NumberAnimation { + target: phoneIcon + property: "opacity" + duration: 500 + to: 0 + } + } + + + ColorIcon { + id: nfcIcon + name: "../images/nfc.svg" + height: app.iconSize * 2 + width: app.iconSize * 2 + anchors.centerIn: parent + anchors.horizontalCenterOffset: - app.iconSize * 2 + } + + Item { + id: phoneIcon + height: app.iconSize * 5 + width: app.iconSize * 5 + scale: 1.5 + anchors.centerIn: parent + anchors.horizontalCenterOffset: app.iconSize * 2 + + Rectangle { + id: vibrateCircle + anchors.centerIn: parent + anchors.fill: parent + radius: width / 2 + color: "transparent" +// border.color: nfcIcon.keyColor + border.color: app.accentColor + border.width: 2 + scale: .8 + opacity: 0 + } + + Rectangle { + anchors.fill: parent + anchors.leftMargin: phoneIcon.width * .21 + anchors.rightMargin: phoneIcon.width * .21 + anchors.topMargin: phoneIcon.height * .1 + anchors.bottomMargin: phoneIcon.height * .1 + color: app.backgroundColor + } + + ColorIcon { + name: "../images/smartphone.svg" + anchors.fill: parent + } + } + + } + + ColumnLayout { + + Label { + Layout.fillWidth: true + Layout.leftMargin: app.margins; Layout.rightMargin: app.margins + text: qsTr("Select the wanted states and tap an NFC tag to store them. When tapping this tag later, they will be restored.").arg(root.thing.name) + wrapMode: Text.WordWrap + font.pixelSize: app.smallFont + } + + ListView { + Layout.fillWidth: true + Layout.fillHeight: true + model: root.thingClass.stateTypes + clip: true + delegate: CheckDelegate { + width: parent.width + text: model.displayName + checked: true + } + } + } + } +} From 621bfaed91d6ebf839738baf817618a1b1c58531 Mon Sep 17 00:00:00 2001 From: Michael Zanetti Date: Mon, 12 Oct 2020 14:59:15 +0200 Subject: [PATCH 3/4] More work on NFC --- .../controlviews/devicecontrolapplication.cpp | 190 +++++++++--- .../controlviews/devicecontrolapplication.h | 10 +- libnymea-app/types/actiontypes.cpp | 3 +- nymea-app/nfchelper.cpp | 163 ++++++++--- nymea-app/nfchelper.h | 47 ++- nymea-app/nymea-app.pro | 3 +- nymea-app/ui/images/nfc.svg | 10 +- nymea-app/ui/magic/WriteNfcTagPage.qml | 272 ++++++++++-------- .../guh/nymeaapp/NymeaAppControlService.java | 3 +- .../nymeaapp/NymeaAppControlsActivity.java | 23 +- .../nymeaapp/NymeaAppServiceConnection.java | 5 +- 11 files changed, 487 insertions(+), 242 deletions(-) diff --git a/androidservice/controlviews/devicecontrolapplication.cpp b/androidservice/controlviews/devicecontrolapplication.cpp index 39a724a1..6e0560a5 100644 --- a/androidservice/controlviews/devicecontrolapplication.cpp +++ b/androidservice/controlviews/devicecontrolapplication.cpp @@ -14,6 +14,7 @@ #include #include #include +#include #include QObject *platformHelperProvider(QQmlEngine *engine, QJSEngine *scriptEngine) @@ -28,75 +29,180 @@ DeviceControlApplication::DeviceControlApplication(int argc, char *argv[]) : QAp setApplicationName("nymea-app"); setOrganizationName("nymea"); - QNearFieldManager *manager = new QNearFieldManager(this); - int ret = manager->registerNdefMessageHandler(this, SLOT(handleNdefMessage(QNdefMessage,QNearFieldTarget*))); - qDebug() << "*** NFC registered" << ret; - - QString nymeaId = QtAndroid::androidActivity().callObjectMethod("nymeaId").toString(); - QString thingId = QtAndroid::androidActivity().callObjectMethod("thingId").toString(); - QSettings settings; m_discovery = new NymeaDiscovery(this); AWSClient::instance()->setConfig(settings.value("cloudEnvironment").toString()); m_discovery->setAwsClient(AWSClient::instance()); - NymeaHost *host = m_discovery->nymeaHosts()->find(nymeaId); - - if (nymeaId.isEmpty() && !host) { - qWarning() << "No such nymea host:" << nymeaId; - // TODO: We could wait here until the discovery finds it... But it really should be cached already... - exit(1); - } m_engine = new Engine(this); - m_engine->jsonRpcClient()->connectToHost(host); - - qDebug() << "Connecting to:" << host; - - qDebug() << "Creating QML view"; m_qmlEngine = new QQmlApplicationEngine(this); - registerQmlTypes(); - qmlRegisterSingletonType("Nymea", 1, 0, "PlatformHelper", platformHelperProvider); qmlRegisterSingletonType(QUrl("qrc:///ui/utils/NymeaUtils.qml"), "Nymea", 1, 0, "NymeaUtils" ); qmlRegisterType("Nymea", 1, 0, "NfcHelper"); - StyleController styleController; - m_qmlEngine->rootContext()->setContextProperty("styleController", &styleController); + StyleController *styleController = new StyleController(this); + m_qmlEngine->rootContext()->setContextProperty("styleController", styleController); m_qmlEngine->rootContext()->setContextProperty("engine", m_engine); m_qmlEngine->rootContext()->setContextProperty("_engine", m_engine); - m_qmlEngine->rootContext()->setContextProperty("controlledThingId", thingId); + m_qmlEngine->rootContext()->setContextProperty("controlledThingId", ""); // Unknown at this point m_qmlEngine->load(QUrl(QLatin1String("qrc:/Main.qml"))); -} -void DeviceControlApplication::handleNdefMessage(QNdefMessage message, QNearFieldTarget *target) -{ - qDebug() << "************* NFC message!" << message.toByteArray() << target; - foreach (const QNdefRecord &record, message) { - QNdefNfcUriRecord uriRecord(record); - qDebug() << "record" << uriRecord.uri(); - QUrl url = uriRecord.uri(); - QUuid nymeaId = QUuid(url.host().split('.').first()); - QUuid thingId = QUuid(url.host().split('.').last()); - QList> queryItems = QUrlQuery(url.query()).queryItems(); - for (int i = 0; i < queryItems.count(); i++) { - QUuid stateTypeId = queryItems.at(i).first; - QVariant value = queryItems.at(i).second; + jboolean startedByNfc = QtAndroid::androidActivity().callMethod("startedByNfc", "()Z"); + if (startedByNfc) { + qDebug() << "**** Started by NFC"; + qDebug() << "Registering NFC handler and waiting for message."; - } + QNearFieldManager *manager = new QNearFieldManager(this); + manager->registerNdefMessageHandler(this, SLOT(handleNdefMessage(QNdefMessage,QNearFieldTarget*))); - NymeaHost *host = m_discovery->nymeaHosts()->find(nymeaId); - m_engine->jsonRpcClient()->connectToHost(host); + } else { + qDebug() << "*** Started by other intent"; + qDebug() << "Expecing nymeaId and thingId in intent extras."; + QString nymeaId = QtAndroid::androidActivity().callObjectMethod("nymeaId").toString(); + QString thingId = QtAndroid::androidActivity().callObjectMethod("thingId").toString(); + + connectToNymea(nymeaId); m_qmlEngine->rootContext()->setContextProperty("controlledThingId", thingId); } } -void DeviceControlApplication::createView() +void DeviceControlApplication::handleNdefMessage(QNdefMessage message, QNearFieldTarget *target) { + qDebug() << "************* NFC message!" << message.toByteArray(); + if (message.count() < 1) { + qWarning() << "NFC message doesn't contain any records..."; + return; + } + // NOTE: At this point we're only supporting one NDEF record per message + QNdefRecord record = message.first(); + QNdefNfcUriRecord uriRecord(record); + QUrl url = uriRecord.uri(); + if (url.scheme() != "nymea") { + qWarning() << "NDEF URI record scheme is not \"nymea://\""; + return; + } + + QUuid nymeaId = QUuid(url.host()); + if (nymeaId.isNull()) { + qWarning() << "Invalid nymea UUID in NDEF record."; + return; + } + + QUuid thingId = QUuid(QUrlQuery(url).queryItemValue("t")); + if (thingId.isNull()) { + qWarning() << "Invalid thing in NDEF record"; + return; + } + + m_pendingNfcAction = url; + + connectToNymea(nymeaId); + m_qmlEngine->rootContext()->setContextProperty("controlledThingId", thingId); + + connect(m_engine->thingManager(), &DeviceManager::fetchingDataChanged, [this](){ + if (m_engine->jsonRpcClient()->connected() && !m_engine->thingManager()->fetchingData()) { + qDebug() << "Ready to process commands"; + runNfcAction(); + } + }); +} + +void DeviceControlApplication::connectToNymea(const QUuid &nymeaId) +{ + NymeaHost *host = m_discovery->nymeaHosts()->find(nymeaId); + if (!host) { + qWarning() << "No such nymea host:" << nymeaId; + // TODO: We could wait here until the discovery finds it... But it really should be cached already... + exit(1); + } + qDebug() << "Connecting to:" << host->name(); + m_engine->jsonRpcClient()->connectToHost(host); +} + +void DeviceControlApplication::runNfcAction() +{ + if (!m_pendingNfcAction.isEmpty()) { + qDebug() << "NFC action:" << m_pendingNfcAction; + } + QUrl url = m_pendingNfcAction; + m_pendingNfcAction.clear(); + + if (url.scheme() != "nymea") { + qWarning() << "NDEF URI record scheme is not \"nymea://\" in" << url.toString(); + return; + } + + QUuid nymeaId = QUuid(url.host()); + if (nymeaId.isNull()) { + qWarning() << "Invalid nymea UUID" << url.host() << "in NDEF record" << url.toString(); + return; + } + + QUuid thingId = QUuid(QUrlQuery(url).queryItemValue("t")); + Device *thing = m_engine->thingManager()->things()->getThing(thingId); + if (!thing) { + qDebug() << "Thing" << thingId.toString() << "from" << url.toString() << "doesn't exist on nymea host" << nymeaId.toString(); + return; + } + + QList> queryItems = QUrlQuery(url.query()).queryItems(); + for (int i = 0; i < queryItems.count(); i++) { + QString entryName = queryItems.at(i).first; + if (entryName == "t") { + continue; + } + if (!entryName.startsWith("a")) { + qDebug() << "Only actions are supported. Skipping query item" << entryName; + continue; + } + + QString actionString = queryItems.at(i).second; + QStringList parts = actionString.split("#"); + if (parts.count() == 0) { + qDebug() << "Invalid action definition:" << actionString; + continue; + } + + QString actionTypeName = parts.at(0); + ActionType *actionType = thing->thingClass()->actionTypes()->findByName(actionTypeName); + if (!actionType) { + qWarning() << "Invalid action name" << actionType << "in url:" << url.toString(); + continue; + } + + QHash paramsInUri; + if (parts.count() > 1) { + QString paramsString = parts.at(1); + foreach (const QString ¶mString, paramsString.split("+")) { + QStringList parts = paramString.split(":"); + if (parts.count() != 2) { + qWarning() << "Invalid param format" << paramString << "in url:" << url.toString(); + continue; + } + paramsInUri.insert(parts.at(0), parts.at(1)); + } + } + + QVariantList params; + for (int j = 0; j < actionType->paramTypes()->rowCount(); j++) { + ParamType *paramType = actionType->paramTypes()->get(j); + QVariantMap param; + param.insert("paramTypeId", paramType->id()); + if (paramsInUri.contains(paramType->name())) { + param.insert("value", paramsInUri.value(paramType->name())); + } else { + param.insert("value", paramType->defaultValue()); + } + params.append(param); + } + + m_engine->thingManager()->executeAction(thingId, actionType->id(), params); + } } diff --git a/androidservice/controlviews/devicecontrolapplication.h b/androidservice/controlviews/devicecontrolapplication.h index aa557ee8..edd6985c 100644 --- a/androidservice/controlviews/devicecontrolapplication.h +++ b/androidservice/controlviews/devicecontrolapplication.h @@ -5,7 +5,9 @@ #include #include #include +#include +#include "types/ruleactions.h" #include "connection/discovery/nymeadiscovery.h" #include "engine.h" @@ -18,12 +20,18 @@ public: private slots: void handleNdefMessage(QNdefMessage message,QNearFieldTarget* target); - void createView(); + void connectToNymea(const QUuid &nymeaId); + + void runNfcAction(); private: NymeaDiscovery *m_discovery = nullptr; Engine *m_engine = nullptr; QQmlApplicationEngine *m_qmlEngine = nullptr; + + QUrl m_pendingNfcAction; + + }; #endif // DEVICECONTROLAPPLICATION_H diff --git a/libnymea-app/types/actiontypes.cpp b/libnymea-app/types/actiontypes.cpp index 7d17e648..14972811 100644 --- a/libnymea-app/types/actiontypes.cpp +++ b/libnymea-app/types/actiontypes.cpp @@ -49,11 +49,12 @@ ActionType *ActionTypes::get(int index) const ActionType *ActionTypes::getActionType(const QUuid &actionTypeId) const { foreach (ActionType *actionType, m_actionTypes) { + qDebug() << "checking:" << actionType->id(); if (actionType->id() == actionTypeId) { return actionType; } } - return 0; + return nullptr; } int ActionTypes::rowCount(const QModelIndex &parent) const diff --git a/nymea-app/nfchelper.cpp b/nymea-app/nfchelper.cpp index a93929aa..6e7fa893 100644 --- a/nymea-app/nfchelper.cpp +++ b/nymea-app/nfchelper.cpp @@ -1,6 +1,9 @@ #include "nfchelper.h" #include "types/deviceclass.h" #include "types/statetype.h" +#include "types/ruleaction.h" +#include "types/ruleactionparams.h" +#include "types/ruleactionparam.h" #include #include @@ -9,36 +12,126 @@ #include #include -NfcHelper::NfcHelper(QObject *parent) : QObject(parent) +NfcHelper::NfcHelper(QObject *parent): + QObject(parent), + m_manager(new QNearFieldManager(this)), + m_actions(new RuleActions(this)) { - m_manager = new QNearFieldManager(this); + connect(m_manager, &QNearFieldManager::targetDetected, this, &NfcHelper::targetDetected); connect(m_manager, &QNearFieldManager::targetLost, this, &NfcHelper::targetLost); + + connect(m_actions, &RuleActions::countChanged, this, &NfcHelper::updateContent); + + m_manager->startTargetDetection(); + } -bool NfcHelper::busy() const +NfcHelper::~NfcHelper() { - return m_busy; + m_manager->stopTargetDetection(); } -void NfcHelper::writeThingStates(Engine *engine, Device *thing) +Engine *NfcHelper::engine() const { - if (m_busy) { - return; + return m_engine; +} + +void NfcHelper::setEngine(Engine *engine) +{ + if (m_engine != engine) { + m_engine = engine; + emit engineChanged(); + updateContent(); } +} + +Device *NfcHelper::thing() const +{ + return m_thing; +} + +void NfcHelper::setThing(Device *thing) +{ + if (m_thing != thing) { + m_thing = thing; + emit thingChanged(); + updateContent(); + } +} + +RuleActions *NfcHelper::actions() const +{ + return m_actions; +} + +int NfcHelper::messageSize() const +{ + return m_currentMessage.toByteArray().size(); + int ret = 0; + for (int i = 0; i < m_currentMessage.size(); i++) { + ret += m_currentMessage.at(i).payload().size(); + } + return ret; +} + +NfcHelper::TagStatus NfcHelper::status() const +{ + return m_status; +} + +void NfcHelper::updateContent() +{ + qDebug() << "Updating" << m_engine << m_thing; + + // Creating an URI type record with this format: + // nymea:// + // ? t= + // & a[0]= + // & a[1]=#: + // & a[2]=#:+: + // & ... + + // NOTE: We're using actionType and paramType *name* instead of the ID because NFC tags are + // small and normally names are shorter than ids so we save some space. + + // NOTE: param values are percentage encoded to prevent messing with the parsing if they + // contain + or : QUrl url; url.setScheme("nymea"); - url.setHost(engine->jsonRpcClient()->currentHost()->uuid().toString().remove(QRegExp("[{}]")) + "." + thing->id().toString().remove(QRegExp("[{}]"))); + if (!m_engine || !m_thing) { + return; + } + url.setHost(m_engine->jsonRpcClient()->currentHost()->uuid().toString().remove(QRegExp("[{}]"))); + QUrlQuery query; - for (int i = 0; i < thing->thingClass()->stateTypes()->rowCount(); i++) { - StateType *stateType = thing->thingClass()->stateTypes()->get(i); - ActionType *actionType = thing->thingClass()->actionTypes()->getActionType(stateType->id()); - if (!actionType) { - continue; // Read only state + + query.addQueryItem("t", m_thing->id().toString().remove(QRegExp("[{}]"))); + + for (int i = 0; i < m_actions->rowCount(); i++) { + RuleAction *action = m_actions->get(i); + QStringList params; + ActionType *at = m_thing->thingClass()->actionTypes()->getActionType(action->actionTypeId()); + if (!at) { + qWarning() << "ActionType not found in thing" << action->actionTypeId(); + continue; } - QVariant currentValue = thing->states()->getState(stateType->id())->value(); - query.addQueryItem(stateType->id().toString().remove(QRegExp("[{}]")), currentValue.toString()); + + for (int j = 0; j < action->ruleActionParams()->rowCount(); j++) { + RuleActionParam *param = action->ruleActionParams()->get(j); + ParamType *pt = at->paramTypes()->getParamType(param->paramTypeId()); + if (!pt) { + qWarning() << "ParamType not found in thing"; + continue; + } + params.append(pt->name() + ":" + param->value().toByteArray().toPercentEncoding()); + } + QString actionString = at->name(); + if (params.length() > 0) { + actionString += "#" + params.join("+"); + } + query.addQueryItem(QString("a[%1]").arg(i), actionString); } url.setQuery(query); qDebug() << "writing message" << url; @@ -49,38 +142,40 @@ void NfcHelper::writeThingStates(Engine *engine, Device *thing) message.append(record); m_currentMessage = message; - m_manager->startTargetDetection(); - m_busy = true; - emit busyChanged(); + emit messageSizeChanged(); + } void NfcHelper::targetDetected(QNearFieldTarget *target) { - connect(target, &QNearFieldTarget::ndefMessagesWritten, this, &NfcHelper::ndefMessageWritten); - connect(target, &QNearFieldTarget::error, this, &NfcHelper::targetError); - + QDateTime startTime = QDateTime::currentDateTime(); + qDebug() << "target detected"; + connect(target, &QNearFieldTarget::error, this, [=](QNearFieldTarget::Error error, const QNearFieldTarget::RequestId &id){ + qDebug() << "Tag error:" << error; + m_status = TagStatusFailed; + emit statusChanged(); + }); + connect(target, &QNearFieldTarget::ndefMessagesWritten, this, [=](){ + qDebug() << "Tag written in" << startTime.msecsTo(QDateTime::currentDateTime()); + m_status = TagStatusWritten; + emit statusChanged(); + }); QNearFieldTarget::RequestId m_request = target->writeNdefMessages(QList() << m_currentMessage); if (!m_request.isValid()) { qDebug() << "Error writing tag"; - //targetError(QNearFieldTarget::NdefWriteError, m_request); + m_status = TagStatusFailed; + emit statusChanged(); } + + m_status = TagStatusWriting; + emit statusChanged(); } void NfcHelper::targetLost(QNearFieldTarget *target) { qDebug() << "Target lost" << target; + m_status = TagStatusWaiting; + emit statusChanged(); } -void NfcHelper::ndefMessageWritten() -{ - qDebug() << "NDEF message written"; - m_manager->stopTargetDetection(); - m_busy = false; - emit busyChanged(); -} - -void NfcHelper::targetError() -{ - qDebug() << "Target error"; -} diff --git a/nymea-app/nfchelper.h b/nymea-app/nfchelper.h index a2df5a3d..51ef05be 100644 --- a/nymea-app/nfchelper.h +++ b/nymea-app/nfchelper.h @@ -7,33 +7,62 @@ #include "types/device.h" #include "engine.h" +#include "types/ruleactions.h" class NfcHelper : public QObject { Q_OBJECT - Q_PROPERTY(bool busy READ busy NOTIFY busyChanged) + Q_PROPERTY(Engine *engine READ engine WRITE setEngine NOTIFY engineChanged) + Q_PROPERTY(Device *thing READ thing WRITE setThing NOTIFY thingChanged) + Q_PROPERTY(RuleActions *actions READ actions CONSTANT) + Q_PROPERTY(int messageSize READ messageSize NOTIFY messageSizeChanged) + Q_PROPERTY(TagStatus status READ status NOTIFY statusChanged) + public: + enum TagStatus { + TagStatusWaiting, + TagStatusWriting, + TagStatusWritten, + TagStatusFailed + }; + Q_ENUM(TagStatus) + explicit NfcHelper(QObject *parent = nullptr); + ~NfcHelper(); - bool busy() const; + Engine *engine() const; + void setEngine(Engine *engine); -public slots: - void writeThingStates(Engine *engine, Device *thing); + Device *thing() const; + void setThing(Device *thing); + + RuleActions *actions() const; + + int messageSize() const; + + TagStatus status() const; signals: - void busyChanged(); + void engineChanged(); + void thingChanged(); + + void messageSizeChanged(); + void statusChanged(); private slots: + void updateContent(); + void targetDetected(QNearFieldTarget *target); void targetLost(QNearFieldTarget *target); - void ndefMessageWritten(); - void targetError(); - private: QNearFieldManager *m_manager = nullptr; - bool m_busy = false; + Engine *m_engine = nullptr; + Device *m_thing = nullptr; + RuleActions* m_actions; + + TagStatus m_status = TagStatusWaiting; QNdefMessage m_currentMessage; diff --git a/nymea-app/nymea-app.pro b/nymea-app/nymea-app.pro index e847d96d..6d444f68 100644 --- a/nymea-app/nymea-app.pro +++ b/nymea-app/nymea-app.pro @@ -165,10 +165,9 @@ BR=$$BRANDING target.path = /usr/bin INSTALLS += target -ANDROID_ABIS = armeabi-v7a - contains(ANDROID_TARGET_ARCH,) { ANDROID_ABIS = \ armeabi-v7a \ arm64-v8a } + diff --git a/nymea-app/ui/images/nfc.svg b/nymea-app/ui/images/nfc.svg index dea348bd..6176e99e 100644 --- a/nymea-app/ui/images/nfc.svg +++ b/nymea-app/ui/images/nfc.svg @@ -21,7 +21,7 @@ @@ -38,9 +38,9 @@ inkscape:window-height="873" id="namedview7" showgrid="true" - inkscape:zoom="2.1413287" - inkscape:cx="32.892399" - inkscape:cy="42.615368" + inkscape:zoom="2.4135194" + inkscape:cx="42.404861" + inkscape:cy="39.439781" inkscape:window-x="60" inkscape:window-y="27" inkscape:window-maximized="1" @@ -89,6 +89,6 @@ diff --git a/nymea-app/ui/magic/WriteNfcTagPage.qml b/nymea-app/ui/magic/WriteNfcTagPage.qml index d5cfe4a1..f31e7611 100644 --- a/nymea-app/ui/magic/WriteNfcTagPage.qml +++ b/nymea-app/ui/magic/WriteNfcTagPage.qml @@ -42,13 +42,16 @@ Page { header: NymeaHeader { text: qsTr("Write NFC tag") onBackPressed: { - root.backPressed(); + pageStack.pop() } } NfcHelper { id: nfcHelper + engine: _engine + thing: root.thing + } // nfcHelper.writeThingStates(engine, root.thing) @@ -56,140 +59,163 @@ Page { anchors.fill: parent columns: app.landscape ? 2 : 1 - Item { - Layout.preferredWidth: Math.min(root.width / parent.columns, root.height) - Layout.preferredHeight: app.iconSize * 8 - - SequentialAnimation { - loops: Animation.Infinite - running: true - - PropertyAction { target: phoneIcon; property: "anchors.horizontalCenterOffset"; value: app.iconSize * 2 } - PropertyAction { target: phoneIcon; property: "scale"; value: 1.3 } - NumberAnimation { - target: phoneIcon - property: "opacity" - duration: 500 - to: 1 - } - - PauseAnimation { duration: 500 } - ParallelAnimation { - NumberAnimation { - target: phoneIcon - property: "anchors.horizontalCenterOffset" - from: app.iconSize * 2 - to: -app.iconSize * 2 - duration: 1500 - easing.type: Easing.OutQuad - } - - NumberAnimation { - target: phoneIcon - property: "scale" - duration: 1500 - from: 1.3 - to: 1 - easing.type: Easing.InOutQuad - } - } - - ParallelAnimation { - loops: 2 - NumberAnimation { - duration: 250 - target: vibrateCircle - property: "scale" - from: .8 - to: 1.5 - } - NumberAnimation { - duration: 250 - target: vibrateCircle - property: "opacity" - from: 1 - to: 0 - } - } - PauseAnimation { - duration: 500 - } - - NumberAnimation { - target: phoneIcon - property: "opacity" - duration: 500 - to: 0 - } - } - - - ColorIcon { - id: nfcIcon - name: "../images/nfc.svg" - height: app.iconSize * 2 - width: app.iconSize * 2 - anchors.centerIn: parent - anchors.horizontalCenterOffset: - app.iconSize * 2 - } - - Item { - id: phoneIcon - height: app.iconSize * 5 - width: app.iconSize * 5 - scale: 1.5 - anchors.centerIn: parent - anchors.horizontalCenterOffset: app.iconSize * 2 - - Rectangle { - id: vibrateCircle - anchors.centerIn: parent - anchors.fill: parent - radius: width / 2 - color: "transparent" -// border.color: nfcIcon.keyColor - border.color: app.accentColor - border.width: 2 - scale: .8 - opacity: 0 - } - - Rectangle { - anchors.fill: parent - anchors.leftMargin: phoneIcon.width * .21 - anchors.rightMargin: phoneIcon.width * .21 - anchors.topMargin: phoneIcon.height * .1 - anchors.bottomMargin: phoneIcon.height * .1 - color: app.backgroundColor - } - - ColorIcon { - name: "../images/smartphone.svg" - anchors.fill: parent - } - } - - } - ColumnLayout { + Layout.preferredWidth: parent.width / parent.columns + Layout.leftMargin: app.margins; Layout.rightMargin: app.margins; Layout.topMargin: app.margins Label { Layout.fillWidth: true - Layout.leftMargin: app.margins; Layout.rightMargin: app.margins - text: qsTr("Select the wanted states and tap an NFC tag to store them. When tapping this tag later, they will be restored.").arg(root.thing.name) + text: { + switch (nfcHelper.status) { + case NfcHelper.TagStatusWaiting: + return qsTr("Tap an NFC tag to link it to %1.").arg(root.thing.name) + case NfcHelper.TagStatusWriting: + return qsTr("Writing NFC tag...") + case NfcHelper.TagStatusWritten: + return qsTr("NFC tag linked to %1.").arg(root.thing.name) + case NfcHelper.TagStatusFailed: + return qsTr("Failed linking the NFC tag to %1.").arg(root.thing.name) + } + } + horizontalAlignment: Text.AlignHCenter wrapMode: Text.WordWrap - font.pixelSize: app.smallFont } + Label { + Layout.fillWidth: true + text: qsTr("Required tag size: %1 bytes").arg(nfcHelper.messageSize) + font.pixelSize: app.smallFont + horizontalAlignment: Text.AlignHCenter + enabled: false + } + + Item { + Layout.fillWidth: true + Layout.preferredHeight: app.iconSize * 8 + + SequentialAnimation { + loops: Animation.Infinite + running: true + + PropertyAction { target: phoneIcon; property: "anchors.horizontalCenterOffset"; value: app.iconSize * 2 } + PropertyAction { target: phoneIcon; property: "scale"; value: 1.3 } + NumberAnimation { target: phoneIcon; property: "opacity"; duration: 500; to: 1 } + PauseAnimation { duration: 500 } + ParallelAnimation { + NumberAnimation { target: phoneIcon; property: "anchors.horizontalCenterOffset"; from: app.iconSize * 2; to: -app.iconSize * 2; duration: 1500; easing.type: Easing.OutQuad } + NumberAnimation { target: phoneIcon; property: "scale"; duration: 1500; from: 1.3; to: 1; easing.type: Easing.InOutQuad } + } + PauseAnimation { duration: 500 } + NumberAnimation { target: phoneIcon; property: "opacity"; duration: 500; to: 0 } + PauseAnimation { duration: 500 } + } + + + ColorIcon { + id: nfcIcon + name: "../images/nfc.svg" + height: app.iconSize * 2 + width: app.iconSize * 2 + anchors.centerIn: parent + anchors.horizontalCenterOffset: - app.iconSize * 2 + visible: nfcHelper.status == NfcHelper.TagStatusWaiting + } + + Item { + id: phoneIcon + height: app.iconSize * 5 + width: app.iconSize * 5 + scale: 1.5 + anchors.centerIn: parent + anchors.horizontalCenterOffset: app.iconSize * 2 + visible: nfcHelper.status == NfcHelper.TagStatusWaiting + + Rectangle { + anchors.fill: parent + anchors.leftMargin: phoneIcon.width * .21 + anchors.rightMargin: phoneIcon.width * .21 + anchors.topMargin: phoneIcon.height * .1 + anchors.bottomMargin: phoneIcon.height * .1 + color: app.backgroundColor + } + + ColorIcon { + name: "../images/smartphone.svg" + anchors.fill: parent + } + } + + Rectangle { + id: tick + anchors.centerIn: parent + height: app.iconSize * 6 + width: app.iconSize * 6 + radius: width / 2 + color: app.backgroundColor + border.width: 4 + border.color: app.foregroundColor + opacity: nfcHelper.status == NfcHelper.TagStatusWaiting ? 0 : 1 + Behavior on opacity { NumberAnimation { duration: 300 } } + + property bool shown: nfcHelper.status == NfcHelper.TagStatusWritten || nfcHelper.status == NfcHelper.TagStatusFailed + + BusyIndicator { + anchors.fill: parent + running: visible + visible: nfcHelper.status == NfcHelper.TagStatusWriting + } + + Item { + anchors.fill: parent + anchors.rightMargin: tick.shown ? 0 : parent.width + Behavior on anchors.rightMargin { NumberAnimation { duration: 500 } } + clip: true + + ColorIcon { + x: (tick.width - width) / 2 + y: (tick.height - height) / 2 + height: app.iconSize * 4 + width: app.iconSize * 4 + name: nfcHelper.status == NfcHelper.TagStatusFailed ? "../images/close.svg" : "../images/tick.svg" + color: nfcHelper.status == NfcHelper.TagStatusFailed ? "red" : "green" + } + } + } + } + } + + + ColumnLayout { + Layout.preferredWidth: parent.width / parent.columns + ListView { Layout.fillWidth: true Layout.fillHeight: true - model: root.thingClass.stateTypes + model: nfcHelper.actions clip: true - delegate: CheckDelegate { + delegate: RuleActionDelegate { + ruleAction: nfcHelper.actions.get(index) width: parent.width - text: model.displayName - checked: true + onRemoveRuleAction: nfcHelper.actions.removeRuleAction(index) + } + } + + Button { + text: qsTr("Add action") + Layout.fillWidth: true + Layout.margins: app.margins + onClicked: { + var action = nfcHelper.actions.createNewRuleAction() + action.thingId = root.thing.id + var page = pageStack.push("SelectRuleActionPage.qml", {ruleAction: action}); + page.done.connect(function() { + nfcHelper.actions.addRuleAction(action); + pageStack.pop(); + }) + page.backPressed.connect(function() { + action.destroy() + pageStack.pop(); + }) } } } diff --git a/packaging/android/src/io/guh/nymeaapp/NymeaAppControlService.java b/packaging/android/src/io/guh/nymeaapp/NymeaAppControlService.java index b35cef9c..b3febd4f 100644 --- a/packaging/android/src/io/guh/nymeaapp/NymeaAppControlService.java +++ b/packaging/android/src/io/guh/nymeaapp/NymeaAppControlService.java @@ -61,9 +61,8 @@ public class NymeaAppControlService extends ControlsProviderService { } } @Override public void onUpdate(UUID nymeaId, UUID thingId) { - Log.d(TAG, "onUpdate()"); if (m_updatePublisher != null && m_activeControlIds.contains(thingId.toString())) { - Log.d(TAG, "Updating publisher for thing: " + thingId); +// Log.d(TAG, "Updating publisher for thing: " + thingId); m_updatePublisher.onNext(thingToControl(nymeaId, thingId)); // m_updatePublisher.onComplete(); } diff --git a/packaging/android/src/io/guh/nymeaapp/NymeaAppControlsActivity.java b/packaging/android/src/io/guh/nymeaapp/NymeaAppControlsActivity.java index 466f66e0..3b94d25e 100644 --- a/packaging/android/src/io/guh/nymeaapp/NymeaAppControlsActivity.java +++ b/packaging/android/src/io/guh/nymeaapp/NymeaAppControlsActivity.java @@ -29,11 +29,13 @@ public class NymeaAppControlsActivity extends org.qtproject.qt5.android.bindings Log.d(TAG, "Resuming..."); } - @Override public void onDestroy() { Log.d(TAG, "Destroying..."); } + public boolean startedByNfc() { + return NfcAdapter.ACTION_NDEF_DISCOVERED.equals(getIntent().getAction()); + } public String nymeaId() { @@ -50,23 +52,4 @@ public class NymeaAppControlsActivity extends org.qtproject.qt5.android.bindings Vibrator v = (Vibrator) getSystemService(Context.VIBRATOR_SERVICE); v.vibrate(duration); } - - @Override - protected void onNewIntent(Intent intent) { - Log.d(TAG, "*************** New intent"); - super.onNewIntent(intent); - if (NfcAdapter.ACTION_NDEF_DISCOVERED.equals(intent.getAction())) { - Parcelable[] rawMessages = - intent.getParcelableArrayExtra(NfcAdapter.EXTRA_NDEF_MESSAGES); - if (rawMessages != null) { - NdefMessage[] messages = new NdefMessage[rawMessages.length]; - for (int i = 0; i < rawMessages.length; i++) { - messages[i] = (NdefMessage) rawMessages[i]; - Log.d(TAG, messages[i].toString()); - } - // Process the messages array. - } - } - } - } diff --git a/packaging/android/src/io/guh/nymeaapp/NymeaAppServiceConnection.java b/packaging/android/src/io/guh/nymeaapp/NymeaAppServiceConnection.java index aa0cb9da..95f1b608 100644 --- a/packaging/android/src/io/guh/nymeaapp/NymeaAppServiceConnection.java +++ b/packaging/android/src/io/guh/nymeaapp/NymeaAppServiceConnection.java @@ -154,7 +154,6 @@ public class NymeaAppServiceConnection implements ServiceConnection { private BroadcastReceiver serviceMessageReceiver = new BroadcastReceiver() { @Override public void onReceive(Context context, Intent intent) { - Log.d(TAG, "In OnReceive broadcast receiver"); if (NymeaAppService.NYMEA_APP_BROADCAST.equals(intent.getAction())) { String payload = intent.getStringExtra("data"); try { @@ -170,7 +169,7 @@ public class NymeaAppServiceConnection implements ServiceConnection { { JSONObject data = new JSONObject(payload); JSONObject params = data.getJSONObject("params"); - Log.d(TAG, "Broadcast received from NymeaAppService: " + data.getString("notification")); +// Log.d(TAG, "Broadcast received from NymeaAppService: " + data.getString("notification")); Log.d(TAG, params.toString()); if (data.getString("notification").equals("ThingStateChanged")) { @@ -178,7 +177,7 @@ public class NymeaAppServiceConnection implements ServiceConnection { UUID thingId = UUID.fromString(params.getString("thingId")); UUID stateTypeId = UUID.fromString(params.getString("stateTypeId")); String value = params.getString("value"); - Log.d(TAG, "Thing state changed: " + thingId + " stateTypeId: " + stateTypeId + " value: " + value); +// Log.d(TAG, "Thing state changed: " + thingId + " stateTypeId: " + stateTypeId + " value: " + value); Thing thing = getThing(thingId); if (thing != null) { From c36000c1e903712da93dc8dc2dcf467783c2da84 Mon Sep 17 00:00:00 2001 From: Michael Zanetti Date: Wed, 14 Oct 2020 18:59:07 +0200 Subject: [PATCH 4/4] Add support for NFC tag writing and reading --- androidservice/androidservice.pro | 2 + .../controlviews/devicecontrolapplication.cpp | 13 +- nymea-app/main.cpp | 5 +- nymea-app/nfchelper.cpp | 175 ++--------------- nymea-app/nfchelper.h | 60 +----- nymea-app/nfcthingactionwriter.cpp | 185 ++++++++++++++++++ nymea-app/nfcthingactionwriter.h | 76 +++++++ nymea-app/nymea-app.pro | 4 + nymea-app/ui/devicepages/DevicePageBase.qml | 17 +- nymea-app/ui/magic/WriteNfcTagPage.qml | 41 ++-- 10 files changed, 330 insertions(+), 248 deletions(-) create mode 100644 nymea-app/nfcthingactionwriter.cpp create mode 100644 nymea-app/nfcthingactionwriter.h diff --git a/androidservice/androidservice.pro b/androidservice/androidservice.pro index 08ab083e..75b241f7 100644 --- a/androidservice/androidservice.pro +++ b/androidservice/androidservice.pro @@ -30,6 +30,7 @@ SOURCES += \ ../nymea-app/stylecontroller.cpp \ ../nymea-app/platformhelper.cpp \ ../nymea-app/nfchelper.cpp \ + ../nymea-app/nfcthingactionwriter.cpp \ ../nymea-app/platformintegration/android/platformhelperandroid.cpp \ service_main.cpp @@ -40,6 +41,7 @@ HEADERS += \ ../nymea-app/stylecontroller.h \ ../nymea-app/platformhelper.h \ ../nymea-app/nfchelper.h \ + ../nymea-app/nfcthingactionwriter.h \ ../nymea-app/platformintegration/android/platformhelperandroid.h \ DISTFILES += \ diff --git a/androidservice/controlviews/devicecontrolapplication.cpp b/androidservice/controlviews/devicecontrolapplication.cpp index 6e0560a5..06758a40 100644 --- a/androidservice/controlviews/devicecontrolapplication.cpp +++ b/androidservice/controlviews/devicecontrolapplication.cpp @@ -7,6 +7,7 @@ #include "../nymea-app/stylecontroller.h" #include "../nymea-app/platformhelper.h" #include "../nymea-app/nfchelper.h" +#include "../nymea-app/nfcthingactionwriter.h" #include "../nymea-app/platformintegration/android/platformhelperandroid.h" #include @@ -41,7 +42,8 @@ DeviceControlApplication::DeviceControlApplication(int argc, char *argv[]) : QAp registerQmlTypes(); qmlRegisterSingletonType("Nymea", 1, 0, "PlatformHelper", platformHelperProvider); qmlRegisterSingletonType(QUrl("qrc:///ui/utils/NymeaUtils.qml"), "Nymea", 1, 0, "NymeaUtils" ); - qmlRegisterType("Nymea", 1, 0, "NfcHelper"); + qmlRegisterType("Nymea", 1, 0, "NfcThingActionWriter"); + qmlRegisterSingletonType("Nymea", 1, 0, "NfcHelper", NfcHelper::nfcHelperProvider); StyleController *styleController = new StyleController(this); m_qmlEngine->rootContext()->setContextProperty("styleController", styleController); @@ -168,6 +170,11 @@ void DeviceControlApplication::runNfcAction() continue; } + if (parts.count() > 2) { + // The parameters might contain a #, let's merge them again + parts[1] = parts.mid(1).join('#'); + } + QString actionTypeName = parts.at(0); ActionType *actionType = thing->thingClass()->actionTypes()->findByName(actionTypeName); if (!actionType) { @@ -188,6 +195,8 @@ void DeviceControlApplication::runNfcAction() } } + qDebug() << "Parameters in NFC uri:" << paramsInUri; + QVariantList params; for (int j = 0; j < actionType->paramTypes()->rowCount(); j++) { ParamType *paramType = actionType->paramTypes()->get(j); @@ -201,6 +210,8 @@ void DeviceControlApplication::runNfcAction() params.append(param); } + qDebug() << "Action parameters:" << qUtf8Printable(QJsonDocument::fromVariant(params).toJson()); + m_engine->thingManager()->executeAction(thingId, actionType->id(), params); } } diff --git a/nymea-app/main.cpp b/nymea-app/main.cpp index f1b0595b..704978c9 100644 --- a/nymea-app/main.cpp +++ b/nymea-app/main.cpp @@ -53,6 +53,7 @@ #include "applogcontroller.h" #include "ruletemplates/messages.h" #include "nfchelper.h" +#include "nfcthingactionwriter.h" QObject *platformHelperProvider(QQmlEngine *engine, QJSEngine *scriptEngine) { @@ -67,7 +68,6 @@ QObject *platformHelperProvider(QQmlEngine *engine, QJSEngine *scriptEngine) #endif } - int main(int argc, char *argv[]) { @@ -125,7 +125,8 @@ int main(int argc, char *argv[]) QQmlApplicationEngine *engine = new QQmlApplicationEngine(); qmlRegisterSingletonType("Nymea", 1, 0, "PlatformHelper", platformHelperProvider); - qmlRegisterType("Nymea", 1, 0, "NfcHelper"); + qmlRegisterSingletonType("Nymea", 1, 0, "NfcHelper", NfcHelper::nfcHelperProvider); + qmlRegisterType("Nymea", 1, 0, "NfcThingActionWriter"); PushNotifications::instance()->connectClient(); qmlRegisterSingletonType("Nymea", 1, 0, "PushNotifications", PushNotifications::pushNotificationsProvider); diff --git a/nymea-app/nfchelper.cpp b/nymea-app/nfchelper.cpp index 6e7fa893..cdac7ed2 100644 --- a/nymea-app/nfchelper.cpp +++ b/nymea-app/nfchelper.cpp @@ -1,181 +1,28 @@ #include "nfchelper.h" -#include "types/deviceclass.h" -#include "types/statetype.h" -#include "types/ruleaction.h" -#include "types/ruleactionparams.h" -#include "types/ruleactionparam.h" #include -#include -#include -#include -#include -#include -NfcHelper::NfcHelper(QObject *parent): - QObject(parent), - m_manager(new QNearFieldManager(this)), - m_actions(new RuleActions(this)) +NfcHelper::NfcHelper(QObject *parent) : QObject(parent) { - connect(m_manager, &QNearFieldManager::targetDetected, this, &NfcHelper::targetDetected); - connect(m_manager, &QNearFieldManager::targetLost, this, &NfcHelper::targetLost); - - connect(m_actions, &RuleActions::countChanged, this, &NfcHelper::updateContent); - - m_manager->startTargetDetection(); - } -NfcHelper::~NfcHelper() +NfcHelper *NfcHelper::instance() { - m_manager->stopTargetDetection(); -} - -Engine *NfcHelper::engine() const -{ - return m_engine; -} - -void NfcHelper::setEngine(Engine *engine) -{ - if (m_engine != engine) { - m_engine = engine; - emit engineChanged(); - updateContent(); + static NfcHelper *thiz = nullptr; + if (!thiz) { + thiz = new NfcHelper(); } + return thiz; } -Device *NfcHelper::thing() const +QObject *NfcHelper::nfcHelperProvider(QQmlEngine */*engine*/, QJSEngine */*scriptEngine*/) { - return m_thing; + return instance(); } -void NfcHelper::setThing(Device *thing) +bool NfcHelper::isAvailable() const { - if (m_thing != thing) { - m_thing = thing; - emit thingChanged(); - updateContent(); - } + QNearFieldManager manager; + return manager.isAvailable(); } - -RuleActions *NfcHelper::actions() const -{ - return m_actions; -} - -int NfcHelper::messageSize() const -{ - return m_currentMessage.toByteArray().size(); - int ret = 0; - for (int i = 0; i < m_currentMessage.size(); i++) { - ret += m_currentMessage.at(i).payload().size(); - } - return ret; -} - -NfcHelper::TagStatus NfcHelper::status() const -{ - return m_status; -} - -void NfcHelper::updateContent() -{ - qDebug() << "Updating" << m_engine << m_thing; - - // Creating an URI type record with this format: - // nymea:// - // ? t= - // & a[0]= - // & a[1]=#: - // & a[2]=#:+: - // & ... - - // NOTE: We're using actionType and paramType *name* instead of the ID because NFC tags are - // small and normally names are shorter than ids so we save some space. - - // NOTE: param values are percentage encoded to prevent messing with the parsing if they - // contain + or : - - QUrl url; - url.setScheme("nymea"); - if (!m_engine || !m_thing) { - return; - } - url.setHost(m_engine->jsonRpcClient()->currentHost()->uuid().toString().remove(QRegExp("[{}]"))); - - QUrlQuery query; - - query.addQueryItem("t", m_thing->id().toString().remove(QRegExp("[{}]"))); - - for (int i = 0; i < m_actions->rowCount(); i++) { - RuleAction *action = m_actions->get(i); - QStringList params; - ActionType *at = m_thing->thingClass()->actionTypes()->getActionType(action->actionTypeId()); - if (!at) { - qWarning() << "ActionType not found in thing" << action->actionTypeId(); - continue; - } - - for (int j = 0; j < action->ruleActionParams()->rowCount(); j++) { - RuleActionParam *param = action->ruleActionParams()->get(j); - ParamType *pt = at->paramTypes()->getParamType(param->paramTypeId()); - if (!pt) { - qWarning() << "ParamType not found in thing"; - continue; - } - params.append(pt->name() + ":" + param->value().toByteArray().toPercentEncoding()); - } - QString actionString = at->name(); - if (params.length() > 0) { - actionString += "#" + params.join("+"); - } - query.addQueryItem(QString("a[%1]").arg(i), actionString); - } - url.setQuery(query); - qDebug() << "writing message" << url; - - QNdefNfcUriRecord record; - record.setUri(url); - QNdefMessage message; - message.append(record); - - m_currentMessage = message; - emit messageSizeChanged(); - -} - -void NfcHelper::targetDetected(QNearFieldTarget *target) -{ - QDateTime startTime = QDateTime::currentDateTime(); - qDebug() << "target detected"; - connect(target, &QNearFieldTarget::error, this, [=](QNearFieldTarget::Error error, const QNearFieldTarget::RequestId &id){ - qDebug() << "Tag error:" << error; - m_status = TagStatusFailed; - emit statusChanged(); - }); - connect(target, &QNearFieldTarget::ndefMessagesWritten, this, [=](){ - qDebug() << "Tag written in" << startTime.msecsTo(QDateTime::currentDateTime()); - m_status = TagStatusWritten; - emit statusChanged(); - }); - - QNearFieldTarget::RequestId m_request = target->writeNdefMessages(QList() << m_currentMessage); - if (!m_request.isValid()) { - qDebug() << "Error writing tag"; - m_status = TagStatusFailed; - emit statusChanged(); - } - - m_status = TagStatusWriting; - emit statusChanged(); -} - -void NfcHelper::targetLost(QNearFieldTarget *target) -{ - qDebug() << "Target lost" << target; - m_status = TagStatusWaiting; - emit statusChanged(); -} - diff --git a/nymea-app/nfchelper.h b/nymea-app/nfchelper.h index 51ef05be..eeab2a66 100644 --- a/nymea-app/nfchelper.h +++ b/nymea-app/nfchelper.h @@ -2,70 +2,22 @@ #define NFCHELPER_H #include -#include -#include - -#include "types/device.h" -#include "engine.h" -#include "types/ruleactions.h" +#include class NfcHelper : public QObject { Q_OBJECT - Q_PROPERTY(Engine *engine READ engine WRITE setEngine NOTIFY engineChanged) - Q_PROPERTY(Device *thing READ thing WRITE setThing NOTIFY thingChanged) - Q_PROPERTY(RuleActions *actions READ actions CONSTANT) - Q_PROPERTY(int messageSize READ messageSize NOTIFY messageSizeChanged) - Q_PROPERTY(TagStatus status READ status NOTIFY statusChanged) - + Q_PROPERTY(bool isAvailable READ isAvailable CONSTANT) public: - enum TagStatus { - TagStatusWaiting, - TagStatusWriting, - TagStatusWritten, - TagStatusFailed - }; - Q_ENUM(TagStatus) + static NfcHelper* instance(); + static QObject *nfcHelperProvider(QQmlEngine *engine, QJSEngine *scriptEngine); - explicit NfcHelper(QObject *parent = nullptr); - ~NfcHelper(); - Engine *engine() const; - void setEngine(Engine *engine); - - Device *thing() const; - void setThing(Device *thing); - - RuleActions *actions() const; - - int messageSize() const; - - TagStatus status() const; - -signals: - void engineChanged(); - void thingChanged(); - - void messageSizeChanged(); - void statusChanged(); - -private slots: - void updateContent(); - - void targetDetected(QNearFieldTarget *target); - void targetLost(QNearFieldTarget *target); + bool isAvailable() const; private: - QNearFieldManager *m_manager = nullptr; - Engine *m_engine = nullptr; - Device *m_thing = nullptr; - RuleActions* m_actions; - - TagStatus m_status = TagStatusWaiting; - - QNdefMessage m_currentMessage; - + explicit NfcHelper(QObject *parent = nullptr); }; #endif // NFCHELPER_H diff --git a/nymea-app/nfcthingactionwriter.cpp b/nymea-app/nfcthingactionwriter.cpp new file mode 100644 index 00000000..920fcca6 --- /dev/null +++ b/nymea-app/nfcthingactionwriter.cpp @@ -0,0 +1,185 @@ +#include "nfcthingactionwriter.h" +#include "types/deviceclass.h" +#include "types/statetype.h" +#include "types/ruleaction.h" +#include "types/ruleactionparams.h" +#include "types/ruleactionparam.h" + +#include +#include +#include +#include +#include +#include + +NfcThingActionWriter::NfcThingActionWriter(QObject *parent): + QObject(parent), + m_manager(new QNearFieldManager(this)), + m_actions(new RuleActions(this)) +{ + connect(m_manager, &QNearFieldManager::targetDetected, this, &NfcThingActionWriter::targetDetected); + connect(m_manager, &QNearFieldManager::targetLost, this, &NfcThingActionWriter::targetLost); + + connect(m_actions, &RuleActions::countChanged, this, &NfcThingActionWriter::updateContent); + + m_manager->startTargetDetection(); + +} + +NfcThingActionWriter::~NfcThingActionWriter() +{ + m_manager->stopTargetDetection(); +} + +bool NfcThingActionWriter::isAvailable() const +{ + return m_manager->isAvailable(); +} + +Engine *NfcThingActionWriter::engine() const +{ + return m_engine; +} + +void NfcThingActionWriter::setEngine(Engine *engine) +{ + if (m_engine != engine) { + m_engine = engine; + emit engineChanged(); + updateContent(); + } +} + +Device *NfcThingActionWriter::thing() const +{ + return m_thing; +} + +void NfcThingActionWriter::setThing(Device *thing) +{ + if (m_thing != thing) { + m_thing = thing; + emit thingChanged(); + updateContent(); + } +} + +RuleActions *NfcThingActionWriter::actions() const +{ + return m_actions; +} + +int NfcThingActionWriter::messageSize() const +{ + return m_currentMessage.toByteArray().size(); + int ret = 0; + for (int i = 0; i < m_currentMessage.size(); i++) { + ret += m_currentMessage.at(i).payload().size(); + } + return ret; +} + +NfcThingActionWriter::TagStatus NfcThingActionWriter::status() const +{ + return m_status; +} + +void NfcThingActionWriter::updateContent() +{ + qDebug() << "Updating" << m_engine << m_thing; + + // Creating an URI type record with this format: + // nymea:// + // ? t= + // & a[0]= + // & a[1]=#: + // & a[2]=#:+: + // & ... + + // NOTE: We're using actionType and paramType *name* instead of the ID because NFC tags are + // small and normally names are shorter than ids so we save some space. + + // NOTE: param values are percentage encoded to prevent messing with the parsing if they + // contain + or : + + QUrl url; + url.setScheme("nymea"); + if (!m_engine || !m_thing) { + return; + } + url.setHost(m_engine->jsonRpcClient()->currentHost()->uuid().toString().remove(QRegExp("[{}]"))); + + QUrlQuery query; + + query.addQueryItem("t", m_thing->id().toString().remove(QRegExp("[{}]"))); + + for (int i = 0; i < m_actions->rowCount(); i++) { + RuleAction *action = m_actions->get(i); + QStringList params; + ActionType *at = m_thing->thingClass()->actionTypes()->getActionType(action->actionTypeId()); + if (!at) { + qWarning() << "ActionType not found in thing" << action->actionTypeId(); + continue; + } + + for (int j = 0; j < action->ruleActionParams()->rowCount(); j++) { + RuleActionParam *param = action->ruleActionParams()->get(j); + ParamType *pt = at->paramTypes()->getParamType(param->paramTypeId()); + if (!pt) { + qWarning() << "ParamType not found in thing"; + continue; + } + params.append(pt->name() + ":" + param->value().toByteArray().toPercentEncoding()); + } + QString actionString = at->name(); + if (params.length() > 0) { + actionString += "#" + params.join("+"); + } + query.addQueryItem(QString("a[%1]").arg(i), actionString); + } + url.setQuery(query); + qDebug() << "writing message" << url; + + QNdefNfcUriRecord record; + record.setUri(url); + QNdefMessage message; + message.append(record); + + m_currentMessage = message; + emit messageSizeChanged(); + +} + +void NfcThingActionWriter::targetDetected(QNearFieldTarget *target) +{ + QDateTime startTime = QDateTime::currentDateTime(); + qDebug() << "target detected"; + connect(target, &QNearFieldTarget::error, this, [=](QNearFieldTarget::Error error, const QNearFieldTarget::RequestId &id){ + qDebug() << "Tag error:" << error; + m_status = TagStatusFailed; + emit statusChanged(); + }); + connect(target, &QNearFieldTarget::ndefMessagesWritten, this, [=](){ + qDebug() << "Tag written in" << startTime.msecsTo(QDateTime::currentDateTime()); + m_status = TagStatusWritten; + emit statusChanged(); + }); + + QNearFieldTarget::RequestId m_request = target->writeNdefMessages(QList() << m_currentMessage); + if (!m_request.isValid()) { + qDebug() << "Error writing tag"; + m_status = TagStatusFailed; + emit statusChanged(); + } + + m_status = TagStatusWriting; + emit statusChanged(); +} + +void NfcThingActionWriter::targetLost(QNearFieldTarget *target) +{ + qDebug() << "Target lost" << target; + m_status = TagStatusWaiting; + emit statusChanged(); +} + diff --git a/nymea-app/nfcthingactionwriter.h b/nymea-app/nfcthingactionwriter.h new file mode 100644 index 00000000..0463b7e5 --- /dev/null +++ b/nymea-app/nfcthingactionwriter.h @@ -0,0 +1,76 @@ +#ifndef NFCTHINGACTIONWRITER_H +#define NFCTHINGACTIONWRITER_H + +#include +#include +#include + +#include "types/device.h" +#include "engine.h" +#include "types/ruleactions.h" + +class NfcThingActionWriter : public QObject +{ + Q_OBJECT + Q_PROPERTY(bool isAvailable READ isAvailable CONSTANT) + Q_PROPERTY(Engine *engine READ engine WRITE setEngine NOTIFY engineChanged) + Q_PROPERTY(Device *thing READ thing WRITE setThing NOTIFY thingChanged) + Q_PROPERTY(RuleActions *actions READ actions CONSTANT) + Q_PROPERTY(int messageSize READ messageSize NOTIFY messageSizeChanged) + Q_PROPERTY(TagStatus status READ status NOTIFY statusChanged) + + +public: + enum TagStatus { + TagStatusWaiting, + TagStatusWriting, + TagStatusWritten, + TagStatusFailed + }; + Q_ENUM(TagStatus) + + static NfcThingActionWriter *instance(); + + explicit NfcThingActionWriter(QObject *parent = nullptr); + ~NfcThingActionWriter(); + + bool isAvailable() const; + + Engine *engine() const; + void setEngine(Engine *engine); + + Device *thing() const; + void setThing(Device *thing); + + RuleActions *actions() const; + + int messageSize() const; + + TagStatus status() const; + +signals: + void engineChanged(); + void thingChanged(); + + void messageSizeChanged(); + void statusChanged(); + +private slots: + void updateContent(); + + void targetDetected(QNearFieldTarget *target); + void targetLost(QNearFieldTarget *target); + +private: + QNearFieldManager *m_manager = nullptr; + Engine *m_engine = nullptr; + Device *m_thing = nullptr; + RuleActions* m_actions; + + TagStatus m_status = TagStatusWaiting; + + QNdefMessage m_currentMessage; + +}; + +#endif // NFCTHINGACTIONWRITER_H diff --git a/nymea-app/nymea-app.pro b/nymea-app/nymea-app.pro index 6d444f68..d8dd59f2 100644 --- a/nymea-app/nymea-app.pro +++ b/nymea-app/nymea-app.pro @@ -15,6 +15,7 @@ PRE_TARGETDEPS += ../libnymea-app HEADERS += \ mainmenumodel.h \ nfchelper.h \ + nfcthingactionwriter.h \ platformintegration/generic/raspberrypihelper.h \ stylecontroller.h \ pushnotifications.h \ @@ -26,6 +27,7 @@ HEADERS += \ SOURCES += main.cpp \ mainmenumodel.cpp \ nfchelper.cpp \ + nfcthingactionwriter.cpp \ platformintegration/generic/raspberrypihelper.cpp \ stylecontroller.cpp \ pushnotifications.cpp \ @@ -171,3 +173,5 @@ contains(ANDROID_TARGET_ARCH,) { arm64-v8a } +ANDROID_ABIS = armeabi-v7a arm64-v8a + diff --git a/nymea-app/ui/devicepages/DevicePageBase.qml b/nymea-app/ui/devicepages/DevicePageBase.qml index 2acfaa2b..10dfcb3a 100644 --- a/nymea-app/ui/devicepages/DevicePageBase.qml +++ b/nymea-app/ui/devicepages/DevicePageBase.qml @@ -112,14 +112,19 @@ Page { })) } - thingMenu.addItem(menuEntryComponent.createObject(thingMenu, - { - text: qsTr("Write NFC tag"), - iconSource: "../images/nfc.svg", - functionName: "writeNfcTag" + print("*** creating menu") + print("NFC", NfcHelper.isAvailable) + if (NfcHelper.isAvailable) { + thingMenu.addItem(menuEntryComponent.createObject(thingMenu, + { + text: qsTr("Write NFC tag"), + iconSource: "../images/nfc.svg", + functionName: "writeNfcTag" - })); + })); + } } + function openDeviceMagicPage() { pageStack.push(Qt.resolvedUrl("../magic/DeviceRulesPage.qml"), {device: root.device}) } diff --git a/nymea-app/ui/magic/WriteNfcTagPage.qml b/nymea-app/ui/magic/WriteNfcTagPage.qml index f31e7611..e4f216cb 100644 --- a/nymea-app/ui/magic/WriteNfcTagPage.qml +++ b/nymea-app/ui/magic/WriteNfcTagPage.qml @@ -47,11 +47,10 @@ Page { } - NfcHelper { - id: nfcHelper + NfcThingActionWriter { + id: nfcWriter engine: _engine thing: root.thing - } // nfcHelper.writeThingStates(engine, root.thing) @@ -66,14 +65,14 @@ Page { Label { Layout.fillWidth: true text: { - switch (nfcHelper.status) { - case NfcHelper.TagStatusWaiting: + switch (nfcWriter.status) { + case NfcThingActionWriter.TagStatusWaiting: return qsTr("Tap an NFC tag to link it to %1.").arg(root.thing.name) - case NfcHelper.TagStatusWriting: + case NfcThingActionWriter.TagStatusWriting: return qsTr("Writing NFC tag...") - case NfcHelper.TagStatusWritten: + case NfcThingActionWriter.TagStatusWritten: return qsTr("NFC tag linked to %1.").arg(root.thing.name) - case NfcHelper.TagStatusFailed: + case NfcThingActionWriter.TagStatusFailed: return qsTr("Failed linking the NFC tag to %1.").arg(root.thing.name) } } @@ -83,7 +82,7 @@ Page { Label { Layout.fillWidth: true - text: qsTr("Required tag size: %1 bytes").arg(nfcHelper.messageSize) + text: qsTr("Required tag size: %1 bytes").arg(nfcWriter.messageSize) font.pixelSize: app.smallFont horizontalAlignment: Text.AlignHCenter enabled: false @@ -118,7 +117,7 @@ Page { width: app.iconSize * 2 anchors.centerIn: parent anchors.horizontalCenterOffset: - app.iconSize * 2 - visible: nfcHelper.status == NfcHelper.TagStatusWaiting + visible: nfcWriter.status == NfcThingActionWriter.TagStatusWaiting } Item { @@ -128,7 +127,7 @@ Page { scale: 1.5 anchors.centerIn: parent anchors.horizontalCenterOffset: app.iconSize * 2 - visible: nfcHelper.status == NfcHelper.TagStatusWaiting + visible: nfcWriter.status == NfcThingActionWriter.TagStatusWaiting Rectangle { anchors.fill: parent @@ -154,15 +153,15 @@ Page { color: app.backgroundColor border.width: 4 border.color: app.foregroundColor - opacity: nfcHelper.status == NfcHelper.TagStatusWaiting ? 0 : 1 + opacity: nfcWriter.status == NfcThingActionWriter.TagStatusWaiting ? 0 : 1 Behavior on opacity { NumberAnimation { duration: 300 } } - property bool shown: nfcHelper.status == NfcHelper.TagStatusWritten || nfcHelper.status == NfcHelper.TagStatusFailed + property bool shown: nfcWriter.status == NfcThingActionWriter.TagStatusWritten || nfcWriter.status == NfcThingActionWriter.TagStatusFailed BusyIndicator { anchors.fill: parent running: visible - visible: nfcHelper.status == NfcHelper.TagStatusWriting + visible: nfcWriter.status == NfcThingActionWriter.TagStatusWriting } Item { @@ -176,8 +175,8 @@ Page { y: (tick.height - height) / 2 height: app.iconSize * 4 width: app.iconSize * 4 - name: nfcHelper.status == NfcHelper.TagStatusFailed ? "../images/close.svg" : "../images/tick.svg" - color: nfcHelper.status == NfcHelper.TagStatusFailed ? "red" : "green" + name: nfcWriter.status == NfcThingActionWriter.TagStatusFailed ? "../images/close.svg" : "../images/tick.svg" + color: nfcWriter.status == NfcThingActionWriter.TagStatusFailed ? "red" : "green" } } } @@ -191,12 +190,12 @@ Page { ListView { Layout.fillWidth: true Layout.fillHeight: true - model: nfcHelper.actions + model: nfcWriter.actions clip: true delegate: RuleActionDelegate { - ruleAction: nfcHelper.actions.get(index) + ruleAction: nfcWriter.actions.get(index) width: parent.width - onRemoveRuleAction: nfcHelper.actions.removeRuleAction(index) + onRemoveRuleAction: nfcWriter.actions.removeRuleAction(index) } } @@ -205,11 +204,11 @@ Page { Layout.fillWidth: true Layout.margins: app.margins onClicked: { - var action = nfcHelper.actions.createNewRuleAction() + var action = nfcWriter.actions.createNewRuleAction() action.thingId = root.thing.id var page = pageStack.push("SelectRuleActionPage.qml", {ruleAction: action}); page.done.connect(function() { - nfcHelper.actions.addRuleAction(action); + nfcWriter.actions.addRuleAction(action); pageStack.pop(); }) page.backPressed.connect(function() {