improve discovery and connecttion

This commit is contained in:
Michael Zanetti 2018-06-04 00:47:36 +02:00
parent 3536204961
commit 383b5b07e9
14 changed files with 998 additions and 401 deletions

View File

@ -1,6 +1,6 @@
/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * /* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
* * * *
* Copyright (C) 2015 Simon Stuerz <stuerz.simon@gmail.com> * * Copyright (C) 2018 Michael Zanetti <michael.zanetti@guh.io> *
* * * *
* This file is part of mea. * * This file is part of mea. *
* * * *
@ -22,38 +22,19 @@
#include <QUrl> #include <QUrl>
DiscoveryDevice::DiscoveryDevice() DiscoveryDevice::DiscoveryDevice(QObject *parent): QObject(parent)
{ {
m_portConfigs = new PortConfigs(this);
} }
QUrl DiscoveryDevice::location() const QUuid DiscoveryDevice::uuid() const
{ {
return m_location; return m_uuid;
} }
void DiscoveryDevice::setLocation(const QUrl &location) void DiscoveryDevice::setUuid(const QUuid &uuid)
{ {
m_location = location; m_uuid = uuid;
}
QString DiscoveryDevice::webSocketUrl() const
{
return m_webSocketUrl;
}
void DiscoveryDevice::setWebSocketUrl(const QString &webSocketUrl)
{
m_webSocketUrl = webSocketUrl;
}
QString DiscoveryDevice::nymeaRpcUrl() const
{
return m_nymeaRpcUrl;
}
void DiscoveryDevice::setNymeaRpcUrl(const QString &nymeaRpcUrl)
{
m_nymeaRpcUrl = nymeaRpcUrl;
} }
QHostAddress DiscoveryDevice::hostAddress() const QHostAddress DiscoveryDevice::hostAddress() const
@ -61,116 +42,156 @@ QHostAddress DiscoveryDevice::hostAddress() const
return m_hostAddress; return m_hostAddress;
} }
void DiscoveryDevice::setHostAddress(const QHostAddress &hostAddress) QString DiscoveryDevice::hostAddressString() const
{ {
m_hostAddress = hostAddress; return m_hostAddress.toString();
} }
int DiscoveryDevice::port() const void DiscoveryDevice::setHostAddress(const QHostAddress &hostAddress)
{
if (m_hostAddress != hostAddress) {
m_hostAddress = hostAddress;
emit hostAddressChanged();
}
}
QString DiscoveryDevice::name() const
{
return m_name;
}
void DiscoveryDevice::setName(const QString &name)
{
if (m_name != name) {
m_name = name;
emit nameChanged();
}
}
QString DiscoveryDevice::version() const
{
return m_version;
}
void DiscoveryDevice::setVersion(const QString &version)
{
if (m_version != version) {
m_version = version;
emit versionChanged();
}
}
PortConfigs* DiscoveryDevice::portConfigs() const
{
return m_portConfigs;
}
QString DiscoveryDevice::toUrl(int portConfigIndex)
{
PortConfig *pc = m_portConfigs->get(portConfigIndex);
if (!pc) {
qWarning() << "No portconfig for index" << portConfigIndex;
}
QString ret = pc->protocol() == PortConfig::ProtocolNymeaRpc ? "nymea" : "ws";
ret += pc->sslEnabled() ? "s" : "";
ret += "://";
ret += m_hostAddress.toString();
ret += ":";
ret += QString::number(pc->port());
return ret;
}
PortConfigs::PortConfigs(QObject *parent): QAbstractListModel(parent)
{
}
int PortConfigs::rowCount(const QModelIndex &parent) const
{
Q_UNUSED(parent)
return m_portConfigs.count();
}
QVariant PortConfigs::data(const QModelIndex &index, int role) const
{
switch (role) {
case RolePort:
return m_portConfigs.at(index.row())->port();
case RoleProtocol:
return m_portConfigs.at(index.row())->protocol();
case RoleSSLEnabled:
return m_portConfigs.at(index.row())->sslEnabled();
}
return QVariant();
}
PortConfig *PortConfigs::find(int port)
{
foreach (PortConfig* pc, m_portConfigs) {
if (pc->port() == port) {
return pc;
}
}
return nullptr;
}
void PortConfigs::insert(PortConfig *portConfig)
{
portConfig->setParent(this);
beginInsertRows(QModelIndex(), m_portConfigs.count(), m_portConfigs.count());
m_portConfigs.append(portConfig);
endInsertRows();
emit countChanged();
}
PortConfig* PortConfigs::get(int index) const
{
return m_portConfigs.at(index);
}
QHash<int, QByteArray> PortConfigs::roleNames() const
{
QHash<int, QByteArray> roles;
roles.insert(RolePort, "port");
roles.insert(RoleProtocol, "protocol");
roles.insert(RoleSSLEnabled, "sslEnabled");
return roles;
}
PortConfig::PortConfig(int port, QObject *parent):
QObject(parent),
m_port(port)
{
}
int PortConfig::port() const
{ {
return m_port; return m_port;
} }
void DiscoveryDevice::setPort(const int &port) PortConfig::Protocol PortConfig::protocol() const
{ {
m_port = port; return m_protocol;
} }
QString DiscoveryDevice::friendlyName() const void PortConfig::setProtocol(PortConfig::Protocol protocol)
{ {
return m_friendlyName; if (m_protocol != protocol) {
m_protocol = protocol;
emit protocolChanged();
}
} }
void DiscoveryDevice::setFriendlyName(const QString &friendlyName) bool PortConfig::sslEnabled() const
{ {
m_friendlyName = friendlyName; return m_sslEnabled;
} }
QString DiscoveryDevice::manufacturer() const void PortConfig::setSslEnabled(bool sslEnabled)
{ {
return m_manufacturer; if (m_sslEnabled != sslEnabled) {
} m_sslEnabled = sslEnabled;
emit sslEnabledChanged();
void DiscoveryDevice::setManufacturer(const QString &manufacturer) }
{
m_manufacturer = manufacturer;
}
QUrl DiscoveryDevice::manufacturerURL() const
{
return m_manufacturerURL;
}
void DiscoveryDevice::setManufacturerURL(const QUrl &manufacturerURL)
{
m_manufacturerURL = manufacturerURL;
}
QString DiscoveryDevice::modelDescription() const
{
return m_modelDescription;
}
void DiscoveryDevice::setModelDescription(const QString &modelDescription)
{
m_modelDescription = modelDescription;
}
QString DiscoveryDevice::modelName() const
{
return m_modelName;
}
void DiscoveryDevice::setModelName(const QString &modelName)
{
m_modelName = modelName;
}
QString DiscoveryDevice::modelNumber() const
{
return m_modelNumber;
}
void DiscoveryDevice::setModelNumber(const QString &modelNumber)
{
m_modelNumber = modelNumber;
}
QUrl DiscoveryDevice::modelURL() const
{
return m_modelURL;
}
void DiscoveryDevice::setModelURL(const QUrl &modelURL)
{
m_modelURL = modelURL;
}
QString DiscoveryDevice::uuid() const
{
return m_uuid;
}
void DiscoveryDevice::setUuid(const QString &uuid)
{
m_uuid = uuid;
}
QDebug operator<<(QDebug debug, const DiscoveryDevice &DiscoveryDevice)
{
debug << "----------------------------------------------\n";
debug << "UPnP device on " << QString("%1:%2").arg(DiscoveryDevice.hostAddress().toString()).arg(DiscoveryDevice.port()) << "\n";
debug << "location | " << DiscoveryDevice.location().toString() << "\n";
debug << "websocket | " << DiscoveryDevice.webSocketUrl() << "\n";
debug << "nymearpc | " << DiscoveryDevice.nymeaRpcUrl() << "\n";
debug << "friendly name | " << DiscoveryDevice.friendlyName() << "\n";
debug << "manufacturer | " << DiscoveryDevice.manufacturer() << "\n";
debug << "manufacturer URL | " << DiscoveryDevice.manufacturerURL().toString() << "\n";
debug << "model name | " << DiscoveryDevice.modelName() << "\n";
debug << "model number | " << DiscoveryDevice.modelNumber() << "\n";
debug << "model description | " << DiscoveryDevice.modelDescription() << "\n";
debug << "model URL | " << DiscoveryDevice.modelURL().toString() << "\n";
debug << "UUID | " << DiscoveryDevice.uuid() << "\n";
return debug;
} }

View File

@ -1,20 +1,20 @@
/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * /* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
* * * *
* Copyright (C) 2015 Simon Stuerz <stuerz.simon@gmail.com> * * Copyright (C) 2018 Michael Zanetti <michael.zanetti@guh.io> *
* * * *
* This file is part of mea. * * This file is part of mea. *
* * * *
* mea is free software: you can redistribute it and/or modify * * mea is free software: you can redistribute it and/or modify *
* it under the terms of the GNU General Public License as published by * * it under the terms of the GNU General Public License as published by *
* the Free Software Foundation, version 3 of the License. * * the Free Software Foundation, version 3 of the License. *
* * * *
* mea is distributed in the hope that it will be useful, * * mea is distributed in the hope that it will be useful, *
* but WITHOUT ANY WARRANTY; without even the implied warranty of * * but WITHOUT ANY WARRANTY; without even the implied warranty of *
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
* GNU General Public License for more details. * * GNU General Public License for more details. *
* * * *
* You should have received a copy of the GNU General Public License * * You should have received a copy of the GNU General Public License *
* along with mea. If not, see <http://www.gnu.org/licenses/>. * * along with mea. If not, see <http://www.gnu.org/licenses/>. *
* * * *
* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
@ -22,72 +22,115 @@
#define DISCOVERYDEVICE_H #define DISCOVERYDEVICE_H
#include <QObject> #include <QObject>
#include <QUuid>
#include <QUrl> #include <QUrl>
#include <QHostAddress> #include <QHostAddress>
#include <QObject>
#include <QAbstractListModel>
class DiscoveryDevice class PortConfig: public QObject
{ {
Q_OBJECT
Q_PROPERTY(int port READ port CONSTANT)
Q_PROPERTY(Protocol protocol READ protocol NOTIFY protocolChanged)
Q_PROPERTY(bool sslEnabled READ sslEnabled NOTIFY sslEnabledChanged)
public: public:
explicit DiscoveryDevice(); enum Protocol {
ProtocolNymeaRpc,
QUrl location() const; ProtocolWebSocket
void setLocation(const QUrl &location); };
Q_ENUM(Protocol)
QString webSocketUrl() const; PortConfig(int port, QObject *parent = nullptr);
void setWebSocketUrl(const QString &webSocketUrl);
QString nymeaRpcUrl() const;
void setNymeaRpcUrl(const QString &nymeaRpcUrl);
QHostAddress hostAddress() const;
void setHostAddress(const QHostAddress &hostAddress);
int port() const; int port() const;
void setPort(const int &port);
QString deviceType() const; Protocol protocol() const;
void setDeviceType(const QString & deviceType); void setProtocol(Protocol protocol);
QString friendlyName() const; bool sslEnabled() const;
void setFriendlyName(const QString &friendlyName); void setSslEnabled(bool sslEnabled);
QString manufacturer() const; signals:
void setManufacturer(const QString &manufacturer); void protocolChanged();
void sslEnabledChanged();
QUrl manufacturerURL() const;
void setManufacturerURL(const QUrl & manufacturerURL);
QString modelDescription() const;
void setModelDescription(const QString & modelDescription);
QString modelName() const;
void setModelName(const QString & modelName);
QString modelNumber() const;
void setModelNumber(const QString &modelNumber);
QUrl modelURL() const;
void setModelURL(const QUrl &modelURL);
QString uuid() const;
void setUuid(const QString &uuid);
private: private:
QUrl m_location; int m_port = -1;
QString m_webSocketUrl; Protocol m_protocol = ProtocolNymeaRpc;
QString m_nymeaRpcUrl; bool m_sslEnabled = false;
QHostAddress m_hostAddress;
int m_port;
QString m_friendlyName;
QString m_manufacturer;
QUrl m_manufacturerURL;
QString m_modelDescription;
QString m_modelName;
QString m_modelNumber;
QUrl m_modelURL;
QString m_uuid;
}; };
QDebug operator<< (QDebug debug, const DiscoveryDevice &discoveryDevice); class PortConfigs: public QAbstractListModel
{
Q_OBJECT
Q_PROPERTY(int count READ rowCount NOTIFY countChanged)
public:
enum Roles {
RolePort,
RoleProtocol,
RoleSSLEnabled
};
Q_ENUM(Roles)
PortConfigs(QObject* parent = nullptr);
int rowCount(const QModelIndex &parent = QModelIndex()) const override;
QVariant data(const QModelIndex & index, int role = Qt::DisplayRole) const override;
PortConfig* find(int port);
void insert(PortConfig* portConfig);
Q_INVOKABLE PortConfig *get(int index) const;
signals:
void countChanged();
protected:
QHash<int, QByteArray> roleNames() const override;
private:
QList<PortConfig*> m_portConfigs;
};
class DiscoveryDevice: public QObject
{
Q_OBJECT
Q_PROPERTY(QUuid uuid READ uuid CONSTANT)
Q_PROPERTY(QString hostAddress READ hostAddressString NOTIFY hostAddressChanged)
Q_PROPERTY(QString name READ name NOTIFY nameChanged)
Q_PROPERTY(QString version READ version NOTIFY versionChanged)
Q_PROPERTY(PortConfigs* portConfigs READ portConfigs CONSTANT)
public:
explicit DiscoveryDevice(QObject *parent = nullptr);
QUuid uuid() const;
void setUuid(const QUuid &uuid);
QHostAddress hostAddress() const;
QString hostAddressString() const;
void setHostAddress(const QHostAddress &hostAddress);
QString name() const;
void setName(const QString &name);
QString version() const;
void setVersion(const QString &version);
PortConfigs *portConfigs() const;
Q_INVOKABLE QString toUrl(int portConfigIndex);
signals:
void nameChanged();
void hostAddressChanged();
void versionChanged();
private:
QUuid m_uuid;
QHostAddress m_hostAddress;
QString m_name;
QString m_version;
PortConfigs *m_portConfigs = nullptr;
};
#endif // DISCOVERYDEVICE_H #endif // DISCOVERYDEVICE_H

View File

@ -25,11 +25,6 @@ DiscoveryModel::DiscoveryModel(QObject *parent) :
{ {
} }
QList<DiscoveryDevice> DiscoveryModel::devices()
{
return m_devices;
}
int DiscoveryModel::rowCount(const QModelIndex &parent) const int DiscoveryModel::rowCount(const QModelIndex &parent) const
{ {
Q_UNUSED(parent) Q_UNUSED(parent)
@ -41,61 +36,54 @@ QVariant DiscoveryModel::data(const QModelIndex &index, int role) const
if (index.row() < 0 || index.row() >= m_devices.count()) if (index.row() < 0 || index.row() >= m_devices.count())
return QVariant(); return QVariant();
DiscoveryDevice device = m_devices.at(index.row()); DiscoveryDevice *device = m_devices.at(index.row());
if (role == NameRole) { switch (role) {
return device.friendlyName(); case UuidRole:
} else if (role == HostAddressRole) { return device->uuid();
return device.hostAddress().toString(); case NameRole:
} else if (role == WebSocketUrlRole) { return device->name();
return device.webSocketUrl(); case HostAddressRole:
} else if (role == PortRole) { return device->hostAddress().toString();
return device.port(); // case WebSocketUrlRole:
} else if (role == VersionRole) { // return device.webSocketUrl();
return device.modelNumber(); // case PortRole:
} else if (role == NymeaRpcUrlRole) { // return device.port();
return device.nymeaRpcUrl(); case VersionRole:
return device->version();
// case NymeaRpcUrlRole:
// return device.nymeaRpcUrl();
} }
return QVariant(); return QVariant();
} }
void DiscoveryModel::addDevice(const DiscoveryDevice &device) void DiscoveryModel::addDevice(DiscoveryDevice *device)
{ {
for (int i = 0; i < m_devices.count(); i++) { for (int i = 0; i < m_devices.count(); i++) {
if (m_devices.at(i).uuid() == device.uuid()) { if (m_devices.at(i)->uuid() == device->uuid()) {
m_devices[i] = device; qWarning() << "Device already added. Update existing device instead.";
emit dataChanged(index(i), index(i));
return; return;
} }
} }
device->setParent(this);
beginInsertRows(QModelIndex(), m_devices.count(), m_devices.count()); beginInsertRows(QModelIndex(), m_devices.count(), m_devices.count());
m_devices.append(device); m_devices.append(device);
endInsertRows(); endInsertRows();
emit countChanged(); emit countChanged();
} }
QString DiscoveryModel::get(int index, DeviceRole role) const DiscoveryDevice *DiscoveryModel::get(int index) const
{ {
return data(this->index(index), role).toString(); return m_devices.at(index);
} }
bool DiscoveryModel::contains(const QString &uuid) const DiscoveryDevice *DiscoveryModel::find(const QUuid &uuid)
{ {
foreach (const DiscoveryDevice &dev, m_devices) { foreach (DiscoveryDevice *dev, m_devices) {
if (dev.uuid() == uuid) { if (dev->uuid() == uuid) {
return true;
}
}
return false;
}
DiscoveryDevice DiscoveryModel::find(const QHostAddress &address) const
{
foreach (const DiscoveryDevice &dev, m_devices) {
if (dev.hostAddress() == address) {
return dev; return dev;
} }
} }
return DiscoveryDevice(); return nullptr;
} }
void DiscoveryModel::clearModel() void DiscoveryModel::clearModel()
@ -109,11 +97,9 @@ void DiscoveryModel::clearModel()
QHash<int, QByteArray> DiscoveryModel::roleNames() const QHash<int, QByteArray> DiscoveryModel::roleNames() const
{ {
QHash<int, QByteArray> roles; QHash<int, QByteArray> roles;
roles[UuidRole] = "uuid";
roles[NameRole] = "name"; roles[NameRole] = "name";
roles[HostAddressRole] = "hostAddress"; roles[HostAddressRole] = "hostAddress";
roles[WebSocketUrlRole] = "webSocketUrl";
roles[NymeaRpcUrlRole] = "nymeaRpcUrl";
roles[PortRole] = "port";
roles[VersionRole] = "version"; roles[VersionRole] = "version";
return roles; return roles;
} }

View File

@ -32,27 +32,22 @@ class DiscoveryModel : public QAbstractListModel
Q_PROPERTY(int count READ rowCount NOTIFY countChanged) Q_PROPERTY(int count READ rowCount NOTIFY countChanged)
public: public:
enum DeviceRole { enum DeviceRole {
UuidRole,
NameRole, NameRole,
HostAddressRole, HostAddressRole,
WebSocketUrlRole,
NymeaRpcUrlRole,
PortRole,
VersionRole VersionRole
}; };
Q_ENUM(DeviceRole) Q_ENUM(DeviceRole)
explicit DiscoveryModel(QObject *parent = 0); explicit DiscoveryModel(QObject *parent = 0);
QList<DiscoveryDevice> devices();
int rowCount(const QModelIndex & parent = QModelIndex()) const; int rowCount(const QModelIndex & parent = QModelIndex()) const;
QVariant data(const QModelIndex & index, int role = Qt::DisplayRole) const; QVariant data(const QModelIndex & index, int role = Qt::DisplayRole) const;
void addDevice(const DiscoveryDevice &device); void addDevice(DiscoveryDevice *device);
Q_INVOKABLE QString get(int index, DiscoveryModel::DeviceRole role) const; Q_INVOKABLE DiscoveryDevice* get(int index) const;
bool contains(const QString &uuid) const; Q_INVOKABLE DiscoveryDevice* find(const QUuid &uuid);
DiscoveryDevice find(const QHostAddress &address) const;
void clearModel(); void clearModel();
@ -63,7 +58,7 @@ protected:
QHash<int, QByteArray> roleNames() const; QHash<int, QByteArray> roleNames() const;
private: private:
QList<DiscoveryDevice> m_devices; QList<DiscoveryDevice*> m_devices;
}; };
#endif // DISCOVERYMODEL_H #endif // DISCOVERYMODEL_H

View File

@ -114,7 +114,7 @@ void UpnpDiscovery::writeDiscoveryPacket()
"MX:2\r\n" "MX:2\r\n"
"ST: ssdp:all\r\n\r\n"); "ST: ssdp:all\r\n\r\n");
qDebug() << "sending discovery packet"; // qDebug() << "sending discovery packet";
writeDatagram(ssdpSearchMessage, m_host, m_port); writeDatagram(ssdpSearchMessage, m_host, m_port);
} }
@ -170,97 +170,104 @@ void UpnpDiscovery::readData()
if (!m_foundDevices.contains(location) && isNymea) { if (!m_foundDevices.contains(location) && isNymea) {
m_foundDevices.append(location); m_foundDevices.append(location);
DiscoveryDevice discoveryDevice;
discoveryDevice.setHostAddress(hostAddress);
discoveryDevice.setPort(port);
discoveryDevice.setLocation(location.toString());
qDebug() << "Getting server data from:" << location; qDebug() << "Getting server data from:" << location;
QNetworkReply *reply = m_networkAccessManager->get(QNetworkRequest(location)); QNetworkReply *reply = m_networkAccessManager->get(QNetworkRequest(location));
connect(reply, &QNetworkReply::sslErrors, [this, reply](const QList<QSslError> &errors){ connect(reply, &QNetworkReply::sslErrors, [this, reply](const QList<QSslError> &errors){
reply->ignoreSslErrors(errors); reply->ignoreSslErrors(errors);
}); });
m_runningReplies.insert(reply, discoveryDevice); m_runningReplies.insert(reply, hostAddress);
} }
} }
} }
void UpnpDiscovery::networkReplyFinished(QNetworkReply *reply) void UpnpDiscovery::networkReplyFinished(QNetworkReply *reply)
{ {
reply->deleteLater();
QHostAddress discoveredAddress = m_runningReplies.take(reply);
int status = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt(); int status = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
if (reply->error() != QNetworkReply::NoError || status != 200) {
qWarning() << "Error fetching UPnP discovery data:" << status << reply->error() << reply->errorString();
return;
}
QByteArray data = reply->readAll(); QByteArray data = reply->readAll();
DiscoveryDevice discoveryDevice = m_runningReplies.take(reply);
switch (status) { QString name;
case(200):{ QString version;
// parse XML data QUuid uuid;
QXmlStreamReader xml(data); QList<PortConfig*> portConfigList;
while (!xml.atEnd() && !xml.hasError()) {
xml.readNext();
if (xml.isStartDocument()) // parse XML data
continue; QXmlStreamReader xml(data);
while (!xml.atEnd() && !xml.hasError()) {
xml.readNext();
if (xml.isStartElement()) { if (xml.isStartDocument())
if (xml.name().toString() == "websocketURL") { continue;
discoveryDevice.setWebSocketUrl(xml.readElementText());
} if (xml.isStartElement()) {
if (xml.name().toString() == "websocketURL") {
QUrl u(xml.readElementText());
PortConfig *pc = new PortConfig(u.port());
pc->setProtocol(PortConfig::ProtocolWebSocket);
pc->setSslEnabled(u.scheme().endsWith('s'));
portConfigList.append(pc);
} }
}
if (xml.isStartElement()) { if (xml.isStartElement()) {
if (xml.name().toString() == "nymeaRpcURL") { if (xml.name().toString() == "nymeaRpcURL") {
discoveryDevice.setNymeaRpcUrl(xml.readElementText()); QUrl u(xml.readElementText());
} qDebug() << "have url" << u << u.scheme();
PortConfig *pc = new PortConfig(u.port());
pc->setProtocol(PortConfig::ProtocolNymeaRpc);
pc->setSslEnabled(u.scheme().endsWith('s'));
portConfigList.append(pc);
} }
}
if (xml.isStartElement()) { if (xml.isStartElement()) {
if (xml.name().toString() == "device") { if (xml.name().toString() == "device") {
while (!xml.atEnd()) { while (!xml.atEnd()) {
if (xml.name() == "friendlyName" && xml.isStartElement()) { if (xml.name() == "friendlyName" && xml.isStartElement()) {
discoveryDevice.setFriendlyName(xml.readElementText()); name = xml.readElementText();
} }
if (xml.name() == "manufacturer" && xml.isStartElement()) { if (xml.name() == "modelNumber" && xml.isStartElement()) {
discoveryDevice.setManufacturer(xml.readElementText()); version = xml.readElementText();
} }
if (xml.name() == "manufacturerURL" && xml.isStartElement()) { if (xml.name() == "UDN" && xml.isStartElement()) {
discoveryDevice.setManufacturerURL(QUrl(xml.readElementText())); uuid = xml.readElementText().split(':').last();
}
if (xml.name() == "modelDescription" && xml.isStartElement()) {
discoveryDevice.setModelDescription(xml.readElementText());
}
if (xml.name() == "modelName" && xml.isStartElement()) {
discoveryDevice.setModelName(xml.readElementText());
}
if (xml.name() == "modelNumber" && xml.isStartElement()) {
discoveryDevice.setModelNumber(xml.readElementText());
}
if (xml.name() == "modelURL" && xml.isStartElement()) {
discoveryDevice.setModelURL(QUrl(xml.readElementText()));
}
if (xml.name() == "UDN" && xml.isStartElement()) {
discoveryDevice.setUuid(xml.readElementText());
}
xml.readNext();
} }
xml.readNext(); xml.readNext();
} }
xml.readNext();
} }
} }
}
qDebug() << "discovered device" << discoveryDevice.friendlyName() << discoveryDevice.hostAddress(); qDebug() << "discovered device" << uuid << name << discoveredAddress << version;
if (discoveryDevice.manufacturer().contains("guh")) { DiscoveryDevice* device = m_discoveryModel->find(uuid);
if (!m_discoveryModel->contains(discoveryDevice.uuid())) { if (!device) {
m_discoveryModel->addDevice(discoveryDevice); device = new DiscoveryDevice(m_discoveryModel);
} device->setUuid(uuid);
qDebug() << "Adding new host to model";
m_discoveryModel->addDevice(device);
}
device->setHostAddress(discoveredAddress);
device->setName(name);
device->setVersion(version);
foreach (PortConfig *pc, portConfigList) {
PortConfig *portConfig = device->portConfigs()->find(pc->port());
if (portConfig) {
qDebug() << "Updating port config" << portConfig->port() << portConfig->sslEnabled() << portConfig->protocol();
portConfig->setProtocol(pc->protocol());
portConfig->setSslEnabled(pc->sslEnabled());
pc->deleteLater();
} else {
qDebug() << "adding new port config" << pc->port() << pc->sslEnabled() << pc->protocol();
device->portConfigs()->insert(pc);
} }
break;
} }
default:
qWarning() << "HTTP request error " << status;
}
reply->deleteLater();
} }

View File

@ -55,7 +55,7 @@ private:
bool m_discovering; bool m_discovering;
bool m_available; bool m_available;
QHash<QNetworkReply *, DiscoveryDevice> m_runningReplies; QHash<QNetworkReply *, QHostAddress> m_runningReplies;
QList<QUrl> m_foundDevices; QList<QUrl> m_foundDevices;
void setDiscovering(const bool &discovering); void setDiscovering(const bool &discovering);

View File

@ -49,6 +49,7 @@ void ZeroconfDiscovery::serviceEntryAdded(const QZeroConfService &entry)
QString uuid; QString uuid;
bool sslEnabled = false; bool sslEnabled = false;
QString serverName; QString serverName;
QString version;
foreach (const QByteArray &key, entry.txt().keys()) { foreach (const QByteArray &key, entry.txt().keys()) {
QPair<QString, QString> txtRecord = qMakePair<QString, QString>(key, entry.txt().value(key)); QPair<QString, QString> txtRecord = qMakePair<QString, QString>(key, entry.txt().value(key));
if (!sslEnabled && txtRecord.first == "sslEnabled") { if (!sslEnabled && txtRecord.first == "sslEnabled") {
@ -60,34 +61,30 @@ void ZeroconfDiscovery::serviceEntryAdded(const QZeroConfService &entry)
if (txtRecord.first == "name") { if (txtRecord.first == "name") {
serverName = txtRecord.second; serverName = txtRecord.second;
} }
if (txtRecord.first == "serverVersion") {
version = txtRecord.second;
}
} }
qDebug() << "avahi service entry added" << serverName << uuid << sslEnabled; qDebug() << "avahi service entry added" << serverName << uuid << sslEnabled;
DiscoveryDevice dev = m_discoveryModel->find(entry.ip());
if (dev.uuid() == uuid && dev.nymeaRpcUrl().startsWith("nymeas") && !sslEnabled) {
// We already have this host and with a more secure configuration... skip this one...
return;
}
qDebug() << "Adding new found entry:" << entry.name() << entry.ip();
dev.setUuid(uuid);
dev.setHostAddress(entry.ip());
dev.setPort(entry.port());
dev.setFriendlyName(serverName + " on " + entry.ip().toString());
QHostAddress address = entry.ip();
QString addressString;
if (address.protocol() == QAbstractSocket::IPv6Protocol) {
addressString = "[" + address.toString() + "]";
} else {
addressString = address.toString();
}
if (entry.type() == "_ws._tcp") {
dev.setWebSocketUrl(QString("%1://%2:%3").arg(sslEnabled ? "wss" : "ws").arg(addressString).arg(entry.port()));
} else {
dev.setNymeaRpcUrl(QString("%1://%2:%3").arg(sslEnabled ? "nymeas" : "nymea").arg(addressString).arg(entry.port()));
}
m_discoveryModel->addDevice(dev);
// DiscoveryDevice *dev = new DiscoveryDevice(); DiscoveryDevice* device = m_discoveryModel->find(uuid);
// dev->setFriendlyName(entry.hostName()); if (!device) {
device = new DiscoveryDevice(m_discoveryModel);
device->setUuid(uuid);
qDebug() << "Adding new host to model";
m_discoveryModel->addDevice(device);
}
device->setHostAddress(entry.ip());
device->setName(serverName);
device->setVersion(version);
PortConfig *portConfig = device->portConfigs()->find(entry.port());
if (!portConfig) {
qDebug() << "Adding new port config";
portConfig = new PortConfig(entry.port());
device->portConfigs()->insert(portConfig);
}
portConfig->setProtocol(entry.type() == "_ws._tcp" ? PortConfig::ProtocolWebSocket : PortConfig::ProtocolNymeaRpc);
portConfig->setSslEnabled(sslEnabled);
} }
#endif #endif

View File

@ -224,7 +224,7 @@ void JsonRpcClient::sendRequest(const QVariantMap &request)
{ {
QVariantMap newRequest = request; QVariantMap newRequest = request;
newRequest.insert("token", m_token); newRequest.insert("token", m_token);
qDebug() << "Sending request" << qUtf8Printable(QJsonDocument::fromVariant(newRequest).toJson()); // qDebug() << "Sending request" << qUtf8Printable(QJsonDocument::fromVariant(newRequest).toJson());
m_connection->sendData(QJsonDocument::fromVariant(newRequest).toJson()); m_connection->sendData(QJsonDocument::fromVariant(newRequest).toJson());
} }
@ -259,7 +259,7 @@ void JsonRpcClient::dataReceived(const QByteArray &data)
// qWarning() << "Could not parse json data from mea" << data << error.errorString(); // qWarning() << "Could not parse json data from mea" << data << error.errorString();
return; return;
} }
qDebug() << "received response" << m_receiveBuffer.left(splitIndex); // qDebug() << "received response" << m_receiveBuffer.left(splitIndex);
m_receiveBuffer = m_receiveBuffer.right(m_receiveBuffer.length() - splitIndex - 1); m_receiveBuffer = m_receiveBuffer.right(m_receiveBuffer.length() - splitIndex - 1);
if (!m_receiveBuffer.isEmpty()) { if (!m_receiveBuffer.isEmpty()) {
staticMetaObject.invokeMethod(this, "dataReceived", Qt::QueuedConnection, Q_ARG(QByteArray, QByteArray())); staticMetaObject.invokeMethod(this, "dataReceived", Qt::QueuedConnection, Q_ARG(QByteArray, QByteArray()));

View File

@ -115,6 +115,9 @@ void registerQmlTypes() {
qmlRegisterType<NymeaDiscovery>(uri, 1, 0, "NymeaDiscovery"); qmlRegisterType<NymeaDiscovery>(uri, 1, 0, "NymeaDiscovery");
qmlRegisterUncreatableType<DiscoveryModel>(uri, 1, 0, "DiscoveryModel", "Get it from NymeaDiscovery"); qmlRegisterUncreatableType<DiscoveryModel>(uri, 1, 0, "DiscoveryModel", "Get it from NymeaDiscovery");
qmlRegisterUncreatableType<DiscoveryDevice>(uri, 1, 0, "DiscoveryDevice", "Get it from DiscoveryModel");
qmlRegisterUncreatableType<PortConfigs>(uri, 1, 0, "PortConfigs", "Get it from DiscoveryDevice");
qmlRegisterUncreatableType<PortConfig>(uri, 1, 0, "PortConfig", "Get it from DiscoveryDevice");
qmlRegisterType<EventDescriptorParamsFilterModel>(uri, 1, 0, "EventDescriptorParamsFilterModel"); qmlRegisterType<EventDescriptorParamsFilterModel>(uri, 1, 0, "EventDescriptorParamsFilterModel");

View File

@ -13,7 +13,7 @@ win32:Debug:LIBS += -L$$top_builddir/libmea-core/debug \
win32:Release:LIBS += -L$$top_builddir/libmea-core/release \ win32:Release:LIBS += -L$$top_builddir/libmea-core/release \
-L$$top_builddir/libnymea-common/release -L$$top_builddir/libnymea-common/release
linux:!android:LIBS += -lavahi-client -lavahi-common linux:!android:LIBS += -lavahi-client -lavahi-common
PRE_TARGETDEPS += ../libmea-core
HEADERS += \ HEADERS += \
stylecontroller.h stylecontroller.h

View File

@ -164,5 +164,7 @@
<file>ui/AppSettingsPage.qml</file> <file>ui/AppSettingsPage.qml</file>
<file>ui/images/stock_application.svg</file> <file>ui/images/stock_application.svg</file>
<file>ui/delegates/ThingDelegate.qml</file> <file>ui/delegates/ThingDelegate.qml</file>
<file>ui/images/network-secure.svg</file>
<file>ui/images/lock-broken.svg</file>
</qresource> </qresource>
</RCC> </RCC>

View File

@ -1,7 +1,7 @@
import QtQuick 2.5 import QtQuick 2.9
import QtQuick.Controls 2.1 import QtQuick.Controls 2.2
import QtQuick.Controls.Material 2.1 import QtQuick.Controls.Material 2.2
import QtQuick.Layouts 1.1 import QtQuick.Layouts 1.3
import Mea 1.0 import Mea 1.0
import "components" import "components"
@ -24,9 +24,8 @@ Page {
target: Engine.connection target: Engine.connection
onVerifyConnectionCertificate: { onVerifyConnectionCertificate: {
print("verify cert!") print("verify cert!")
certDialog.issuerInfo = issuerInfo var popup = certDialogComponent.createObject(app, {issuerInfo: issuerInfo, fingerprint: fingerprint});
certDialog.fingerprint = fingerprint popup.open();
certDialog.open();
} }
onConnectionError: { onConnectionError: {
pageStack.pop(root) pageStack.pop(root)
@ -118,34 +117,81 @@ Page {
model: discovery.discoveryModel model: discovery.discoveryModel
clip: true clip: true
delegate: ItemDelegate { delegate: SwipeDelegate {
width: parent.width width: parent.width
height: app.delegateHeight height: app.delegateHeight
objectName: "discoveryDelegate" + index objectName: "discoveryDelegate" + index
property string hostAddress: model.hostAddress contentItem: RowLayout {
ColumnLayout { ColumnLayout {
anchors.fill: parent Layout.fillWidth: true
anchors.margins: app.margins Label {
Label { text: model.name
text: model.name Layout.fillWidth: true
elide: Text.ElideRight
}
Label {
text: model.hostAddress
font.pixelSize: app.smallFont
}
} }
Label { ColorIcon {
text: model.hostAddress Layout.fillHeight: true
font.pixelSize: app.smallFont Layout.preferredWidth: height
name: "../images/network-secure.svg"
visible: {
var discoveryDevice = discovery.discoveryModel.get(index);
for (var i = 0; i < discoveryDevice.portConfigs.count; i++) {
if (discoveryDevice.portConfigs.get(i).sslEnabled) {
return true;
}
}
return false;
}
} }
} }
onClicked: {
var url;
if (model.nymeaRpcUrl) {
url = model.nymeaRpcUrl;
} else {
url = model.webSocketUrl;
}
print("Should connect to", url)
Engine.connection.connect(url)
onClicked: {
var discoveryDevice = discovery.discoveryModel.get(index);
print("discoveryDevice:", discoveryDevice.name, discoveryDevice.uuid, discoveryDevice.hostAddress)
var usedConfigIndex = 0;
for (var i = 1; i < discoveryDevice.portConfigs.count; i++) {
var oldConfig = discoveryDevice.portConfigs.get(usedConfigIndex);
var newConfig = discoveryDevice.portConfigs.get(i);
// prefer secure over insecure
if (!oldConfig.sslEnabled && newConfig.sslEnabled) {
usedConfigIndex = i;
continue;
}
if (oldConfig.sslEnabled && !newConfig.sslEnabled) {
continue; // discard new one as the one we already have is more secure
}
// both options are new either secure or insecure, prefer nymearpc over websocket for less overhead
if (oldConfig.protocol === PortConfig.ProtocolWebSocket && newConfig.protocol === PortConfig.ProtocolNymeaRpc) {
usedConfigIndex = i;
}
}
Engine.connection.connect(discoveryDevice.toUrl(usedConfigIndex))
pageStack.push(connectingPage) pageStack.push(connectingPage)
} }
swipe.right: MouseArea {
height: parent.height
width: height
anchors.right: parent.right
ColorIcon {
anchors.fill: parent
anchors.margins: app.margins
name: "../images/info.svg"
}
onClicked: {
swipe.close()
var popup = infoDialog.createObject(app,{discoveryDevice: discovery.discoveryModel.get(index)})
popup.open()
}
}
} }
Column { Column {
@ -317,82 +363,231 @@ Page {
} }
} }
Dialog { Component {
id: certDialog id: certDialogComponent
width: Math.min(parent.width * .9, 400)
x: (parent.width - width) / 2
y: (parent.height - height) / 2
standardButtons: Dialog.Yes | Dialog.No
property var fingerprint Dialog {
property var issuerInfo id: certDialog
width: Math.min(parent.width * .9, 400)
x: (parent.width - width) / 2
y: (parent.height - height) / 2
standardButtons: Dialog.Yes | Dialog.No
ColumnLayout { property var fingerprint
anchors { left: parent.left; right: parent.right; top: parent.top } property var issuerInfo
spacing: app.margins
RowLayout { ColumnLayout {
Layout.fillWidth: true id: certLayout
spacing: app.margins anchors.fill: parent
ColorIcon { // spacing: app.margins
Layout.preferredHeight: app.iconSize * 2
Layout.preferredWidth: height RowLayout {
name: "../images/dialog-warning-symbolic.svg" Layout.fillWidth: true
color: app.guhAccent spacing: app.margins
ColorIcon {
Layout.preferredHeight: app.iconSize * 2
Layout.preferredWidth: height
name: "../images/dialog-warning-symbolic.svg"
color: app.guhAccent
}
Label {
id: titleLabel
Layout.fillWidth: true
wrapMode: Text.WordWrap
text: qsTr("Warning")
color: app.guhAccent
font.pixelSize: app.largeFont
}
} }
Label { Label {
id: titleLabel
Layout.fillWidth: true Layout.fillWidth: true
wrapMode: Text.WordWrap wrapMode: Text.WordWrap
text: qsTr("Warning") text: qsTr("The authenticity of this %1 box cannot be verified.").arg(app.systemName)
color: app.guhAccent }
font.pixelSize: app.largeFont
Label {
Layout.fillWidth: true
wrapMode: Text.WordWrap
text: qsTr("If this is the first time you connect to this box, this is expected. Once you trust a box, you should never see this message again for that one. If you see this message multiple times for the same box, something suspicious is going on!")
}
ThinDivider {}
Item {
Layout.fillWidth: true
Layout.fillHeight: true
implicitHeight: certGridLayout.implicitHeight
Flickable {
anchors.fill: parent
contentHeight: certGridLayout.implicitHeight
clip: true
GridLayout {
id: certGridLayout
columns: 2
width: parent.width
Repeater {
model: certDialog.issuerInfo
Label {
Layout.fillWidth: true
wrapMode: Text.WordWrap
text: modelData
}
}
Label {
Layout.fillWidth: true
Layout.columnSpan: 2
wrapMode: Text.WrapAtWordBoundaryOrAnywhere
text: qsTr("Fingerprint: ") + certDialog.fingerprint
}
}
}
}
ThinDivider {}
Label {
Layout.fillWidth: true
wrapMode: Text.WordWrap
text: qsTr("Do you want to trust this device?")
font.bold: true
} }
} }
Label {
Layout.fillWidth: true onAccepted: {
wrapMode: Text.WordWrap Engine.connection.acceptCertificate(certDialog.fingerprint)
text: qsTr("The authenticity of this %1 box cannot be verified.").arg(app.systemName)
} }
}
}
Label {
Layout.fillWidth: true
wrapMode: Text.WordWrap
text: qsTr("If this is the first time you connect to this box, this is expected. Once you trust a box, you should never see this message again for that one. If you see this message multiple times for the same box, something suspicious is going on!")
}
GridLayout { Component {
columns: 2 id: infoDialog
Dialog {
id: dialog
width: Math.min(parent.width, contentGrid.implicitWidth)
x: (parent.width - width) / 2
y: (parent.height - height) / 2
modal: true
title: qsTr("Box information")
Repeater { standardButtons: Dialog.Ok
model: certDialog.issuerInfo
property var discoveryDevice: null
header: Item {
implicitHeight: headerRow.height + app.margins * 2
implicitWidth: parent.width
RowLayout {
id: headerRow
anchors { left: parent.left; right: parent.right; top: parent.top; margins: app.margins }
spacing: app.margins
ColorIcon {
Layout.preferredHeight: app.iconSize * 2
Layout.preferredWidth: height
name: "../images/info.svg"
color: app.guhAccent
}
Label { Label {
id: titleLabel
Layout.fillWidth: true Layout.fillWidth: true
wrapMode: Text.WordWrap wrapMode: Text.WrapAtWordBoundaryOrAnywhere
text: modelData text: dialog.title
color: app.guhAccent
font.pixelSize: app.largeFont
} }
} }
} }
Label { GridLayout {
Layout.fillWidth: true id: contentGrid
wrapMode: Text.WrapAtWordBoundaryOrAnywhere anchors.fill: parent
text: qsTr("Fingerprint: ") + certDialog.fingerprint rowSpacing: app.margins
} columns: 2
Label {
text: "Name:"
}
Label {
text: dialog.discoveryDevice.name
Layout.fillWidth: true
elide: Text.ElideRight
}
Label {
text: "UUID:"
}
Label {
text: dialog.discoveryDevice.uuid
Layout.fillWidth: true
elide: Text.ElideRight
}
Label {
text: "Version:"
}
Label {
text: dialog.discoveryDevice.version
Layout.fillWidth: true
elide: Text.ElideRight
}
Label {
text: "IP Address:"
}
Label {
text: dialog.discoveryDevice.hostAddress
Layout.fillWidth: true
elide: Text.ElideRight
}
ThinDivider { Layout.columnSpan: 2 }
Label {
Layout.columnSpan: 2
text: qsTr("Available connections")
}
Label { Flickable {
Layout.fillWidth: true Layout.columnSpan: 2
wrapMode: Text.WordWrap Layout.fillWidth: true
text: qsTr("Do you want to trust this device?") Layout.preferredHeight: 200
font.bold: true contentHeight: contentColumn.implicitHeight
} clip: true
} ColumnLayout {
id: contentColumn
width: parent.width
Repeater {
model: dialog.discoveryDevice.portConfigs
ItemDelegate {
Layout.fillWidth: true
contentItem: RowLayout {
ColumnLayout {
Layout.fillWidth: true
Label {
text: qsTr("Port: %1").arg(model.port)
}
Label {
text: model.protocol === PortConfig.ProtocolNymeaRpc ? "nymea-rpc" : "websocket"
Layout.fillWidth: true
font.pixelSize: app.smallFont
}
}
onAccepted: { ColorIcon {
Engine.connection.acceptCertificate(certDialog.fingerprint) Layout.preferredHeight: app.iconSize
Layout.preferredWidth: height
name: model.sslEnabled ? "../images/network-secure.svg" : "../images/lock-broken.svg"
color: model.sslEnabled ? app.guhAccent : "red"
}
}
onClicked: {
Engine.connection.connect(dialog.discoveryDevice.toUrl(index))
}
}
}
}
}
}
} }
} }
} }

View File

@ -0,0 +1,173 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<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"
width="96"
height="96"
id="svg4874"
version="1.1"
inkscape:version="0.91+devel r"
viewBox="0 0 96 96.000001"
sodipodi:docname="lock-broken.svg">
<defs
id="defs4876" />
<sodipodi:namedview
id="base"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:pageopacity="0.0"
inkscape:pageshadow="2"
inkscape:zoom="5.6199993"
inkscape:cx="-18.558726"
inkscape:cy="57.553367"
inkscape:document-units="px"
inkscape:current-layer="g4780"
showgrid="true"
showborder="true"
fit-margin-top="0"
fit-margin-left="0"
fit-margin-right="0"
fit-margin-bottom="0"
inkscape:snap-bbox="true"
inkscape:bbox-paths="true"
inkscape:bbox-nodes="true"
inkscape:snap-bbox-edge-midpoints="true"
inkscape:snap-bbox-midpoints="true"
inkscape:object-paths="true"
inkscape:snap-intersection-paths="true"
inkscape:object-nodes="true"
inkscape:snap-smooth-nodes="true"
inkscape:snap-midpoints="true"
inkscape:snap-object-midpoints="true"
inkscape:snap-center="true"
showguides="true"
inkscape:guide-bbox="true">
<inkscape:grid
type="xygrid"
id="grid5451"
empspacing="8" />
<sodipodi:guide
orientation="1,0"
position="8,-8.0000001"
id="guide4063" />
<sodipodi:guide
orientation="1,0"
position="4,-8.0000001"
id="guide4065" />
<sodipodi:guide
orientation="0,1"
position="-8,88.000001"
id="guide4067" />
<sodipodi:guide
orientation="0,1"
position="-8,92.000001"
id="guide4069" />
<sodipodi:guide
orientation="0,1"
position="104,4"
id="guide4071" />
<sodipodi:guide
orientation="0,1"
position="-5,8.0000001"
id="guide4073" />
<sodipodi:guide
orientation="1,0"
position="92,-8.0000001"
id="guide4075" />
<sodipodi:guide
orientation="1,0"
position="88,-8.0000001"
id="guide4077" />
<sodipodi:guide
orientation="0,1"
position="-8,84.000001"
id="guide4074" />
<sodipodi:guide
orientation="1,0"
position="12,-8.0000001"
id="guide4076" />
<sodipodi:guide
orientation="0,1"
position="-5,12"
id="guide4078" />
<sodipodi:guide
orientation="1,0"
position="84,-9.0000001"
id="guide4080" />
<sodipodi:guide
position="48,-8.0000001"
orientation="1,0"
id="guide4170" />
<sodipodi:guide
position="-8,48"
orientation="0,1"
id="guide4172" />
</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
inkscape:label="Layer 1"
inkscape:groupmode="layer"
id="layer1"
transform="translate(67.857146,-78.50504)">
<g
transform="matrix(0,-1,-1,0,373.50506,516.50504)"
id="g4845"
style="display:inline">
<g
inkscape:export-ydpi="90"
inkscape:export-xdpi="90"
inkscape:export-filename="next01.png"
transform="matrix(-0.9996045,0,0,1,575.94296,-611.00001)"
id="g4778"
inkscape:label="Layer 1">
<g
transform="matrix(-1,0,0,1,575.99999,611)"
id="g4780"
style="display:inline">
<rect
style="color:#000000;display:inline;overflow:visible;visibility:visible;fill:none;stroke:none;stroke-width:4;marker:none;enable-background:accumulate"
id="rect4782"
width="96.037987"
height="96"
x="-438.00244"
y="345.36221"
transform="scale(-1,1)" />
<path
style="color:#000000;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:medium;line-height:normal;font-family:sans-serif;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-align:start;text-decoration:none;text-decoration-line:none;text-decoration-style:solid;text-decoration-color:#000000;letter-spacing:normal;word-spacing:normal;text-transform:none;direction:ltr;block-progression:tb;writing-mode:lr-tb;baseline-shift:baseline;text-anchor:start;white-space:normal;shape-padding:0;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:#808080;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:4.00079155;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker:none;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate"
d="m 393.98438,425.36133 -2,0 -46.01758,0 0,-64 48.01758,0 0,64 z m -4,-4 0,-55.99805 -40.01758,0 0,55.99805 40.01758,0 z"
id="rect4269"
inkscape:connector-curvature="0" />
<path
style="color:#000000;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:medium;line-height:normal;font-family:sans-serif;text-indent:0;text-align:start;text-decoration:none;text-decoration-line:none;text-decoration-style:solid;text-decoration-color:#000000;letter-spacing:normal;word-spacing:normal;text-transform:none;direction:ltr;block-progression:tb;writing-mode:lr-tb;baseline-shift:baseline;text-anchor:start;white-space:normal;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:#808080;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:4.00000048;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker:none;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate"
d="M 48 4 C 34.18438 4 22.998047 15.180068 22.998047 28.990234 L 22.998047 45 L 27 45 L 27 28.990234 C 27 17.326469 36.33162 7.9980469 48 7.9980469 C 56.909544 7.9980469 64.43796 13.446423 67.507812 21.210938 L 71.169922 19.642578 C 67.468388 10.473439 58.504779 4 48 4 z M 68.998047 34 L 68.998047 45 L 73 45 L 73 34 L 68.998047 34 z "
transform="matrix(0,-1,-1.0003957,0,438.00245,441.36222)"
id="rect4271" />
<path
style="color:#000000;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:medium;line-height:normal;font-family:sans-serif;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-align:start;text-decoration:none;text-decoration-line:none;text-decoration-style:solid;text-decoration-color:#000000;letter-spacing:normal;word-spacing:normal;text-transform:none;direction:ltr;block-progression:tb;writing-mode:lr-tb;baseline-shift:baseline;text-anchor:start;white-space:normal;shape-padding:0;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:#808080;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:4.00079107;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:1.5999999;stroke-opacity:1;marker:none;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate"
d="m 364.03516,387.09375 -16.96289,8.47852 1.78906,3.58007 15.04883,-7.52148 14.06836,8.03516 14.99804,-8.56641 -1.98437,-3.47461 -13.01367,7.43359 -13.94336,-7.96484 z"
id="path4179"
inkscape:connector-curvature="0" />
</g>
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 8.7 KiB

View File

@ -0,0 +1,175 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<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"
width="96"
height="96"
id="svg4874"
version="1.1"
inkscape:version="0.91+devel r"
viewBox="0 0 96 96.000001"
sodipodi:docname="lock.svg">
<defs
id="defs4876" />
<sodipodi:namedview
id="base"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:pageopacity="0.0"
inkscape:pageshadow="2"
inkscape:zoom="5.6199992"
inkscape:cx="5.4626288"
inkscape:cy="25.355855"
inkscape:document-units="px"
inkscape:current-layer="g4780"
showgrid="true"
showborder="true"
fit-margin-top="0"
fit-margin-left="0"
fit-margin-right="0"
fit-margin-bottom="0"
inkscape:snap-bbox="true"
inkscape:bbox-paths="true"
inkscape:bbox-nodes="true"
inkscape:snap-bbox-edge-midpoints="true"
inkscape:snap-bbox-midpoints="true"
inkscape:object-paths="true"
inkscape:snap-intersection-paths="true"
inkscape:object-nodes="true"
inkscape:snap-smooth-nodes="true"
inkscape:snap-midpoints="true"
inkscape:snap-object-midpoints="true"
inkscape:snap-center="true"
showguides="true"
inkscape:guide-bbox="true">
<inkscape:grid
type="xygrid"
id="grid5451"
empspacing="8" />
<sodipodi:guide
orientation="1,0"
position="8,-8.0000001"
id="guide4063" />
<sodipodi:guide
orientation="1,0"
position="4,-8.0000001"
id="guide4065" />
<sodipodi:guide
orientation="0,1"
position="-8,88.000001"
id="guide4067" />
<sodipodi:guide
orientation="0,1"
position="-8,92.000001"
id="guide4069" />
<sodipodi:guide
orientation="0,1"
position="104,4"
id="guide4071" />
<sodipodi:guide
orientation="0,1"
position="-5,8.0000001"
id="guide4073" />
<sodipodi:guide
orientation="1,0"
position="92,-8.0000001"
id="guide4075" />
<sodipodi:guide
orientation="1,0"
position="88,-8.0000001"
id="guide4077" />
<sodipodi:guide
orientation="0,1"
position="-8,84.000001"
id="guide4074" />
<sodipodi:guide
orientation="1,0"
position="12,-8.0000001"
id="guide4076" />
<sodipodi:guide
orientation="0,1"
position="-5,12"
id="guide4078" />
<sodipodi:guide
orientation="1,0"
position="84,-9.0000001"
id="guide4080" />
<sodipodi:guide
position="48,-8.0000001"
orientation="1,0"
id="guide4170" />
<sodipodi:guide
position="-8,48"
orientation="0,1"
id="guide4172" />
</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
inkscape:label="Layer 1"
inkscape:groupmode="layer"
id="layer1"
transform="translate(67.857146,-78.50504)">
<g
transform="matrix(0,-1,-1,0,373.50506,516.50504)"
id="g4845"
style="display:inline">
<g
inkscape:export-ydpi="90"
inkscape:export-xdpi="90"
inkscape:export-filename="next01.png"
transform="matrix(-0.9996045,0,0,1,575.94296,-611.00001)"
id="g4778"
inkscape:label="Layer 1">
<g
transform="matrix(-1,0,0,1,575.99999,611)"
id="g4780"
style="display:inline">
<rect
style="color:#000000;display:inline;overflow:visible;visibility:visible;fill:none;stroke:none;stroke-width:4;marker:none;enable-background:accumulate"
id="rect4782"
width="96.037987"
height="96"
x="-438.00244"
y="345.36221"
transform="scale(-1,1)" />
<path
style="color:#000000;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:medium;line-height:normal;font-family:sans-serif;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-align:start;text-decoration:none;text-decoration-line:none;text-decoration-style:solid;text-decoration-color:#000000;letter-spacing:normal;word-spacing:normal;text-transform:none;direction:ltr;block-progression:tb;writing-mode:lr-tb;baseline-shift:baseline;text-anchor:start;white-space:normal;shape-padding:0;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:#808080;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:4.00079155;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker:none;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate"
d="m 393.98438,425.36133 -2,0 -46.01758,0 0,-64 48.01758,0 0,64 z m -4,-4 0,-55.99805 -40.01758,0 0,55.99805 40.01758,0 z"
id="rect4269"
inkscape:connector-curvature="0" />
<path
style="color:#000000;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:medium;line-height:normal;font-family:sans-serif;text-indent:0;text-align:start;text-decoration:none;text-decoration-line:none;text-decoration-style:solid;text-decoration-color:#000000;letter-spacing:normal;word-spacing:normal;text-transform:none;direction:ltr;block-progression:tb;writing-mode:lr-tb;baseline-shift:baseline;text-anchor:start;white-space:normal;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:#808080;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:4.00079155;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker:none;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate"
d="m 434.00087,393.36222 c 0,13.81562 -11.1845,25.00195 -25.00013,25.00195 l -16.0161,0 0,-4.00195 16.0161,0 c 11.66838,0 21.0005,-9.33162 21.0005,-21 0,-11.66838 -9.33212,-20.99805 -21.0005,-20.99805 l -16.0161,0 0,-4.00195 16.0161,0 c 13.81563,0 25.00013,11.18438 25.00013,25 z"
id="rect4271"
inkscape:connector-curvature="0" />
<path
transform="matrix(0,-1,-1,0,0,0)"
style="color:#000000;display:inline;overflow:visible;visibility:visible;opacity:1;fill:#808080;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:4;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker:none;enable-background:accumulate"
d="m -397.36221,-377.9787 8,-2.00079 0,20.00794 -8,0 z"
id="rect4280"
inkscape:connector-curvature="0"
sodipodi:nodetypes="ccccc" />
</g>
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 7.7 KiB