From 509b649fbe436179657acc97245675b93772b1b7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20St=C3=BCrz?= Date: Wed, 5 Jan 2022 10:36:49 +0100 Subject: [PATCH] Make gpios work with arduino based boards --- owlet/integrationpluginowlet.cpp | 691 ++++++++++++++++++++++++++++-- owlet/integrationpluginowlet.h | 13 + owlet/integrationpluginowlet.json | 244 ++++++++++- owlet/owlet.pro | 2 + owlet/owletserialclient.cpp | 249 +++++++++++ owlet/owletserialclient.h | 150 +++++++ owlet/owletserialtransport.cpp | 16 +- 7 files changed, 1315 insertions(+), 50 deletions(-) create mode 100644 owlet/owletserialclient.cpp create mode 100644 owlet/owletserialclient.h diff --git a/owlet/integrationpluginowlet.cpp b/owlet/integrationpluginowlet.cpp index a8c54695..b7f319d9 100644 --- a/owlet/integrationpluginowlet.cpp +++ b/owlet/integrationpluginowlet.cpp @@ -31,6 +31,7 @@ #include "integrationpluginowlet.h" #include "plugininfo.h" #include "owletclient.h" +#include "owletserialclient.h" #include "hardwaremanager.h" #include "platform/platformzeroconfcontroller.h" @@ -56,16 +57,44 @@ void IntegrationPluginOwlet::init() m_owletSerialPortParamTypeMap.insert(arduinoMiniProThingClassId, arduinoMiniProThingSerialPortParamTypeId); m_zeroConfBrowser = hardwareManager()->zeroConfController()->createServiceBrowser("_nymea-owlet._tcp"); + + // Pin params of thins + m_owletSerialPinParamTypeMap.insert(digitalOutputSerialThingClassId, digitalOutputSerialThingPinParamTypeId); + m_owletSerialPinParamTypeMap.insert(digitalInputSerialThingClassId, digitalInputSerialThingPinParamTypeId); + m_owletSerialPinParamTypeMap.insert(analogOutputSerialThingClassId, analogOutputSerialThingPinParamTypeId); + m_owletSerialPinParamTypeMap.insert(analogInputSerialThingClassId, analogInputSerialThingPinParamTypeId); + m_owletSerialPinParamTypeMap.insert(servoSerialThingClassId, servoSerialThingPinParamTypeId); + + // Arduino Mini Pro mapping + m_arduinoMiniProPinMapping.insert(arduinoMiniProSettingsPin2ParamTypeId, 2); + m_arduinoMiniProPinMapping.insert(arduinoMiniProSettingsPin3ParamTypeId, 3); + m_arduinoMiniProPinMapping.insert(arduinoMiniProSettingsPin4ParamTypeId, 4); + m_arduinoMiniProPinMapping.insert(arduinoMiniProSettingsPin5ParamTypeId, 5); + m_arduinoMiniProPinMapping.insert(arduinoMiniProSettingsPin6ParamTypeId, 6); + m_arduinoMiniProPinMapping.insert(arduinoMiniProSettingsPin7ParamTypeId, 7); + m_arduinoMiniProPinMapping.insert(arduinoMiniProSettingsPin8ParamTypeId, 8); + m_arduinoMiniProPinMapping.insert(arduinoMiniProSettingsPin9ParamTypeId, 9); + m_arduinoMiniProPinMapping.insert(arduinoMiniProSettingsPin10ParamTypeId, 10); + m_arduinoMiniProPinMapping.insert(arduinoMiniProSettingsPin11ParamTypeId, 11); + m_arduinoMiniProPinMapping.insert(arduinoMiniProSettingsPin12ParamTypeId, 12); + m_arduinoMiniProPinMapping.insert(arduinoMiniProSettingsPin13ParamTypeId, 13); + m_arduinoMiniProPinMapping.insert(arduinoMiniProSettingsPinA1ParamTypeId, 15); + m_arduinoMiniProPinMapping.insert(arduinoMiniProSettingsPinA2ParamTypeId, 16); + m_arduinoMiniProPinMapping.insert(arduinoMiniProSettingsPinA3ParamTypeId, 17); + m_arduinoMiniProPinMapping.insert(arduinoMiniProSettingsPinA4ParamTypeId, 18); + m_arduinoMiniProPinMapping.insert(arduinoMiniProSettingsPinA5ParamTypeId, 19); + m_arduinoMiniProPinMapping.insert(arduinoMiniProSettingsPinA6ParamTypeId, 20); + m_arduinoMiniProPinMapping.insert(arduinoMiniProSettingsPinA7ParamTypeId, 21); } void IntegrationPluginOwlet::discoverThings(ThingDiscoveryInfo *info) { if (info->thingClassId() == arduinoMiniProThingClassId) { // Discover serial ports for arduino bords - foreach(QSerialPortInfo port, QSerialPortInfo::availablePorts()) { + foreach(const QSerialPortInfo &port, QSerialPortInfo::availablePorts()) { qCDebug(dcOwlet()) << "Found serial port" << port.systemLocation(); QString description = port.systemLocation() + " " + port.manufacturer() + " " + port.description(); - ThingDescriptor thingDescriptor(info->thingClassId(), "Owlet Arduino Pro Mini", description); + ThingDescriptor thingDescriptor(info->thingClassId(), "Arduino Pro Mini Owlet", description); ParamList parameters; foreach (Thing *existingThing, myThings()) { @@ -99,38 +128,326 @@ void IntegrationPluginOwlet::discoverThings(ThingDiscoveryInfo *info) void IntegrationPluginOwlet::setupThing(ThingSetupInfo *info) { Thing *thing = info->thing(); - OwletTransport *transport = nullptr; + + if (thing->thingClassId() == arduinoMiniProThingClassId) { + QString serialPort = thing->paramValue(arduinoMiniProThingSerialPortParamTypeId).toString(); + qCDebug(dcOwlet()) << "Setup arduino mini pro owlet on" << serialPort; + OwletTransport *transport = new OwletSerialTransport(serialPort, 115200, this); + OwletSerialClient *client = new OwletSerialClient(transport, transport); + + // During setup + connect(client, &OwletSerialClient::connected, info, [=](){ + qCDebug(dcOwlet()) << "Connected to serial owlet" << client->firmwareVersion(); + thing->setStateValue("connected", true); + }); + + connect(client, &OwletSerialClient::error, info, [=](){ + //info->finish(Thing::ThingErrorHardwareFailure); + transport->deleteLater(); + }); + + // Runtime + connect(client, &OwletSerialClient::connected, thing, [=](){ + thing->setStateValue("connected", true); + thing->setStateValue(arduinoMiniProCurrentVersionStateTypeId, client->firmwareVersion()); + foreach (Thing *childThing, myThings().filterByParentId(thing->id())) { + childThing->setStateValue("connected", true); + } + }); + + connect(client, &OwletSerialClient::disconnected, thing, [=](){ + thing->setStateValue("connected", false); + foreach (Thing *childThing, myThings().filterByParentId(thing->id())) { + childThing->setStateValue("connected", false); + } + }); + + connect(thing, &Thing::settingChanged, thing, [=](const ParamTypeId ¶mTypeId, const QVariant &value){ + qCDebug(dcOwlet()) << "Arduino Mini Pro settings changed" << paramTypeId << value; + quint8 pinId = m_arduinoMiniProPinMapping.value(paramTypeId); + OwletSerialClient::PinMode pinMode = getPinModeFromSettingsValue(value.toString()); + + // Check if we have a thing for this pin and if we need to remove it before setting up + Thing *existingThing = nullptr; + foreach (Thing *childThing, myThings().filterByParentId(thing->id())) { + int existingPinId = childThing->paramValue(m_owletSerialPinParamTypeMap.value(childThing->thingClassId())).toUInt(); + if (existingPinId == pinId) { + qCDebug(dcOwlet()) << "Found already configured thing for pin" << pinId; + existingThing = childThing; + break; + } + } + + if (existingThing) { + if ((pinMode == OwletSerialClient::PinModeDigitalOutput && existingThing->thingClassId() == digitalOutputSerialThingClassId) || + (pinMode == OwletSerialClient::PinModeDigitalInput && existingThing->thingClassId() == digitalInputSerialThingClassId) || + (pinMode == OwletSerialClient::PinModeAnalogOutput && existingThing->thingClassId() == analogOutputSerialThingClassId) || + (pinMode == OwletSerialClient::PinModeAnalogInput && existingThing->thingClassId() == analogInputSerialThingClassId) || + (pinMode == OwletSerialClient::PinModeServo && existingThing->thingClassId() == servoSerialThingClassId)) { + + qCDebug(dcOwlet()) << "Thing for pin" << pinId << "is already configured as" << pinMode; + return; + } else { + qCDebug(dcOwlet()) << "Have thing for pin" << pinId << "but should be configured as" << pinMode; + qCDebug(dcOwlet()) << "Remove existing thing before setup a new one"; + emit autoThingDisappeared(existingThing->id()); + } + } + + setupArduinoChildThing(client, pinId, pinMode); + }); + + m_serialClients.insert(thing, client); + info->finish(Thing::ThingErrorNoError); + + client->transport()->connectTransport(); + return; + } + + if (thing->thingClassId() == digitalOutputSerialThingClassId) { + info->finish(Thing::ThingErrorNoError); + + // Update states + Thing *parent = myThings().findById(thing->parentId()); + bool parentConnected = false; + if (parent) + parentConnected = parent->stateValue("connected").toBool(); + + thing->setStateValue("connected", parentConnected); + + OwletSerialClient *client = m_serialClients.value(parent); + + quint8 pinId = thing->paramValue(digitalOutputSerialThingPinParamTypeId).toUInt(); + OwletSerialClient::PinMode pinMode = OwletSerialClient::PinModeDigitalOutput; + + connect(client, &OwletSerialClient::connected, thing, [=](){ + configurePin(client, pinId, pinMode); + }); + + if (parentConnected) { + configurePin(client, pinId, pinMode); + } + } + + if (thing->thingClassId() == digitalInputSerialThingClassId) { + info->finish(Thing::ThingErrorNoError); + + // Update states + Thing *parent = myThings().findById(thing->parentId()); + bool parentConnected = false; + if (parent) + parentConnected = parent->stateValue("connected").toBool(); + + thing->setStateValue("connected", parentConnected); + + quint8 pinId = thing->paramValue(digitalInputSerialThingPinParamTypeId).toUInt(); + OwletSerialClient::PinMode pinMode = OwletSerialClient::PinModeDigitalInput; + + OwletSerialClient *client = m_serialClients.value(parent); + connect(client, &OwletSerialClient::connected, thing, [=](){ + thing->setStateValue("connected", true); + + configurePin(client, pinId, pinMode); + + // Read current value + quint8 pinId = thing->paramValue(digitalInputSerialThingPinParamTypeId).toUInt(); + OwletSerialClientReply *reply = client->readDigitalValue(pinId); + connect(reply, &OwletSerialClientReply::finished, this, [=](){ + if (reply->status() != OwletSerialClient::StatusSuccess) { + qCWarning(dcOwlet()) << "Failed to read digital pin value from" << thing << reply->status(); + return; + } + + if (reply->responsePayload().count() < 2) { + qCWarning(dcOwlet()) << "Invalid response payload size from request" << pinId; + return; + } + + OwletSerialClient::GPIOError configurationError = static_cast(reply->responsePayload().at(0)); + quint8 value = static_cast(reply->responsePayload().at(1)); + if (configurationError != OwletSerialClient::GPIOErrorNoError) { + qCWarning(dcOwlet()) << "Configure pin request finished with error" << configurationError; + return; + } + + thing->setStateValue(digitalInputSerialPowerStateTypeId, value != 0x00); + }); + }); + + connect(client, &OwletSerialClient::pinValueChanged, this, [=](quint8 id, bool power){ + if (id != pinId) + return; + + thing->setStateValue(digitalInputSerialPowerStateTypeId, power); + }); + + if (parentConnected) { + configurePin(client, pinId, pinMode); + } + + return; + } + + if (thing->thingClassId() == analogInputSerialThingClassId) { + info->finish(Thing::ThingErrorNoError); + + // Update states + Thing *parent = myThings().findById(thing->parentId()); + bool parentConnected = false; + if (parent) + parentConnected = parent->stateValue("connected").toBool(); + + thing->setStateValue("connected", parentConnected); + + OwletSerialClient *client = m_serialClients.value(parent); + + QTimer *refreshTimer = new QTimer(thing); + refreshTimer->setSingleShot(false); + refreshTimer->setInterval(thing->setting(analogInputSerialSettingsRefreshRateParamTypeId).toUInt()); + connect(refreshTimer, &QTimer::timeout, this, [=](){ + // Read current value + quint8 pinId = thing->paramValue(analogInputSerialThingPinParamTypeId).toUInt(); + OwletSerialClientReply *reply = client->readAnalogValue(pinId); + connect(reply, &OwletSerialClientReply::finished, this, [=](){ + if (reply->status() != OwletSerialClient::StatusSuccess) { + qCWarning(dcOwlet()) << "Failed to read analog pin value from" << thing << reply->status(); + return; + } + + if (reply->responsePayload().count() < 3) { + qCWarning(dcOwlet()) << "Invalid response payload size from request" << pinId; + return; + } + + QDataStream stream(reply->responsePayload()); + quint8 errorCode; + stream >> errorCode; + OwletSerialClient::GPIOError configurationError = static_cast(errorCode); + if (configurationError != OwletSerialClient::GPIOErrorNoError) { + qCWarning(dcOwlet()) << "Configure pin request finished with error" << configurationError; + return; + } + quint32 value; + stream >> value; + qCDebug(dcOwlet()) << "Analog value of" << thing << value; + thing->setStateValue(analogInputSerialAnalogValueStateTypeId, value); + }); + }); + + quint8 pinId = thing->paramValue(analogInputSerialThingPinParamTypeId).toUInt(); + OwletSerialClient::PinMode pinMode = OwletSerialClient::PinModeAnalogInput; + + connect(client, &OwletSerialClient::connected, thing, [=](){ + qCDebug(dcOwlet()) << "Starting refresh timer for" << thing; + configurePin(client, pinId, pinMode); + refreshTimer->start(); + }); + + connect(client, &OwletSerialClient::disconnected, thing, [=](){ + qCDebug(dcOwlet()) << "Stopping refresh timer for" << thing; + refreshTimer->stop(); + }); + + // Start reading if already connected + if (parentConnected) { + qCDebug(dcOwlet()) << "Starting refresh timer for" << thing; + configurePin(client, pinId, pinMode); + refreshTimer->start(); + } + + connect(thing, &Thing::settingChanged, refreshTimer, [=](const ParamTypeId ¶mTypeId, const QVariant &value){ + if (paramTypeId == analogInputSerialSettingsRefreshRateParamTypeId) { + refreshTimer->setInterval(value.toUInt()); + refreshTimer->start(); + } + }); + + return; + } + + + if (thing->thingClassId() == analogOutputSerialThingClassId) { + info->finish(Thing::ThingErrorNoError); + + // Update states + Thing *parent = myThings().findById(thing->parentId()); + bool parentConnected = false; + if (parent) + parentConnected = parent->stateValue("connected").toBool(); + + thing->setStateValue("connected", parentConnected); + + OwletSerialClient *client = m_serialClients.value(parent); + + quint8 pinId = thing->paramValue(analogInputSerialThingPinParamTypeId).toUInt(); + OwletSerialClient::PinMode pinMode = OwletSerialClient::PinModeAnalogInput; + + connect(client, &OwletSerialClient::connected, thing, [=](){ + configurePin(client, pinId, pinMode); + }); + + // Start reading if already connected + if (parentConnected) { + configurePin(client, pinId, pinMode); + } + + return; + } + + + if (thing->thingClassId() == servoSerialThingClassId) { + info->finish(Thing::ThingErrorNoError); + + // Update states + Thing *parent = myThings().findById(thing->parentId()); + bool parentConnected = false; + if (parent) + parentConnected = parent->stateValue("connected").toBool(); + + thing->setStateValue("connected", parentConnected); + + OwletSerialClient *client = m_serialClients.value(parent); + + quint8 pinId = thing->paramValue(servoSerialThingPinParamTypeId).toUInt(); + OwletSerialClient::PinMode pinMode = OwletSerialClient::PinModeServo; + + connect(client, &OwletSerialClient::connected, thing, [=](){ + configurePin(client, pinId, pinMode); + }); + + // Start reading if already connected + if (parentConnected) { + configurePin(client, pinId, pinMode); + } + + return; + } + + QHostAddress ip; int port = 5555; - if (thing->thingClassId() != arduinoMiniProThingClassId) { - foreach (const ZeroConfServiceEntry &entry, m_zeroConfBrowser->serviceEntries()) { - if (entry.txt("id") == info->thing()->paramValue(m_owletIdParamTypeMap.value(info->thing()->thingClassId()))) { - ip = entry.hostAddress(); - port = entry.port(); - break; - } + foreach (const ZeroConfServiceEntry &entry, m_zeroConfBrowser->serviceEntries()) { + if (entry.txt("id") == info->thing()->paramValue(m_owletIdParamTypeMap.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; - } - - transport = new OwletTcpTransport(ip, port, this); - - } else if (thing->thingClassId() == arduinoMiniProThingClassId) { - QString serialPort = thing->paramValue(arduinoMiniProThingSerialPortParamTypeId).toString(); - qCDebug(dcOwlet()) << "Setup arduino mini pro owlet on" << serialPort; - transport = new OwletSerialTransport(serialPort, 115200, this); } + // 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; + } + + OwletTransport *transport = new OwletTcpTransport(ip, port, this); OwletClient *client = new OwletClient(transport, this); connect(client, &OwletClient::connected, info, [=](){ @@ -169,12 +486,9 @@ void IntegrationPluginOwlet::setupThing(ThingSetupInfo *info) connect(client, &OwletClient::connected, thing, [=](){ thing->setStateValue("connected", true); - // FIXME: find a better way - if (thing->thingClassId() != arduinoMiniProThingClassId) { - pluginStorage()->beginGroup(thing->id().toString()); - pluginStorage()->setValue("cachedIP", ip.toString()); - pluginStorage()->endGroup(); - } + pluginStorage()->beginGroup(thing->id().toString()); + pluginStorage()->setValue("cachedIP", ip.toString()); + pluginStorage()->endGroup(); qCDebug(dcOwlet()) << "Sending get platform information request..."; int id = client->sendCommand("Platform.GetInformation"); @@ -286,6 +600,184 @@ void IntegrationPluginOwlet::executeAction(ThingActionInfo *info) return; } + if (info->thing()->thingClassId() == arduinoMiniProThingClassId) { + OwletSerialClient *client = m_serialClients.value(info->thing()); + if (!client) { + qCWarning(dcOwlet()) << "Could not execute action. There is no client available for this thing"; + info->finish(Thing::ThingErrorHardwareFailure); + return; + } + + if (!client->ready()) { + qCWarning(dcOwlet()) << "Could not execute action. The serial client is not ready or connected."; + info->finish(Thing::ThingErrorHardwareNotAvailable); + return; + } + if (info->action().actionTypeId() == arduinoMiniProPerformUpdateActionTypeId) { + qCDebug(dcOwlet()) << "Perform firmware update on" << info->thing(); + //if (client->firmwareVersion() != ) + + // TODO: run upgrade process using avrdude + + info->finish(Thing::ThingErrorNoError); + return; + } + } + + if (info->thing()->thingClassId() == analogOutputSerialThingClassId) { + OwletSerialClient *client = m_serialClients.value(myThings().findById(info->thing()->parentId())); + if (!client) { + qCWarning(dcOwlet()) << "Could not execute action. There is no client available for this thing"; + info->finish(Thing::ThingErrorHardwareFailure); + return; + } + + if (!client->ready()) { + qCWarning(dcOwlet()) << "Could not execute action. The serial client is not ready or connected."; + info->finish(Thing::ThingErrorHardwareNotAvailable); + return; + } + + if (info->action().actionTypeId() == analogOutputSerialDutyCycleActionTypeId) { + quint8 dutyCycle = info->action().paramValue(analogOutputSerialDutyCycleActionDutyCycleParamTypeId).toUInt(); + qCDebug(dcOwlet()) << "Set PWM duty cycle of" << info->thing() << "to" << dutyCycle; + quint8 pinId = info->thing()->paramValue(analogOutputSerialThingPinParamTypeId).toUInt(); + + OwletSerialClientReply *reply = client->writeAnalogValue(pinId, dutyCycle); + connect(reply, &OwletSerialClientReply::finished, this, [=](){ + if (reply->status() != OwletSerialClient::StatusSuccess) { + qCWarning(dcOwlet()) << "Failed to set power on pin" << pinId << dutyCycle << reply->status(); + info->finish(Thing::ThingErrorHardwareFailure); + return; + } + + if (reply->responsePayload().count() < 1) { + qCWarning(dcOwlet()) << "Failed to set power on pin" << pinId << dutyCycle << "Invalid response payload size from request"; + info->finish(Thing::ThingErrorHardwareFailure); + return; + } + + OwletSerialClient::GPIOError gpioError = static_cast(reply->responsePayload().at(0)); + if (gpioError != OwletSerialClient::GPIOErrorNoError) { + qCWarning(dcOwlet()) << "Set power on pin" << pinId << dutyCycle << "request finished with error" << gpioError; + info->finish(Thing::ThingErrorHardwareFailure); + return; + } + + qCDebug(dcOwlet()) << "Set PWM duty cycle finished successfully" << pinId << dutyCycle; + info->thing()->setStateValue(analogOutputSerialDutyCycleStateTypeId, dutyCycle); + info->finish(Thing::ThingErrorNoError); + return; + }); + + return; + } + } + + + if (info->thing()->thingClassId() == digitalOutputSerialThingClassId) { + OwletSerialClient *client = m_serialClients.value(myThings().findById(info->thing()->parentId())); + if (!client) { + qCWarning(dcOwlet()) << "Could not execute action. There is no client available for this thing"; + info->finish(Thing::ThingErrorHardwareFailure); + return; + } + + if (!client->ready()) { + qCWarning(dcOwlet()) << "Could not execute action. The serial client is not ready or connected."; + info->finish(Thing::ThingErrorHardwareNotAvailable); + return; + } + + if (info->action().actionTypeId() == digitalOutputSerialPowerActionTypeId) { + quint8 pinId = info->thing()->paramValue(digitalOutputSerialThingPinParamTypeId).toUInt(); + bool power = info->action().paramValue(digitalOutputSerialPowerActionPowerParamTypeId).toBool(); + OwletSerialClientReply *reply = client->writeDigitalValue(pinId, power); + connect(reply, &OwletSerialClientReply::finished, this, [=](){ + if (reply->status() != OwletSerialClient::StatusSuccess) { + qCWarning(dcOwlet()) << "Failed to set power on pin" << pinId << power << reply->status(); + info->finish(Thing::ThingErrorHardwareFailure); + return; + } + + if (reply->responsePayload().count() < 1) { + qCWarning(dcOwlet()) << "Failed to set power on pin" << pinId << power << "Invalid response payload size from request"; + info->finish(Thing::ThingErrorHardwareFailure); + return; + } + + OwletSerialClient::GPIOError gpioError = static_cast(reply->responsePayload().at(0)); + if (gpioError != OwletSerialClient::GPIOErrorNoError) { + qCWarning(dcOwlet()) << "Set power on pin" << pinId << power << "request finished with error" << gpioError; + info->finish(Thing::ThingErrorHardwareFailure); + return; + } + + qCDebug(dcOwlet()) << "Set power finished successfully" << pinId << power; + info->thing()->setStateValue(digitalOutputSerialPowerStateTypeId, power); + info->finish(Thing::ThingErrorNoError); + return; + }); + + return; + } + + info->finish(Thing::ThingErrorUnsupportedFeature); + return; + } + + + if (info->thing()->thingClassId() == servoSerialThingClassId) { + OwletSerialClient *client = m_serialClients.value(myThings().findById(info->thing()->parentId())); + if (!client) { + qCWarning(dcOwlet()) << "Could not execute action. There is no client available for this thing"; + info->finish(Thing::ThingErrorHardwareFailure); + return; + } + + if (!client->ready()) { + qCWarning(dcOwlet()) << "Could not execute action. The serial client is not ready or connected."; + info->finish(Thing::ThingErrorHardwareNotAvailable); + return; + } + + if (info->action().actionTypeId() == servoSerialAngleActionTypeId) { + quint8 pinId = info->thing()->paramValue(servoSerialThingPinParamTypeId).toUInt(); + quint8 angle = info->action().paramValue(servoSerialAngleActionAngleParamTypeId).toUInt(); + OwletSerialClientReply *reply = client->writeServoValue(pinId, angle); + connect(reply, &OwletSerialClientReply::finished, this, [=](){ + if (reply->status() != OwletSerialClient::StatusSuccess) { + qCWarning(dcOwlet()) << "Failed to set servo angle on pin" << pinId << angle << reply->status(); + info->finish(Thing::ThingErrorHardwareFailure); + return; + } + + if (reply->responsePayload().count() < 1) { + qCWarning(dcOwlet()) << "Failed to set servo angle on pin" << pinId << angle << "Invalid response payload size from request"; + info->finish(Thing::ThingErrorHardwareFailure); + return; + } + + OwletSerialClient::GPIOError gpioError = static_cast(reply->responsePayload().at(0)); + if (gpioError != OwletSerialClient::GPIOErrorNoError) { + qCWarning(dcOwlet()) << "Set angle on servo pin" << pinId << angle << "request finished with error" << gpioError; + info->finish(Thing::ThingErrorHardwareFailure); + return; + } + + qCDebug(dcOwlet()) << "Set servo angle finished successfully" << pinId << angle; + info->thing()->setStateValue(servoSerialAngleStateTypeId, angle); + info->finish(Thing::ThingErrorNoError); + return; + }); + + return; + } + + info->finish(Thing::ThingErrorUnsupportedFeature); + return; + } + Q_ASSERT_X(false, "IntegrationPluginOwlet", "Not implemented"); @@ -294,5 +786,132 @@ void IntegrationPluginOwlet::executeAction(ThingActionInfo *info) void IntegrationPluginOwlet::thingRemoved(Thing *thing) { - Q_UNUSED(thing) + if (thing->thingClassId() == arduinoMiniProThingClassId && m_serialClients.contains(thing)) { + m_serialClients.take(thing)->deleteLater(); + } } + +OwletSerialClient::PinMode IntegrationPluginOwlet::getPinModeFromSettingsValue(const QString &settingsValue) +{ + if (settingsValue == "Output") { + return OwletSerialClient::PinModeDigitalOutput; + } else if (settingsValue == "Input") { + return OwletSerialClient::PinModeDigitalInput; + } else if (settingsValue == "PWM") { + return OwletSerialClient::PinModeAnalogOutput; + } else if (settingsValue == "Analog Input") { + return OwletSerialClient::PinModeAnalogInput; + } else if (settingsValue == "Servo") { + return OwletSerialClient::PinModeServo; + } else { + return OwletSerialClient::PinModeUnconfigured; + } +} + +void IntegrationPluginOwlet::setupArduinoChildThing(OwletSerialClient *client, quint8 pinId, OwletSerialClient::PinMode pinMode) +{ + Thing *parentThing = m_serialClients.key(client); + if (!parentThing) { + qCWarning(dcOwlet()) << "Could not setup child thing because the parent thing seems not to be available"; + return; + } + + // Note: pin has no thing here any more, we can set up a new one if required + OwletSerialClientReply *reply = client->configurePin(pinId, pinMode); + connect(reply, &OwletSerialClientReply::finished, this, [=](){ + if (reply->status() != OwletSerialClient::StatusSuccess) { + qCWarning(dcOwlet()) << "Failed to configure pin" << pinId << pinMode << reply->status(); + return; + } + + if (reply->responsePayload().count() < 1) { + qCWarning(dcOwlet()) << "Invalid configure pin response payload size from request" << pinId << pinMode; + return; + } + + OwletSerialClient::GPIOError configurationError = static_cast(reply->responsePayload().at(0)); + if (configurationError != OwletSerialClient::GPIOErrorNoError) { + qCWarning(dcOwlet()) << "Configure pin request finished with error" << configurationError; + return; + } + + qCDebug(dcOwlet()) << "Configure pin request finished successfully" << pinId << pinMode; + + // Setup child devices + switch (pinMode) { + case OwletSerialClient::PinModeDigitalOutput: { + qCDebug(dcOwlet()) << "Setting up digital output on serial owlet for pin" << pinId; + ThingDescriptor descriptor(digitalOutputSerialThingClassId, thingClass(digitalOutputSerialThingClassId).displayName() + " (" + QString::number(pinId) + ")", QString(), parentThing->id()); + ParamList params; + params.append(Param(digitalOutputSerialThingPinParamTypeId, pinId)); + descriptor.setParams(params); + emit autoThingsAppeared(ThingDescriptors() << descriptor); + break; + } + case OwletSerialClient::PinModeDigitalInput: { + qCDebug(dcOwlet()) << "Setting up digital input on serial owlet for pin" << pinId; + ThingDescriptor descriptor(digitalInputSerialThingClassId, thingClass(digitalInputSerialThingClassId).displayName() + " (" + QString::number(pinId) + ")", QString(), parentThing->id()); + ParamList params; + params.append(Param(digitalInputSerialThingPinParamTypeId, pinId)); + descriptor.setParams(params); + emit autoThingsAppeared(ThingDescriptors() << descriptor); + break; + } + case OwletSerialClient::PinModeAnalogOutput: { + qCDebug(dcOwlet()) << "Setting up digital output on serial owlet for pin" << pinId; + ThingDescriptor descriptor(analogOutputSerialThingClassId, thingClass(analogOutputSerialThingClassId).displayName() + " (" + QString::number(pinId) + ")", QString(), parentThing->id()); + ParamList params; + params.append(Param(analogOutputSerialThingPinParamTypeId, pinId)); + descriptor.setParams(params); + emit autoThingsAppeared(ThingDescriptors() << descriptor); + break; + } + case OwletSerialClient::PinModeAnalogInput: { + qCDebug(dcOwlet()) << "Setting up analog input on serial owlet for pin" << pinId; + ThingDescriptor descriptor(analogInputSerialThingClassId, thingClass(analogInputSerialThingClassId).displayName() + " (" + QString::number(pinId) + ")", QString(), parentThing->id()); + ParamList params; + params.append(Param(analogInputSerialThingPinParamTypeId, pinId)); + descriptor.setParams(params); + emit autoThingsAppeared(ThingDescriptors() << descriptor); + break; + } + case OwletSerialClient::PinModeServo: { + qCDebug(dcOwlet()) << "Setting up servo on serial owlet for pin" << pinId; + ThingDescriptor descriptor(servoSerialThingClassId, thingClass(servoSerialThingClassId).displayName() + " (" + QString::number(pinId) + ")", QString(), parentThing->id()); + ParamList params; + params.append(Param(servoSerialThingPinParamTypeId, pinId)); + descriptor.setParams(params); + emit autoThingsAppeared(ThingDescriptors() << descriptor); + break; + } + case OwletSerialClient::PinModeUnconfigured: + + break; + } + }); +} + +void IntegrationPluginOwlet::configurePin(OwletSerialClient *client, quint8 pinId, OwletSerialClient::PinMode pinMode) +{ + OwletSerialClientReply *reply = client->configurePin(pinId, pinMode); + connect(reply, &OwletSerialClientReply::finished, this, [=](){ + if (reply->status() != OwletSerialClient::StatusSuccess) { + qCWarning(dcOwlet()) << "Failed to set pin mode on pin" << pinId << pinMode << reply->status(); + return; + } + + if (reply->responsePayload().count() < 1) { + qCWarning(dcOwlet()) << "Failed to set pin mode on pin" << pinId << pinMode << "Invalid response payload size from request"; + return; + } + + OwletSerialClient::GPIOError gpioError = static_cast(reply->responsePayload().at(0)); + if (gpioError != OwletSerialClient::GPIOErrorNoError) { + qCWarning(dcOwlet()) << "Set pin mode on pin" << pinId << pinMode << "request finished with error" << gpioError; + return; + } + + qCDebug(dcOwlet()) << "Set pin mode finished successfully" << pinId << pinMode; + }); +} + diff --git a/owlet/integrationpluginowlet.h b/owlet/integrationpluginowlet.h index 902323bd..c40c3273 100644 --- a/owlet/integrationpluginowlet.h +++ b/owlet/integrationpluginowlet.h @@ -34,6 +34,8 @@ #include "integrations/integrationplugin.h" #include "extern-plugininfo.h" +#include "owletserialclient.h" + class ZeroConfServiceBrowser; class OwletClient; @@ -57,8 +59,19 @@ private: ZeroConfServiceBrowser *m_zeroConfBrowser = nullptr; QHash m_clients; + QHash m_serialClients; + QHash m_owletIdParamTypeMap; + + // Serial owlets QHash m_owletSerialPortParamTypeMap; + QHash m_owletSerialPinParamTypeMap; + + QHash m_arduinoMiniProPinMapping; + OwletSerialClient::PinMode getPinModeFromSettingsValue(const QString &settingsValue); + + void setupArduinoChildThing(OwletSerialClient *client, quint8 pinId, OwletSerialClient::PinMode pinMode); + void configurePin(OwletSerialClient *client, quint8 pinId, OwletSerialClient::PinMode pinMode); }; #endif // INTEGRATIONPLUGINOWLET_H diff --git a/owlet/integrationpluginowlet.json b/owlet/integrationpluginowlet.json index 95b73968..4c006ce5 100644 --- a/owlet/integrationpluginowlet.json +++ b/owlet/integrationpluginowlet.json @@ -225,7 +225,7 @@ "name": "arduinoMiniPro", "displayName": "Arduino Pro Mini Owlet", "createMethods": ["user", "discovery"], - "interfaces": ["gateway", "connectable"], + "interfaces": ["gateway", "update", "connectable"], "paramTypes": [ { "id": "589715a6-aaf7-4ffb-9c34-015ac22d527e", @@ -252,8 +252,7 @@ "allowedValues": [ "None", "Input", - "Output", - "Interrupt" + "Output" ], "defaultValue": "None" }, @@ -267,7 +266,7 @@ "Input", "Output", "PWM", - "Interrupt" + "Servo" ], "defaultValue": "None" }, @@ -292,7 +291,8 @@ "None", "Input", "Output", - "PWM" + "PWM", + "Servo" ], "defaultValue": "None" }, @@ -305,7 +305,8 @@ "None", "Input", "Output", - "PWM" + "PWM", + "Servo" ], "defaultValue": "None" }, @@ -342,7 +343,8 @@ "None", "Input", "Output", - "PWM" + "PWM", + "Servo" ], "defaultValue": "None" }, @@ -355,7 +357,8 @@ "None", "Input", "Output", - "PWM" + "PWM", + "Servo" ], "defaultValue": "None" }, @@ -368,7 +371,8 @@ "None", "Input", "Output", - "PWM" + "PWM", + "Servo" ], "defaultValue": "None" }, @@ -497,6 +501,228 @@ "type": "bool", "defaultValue": false, "cached": false + }, + { + "id": "f7e86ecd-761b-412e-b5de-a2547f883f76", + "name": "currentVersion", + "displayName": "Firmware version", + "displayNameEvent": "Firmware version changed", + "type": "QString", + "defaultValue": "" + }, + { + "id": "43afe87d-3465-4ced-be0f-170f972afea3", + "name": "updateStatus", + "displayName": "Update status", + "displayNameEvent": "Update status changed", + "type": "QString", + "possibleValues": ["idle", "available", "updating"], + "defaultValue": "idle" + } + ], + "actionTypes": [ + { + "id": "8cfbb619-de54-441a-8761-d29c6f0c659a", + "name": "performUpdate", + "displayName": "Update firmware" + } + ] + }, + { + "id": "43aca5c1-3972-49ae-b21f-4d1486942e1f", + "displayName": "Digital GPIO output on owlet", + "name": "digitalOutputSerial", + "createMethods": ["auto"], + "interfaces": [ "power", "connectable" ], + "paramTypes": [ + { + "id": "6e17a423-89c0-430b-ac78-1af60a63ce50", + "name": "pin", + "displayName": "Pin number", + "type": "uint", + "defaultValue": 1 + } + ], + "stateTypes": [ + { + "id": "3da148b9-fe85-4bf1-9005-082b3cf0c014", + "name": "connected", + "displayName": "Connected", + "displayNameEvent": "Connected changed", + "defaultValue": false, + "type": "bool" + }, + { + "id": "2f7dd16b-c1ee-4b2b-859e-e3eacc679400", + "name": "power", + "displayName": "Power", + "type": "bool", + "defaultValue": false, + "writable": true, + "displayNameEvent": "Power changed", + "displayNameAction": "Set power", + "ioType": "digitalOutput" + } + ] + }, + { + "id": "884cb983-3099-4355-a2e6-693f5c1b85e7", + "displayName": "Digital GPIO input on owlet", + "name": "digitalInputSerial", + "createMethods": ["auto"], + "interfaces": [ "connectable" ], + "paramTypes": [ + { + "id": "6e872f08-9110-425c-9e17-e24d83452b7d", + "name": "pin", + "displayName": "Pin number", + "type": "uint", + "defaultValue": 1 + } + ], + "stateTypes": [ + { + "id": "112dad0b-9c94-4310-8962-f81e1050c713", + "name": "connected", + "displayName": "Connected", + "displayNameEvent": "Connected changed", + "defaultValue": false, + "type": "bool" + }, + { + "id": "9fd72a32-a266-4474-b5b1-e6da9835da6d", + "name": "power", + "displayName": "Powered", + "type": "bool", + "defaultValue": false, + "displayNameEvent": "Powered changed", + "ioType": "digitalInput" + } + ] + }, + { + "id": "c6f3cbd9-1863-430a-9fc2-19f7b57b47d7", + "displayName": "Analog input GPIO on owlet", + "name": "analogInputSerial", + "createMethods": ["auto"], + "interfaces": [ "connectable" ], + "paramTypes": [ + { + "id": "55b142ab-ae9a-41cd-a23a-5a78ec141013", + "name": "pin", + "displayName": "Pin number", + "type": "uint", + "defaultValue": 1 + } + ], + "settingsTypes": [ + { + "id": "cfdc6cd0-02ae-403c-aab6-8e86306405af", + "name": "refreshRate", + "displayName": "Refresh rate", + "type": "uint", + "unit": "MilliSeconds", + "minValue": 200, + "defaultValue": 500 + } + ], + "stateTypes": [ + { + "id": "b8fdd65e-dd33-451e-b02f-d7cf73a2e4e2", + "name": "connected", + "displayName": "Connected", + "displayNameEvent": "Connected changed", + "defaultValue": false, + "type": "bool" + }, + { + "id": "5b7b50c8-a3aa-494d-a454-96c38f257836", + "name": "analogValue", + "displayName": "Analog value", + "displayNameEvent": "Analog value changed", + "type": "uint", + "defaultValue": 0, + "minValue": 0, + "maxValue": 1023, + "ioType": "analogInput" + } + ] + }, + { + "id": "ee228a56-9cf2-4401-84c2-8e25f288f136", + "displayName": "PWM on owlet", + "name": "analogOutputSerial", + "createMethods": ["auto"], + "interfaces": [ "connectable" ], + "paramTypes": [ + { + "id": "e2da646f-003f-495b-8250-28067732973a", + "name": "pin", + "displayName": "Pin number", + "type": "uint", + "defaultValue": 1 + } + ], + "stateTypes": [ + { + "id": "d08ed1bd-0ddf-4bad-9689-cc7730a54b05", + "name": "connected", + "displayName": "Connected", + "displayNameEvent": "Connected changed", + "defaultValue": false, + "type": "bool" + }, + { + "id": "8c8065f7-b335-41bf-a015-fd2cd7e68e81", + "name": "dutyCycle", + "displayName": "Duty cycle", + "displayNameEvent": "Duty cycle changed", + "displayNameAction": "Set duty cycle", + "type": "uint", + "minValue": 0, + "maxValue": 255, + "defaultValue": 0, + "writable": true, + "ioType": "analogOutput" + } + ] + }, + { + "id": "66d9725a-b096-4300-b0e1-adc043ee12c3", + "displayName": "Servo on owlet", + "name": "servoSerial", + "createMethods": ["auto"], + "interfaces": [ "connectable" ], + "paramTypes": [ + { + "id": "4596b43d-93e9-46e4-8831-59f96f998606", + "name": "pin", + "displayName": "Pin number", + "type": "uint", + "defaultValue": 1 + } + ], + "stateTypes": [ + { + "id": "0ae0d6a4-8eb1-4d4f-b093-59ce42355ad7", + "name": "connected", + "displayName": "Connected", + "displayNameEvent": "Connected changed", + "defaultValue": false, + "type": "bool" + }, + { + "id": "eee4e07a-1cb1-44d1-98d5-d66d2e63f704", + "name": "angle", + "displayName": "Angle", + "displayNameEvent": "Angle changed", + "displayNameAction": "Set angle", + "type": "uint", + "minValue": 0, + "maxValue": 180, + "defaultValue": 0, + "writable": true, + "ioType": "analogOutput" } ] } diff --git a/owlet/owlet.pro b/owlet/owlet.pro index b940baf5..f8959c65 100644 --- a/owlet/owlet.pro +++ b/owlet/owlet.pro @@ -5,6 +5,7 @@ QT += network serialport SOURCES += \ integrationpluginowlet.cpp \ owletclient.cpp \ + owletserialclient.cpp \ owletserialtransport.cpp \ owlettcptransport.cpp \ owlettransport.cpp @@ -12,6 +13,7 @@ SOURCES += \ HEADERS += \ integrationpluginowlet.h \ owletclient.h \ + owletserialclient.h \ owletserialtransport.h \ owlettcptransport.h \ owlettransport.h diff --git a/owlet/owletserialclient.cpp b/owlet/owletserialclient.cpp new file mode 100644 index 00000000..c0b6ec3d --- /dev/null +++ b/owlet/owletserialclient.cpp @@ -0,0 +1,249 @@ +#include "owletserialclient.h" +#include "extern-plugininfo.h" + +#include + +OwletSerialClient::OwletSerialClient(OwletTransport *transport, QObject *parent) : + QObject(parent), + m_transport(transport) +{ + connect(m_transport, &OwletTransport::dataReceived, this, &OwletSerialClient::dataReceived); + connect(m_transport, &OwletTransport::error, this, &OwletSerialClient::error); + connect(m_transport, &OwletTransport::connectedChanged, this, [=](bool transportConnected){ + if (!transportConnected) { + m_ready = false; + emit disconnected(); + + // Clean up queue + qDeleteAll(m_pendingRequests); + m_pendingRequests.clear(); + } + }); +} + +OwletTransport *OwletSerialClient::transport() const +{ + return m_transport; +} + +bool OwletSerialClient::ready() const +{ + return m_ready; +} + +OwletSerialClientReply *OwletSerialClient::getFirmwareVersion() +{ + qCDebug(dcOwlet()) << "Request owlet firmware version"; + QByteArray requestData; + QDataStream stream(&requestData, QIODevice::WriteOnly); + stream << static_cast(OwletSerialClient::CommandGetFirmwareVersion); + stream << m_requestId++; + + OwletSerialClientReply *reply = createReply(requestData); + m_pendingRequests.enqueue(reply); + sendNextRequest(); + return reply; +} + +OwletSerialClientReply *OwletSerialClient::configurePin(quint8 pinId, PinMode pinMode) +{ + qCDebug(dcOwlet()) << "Configure pin" << pinId << pinMode; + QByteArray requestData; + QDataStream stream(&requestData, QIODevice::WriteOnly); + stream << static_cast(OwletSerialClient::CommandConfigurePin); + stream << m_requestId++; + stream << pinId << static_cast(pinMode); + + OwletSerialClientReply *reply = createReply(requestData); + m_pendingRequests.enqueue(reply); + sendNextRequest(); + return reply; +} + +OwletSerialClientReply *OwletSerialClient::writeDigitalValue(quint8 pinId, bool power) +{ + qCDebug(dcOwlet()) << "Setting gpio output power of pin" << pinId << power; + QByteArray requestData; + QDataStream stream(&requestData, QIODevice::WriteOnly); + stream << static_cast(OwletSerialClient::CommandWriteDigitalPin); + stream << m_requestId++; + stream << pinId << static_cast(power ? 0x01 : 0x00); + + OwletSerialClientReply *reply = createReply(requestData); + m_pendingRequests.enqueue(reply); + sendNextRequest(); + return reply; +} + +OwletSerialClientReply *OwletSerialClient::readDigitalValue(quint8 pinId) +{ + qCDebug(dcOwlet()) << "Reading digital gpio value of pin" << pinId; + QByteArray requestData; + QDataStream stream(&requestData, QIODevice::WriteOnly); + stream << static_cast(OwletSerialClient::CommandReadDigitalPin); + stream << m_requestId++; + stream << pinId; + + OwletSerialClientReply *reply = createReply(requestData); + m_pendingRequests.enqueue(reply); + sendNextRequest(); + return reply; +} + +OwletSerialClientReply *OwletSerialClient::writeAnalogValue(quint8 pinId, quint8 dutyCycle) +{ + qCDebug(dcOwlet()) << "Write analog gpio value of pin" << pinId << dutyCycle; + QByteArray requestData; + QDataStream stream(&requestData, QIODevice::WriteOnly); + stream << static_cast(OwletSerialClient::CommandWriteAnalogPin); + stream << m_requestId++; + stream << pinId; + stream << dutyCycle; + + OwletSerialClientReply *reply = createReply(requestData); + m_pendingRequests.enqueue(reply); + sendNextRequest(); + return reply; +} + +OwletSerialClientReply *OwletSerialClient::readAnalogValue(quint8 pinId) +{ + qCDebug(dcOwlet()) << "Reading analog gpio value of pin" << pinId; + QByteArray requestData; + QDataStream stream(&requestData, QIODevice::WriteOnly); + stream << static_cast(OwletSerialClient::CommandReadAnalogPin); + stream << m_requestId++; + stream << pinId; + + OwletSerialClientReply *reply = createReply(requestData); + m_pendingRequests.enqueue(reply); + sendNextRequest(); + return reply; +} + +OwletSerialClientReply *OwletSerialClient::writeServoValue(quint8 pinId, quint8 angle) +{ + qCDebug(dcOwlet()) << "Setting servo angle of pin" << pinId << angle; + QByteArray requestData; + QDataStream stream(&requestData, QIODevice::WriteOnly); + stream << static_cast(OwletSerialClient::CommandWriteServoPin); + stream << m_requestId++; + stream << pinId; + stream << angle; + + OwletSerialClientReply *reply = createReply(requestData); + m_pendingRequests.enqueue(reply); + sendNextRequest(); + return reply; +} + +QString OwletSerialClient::firmwareVersion() const +{ + return m_firmwareVersion; +} + +void OwletSerialClient::dataReceived(const QByteArray &data) +{ + QDataStream stream(data); + quint8 commandId; quint8 requestId; + stream >> commandId >> requestId; + + // Check if command or notification + if (commandId < 0xF0) { + quint8 statusId; + stream >> statusId; + OwletSerialClient::Command command = static_cast(commandId); + OwletSerialClient::Status status = static_cast(statusId); + if (m_currentReply) { + if (m_currentReply->command() == command && m_currentReply->requestId() == requestId) { + m_currentReply->m_timer.stop(); + m_currentReply->m_status = status; + m_currentReply->m_responsePayload = data.right(data.length() - 3); + + if (status != OwletSerialClient::StatusSuccess) { + qCWarning(dcOwlet()) << "Request finished with error" << command << "ID:" << m_currentReply->requestId() << status; + } else { + qCDebug(dcOwlet()) << "Request finished" << command << "ID:" << m_currentReply->requestId() << status << "Payload:" << m_currentReply->responsePayload().toHex(); + } + + emit m_currentReply->finished(); + } + } else { + qCWarning(dcOwlet()) << "Received unhandled command response data" << data.toHex(); + } + } else { + OwletSerialClient::Notification notification = static_cast(commandId); + QByteArray notificationPayload = data.right(data.length() - 2); + qCDebug(dcOwlet()) << "Notification received" << notification << "ID:" << requestId << notificationPayload.toHex(); + switch (notification) { + case OwletSerialClient::NotificationReady: { + OwletSerialClientReply *reply = getFirmwareVersion(); + connect(reply, &OwletSerialClientReply::finished, this, [this, reply](){ + if (reply->status() != OwletSerialClient::StatusSuccess) { + qCWarning(dcOwlet()) << "Failed to get firmware version" << reply->status(); + emit error(); + return; + } + + if (reply->responsePayload().count() != 3) { + qCWarning(dcOwlet()) << "Invalid firmware version payload size"; + emit error(); + return; + } + + quint8 major = reply->responsePayload().at(0); + quint8 minor = reply->responsePayload().at(1); + quint8 patch = reply->responsePayload().at(2); + + m_firmwareVersion = QString("%1.%2.%3").arg(major).arg(minor).arg(patch); + qCDebug(dcOwlet()) << "Connected successfully to firmware" << m_firmwareVersion; + m_ready = true; + emit connected(); + }); + break; + } + case OwletSerialClient::NotificationGpioPinChanged: { + quint8 pinId; quint8 powerValue; + stream >> pinId >> powerValue; + bool power = powerValue != 0x00; + qCDebug(dcOwlet()) << "Pin value changed" << pinId << power; + emit pinValueChanged(pinId, power); + break; + } + case OwletSerialClient::NotificationDebugMessage: + qCDebug(dcOwlet()) << "Firmware debug:" << QString::fromUtf8(data.right(data.length() - 2)); + break; + default: + qCWarning(dcOwlet()) << "Unhandled notification received" << data.toHex(); + break; + } + } +} + +OwletSerialClientReply *OwletSerialClient::createReply(const QByteArray &requestData) +{ + OwletSerialClientReply *reply = new OwletSerialClientReply(requestData, this); + connect(reply, &OwletSerialClientReply::finished, reply, [=](){ + reply->deleteLater(); + if (reply == m_currentReply) { + m_currentReply = nullptr; + } + sendNextRequest(); + }); + + return reply; +} + +void OwletSerialClient::sendNextRequest() +{ + if (m_currentReply) + return; + + if (m_pendingRequests.isEmpty()) + return; + + m_currentReply = m_pendingRequests.dequeue(); + qCDebug(dcOwlet()) << "Sending request" << m_currentReply->command() << "ID:" << m_currentReply->requestId() << "Payload:" << m_currentReply->requestData().right(m_currentReply->requestData().length() - 2).toHex(); + m_transport->sendData(m_currentReply->requestData()); + m_currentReply->m_timer.start(); +} diff --git a/owlet/owletserialclient.h b/owlet/owletserialclient.h new file mode 100644 index 00000000..2e20188f --- /dev/null +++ b/owlet/owletserialclient.h @@ -0,0 +1,150 @@ +#ifndef OWLETSERIALCLIENT_H +#define OWLETSERIALCLIENT_H + +#include +#include +#include + +#include "owlettransport.h" + +class OwletSerialClientReply; + +class OwletSerialClient : public QObject +{ + Q_OBJECT +public: + enum Command { + CommandGetFirmwareVersion = 0x00, + CommandConfigurePin = 0x01, + CommandWriteDigitalPin = 0x02, + CommandReadDigitalPin = 0x03, + CommandWriteAnalogPin = 0x04, + CommandReadAnalogPin = 0x05, + CommandWriteServoPin = 0x06 + }; + Q_ENUM(Command) + + enum Notification { + NotificationReady = 0xf0, + NotificationGpioPinChanged = 0xf1, + NotificationDebugMessage = 0xff + }; + Q_ENUM(Notification) + + enum Status { + StatusSuccess = 0x00, + StatusInvalidProtocol = 0x01, + StatusInvalidCommand = 0x02, + StatusInvalidPlayload = 0x03, + StatusTimeout = 0xfe, + StatusUnknownError = 0xff + }; + Q_ENUM(Status) + + enum GPIOError { + GPIOErrorNoError = 0x00, + GPIOErrorUnconfigured = 0x01, + GPIOErrorUnsupported = 0x02, + GPIOErrorConfigurationMismatch = 0x03, + GPIOErrorInvalidParameter = 0x04, + GPIOErrorInvalidPin = 0x05 + }; + Q_ENUM(GPIOError) + + enum PinMode { + PinModeUnconfigured = 0x00, + PinModeDigitalInput = 0x01, + PinModeDigitalOutput = 0x02, + PinModeAnalogInput = 0x03, + PinModeAnalogOutput = 0x04, + PinModeServo = 0x05 + }; + Q_ENUM(PinMode) + + explicit OwletSerialClient(OwletTransport *transport, QObject *parent = nullptr); + + OwletTransport *transport() const; + + bool ready() const; + + OwletSerialClientReply *getFirmwareVersion(); + OwletSerialClientReply *configurePin(quint8 pinId, PinMode pinMode); + OwletSerialClientReply *writeDigitalValue(quint8 pinId, bool power); + OwletSerialClientReply *readDigitalValue(quint8 pinId); + OwletSerialClientReply *writeAnalogValue(quint8 pinId, quint8 dutyCycle); + OwletSerialClientReply *readAnalogValue(quint8 pinId); + OwletSerialClientReply *writeServoValue(quint8 pinId, quint8 angle); + + QString firmwareVersion() const; + +signals: + void connected(); + void disconnected(); + void error(); + + void pinValueChanged(quint8 pinId, bool power); + +private slots: + void dataReceived(const QByteArray &data); + +private: + OwletTransport *m_transport = nullptr; + bool m_ready = false; + + quint8 m_requestId = 0; + + OwletSerialClientReply *m_currentReply = nullptr; + QQueue m_pendingRequests; + + QString m_firmwareVersion; + + OwletSerialClientReply *createReply(const QByteArray &requestData); + void sendNextRequest(); +}; + + +class OwletSerialClientReply : public QObject +{ + Q_OBJECT + + friend class OwletSerialClient; + +public: + QByteArray requestData() const { return m_requestData; }; + OwletSerialClient::Command command() const { return m_command; }; + quint8 requestId() const { return m_requestId; }; + OwletSerialClient::Status status() const { return m_status; }; + QByteArray responsePayload() const { return m_responsePayload; }; + +signals: + void finished(); + +private: + explicit OwletSerialClientReply(const QByteArray &requestData, QObject *parent = nullptr) : + QObject(parent), + m_requestData(requestData) + { + Q_ASSERT(m_requestData.length() >= 2); + m_command = static_cast(m_requestData.at(0)); + m_requestId = static_cast(m_requestData.at(1)); + + m_timer.setInterval(1000); + m_timer.setSingleShot(true); + connect(&m_timer, &QTimer::timeout, this, [=](){ + m_status = OwletSerialClient::StatusTimeout; + emit finished(); + }); + } + + QTimer m_timer; + QByteArray m_requestData; + OwletSerialClient::Command m_command; + quint8 m_requestId = 0; + + // Response + OwletSerialClient::Status m_status = OwletSerialClient::StatusUnknownError; + QByteArray m_responsePayload; +}; + + +#endif // OWLETSERIALCLIENT_H diff --git a/owlet/owletserialtransport.cpp b/owlet/owletserialtransport.cpp index 3fb28a0b..b49b1de1 100644 --- a/owlet/owletserialtransport.cpp +++ b/owlet/owletserialtransport.cpp @@ -27,12 +27,13 @@ OwletSerialTransport::OwletSerialTransport(const QString &serialPortName, uint b connect(m_serialPort, &QSerialPort::errorOccurred, this, &OwletSerialTransport::onError); #endif - m_reconnectTimer = new QTimer(this); m_reconnectTimer->setInterval(5000); m_reconnectTimer->setSingleShot(false); connect(m_reconnectTimer, &QTimer::timeout, this, [=](){ if (m_serialPort->isOpen()) { + // Clear any buffer content + m_serialPort->clear(); m_reconnectTimer->stop(); return; } else { @@ -48,6 +49,8 @@ bool OwletSerialTransport::connected() const void OwletSerialTransport::sendData(const QByteArray &data) { + qCDebug(dcOwlet()) << "UART -->" << data.toHex(); + // Stream bytes using SLIP QByteArray message; QDataStream stream(&message, QIODevice::WriteOnly); @@ -71,7 +74,8 @@ void OwletSerialTransport::sendData(const QByteArray &data) } stream << static_cast(SlipProtocolEnd); - qCDebug(dcOwlet()) << "UART -->" << qUtf8Printable(data) << message.toHex(); + //qCDebug(dcOwlet()) << "UART -->" << message.toHex(); + m_serialPort->write(message); m_serialPort->flush(); } @@ -102,6 +106,7 @@ void OwletSerialTransport::connectTransport() if (!m_serialPort->open(QIODevice::ReadWrite)) { qCWarning(dcOwlet()) << "Could not open serial port on" << m_serialPortName << m_serialPort->errorString(); m_reconnectTimer->start(); + emit error(); return; } @@ -120,7 +125,7 @@ void OwletSerialTransport::disconnectTransport() void OwletSerialTransport::onReadyRead() { QByteArray data = m_serialPort->readAll(); - qCDebug(dcOwlet()) << "UART <-- raw:" << data.toHex() << qUtf8Printable(data); + //qCDebug(dcOwlet()) << "UART <--" << data.toHex(); for (int i = 0; i < data.length(); i++) { quint8 receivedByte = data.at(i); @@ -144,8 +149,8 @@ void OwletSerialTransport::onReadyRead() switch (receivedByte) { case SlipProtocolEnd: // We are done with this package, process it and reset the buffer - if (!m_buffer.isEmpty() && m_buffer.length() >= 3) { - qCDebug(dcOwlet()) << "UART <--" << m_buffer.toHex() << qUtf8Printable(m_buffer); + if (!m_buffer.isEmpty() && m_buffer.length() >= 2) { + qCDebug(dcOwlet()) << "UART <--" << m_buffer.toHex(); emit dataReceived(m_buffer); } m_buffer.clear(); @@ -168,6 +173,7 @@ void OwletSerialTransport::onError(QSerialPort::SerialPortError serialPortError) if (serialPortError != QSerialPort::NoError && serialPortError != QSerialPort::OpenError && m_serialPort->isOpen()) { qCWarning(dcOwlet()) << "Serial port error occured" << serialPortError << m_serialPort->errorString(); emit error(); + m_reconnectTimer->start(); if (m_serialPort->isOpen()) { m_serialPort->close();