add api to deploy certificates
This commit is contained in:
parent
1dc142bb45
commit
a29a0483b2
@ -25,12 +25,17 @@
|
||||
#include "cloudnotifications.h"
|
||||
#include "nymeaconfiguration.h"
|
||||
#include "cloudtransport.h"
|
||||
#include "nymeaconfiguration.h"
|
||||
#include "nymeasettings.h"
|
||||
|
||||
#include "nymea-remoteproxyclient/remoteproxyconnection.h"
|
||||
#include <QDir>
|
||||
|
||||
using namespace remoteproxyclient;
|
||||
|
||||
CloudManager::CloudManager(NetworkManager *networkManager, QObject *parent) : QObject(parent),
|
||||
CloudManager::CloudManager(NymeaConfiguration *configuration, NetworkManager *networkManager, QObject *parent):
|
||||
QObject(parent),
|
||||
m_configuration(configuration),
|
||||
m_networkManager(networkManager)
|
||||
{
|
||||
m_awsConnector = new AWSConnector(this);
|
||||
@ -49,35 +54,30 @@ CloudManager::CloudManager(NetworkManager *networkManager, QObject *parent) : QO
|
||||
|
||||
m_transport = new CloudTransport(ServerConfiguration());
|
||||
connect(m_awsConnector, &AWSConnector::proxyConnectionRequestReceived, m_transport, &CloudTransport::connectToCloud);
|
||||
|
||||
m_deviceId = m_configuration->serverUuid();
|
||||
m_deviceName = m_configuration->serverName();
|
||||
m_serverUrl = m_configuration->cloudServerUrl();
|
||||
m_caCertificate = m_configuration->cloudCertificateCA();
|
||||
m_clientCertificate = m_configuration->cloudCertificate();
|
||||
m_clientCertificateKey = m_configuration->cloudCertificateKey();
|
||||
|
||||
setEnabled(m_configuration->cloudEnabled());
|
||||
connect(m_configuration, &NymeaConfiguration::cloudEnabledChanged, this, &CloudManager::setEnabled);
|
||||
connect(m_configuration, &NymeaConfiguration::serverNameChanged, this, &CloudManager::setDeviceName);
|
||||
|
||||
}
|
||||
|
||||
CloudManager::~CloudManager()
|
||||
{
|
||||
}
|
||||
|
||||
void CloudManager::setServerUrl(const QString &serverUrl)
|
||||
{
|
||||
m_serverUrl = serverUrl;
|
||||
}
|
||||
|
||||
void CloudManager::setDeviceId(const QUuid &deviceId)
|
||||
{
|
||||
m_deviceId = deviceId;
|
||||
}
|
||||
|
||||
void CloudManager::setDeviceName(const QString &name)
|
||||
{
|
||||
m_deviceName = name;
|
||||
m_awsConnector->setDeviceName(name);
|
||||
}
|
||||
|
||||
void CloudManager::setClientCertificates(const QString &caCertificate, const QString &clientCertificate, const QString &clientCertificateKey)
|
||||
{
|
||||
m_caCertificate = caCertificate;
|
||||
m_clientCertificate = clientCertificate;
|
||||
m_clientCertificateKey = clientCertificateKey;
|
||||
}
|
||||
|
||||
bool CloudManager::enabled() const
|
||||
{
|
||||
return m_enabled;
|
||||
@ -86,6 +86,9 @@ bool CloudManager::enabled() const
|
||||
void CloudManager::setEnabled(bool enabled)
|
||||
{
|
||||
if (enabled) {
|
||||
m_enabled = true;
|
||||
emit connectionStateChanged();
|
||||
|
||||
bool missingConfig = false;
|
||||
if (m_deviceId.isNull()) {
|
||||
qCWarning(dcCloud()) << "Don't have a unique device ID.";
|
||||
@ -117,7 +120,6 @@ void CloudManager::setEnabled(bool enabled)
|
||||
}
|
||||
|
||||
qCDebug(dcCloud()) << "Enabling cloud connection.";
|
||||
m_enabled = true;
|
||||
if (!m_awsConnector->isConnected() && m_networkManager->state() == NetworkManager::NetworkManagerStateConnectedGlobal) {
|
||||
connect2aws();
|
||||
}
|
||||
@ -125,12 +127,77 @@ void CloudManager::setEnabled(bool enabled)
|
||||
qCDebug(dcCloud()) << "Disabling cloud connection.";
|
||||
m_enabled = false;
|
||||
m_awsConnector->disconnectAWS();
|
||||
emit connectionStateChanged();
|
||||
}
|
||||
}
|
||||
|
||||
bool CloudManager::connected() const
|
||||
bool CloudManager::installClientCertificates(const QByteArray &rootCA, const QByteArray &certificatePEM, const QByteArray &publicKey, const QByteArray &privateKey, const QString &endpoint)
|
||||
{
|
||||
return m_awsConnector->isConnected();
|
||||
QString baseDir = NymeaSettings::storagePath() + "/certs/cloud/";
|
||||
QDir dir;
|
||||
// We never delete old certs, cycle until we find an unused path
|
||||
int i = 0;
|
||||
do {
|
||||
dir = QDir(baseDir + QString::number(i++) + '/');
|
||||
} while (dir.exists());
|
||||
|
||||
if (!dir.mkpath(dir.absolutePath())) {
|
||||
qCWarning(dcCloud) << "Cannot install cloud certificates. Unable to create path.";
|
||||
return false;
|
||||
}
|
||||
QFile ca(dir.absoluteFilePath("aws-certification-authority.crt"));
|
||||
if (!ca.open(QFile::WriteOnly) || ca.write(rootCA) != rootCA.length()) {
|
||||
qCWarning(dcCloud()) << "Cannot install cloud certificates. Unable to write CA file" << dir.absoluteFilePath(ca.fileName());
|
||||
ca.close();
|
||||
return false;
|
||||
}
|
||||
ca.close();
|
||||
QFile pem(dir.absoluteFilePath("guh-cloud.pem"));
|
||||
if (!pem.open(QFile::WriteOnly) || pem.write(certificatePEM) != certificatePEM.length()) {
|
||||
qCWarning(dcCloud()) << "Cannot install cloud certificates. Unable to write certificate file" << dir.absoluteFilePath(pem.fileName());
|
||||
pem.close();
|
||||
return false;
|
||||
}
|
||||
pem.close();
|
||||
QFile pub(dir.absoluteFilePath("guh-cloud.pub"));
|
||||
if (!pub.open(QFile::WriteOnly) || pub.write(publicKey) != publicKey.length()) {
|
||||
qCWarning(dcCloud()) << "Cannot install cloud certificates. Unable to write public key file" << dir.absoluteFilePath(pub.fileName());
|
||||
pub.close();
|
||||
return false;
|
||||
}
|
||||
pub.close();
|
||||
QFile key(dir.absoluteFilePath("guh-cloud.key"));
|
||||
if (!key.open(QFile::WriteOnly) || key.write(privateKey) != privateKey.length()) {
|
||||
qCWarning(dcCloud()) << "Cannot install cloud certificates. Unable to write private key file" << dir.absoluteFilePath(key.fileName());
|
||||
key.close();
|
||||
return false;
|
||||
}
|
||||
key.close();
|
||||
qCDebug(dcCloud) << "Installed cloud certificates to" << dir.absolutePath();
|
||||
m_caCertificate = dir.absoluteFilePath("aws-certification-authority.crt");
|
||||
m_clientCertificate = dir.absoluteFilePath("guh-cloud.pem");
|
||||
m_clientCertificateKey = dir.absoluteFilePath("guh-cloud.key");
|
||||
m_serverUrl = endpoint;
|
||||
|
||||
if (m_enabled) {
|
||||
m_awsConnector->disconnectAWS();
|
||||
connect2aws();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
CloudManager::CloudConnectionState CloudManager::connectionState() const
|
||||
{
|
||||
if (m_awsConnector->isConnected()) {
|
||||
return CloudConnectionStateConnected;
|
||||
}
|
||||
if (!m_enabled) {
|
||||
return CloudConnectionStateDisabled;
|
||||
}
|
||||
if (m_deviceId.isNull() || m_deviceName.isEmpty() || m_serverUrl.isEmpty() || m_clientCertificate.isEmpty() || m_clientCertificateKey.isEmpty() || m_caCertificate.isEmpty()) {
|
||||
return CloudConnectionStateUnconfigured;
|
||||
}
|
||||
return CloudConnectionStateConnecting;
|
||||
}
|
||||
|
||||
void CloudManager::pairDevice(const QString &idToken, const QString &userId)
|
||||
@ -191,10 +258,10 @@ void CloudManager::onJanusWebRtcHandshakeMessageReceived(const QString &transact
|
||||
|
||||
void CloudManager::awsConnected()
|
||||
{
|
||||
emit connectedChanged(true);
|
||||
emit connectionStateChanged();
|
||||
}
|
||||
|
||||
void CloudManager::awsDisconnected()
|
||||
{
|
||||
emit connectedChanged(false);
|
||||
emit connectionStateChanged();
|
||||
}
|
||||
|
||||
@ -37,22 +37,33 @@ class RemoteProxyConnection;
|
||||
|
||||
namespace nymeaserver {
|
||||
|
||||
class NymeaConfiguration;
|
||||
class CloudTransport;
|
||||
class CloudManager : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
public:
|
||||
explicit CloudManager(NetworkManager *networkManager, QObject *parent = nullptr);
|
||||
enum CloudConnectionState {
|
||||
CloudConnectionStateDisabled,
|
||||
CloudConnectionStateUnconfigured,
|
||||
CloudConnectionStateConnecting,
|
||||
CloudConnectionStateConnected
|
||||
};
|
||||
Q_ENUM(CloudConnectionState)
|
||||
|
||||
explicit CloudManager(NymeaConfiguration *configuration, NetworkManager *networkManager, QObject *parent = nullptr);
|
||||
~CloudManager();
|
||||
|
||||
void setServerUrl(const QString &serverUrl);
|
||||
void setDeviceId(const QUuid &deviceId);
|
||||
void setDeviceName(const QString &name);
|
||||
void setClientCertificates(const QString &caCertificate, const QString &clientCertificate, const QString &clientCertificateKey);
|
||||
// void setServerUrl(const QString &serverUrl);
|
||||
// void setDeviceId(const QUuid &deviceId);
|
||||
// void setClientCertificates(const QString &caCertificate, const QString &clientCertificate, const QString &clientCertificateKey);
|
||||
|
||||
bool enabled() const;
|
||||
void setEnabled(bool enabled);
|
||||
bool connected() const;
|
||||
|
||||
bool installClientCertificates(const QByteArray &rootCA, const QByteArray &certificatePEM, const QByteArray &publicKey, const QByteArray &privateKey, const QString &endpoint);
|
||||
|
||||
CloudConnectionState connectionState() const;
|
||||
|
||||
void pairDevice(const QString &idToken, const QString &userId);
|
||||
|
||||
@ -62,7 +73,7 @@ public:
|
||||
CloudTransport* createTransportInterface() const;
|
||||
|
||||
signals:
|
||||
void connectedChanged(bool connected);
|
||||
void connectionStateChanged();
|
||||
|
||||
void pairingReply(QString cognitoUserId, int status, const QString &message);
|
||||
|
||||
@ -76,12 +87,14 @@ private slots:
|
||||
void onJanusWebRtcHandshakeMessageReceived(const QString &transactionId, const QVariantMap &data);
|
||||
void awsConnected();
|
||||
void awsDisconnected();
|
||||
void setDeviceName(const QString &name);
|
||||
|
||||
private:
|
||||
QTimer m_reconnectTimer;
|
||||
bool m_enabled = false;
|
||||
AWSConnector *m_awsConnector = nullptr;
|
||||
JanusConnector *m_janusConnector = nullptr;
|
||||
NymeaConfiguration *m_configuration = nullptr;
|
||||
NetworkManager *m_networkManager = nullptr;
|
||||
|
||||
QString m_serverUrl;
|
||||
|
||||
@ -165,6 +165,17 @@ JsonRPCServer::JsonRPCServer(const QSslConfiguration &sslConfiguration, QObject
|
||||
returns.insert("error", JsonTypes::userErrorRef());
|
||||
setReturns("RemoveToken", returns);
|
||||
|
||||
params.clear(); returns.clear();
|
||||
setDescription("SetupCloudConnection", "Sets up the cloud connection by deploying a certificate and its configuration.");
|
||||
params.insert("rootCA", JsonTypes::basicTypeToString(JsonTypes::String));
|
||||
params.insert("certificatePEM", JsonTypes::basicTypeToString(JsonTypes::String));
|
||||
params.insert("publicKey", JsonTypes::basicTypeToString(JsonTypes::String));
|
||||
params.insert("privateKey", JsonTypes::basicTypeToString(JsonTypes::String));
|
||||
params.insert("endpoint", JsonTypes::basicTypeToString(JsonTypes::String));
|
||||
setParams("SetupCloudConnection", params);
|
||||
returns.insert("success", JsonTypes::basicTypeToString(JsonTypes::Bool));
|
||||
setReturns("SetupCloudConnection", returns);
|
||||
|
||||
params.clear(); returns.clear();
|
||||
setDescription("SetupRemoteAccess", "Setup the remote connection by providing AWS token information. This requires the cloud to be connected.");
|
||||
params.insert("idToken", JsonTypes::basicTypeToString(JsonTypes::String));
|
||||
@ -175,9 +186,10 @@ JsonRPCServer::JsonRPCServer(const QSslConfiguration &sslConfiguration, QObject
|
||||
setReturns("SetupRemoteAccess", returns);
|
||||
|
||||
params.clear(); returns.clear();
|
||||
setDescription("IsCloudConnected", "Check whether the cloud is currently connected.");
|
||||
setDescription("IsCloudConnected", "Check whether the cloud is currently connected. \"connected\" will be true whenever connectionState equals CloudConnectionStateConnected and is deprecated. Please use the connectionState value instead.");
|
||||
setParams("IsCloudConnected", params);
|
||||
returns.insert("connected", JsonTypes::basicTypeToString(JsonTypes::Bool));
|
||||
returns.insert("connectionState", JsonTypes::cloudConnectionStateRef());
|
||||
setReturns("IsCloudConnected", returns);
|
||||
|
||||
params.clear(); returns.clear();
|
||||
@ -329,6 +341,25 @@ JsonReply *JsonRPCServer::RemoveToken(const QVariantMap ¶ms)
|
||||
return createReply(ret);
|
||||
}
|
||||
|
||||
JsonReply *JsonRPCServer::SetupCloudConnection(const QVariantMap ¶ms)
|
||||
{
|
||||
if (NymeaCore::instance()->cloudManager()->connectionState() != CloudManager::CloudConnectionStateUnconfigured) {
|
||||
qCDebug(dcCloud) << "Cloud already configured. Not changing configuration as it won't work anyways. If you want to reconfigure this instance to a different cloud, change the system UUID and wipe the cloud settings from the config.";
|
||||
QVariantMap data;
|
||||
data.insert("success", false);
|
||||
return createReply(data);
|
||||
}
|
||||
QByteArray rootCA = params.value("rootCA").toByteArray();
|
||||
QByteArray certificatePEM = params.value("certificatePEM").toByteArray();
|
||||
QByteArray publicKey = params.value("publicKey").toByteArray();
|
||||
QByteArray privateKey = params.value("privateKey").toByteArray();
|
||||
QString endpoint = params.value("endpoint").toString();
|
||||
bool status = NymeaCore::instance()->cloudManager()->installClientCertificates(rootCA, certificatePEM, publicKey, privateKey, endpoint);
|
||||
QVariantMap ret;
|
||||
ret.insert("success", status);
|
||||
return createReply(ret);
|
||||
}
|
||||
|
||||
JsonReply *JsonRPCServer::SetupRemoteAccess(const QVariantMap ¶ms)
|
||||
{
|
||||
QString idToken = params.value("idToken").toString();
|
||||
@ -345,9 +376,10 @@ JsonReply *JsonRPCServer::SetupRemoteAccess(const QVariantMap ¶ms)
|
||||
JsonReply *JsonRPCServer::IsCloudConnected(const QVariantMap ¶ms)
|
||||
{
|
||||
Q_UNUSED(params)
|
||||
bool connected = NymeaCore::instance()->cloudManager()->connected();
|
||||
bool connected = NymeaCore::instance()->cloudManager()->connectionState() == CloudManager::CloudConnectionStateConnected;
|
||||
QVariantMap data;
|
||||
data.insert("connected", connected);
|
||||
data.insert("connectionState", JsonTypes::cloudConnectionStateToString(NymeaCore::instance()->cloudManager()->connectionState()));
|
||||
return createReply(data);
|
||||
}
|
||||
|
||||
@ -457,7 +489,7 @@ void JsonRPCServer::setup()
|
||||
registerHandler(new TagsHandler(this));
|
||||
|
||||
connect(NymeaCore::instance()->cloudManager(), &CloudManager::pairingReply, this, &JsonRPCServer::pairingFinished);
|
||||
connect(NymeaCore::instance()->cloudManager(), &CloudManager::connectedChanged, this, &JsonRPCServer::onCloudConnectedChanged);
|
||||
connect(NymeaCore::instance()->cloudManager(), &CloudManager::connectionStateChanged, this, &JsonRPCServer::onCloudConnectionStateChanged);
|
||||
}
|
||||
|
||||
void JsonRPCServer::processData(const QUuid &clientId, const QByteArray &data)
|
||||
@ -616,10 +648,11 @@ void JsonRPCServer::pairingFinished(QString cognitoUserId, int status, const QSt
|
||||
reply->finished();
|
||||
}
|
||||
|
||||
void JsonRPCServer::onCloudConnectedChanged(bool connected)
|
||||
void JsonRPCServer::onCloudConnectionStateChanged()
|
||||
{
|
||||
QVariantMap params;
|
||||
params.insert("connected", connected);
|
||||
params.insert("connected", NymeaCore::instance()->cloudManager()->connectionState() == CloudManager::CloudConnectionStateConnected);
|
||||
params.insert("connectionState", JsonTypes::cloudConnectionStateToString(NymeaCore::instance()->cloudManager()->connectionState()));
|
||||
emit CloudConnectedChanged(params);
|
||||
}
|
||||
|
||||
|
||||
@ -43,7 +43,7 @@ class JsonRPCServer: public JsonHandler
|
||||
{
|
||||
Q_OBJECT
|
||||
public:
|
||||
JsonRPCServer(const QSslConfiguration &sslConfiguration = QSslConfiguration(), QObject *parent = 0);
|
||||
JsonRPCServer(const QSslConfiguration &sslConfiguration = QSslConfiguration(), QObject *parent = nullptr);
|
||||
|
||||
// JsonHandler API implementation
|
||||
QString name() const;
|
||||
@ -57,6 +57,7 @@ public:
|
||||
Q_INVOKABLE JsonReply *RequestPushButtonAuth(const QVariantMap ¶ms);
|
||||
Q_INVOKABLE JsonReply *Tokens(const QVariantMap ¶ms) const;
|
||||
Q_INVOKABLE JsonReply *RemoveToken(const QVariantMap ¶ms);
|
||||
Q_INVOKABLE JsonReply *SetupCloudConnection(const QVariantMap ¶ms);
|
||||
Q_INVOKABLE JsonReply *SetupRemoteAccess(const QVariantMap ¶ms);
|
||||
Q_INVOKABLE JsonReply *IsCloudConnected(const QVariantMap ¶ms);
|
||||
Q_INVOKABLE JsonReply *KeepAlive(const QVariantMap ¶ms);
|
||||
@ -91,7 +92,7 @@ private slots:
|
||||
void asyncReplyFinished();
|
||||
|
||||
void pairingFinished(QString cognitoUserId, int status, const QString &message);
|
||||
void onCloudConnectedChanged(bool connected);
|
||||
void onCloudConnectionStateChanged();
|
||||
void onPushButtonAuthFinished(int transactionId, bool success, const QByteArray &token);
|
||||
|
||||
private:
|
||||
|
||||
@ -90,6 +90,7 @@ QVariantList JsonTypes::s_networkManagerState;
|
||||
QVariantList JsonTypes::s_networkDeviceState;
|
||||
QVariantList JsonTypes::s_userError;
|
||||
QVariantList JsonTypes::s_tagError;
|
||||
QVariantList JsonTypes::s_cloudConnectionState;
|
||||
|
||||
QVariantMap JsonTypes::s_paramType;
|
||||
QVariantMap JsonTypes::s_param;
|
||||
@ -151,6 +152,7 @@ void JsonTypes::init()
|
||||
s_networkDeviceState = enumToStrings(NetworkDevice::staticMetaObject, "NetworkDeviceState");
|
||||
s_userError = enumToStrings(UserManager::staticMetaObject, "UserError");
|
||||
s_tagError = enumToStrings(TagsStorage::staticMetaObject, "TagError");
|
||||
s_cloudConnectionState = enumToStrings(CloudManager::staticMetaObject, "CloudConnectionState");
|
||||
|
||||
// ParamType
|
||||
s_paramType.insert("id", basicTypeToString(Uuid));
|
||||
@ -2124,6 +2126,12 @@ QPair<bool, QString> JsonTypes::validateVariant(const QVariant &templateVariant,
|
||||
qCWarning(dcJsonRpc()) << QString("Value %1 not allowed in %2").arg(variant.toString()).arg(logEntryRef());
|
||||
return result;
|
||||
}
|
||||
} else if (refName == cloudConnectionStateRef()) {
|
||||
QPair<bool, QString> result = validateEnum(s_cloudConnectionState, variant);
|
||||
if (!result.first) {
|
||||
qCWarning(dcJsonRpc()) << QString("Value %1 not allowed in %2").arg(variant.toString()).arg(cloudConnectionStateRef());
|
||||
return result;
|
||||
}
|
||||
} else {
|
||||
Q_ASSERT_X(false, "JsonTypes", QString("Unhandled ref: %1").arg(refName).toLatin1().data());
|
||||
return report(false, QString("Unhandled ref %1. Server implementation incomplete.").arg(refName));
|
||||
|
||||
@ -56,6 +56,8 @@
|
||||
#include "networkmanager/wirelessnetworkdevice.h"
|
||||
#include "networkmanager/wirelessaccesspoint.h"
|
||||
|
||||
#include "cloud/cloudmanager.h"
|
||||
|
||||
#include <QObject>
|
||||
|
||||
#include <QVariantMap>
|
||||
@ -139,6 +141,7 @@ public:
|
||||
DECLARE_TYPE(networkDeviceState, "NetworkDeviceState", NetworkDevice, NetworkDeviceState)
|
||||
DECLARE_TYPE(userError, "UserError", UserManager, UserError)
|
||||
DECLARE_TYPE(tagError, "TagError", TagsStorage, TagError)
|
||||
DECLARE_TYPE(cloudConnectionState, "CloudConnectionState", CloudManager, CloudConnectionState)
|
||||
|
||||
DECLARE_OBJECT(paramType, "ParamType")
|
||||
DECLARE_OBJECT(param, "Param")
|
||||
|
||||
@ -549,12 +549,7 @@ void NymeaCore::init() {
|
||||
m_debugServerHandler = new DebugServerHandler(this);
|
||||
|
||||
qCDebug(dcApplication) << "Creating Cloud Manager";
|
||||
m_cloudManager = new CloudManager(m_networkManager, this);
|
||||
m_cloudManager->setDeviceId(m_configuration->serverUuid());
|
||||
m_cloudManager->setDeviceName(m_configuration->serverName());
|
||||
m_cloudManager->setServerUrl(m_configuration->cloudServerUrl());
|
||||
m_cloudManager->setClientCertificates(m_configuration->cloudCertificateCA(), m_configuration->cloudCertificate(), m_configuration->cloudCertificateKey());
|
||||
m_cloudManager->setEnabled(m_configuration->cloudEnabled());
|
||||
m_cloudManager = new CloudManager(m_configuration, m_networkManager, this);
|
||||
|
||||
CloudNotifications *cloudNotifications = m_cloudManager->createNotificationsPlugin();
|
||||
m_deviceManager->registerStaticPlugin(cloudNotifications, cloudNotifications->metaData());
|
||||
@ -563,8 +558,6 @@ void NymeaCore::init() {
|
||||
m_serverManager->jsonServer()->registerTransportInterface(cloudTransport, false);
|
||||
|
||||
connect(m_configuration, &NymeaConfiguration::localeChanged, this, &NymeaCore::onLocaleChanged);
|
||||
connect(m_configuration, &NymeaConfiguration::cloudEnabledChanged, m_cloudManager, &CloudManager::setEnabled);
|
||||
connect(m_configuration, &NymeaConfiguration::serverNameChanged, m_cloudManager, &CloudManager::setDeviceName);
|
||||
connect(m_configuration, &NymeaConfiguration::serverNameChanged, m_serverManager, &ServerManager::setServerName);
|
||||
|
||||
connect(m_deviceManager, &DeviceManager::pluginConfigChanged, this, &NymeaCore::pluginConfigChanged);
|
||||
|
||||
Reference in New Issue
Block a user