Initial stab on NFC support

pull/451/head
Michael Zanetti 2020-10-01 16:56:16 +02:00
parent d0bbdc5eb6
commit 3ea0ec1f9f
13 changed files with 337 additions and 9 deletions

View File

@ -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)

View File

@ -13,6 +13,7 @@
#include <QtQml>
#include <QtAndroid>
#include <QAndroidJniObject>
#include <QNdefNfcUriRecord>
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<jstring>("nymeaId").toString();
QString thingId = QtAndroid::androidActivity().callObjectMethod<jstring>("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<QPair<QString, QString>> 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()
{
}

View File

@ -2,6 +2,11 @@
#define DEVICECONTROLAPPLICATION_H
#include <QApplication>
#include <QNearFieldManager>
#include <QNdefMessage>
#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

View File

@ -2,6 +2,8 @@
#define NYMEAAPPSERVICE_H
#include <QAndroidService>
#include <QNearFieldManager>
#include <QNdefMessage>
#include "engine.h"

View File

@ -236,5 +236,6 @@
<file>ui/images/connections/nm-signal-100-secure.svg</file>
<file>ui/images/connections/bluetooth.svg</file>
<file>ui/images/connections/network-wired-disabled.svg</file>
<file>ui/images/nfc.svg</file>
</qresource>
</RCC>

View File

@ -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<PlatformHelper>("Nymea", 1, 0, "PlatformHelper", platformHelperProvider);
qmlRegisterType<NfcHelper>("Nymea", 1, 0, "NfcHelper");
PushNotifications::instance()->connectClient();
qmlRegisterSingletonType<PushNotifications>("Nymea", 1, 0, "PushNotifications", PushNotifications::pushNotificationsProvider);

86
nymea-app/nfchelper.cpp Normal file
View File

@ -0,0 +1,86 @@
#include "nfchelper.h"
#include "types/deviceclass.h"
#include "types/statetype.h"
#include <QNearFieldManager>
#include <QNdefMessage>
#include <QDebug>
#include <QNdefNfcUriRecord>
#include <QUrl>
#include <QUrlQuery>
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<QNdefMessage>() << 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";
}

42
nymea-app/nfchelper.h Normal file
View File

@ -0,0 +1,42 @@
#ifndef NFCHELPER_H
#define NFCHELPER_H
#include <QObject>
#include <QNearFieldManager>
#include <QNdefMessage>
#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

View File

@ -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

View File

@ -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 {

View File

@ -0,0 +1,94 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
id="svg4874"
height="96"
viewBox="0 0 96 96.000001"
width="96"
version="1.1"
sodipodi:docname="nfc.svg"
inkscape:version="1.0.1 (1.0.1+r73)">
<path
id="path884"
style="color:#000000;fill:none"
d="M 30.00026,1.073013 V -94.92699 h 96 V 1.073013 Z" />
<path
id="path886"
style="color:#000000;font-variant-ligatures:none;font-variant-position:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-alternates:normal;font-feature-settings:normal;text-indent:0;text-decoration:none;text-decoration-line:none;text-decoration-style:solid;text-decoration-color:#000000;text-transform:none;white-space:normal;shape-padding:0;isolation:auto;mix-blend-mode:normal;solid-color:#000000;fill:#808080;fill-opacity:1;color-rendering:auto;image-rendering:auto;shape-rendering:auto"
d="m 42.041184,59.03214 21.92185,21.147163 c 2.283959,1.18636 3.037486,-1.033283 -0.0035,-5.561123 L 42.236652,53.66296 M 68.001221,84.147015 V 20.14502 c 0,-2.63041 -0.23782,-4.7121 -0.89648,-6.4668 -0.65866,-1.75461 -1.85,-3.15961 -3.3555,-3.9902 -3.011,-1.6613 -6.6918,-1.4848 -11.725,-1.543 h -0.01172 l -0.03472,4.0001 h 0.02344 c 5.0383,0.0588 8.3519,0.23688 9.8164,1.0449 0.73364,0.40478 1.1508,0.85491 1.541,1.8945 0.39025,1.0396 0.64258,2.691 0.64258,5.0606 v 60.072038 z"
sodipodi:nodetypes="cccccsccccccscscc" />
<defs
id="defs9" />
<sodipodi:namedview
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1"
objecttolerance="10"
gridtolerance="10"
guidetolerance="10"
inkscape:pageopacity="0"
inkscape:pageshadow="2"
inkscape:window-width="1380"
inkscape:window-height="873"
id="namedview7"
showgrid="true"
inkscape:zoom="2.1413287"
inkscape:cx="32.892399"
inkscape:cy="42.615368"
inkscape:window-x="60"
inkscape:window-y="27"
inkscape:window-maximized="1"
inkscape:current-layer="svg4874">
<inkscape:grid
type="xygrid"
id="grid834" />
</sodipodi:namedview>
<metadata
id="metadata4879">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title></dc:title>
</cc:Work>
</rdf:RDF>
</metadata>
<g
id="layer1"
transform="translate(67.856999,-78.504993)">
<rect
id="rect4782"
style="color:#000000;fill:none"
transform="rotate(90)"
height="96"
width="96"
y="-28.143"
x="78.504997" />
<path
id="path4643"
style="color:#000000;font-variant-ligatures:none;font-variant-position:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-alternates:normal;font-feature-settings:normal;text-indent:0;text-decoration:none;text-decoration-line:none;text-decoration-style:solid;text-decoration-color:#000000;text-transform:none;white-space:normal;shape-padding:0;isolation:auto;mix-blend-mode:normal;solid-color:#000000;fill:#808080;color-rendering:auto;image-rendering:auto;shape-rendering:auto"
d="m -43.869,86.504 -0.01172,0.002 c -5.0328,0.05818 -8.7136,-0.12027 -11.725,1.541 -1.5055,0.83064 -2.6968,2.2356 -3.3555,3.9902 -0.65866,1.7547 -0.89648,3.8364 -0.89648,6.4668 v 56.002 c 0,2.6304 0.23782,4.7121 0.89648,6.4668 0.65866,1.7546 1.85,3.1596 3.3555,3.9902 3.011,1.6613 6.6918,1.4848 11.725,1.543 H -43.869 4.154 4.165719 c 5.0328,-0.0582 8.7136,0.11832 11.725,-1.543 1.5055,-0.83064 2.6968,-2.2356 3.3555,-3.9902 0.65866,-1.7547 0.89648,-3.8364 0.89648,-6.4668 V 98.504 c 0,-2.6304 -0.23782,-4.7121 -0.89648,-6.4668 -0.66,-1.759 -1.851,-3.163 -3.356,-3.994 -3.011,-1.661 -6.6922,-1.483 -11.725,-1.541 L 4.1535,86.5002 h -48.023 z m 0.01172,4 h 48 c 5.0383,0.05877 8.3519,0.23688 9.8164,1.0449 0.73364,0.40478 1.1527,0.85491 1.543,1.8945 0.39025,1.0396 0.64062,2.691 0.64062,5.0605 v 56.002 c 0,2.3696 -0.25037,4.0209 -0.64062,5.0606 -0.39025,1.0396 -0.80933,1.4898 -1.543,1.8945 -1.4645,0.80804 -4.7782,0.98616 -9.8164,1.0449 h -47.977 -0.02344 c -5.0383,-0.0588 -8.3519,-0.23688 -9.8164,-1.0449 -0.73364,-0.40478 -1.1508,-0.85491 -1.541,-1.8945 -0.39025,-1.0396 -0.64258,-2.691 -0.64258,-5.0606 v -56.002 c 0,-2.3696 0.25232,-4.0209 0.64258,-5.0605 0.39025,-1.0396 0.80738,-1.4898 1.541,-1.8945 1.4645,-0.80804 4.7782,-0.98616 9.8164,-1.0449 z" />
</g>
<g
id="g868"
transform="translate(87.856999,-78.504993)">
<path
id="rect864"
style="color:#000000;fill:none"
transform="rotate(90)"
d="M 78.504997,-28.143 H 174.505 v 96 H 78.504997 Z" />
</g>
<path
id="path892"
style="color:#000000;font-variant-ligatures:none;font-variant-position:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-alternates:normal;font-feature-settings:normal;text-indent:0;text-decoration:none;text-decoration-line:none;text-decoration-style:solid;text-decoration-color:#000000;text-transform:none;white-space:normal;shape-padding:0;isolation:auto;mix-blend-mode:normal;solid-color:#000000;fill:#808080;fill-opacity:1;color-rendering:auto;image-rendering:auto;shape-rendering:auto"
d="M 54.001221,37.259895 32.079371,16.112732 c -2.283959,-1.18636 -3.037486,1.033283 0.0035,5.561123 l 21.722882,20.95522 M 28.041184,12.14502 v 64.001995 c 0,2.63041 0.23782,4.7121 0.89648,6.4668 0.65866,1.75461 1.85,3.15961 3.3555,3.9902 3.011,1.6613 6.6918,1.4848 11.725,1.543 h 0.01172 l 0.03472,-4.0001 h -0.02344 c -5.0383,-0.0588 -8.3519,-0.23688 -9.8164,-1.0449 -0.73364,-0.40478 -1.1508,-0.85491 -1.541,-1.8945 -0.39025,-1.0396 -0.64258,-2.691 -0.64258,-5.0606 V 16.074877 Z"
sodipodi:nodetypes="cccccsccccccscscc" />
</svg>

After

Width:  |  Height:  |  Size: 6.0 KiB

View File

@ -64,6 +64,11 @@
<activity android:process=":qt_controlsActivity" android:configChanges="orientation|uiMode|screenLayout|screenSize|smallestScreenSize|layoutDirection|locale|fontScale|keyboard|keyboardHidden|navigation|mcc|mnc|density" android:name="io.guh.nymeaapp.NymeaAppControlsActivity" android:label="nymea:app" android:screenOrientation="unspecified" android:launchMode="standard">
<intent-filter>
<action android:name="android.nfc.action.NDEF_DISCOVERED"/>
<category android:name="android.intent.category.DEFAULT"/>
<data android:scheme="nymea"/>
</intent-filter>
<meta-data android:name="android.app.lib_name" android:value="service"/>
<meta-data android:name="android.app.arguments" android:value="--controlActivity"/>
<meta-data android:name="android.app.qt_sources_resource_id" android:resource="@array/qt_sources"/>
@ -95,6 +100,11 @@
<!-- For adding service(s) please check: https://wiki.qt.io/AndroidServices -->
<service android:process=":qt_service" android:name="io.guh.nymeaapp.NymeaAppService">
<intent-filter>
<action android:name="android.nfc.action.NDEF_DISCOVERED"/>
<category android:name="android.intent.category.DEFAULT"/>
<data android:scheme="xbmc"/>
</intent-filter>
<meta-data android:name="android.app.lib_name" android:value="service"/>
<meta-data android:name="android.app.qt_sources_resource_id" android:resource="@array/qt_sources"/>
<meta-data android:name="android.app.repository" android:value="default"/>
@ -145,4 +155,5 @@
Remove the comment if you do not require these default features. -->
<!-- %%INSERT_FEATURES -->
<uses-permission android:name="android.permission.VIBRATE"/>
<uses-permission android:name="android.permission.NFC" />
</manifest>

View File

@ -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.
}
}
}
}