diff --git a/debian/control b/debian/control index a83af2d5f..a119c5edd 100644 --- a/debian/control +++ b/debian/control @@ -656,6 +656,17 @@ Description: nymea.io plugin for osdomotics This package will install the nymea.io plugin for osdomotics +Package: nymea-plugin-owlet +Architecture: any +Depends: ${shlibs:Depends}, + ${misc:Depends}, + nymea-plugins-translations, +Description: nymea.io plugin for nymea:owlet + nymea:owlet is a firmware for microcontrollers (Arduino/ESP8266/ESP32) which + exposes pins of the microcontroller to nymea and allows using them for + whatever purpose like moodlights, control relays, inputs etc. + + Package: nymea-plugin-philipshue Architecture: any Depends: ${shlibs:Depends}, diff --git a/debian/nymea-plugin-owlet.install.in b/debian/nymea-plugin-owlet.install.in new file mode 100644 index 000000000..2e5fe9706 --- /dev/null +++ b/debian/nymea-plugin-owlet.install.in @@ -0,0 +1 @@ +usr/lib/@DEB_HOST_MULTIARCH@/nymea/plugins/libnymea_integrationpluginowlet.so diff --git a/nymea-plugins.pro b/nymea-plugins.pro index 1118b516f..e5c8eb57b 100644 --- a/nymea-plugins.pro +++ b/nymea-plugins.pro @@ -45,6 +45,7 @@ PLUGIN_DIRS = \ openuv \ openweathermap \ osdomotics \ + owlet \ philipshue \ pushbullet \ pushnotifications \ diff --git a/owlet/integrationpluginowlet.cpp b/owlet/integrationpluginowlet.cpp new file mode 100644 index 000000000..db30ac4c2 --- /dev/null +++ b/owlet/integrationpluginowlet.cpp @@ -0,0 +1,248 @@ +/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * +* +* Copyright 2013 - 2021, nymea GmbH +* Contact: contact@nymea.io +* +* This file is part of nymea. +* This project including source code and documentation is protected by +* copyright law, and remains the property of nymea GmbH. All rights, including +* reproduction, publication, editing and translation, are reserved. The use of +* this project is subject to the terms of a license agreement to be concluded +* with nymea GmbH in accordance with the terms of use of nymea GmbH, available +* under https://nymea.io/license +* +* GNU Lesser General Public License Usage +* Alternatively, this project may be redistributed and/or modified under the +* terms of the GNU Lesser General Public License as published by the Free +* Software Foundation; version 3. This project is distributed in the hope that +* it will be useful, but WITHOUT ANY WARRANTY; without even the implied +* warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +* Lesser General Public License for more details. +* +* You should have received a copy of the GNU Lesser General Public License +* along with this project. If not, see . +* +* For any further details and any questions please contact us under +* contact@nymea.io or see our FAQ/Licensing Information on +* https://nymea.io/license/faq +* +* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ + +#include "integrationpluginowlet.h" +#include "plugininfo.h" +#include "owletclient.h" + +#include "hardwaremanager.h" +#include "platform/platformzeroconfcontroller.h" +#include "network/zeroconf/zeroconfservicebrowser.h" +#include "network/zeroconf/zeroconfserviceentry.h" + +#include +#include + +static QHash idParamTypeMap = { + { digitalOutputThingClassId, digitalOutputThingOwletIdParamTypeId }, + { digitalInputThingClassId, digitalInputThingOwletIdParamTypeId }, + { ws2812ThingClassId, ws2812ThingOwletIdParamTypeId } +}; + +IntegrationPluginOwlet::IntegrationPluginOwlet() +{ +} + +void IntegrationPluginOwlet::init() +{ + m_zeroConfBrowser = hardwareManager()->zeroConfController()->createServiceBrowser("_nymea-owlet._tcp"); +} + +void IntegrationPluginOwlet::discoverThings(ThingDiscoveryInfo *info) +{ + foreach (const ZeroConfServiceEntry &entry, m_zeroConfBrowser->serviceEntries()) { + qCDebug(dcOwlet()) << "Found owlet:" << entry; + ThingDescriptor descriptor(info->thingClassId(), entry.name(), entry.txt("platform")); + descriptor.setParams(ParamList() << Param(idParamTypeMap.value(info->thingClassId()), entry.txt("id"))); + foreach (Thing *existingThing, myThings().filterByParam(idParamTypeMap.value(info->thingClassId()), entry.txt("id"))) { + descriptor.setThingId(existingThing->id()); + break; + } + info->addThingDescriptor(descriptor); + } + info->finish(Thing::ThingErrorNoError); +} + + +void IntegrationPluginOwlet::setupThing(ThingSetupInfo *info) +{ + Thing *thing = info->thing(); + + QHostAddress ip; + int port = 5555; + foreach (const ZeroConfServiceEntry &entry, m_zeroConfBrowser->serviceEntries()) { + if (entry.txt("id") == info->thing()->paramValue(idParamTypeMap.value(info->thing()->thingClassId()))) { + ip = entry.hostAddress(); + port = entry.port(); + break; + } + } + // Try cached ip + if (ip.isNull()) { + pluginStorage()->beginGroup(thing->id().toString()); + ip = QHostAddress(pluginStorage()->value("cachedIP").toString()); + pluginStorage()->endGroup(); + } + + if (ip.isNull()) { + qCWarning(dcOwlet()) << "Can't find owlet in the local network."; + info->finish(Thing::ThingErrorHardwareNotAvailable); + return; + } + + OwletClient *client = new OwletClient(this); + + connect(client, &OwletClient::connected, info, [=](){ + qCDebug(dcOwlet()) << "Connected to owleet"; + m_clients.insert(thing, client); + + if (thing->thingClassId() == digitalOutputThingClassId) { + QVariantMap params; + params.insert("id", thing->paramValue(digitalOutputThingPinParamTypeId).toInt()); + params.insert("mode", "GPIOOutput"); + client->sendCommand("GPIO.ConfigurePin", params); + } + if (thing->thingClassId() == digitalInputThingClassId) { + QVariantMap params; + params.insert("id", thing->paramValue(digitalInputThingPinParamTypeId).toInt()); + params.insert("mode", "GPIOInput"); + client->sendCommand("GPIO.ConfigurePin", params); + } + if (thing->thingClassId() == ws2812ThingClassId) { + QVariantMap params; + params.insert("id", thing->paramValue(ws2812ThingPinParamTypeId).toInt()); + params.insert("mode", "WS2812"); + params.insert("ledCount", thing->paramValue(ws2812ThingLedCountParamTypeId).toUInt()); + params.insert("ledMode", "WS2812Mode" + thing->paramValue(ws2812ThingLedModeParamTypeId).toString()); + params.insert("ledClock", "WS2812Clock" + thing->paramValue(ws2812ThingLedClockParamTypeId).toString()); + client->sendCommand("GPIO.ConfigurePin", params); + } + + info->finish(Thing::ThingErrorNoError); + }); + connect(client, &OwletClient::error, info, [=](){ + info->finish(Thing::ThingErrorHardwareFailure); + }); + connect(client, &OwletClient::connected, thing, [=](){ + thing->setStateValue("connected", true); + pluginStorage()->beginGroup(thing->id().toString()); + pluginStorage()->setValue("cachedIP", ip.toString()); + pluginStorage()->endGroup(); + }); + connect(client, &OwletClient::disconnected, thing, [=](){ + thing->setStateValue("connected", false); + }); + + connect(client, &OwletClient::notificationReceived, this, [=](const QString &name, const QVariantMap ¶ms){ + qCDebug(dcOwlet()) << "***Notif" << name << params; + if (thing->thingClassId() == digitalInputThingClassId) { + if (params.value("id").toInt() == thing->paramValue(digitalInputThingPinParamTypeId)) { + thing->setStateValue(digitalInputPowerStateTypeId, params.value("power").toBool()); + } + } + if (thing->thingClassId() == digitalOutputThingClassId) { + if (params.value("id").toInt() == thing->paramValue(digitalOutputThingPinParamTypeId)) { + thing->setStateValue(digitalOutputPowerStateTypeId, params.value("power").toBool()); + } + } + if (thing->thingClassId() == ws2812ThingClassId) { + if (name == "GPIO.PinChanged") { + if (params.contains("power")) { + thing->setStateValue(ws2812PowerStateTypeId, params.value("power").toBool()); + } + if (params.contains("brightness")) { + thing->setStateValue(ws2812BrightnessStateTypeId, params.value("brightness").toInt()); + } + if (params.contains("color")) { + thing->setStateValue(ws2812ColorStateTypeId, params.value("color").value()); + } + if (params.contains("effect")) { + thing->setStateValue(ws2812EffectStateTypeId, params.value("effect").toInt()); + } + } + } + }); + + client->connectToHost(ip, port); +} + + +void IntegrationPluginOwlet::executeAction(ThingActionInfo *info) +{ + if (info->thing()->thingClassId() == digitalOutputThingClassId) { + OwletClient *client = m_clients.value(info->thing()); + QVariantMap params; + params.insert("id", info->thing()->paramValue(digitalOutputThingPinParamTypeId).toInt()); + params.insert("power", info->action().paramValue(digitalOutputPowerActionPowerParamTypeId).toBool()); + qCDebug(dcOwlet()) << "Sending ControlPin" << params; + int id = client->sendCommand("GPIO.ControlPin", params); + connect(client, &OwletClient::replyReceived, info, [=](int commandId, const QVariantMap ¶ms){ + if (id != commandId) { + return; + } + qCDebug(dcOwlet()) << "reply from owlet:" << params; + QString error = params.value("error").toString(); + if (error == "GPIOErrorNoError") { + info->thing()->setStateValue(digitalOutputPowerStateTypeId, info->action().paramValue(digitalOutputPowerActionPowerParamTypeId).toBool()); + info->finish(Thing::ThingErrorNoError); + } else { + info->finish(Thing::ThingErrorHardwareFailure); + } + }); + + return; + } + + if (info->thing()->thingClassId() == ws2812ThingClassId) { + OwletClient *client = m_clients.value(info->thing()); + QVariantMap params; + params.insert("id", info->thing()->paramValue(ws2812ThingPinParamTypeId).toUInt()); + + if (info->action().actionTypeId() == ws2812PowerActionTypeId) { + params.insert("power", info->action().paramValue(ws2812PowerActionPowerParamTypeId).toBool()); + } + if (info->action().actionTypeId() == ws2812BrightnessActionTypeId) { + params.insert("brightness", info->action().paramValue(ws2812BrightnessActionBrightnessParamTypeId).toInt()); + } + if (info->action().actionTypeId() == ws2812ColorActionTypeId) { + QColor color = info->action().paramValue(ws2812ColorActionColorParamTypeId).value(); + params.insert("color", (color.rgb() & 0xFFFFFF)); + } + if (info->action().actionTypeId() == ws2812EffectActionTypeId) { + int effect = info->action().paramValue(ws2812EffectActionEffectParamTypeId).toInt(); + params.insert("effect", effect); + } + + int id = client->sendCommand("GPIO.ControlPin", params); + connect(client, &OwletClient::replyReceived, info, [=](int commandId, const QVariantMap ¶ms){ + if (id != commandId) { + return; + } + qCDebug(dcOwlet()) << "reply from owlet:" << params; + QString error = params.value("error").toString(); + if (error == "GPIOErrorNoError") { + info->finish(Thing::ThingErrorNoError); + } else { + info->finish(Thing::ThingErrorHardwareFailure); + } + }); + return; + } + + + + Q_ASSERT_X(false, "IntegrationPluginOwlet", "Not implemented"); + info->finish(Thing::ThingErrorUnsupportedFeature); +} + +void IntegrationPluginOwlet::thingRemoved(Thing *thing) +{ + Q_UNUSED(thing) +} diff --git a/owlet/integrationpluginowlet.h b/owlet/integrationpluginowlet.h new file mode 100644 index 000000000..9d6337ab9 --- /dev/null +++ b/owlet/integrationpluginowlet.h @@ -0,0 +1,63 @@ +/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * +* +* Copyright 2013 - 2021, nymea GmbH +* Contact: contact@nymea.io +* +* This file is part of nymea. +* This project including source code and documentation is protected by +* copyright law, and remains the property of nymea GmbH. All rights, including +* reproduction, publication, editing and translation, are reserved. The use of +* this project is subject to the terms of a license agreement to be concluded +* with nymea GmbH in accordance with the terms of use of nymea GmbH, available +* under https://nymea.io/license +* +* GNU Lesser General Public License Usage +* Alternatively, this project may be redistributed and/or modified under the +* terms of the GNU Lesser General Public License as published by the Free +* Software Foundation; version 3. This project is distributed in the hope that +* it will be useful, but WITHOUT ANY WARRANTY; without even the implied +* warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +* Lesser General Public License for more details. +* +* You should have received a copy of the GNU Lesser General Public License +* along with this project. If not, see . +* +* For any further details and any questions please contact us under +* contact@nymea.io or see our FAQ/Licensing Information on +* https://nymea.io/license/faq +* +* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ + +#ifndef INTEGRATIONPLUGINOWLET_H +#define INTEGRATIONPLUGINOWLET_H + +#include "integrations/integrationplugin.h" +#include "extern-plugininfo.h" + +class ZeroConfServiceBrowser; +class OwletClient; + +class IntegrationPluginOwlet: public IntegrationPlugin +{ + Q_OBJECT + + Q_PLUGIN_METADATA(IID "io.nymea.IntegrationPlugin" FILE "integrationpluginowlet.json") + Q_INTERFACES(IntegrationPlugin) + +public: + explicit IntegrationPluginOwlet(); + + void init() override; + void discoverThings(ThingDiscoveryInfo *info) override; + void setupThing(ThingSetupInfo *info) override; + void executeAction(ThingActionInfo *info) override; + void thingRemoved(Thing *thing) override; + +private: + ZeroConfServiceBrowser *m_zeroConfBrowser = nullptr; + + QHash m_clients; + +}; + +#endif // INTEGRATIONPLUGINOWLET_H diff --git a/owlet/integrationpluginowlet.json b/owlet/integrationpluginowlet.json new file mode 100644 index 000000000..7fef3d74e --- /dev/null +++ b/owlet/integrationpluginowlet.json @@ -0,0 +1,226 @@ +{ + "displayName": "nymea owlet", + "name": "owlet", + "id": "699a5b6d-d90f-4554-a8de-9205768a4a98", + "vendors": [ + { + "displayName": "nymea GmbH", + "name": "nymea", + "id": "2062d64d-3232-433c-88bc-0d33c0ba2ba6", + "thingClasses": [ + { + "id": "5a079c4e-9309-4d98-9ff1-9beeda210958", + "displayName": "Digital GPIO output on owlet", + "name": "digitalOutput", + "createMethods": ["discovery"], + "interfaces": ["power"], + "paramTypes": [ + { + "id": "de8cda8f-b8f1-425d-ae16-fd0f5a885ca4", + "name": "owletId", + "displayName": "Owlet ID", + "type": "QString", + "defaultValue": "" + }, + { + "id": "31dbcdea-04f3-4a0c-b131-7eda8a92c602", + "name": "pin", + "displayName": "Pin number", + "type": "uint", + "defaultValue": 1 + } + ], + "stateTypes": [ + { + "id": "dd97a6b1-e98e-4a60-b16a-b27240b91439", + "name": "power", + "displayName": "Power", + "type": "bool", + "defaultValue": false, + "writable": true, + "displayNameEvent": "Power changed", + "displayNameAction": "Set power", + "ioType": "digitalOutput" + } + ] + }, + { + "id": "673512a3-75d8-44a6-9930-198c9f1a1f03", + "displayName": "Digital GPIO Input on owlet", + "name": "digitalInput", + "createMethods": ["discovery"], + "paramTypes": [ + { + "id": "dd7eca3f-13f6-4320-aaaa-b0be8fbfeebf", + "name": "owletId", + "displayName": "Owlet ID", + "type": "QString", + "defaultValue": "" + }, + { + "id": "f6b60a4b-e7a2-4328-884d-818b0e2a361e", + "name": "pin", + "displayName": "Pin number", + "type": "uint", + "defaultValue": 1 + } + ], + "stateTypes": [ + { + "id": "df1fbd9f-10b1-4788-a00e-de3f3f411cc6", + "name": "power", + "displayName": "Powered", + "type": "bool", + "defaultValue": false, + "displayNameEvent": "Powered changed", + "ioType": "digitalInput" + } + ] + }, + { + "id": "76f4ef8e-8e17-4528-a667-3d3f5afdd6a7", + "name": "ws2812", + "displayName": "WS2812 on owlet", + "createMethods": ["discovery"], + "interfaces": ["colorlight", "wirelessconnectable"], + "paramTypes": [ + { + "id": "8c00f42b-5d34-4595-8ae9-6f48056a8be0", + "name": "owletId", + "displayName": "Owlet ID", + "type": "QString", + "defaultValue": "" + }, + { + "id": "d674ee68-7f24-4dec-a75a-647a083d3580", + "name": "pin", + "displayName": "Pin number", + "type": "uint", + "defaultValue": 1 + }, + { + "id": "6c6df8eb-cdf1-424c-b29d-60f7dd19ae41", + "name": "ledCount", + "displayName": "LED count", + "type": "uint", + "defaultValue": 1 + }, + { + "id": "69c7f0e5-fdc4-4f9f-a117-90f165af3178", + "name": "ledMode", + "displayName": "LED color mode", + "type": "QString", + "allowedValues": [ + "RGB", + "RBG", + "GRB", + "GBR", + "BRG", + "BGR", + "WRGB", + "WRBG", + "WGRB", + "WGBR", + "WBRG", + "WBRG", + "RWGB", + "RWBG", + "RGWB", + "RGBW", + "RBWG", + "RBGW", + "GWRB", + "GWBR", + "GRWB", + "GRBW", + "GBWR", + "GBRW", + "BWRG", + "BWGR", + "BRWG", + "BRGW", + "BGWR", + "BGRW" + ], + "defaultValue": "RGB" + }, + { + "id": "c4d99f98-1b46-4b38-bdd4-4bd5559dbb6f", + "name": "ledClock", + "displayName": "LED clock speed", + "type": "QString", + "allowedValues": ["400kHz", "800kHz"], + "defaultValue": "800kHz" + } + ], + "stateTypes": [ + { + "id": "0dbdd49b-578d-4404-87d2-b5a921df6aa6", + "name": "connected", + "displayName": "Connected", + "displayNameEvent": "Connected or disconnected", + "type": "bool", + "defaultValue": false, + "cached": false + }, + { + "id": "58a8b3ca-720c-458e-b045-b99b5aadabd7", + "name": "power", + "displayName": "Power", + "displayNameEvent": "Power changed", + "displayNameAction": "Set power", + "type": "bool", + "defaultValue": false, + "writable": true + }, + { + "id": "b56a9368-db2a-4ee6-99de-9ee8e1ffebd3", + "name": "brightness", + "displayName": "Brightness", + "displayNameEvent": "Brightness changed", + "displayNameAction": "Set brightness", + "type": "int", + "minValue": 0, + "maxValue": 100, + "unit": "Percentage", + "defaultValue": 100, + "writable": true + }, + { + "id": "684c9118-20f3-41a0-928e-b7290d40166d", + "name": "color", + "displayName": "Color", + "displayNameEvent": "Color changed", + "displayNameAction": "Set color", + "type": "QColor", + "defaultValue": "white", + "writable": true + }, + { + "id": "f92ea731-a86e-49b5-955b-9c245c7f874f", + "name": "colorTemperature", + "displayName": "Color temperature", + "displayNameEvent": "Color temperature changed", + "displayNameAction": "Set color temperature", + "type": "int", + "minValue": 0, + "maxValue": 100, + "defaultValue": 50, + "writable": true + }, + { + "id": "cb90f7bf-bcb0-42e8-a03b-442d84c5871f", + "name": "effect", + "displayName": "Effect", + "displayNameEvent": "Effect changed", + "displayNameAction": "Set effect", + "type": "int", + "defaultValue": 0, + "writable": true + } + ] + } + ] + } + ] +} diff --git a/owlet/owlet.pro b/owlet/owlet.pro new file mode 100644 index 000000000..572785b2d --- /dev/null +++ b/owlet/owlet.pro @@ -0,0 +1,13 @@ +include(../plugins.pri) + +QT += network + +SOURCES += \ + integrationpluginowlet.cpp \ + owletclient.cpp + +HEADERS += \ + integrationpluginowlet.h \ + owletclient.h + + diff --git a/owlet/owletclient.cpp b/owlet/owletclient.cpp new file mode 100644 index 000000000..cefd94947 --- /dev/null +++ b/owlet/owletclient.cpp @@ -0,0 +1,90 @@ +#include "owletclient.h" + +#include "extern-plugininfo.h" + +#include +#include + +OwletClient::OwletClient(QObject *parent) : QObject(parent) +{ + +} + +void OwletClient::connectToHost(const QHostAddress &address, int port) +{ + if (m_socket) { + m_socket->abort(); + m_socket->deleteLater(); + } + + m_socket = new QTcpSocket(this); + connect(m_socket, &QTcpSocket::connected, this, [this](){ + emit connected(); + }); + connect(m_socket, &QTcpSocket::disconnected, this, [this, address, port](){ + qCDebug(dcOwlet()) << "Disconnected from owleet"; + emit disconnected(); + QTimer::singleShot(1000, this, [=]{ + connectToHost(address, port); + }); + + }); + connect(m_socket, &QTcpSocket::errorOccurred, this, [this](){ + qCDebug(dcOwlet()) << "Error in owlet communication"; + emit error(); + }); + + connect(m_socket, &QTcpSocket::readyRead, this, [this](){ + dataReceived(m_socket->readAll()); + }); + m_socket->connectToHost(address, port); +} + +int OwletClient::sendCommand(const QString &method, const QVariantMap ¶ms) +{ + if (!m_socket) { + qCWarning(dcOwlet()) << "Not connected to owlet. Not sending command."; + return -1; + } + + int id = ++m_commandId; + + QVariantMap packet; + packet.insert("id", id); + packet.insert("method", method); + packet.insert("params", params); + m_socket->write(QJsonDocument::fromVariant(packet).toJson(QJsonDocument::Compact)); + return id; +} + +void OwletClient::dataReceived(const QByteArray &data) +{ + m_receiveBuffer.append(data); + + int splitIndex = m_receiveBuffer.indexOf("}\n{") + 1; + if (splitIndex <= 0) { + splitIndex = m_receiveBuffer.length(); + } + QJsonParseError error; + QJsonDocument jsonDoc = QJsonDocument::fromJson(m_receiveBuffer.left(splitIndex), &error); + if (error.error != QJsonParseError::NoError) { + // qWarning() << "Could not parse json data from nymea" << m_receiveBuffer.left(splitIndex) << error.errorString(); + return; + } + // qDebug() << "received response" << qUtf8Printable(jsonDoc.toJson(QJsonDocument::Indented)); + m_receiveBuffer = m_receiveBuffer.right(m_receiveBuffer.length() - splitIndex - 1); + if (!m_receiveBuffer.isEmpty()) { + staticMetaObject.invokeMethod(this, "dataReceived", Qt::QueuedConnection, Q_ARG(QByteArray, QByteArray())); + } + + QVariantMap packet = jsonDoc.toVariant().toMap(); + + if (packet.contains("notification")) { + qCDebug(dcOwlet()) << "Notification received:" << packet; + emit notificationReceived(packet.value("notification").toString(), packet.value("params").toMap()); + } else if (packet.contains("id")) { + qCDebug(dcOwlet()) << "reply received:" << packet; + int id = packet.value("id").toInt(); + emit replyReceived(id, packet.value("params").toMap()); + } +} diff --git a/owlet/owletclient.h b/owlet/owletclient.h new file mode 100644 index 000000000..289374dea --- /dev/null +++ b/owlet/owletclient.h @@ -0,0 +1,37 @@ +#ifndef OWLETCLIENT_H +#define OWLETCLIENT_H + +#include +#include +#include + +class OwletClient : public QObject +{ + Q_OBJECT +public: + explicit OwletClient(QObject *parent = nullptr); + + void connectToHost(const QHostAddress &address, int port); + + int sendCommand(const QString &method, const QVariantMap ¶ms); + +signals: + void connected(); + void disconnected(); + void error(); + + void replyReceived(int commandId, const QVariantMap ¶ms); + void notificationReceived(const QString &name, const QVariantMap ¶ms); + +private slots: + void dataReceived(const QByteArray &data); + +private: + QTcpSocket *m_socket = nullptr; + int m_commandId = 0; + + QByteArray m_receiveBuffer; + +}; + +#endif // OWLETCLIENT_H