* Copyright (c) 2021 dresden elektronik ingenieurtechnik gmbh.
* All rights reserved.
*
* The software in this package is published under the terms of the BSD
* style license a copy of which has been included with this distribution in
* the LICENSE.txt file.
*
*/
#include <QBasicTimer>
#include <QElapsedTimer>
#include <QTimer>
#include <QTimerEvent>
#include <QMetaObject>
#include <array>
#include <deconz/dbg_trace.h>
#include <deconz/node.h>
#include "device.h"
#include "device_access_fn.h"
#include "device_descriptions.h"
#include "event.h"
#include "event_emitter.h"
#include "utils/utils.h"
#include "zcl/zcl.h"
#include "zdp/zdp.h"
#define STATE_LEVEL_BINDING StateLevel1
#define STATE_LEVEL_POLL StateLevel2
#define MGMT_BIND_SUPPORT_UNKNOWN -1
#define MGMT_BIND_SUPPORTED 1
#define MGMT_BIND_NOT_SUPPORTED 0
#define DEV_INVALID_DEVICE_ID -1
typedef void (*DeviceStateHandler)(Device *, const Event &);
https://github.com/dresden-elektronik/deconz-rest-plugin-v2/wiki/Device-Class#state-machine
*/
void DEV_InitStateHandler(Device *device, const Event &event);
void DEV_IdleStateHandler(Device *device, const Event &event);
void DEV_NodeDescriptorStateHandler(Device *device, const Event &event);
void DEV_ActiveEndpointsStateHandler(Device *device, const Event &event);
void DEV_SimpleDescriptorStateHandler(Device *device, const Event &event);
void DEV_BasicClusterStateHandler(Device *device, const Event &event);
void DEV_GetDeviceDescriptionHandler(Device *device, const Event &event);
static const deCONZ::SimpleDescriptor *DEV_GetSimpleDescriptorForServerCluster(const Device *device, deCONZ::ZclClusterId_t clusterId);
void DEV_BindingHandler(Device *device, const Event &event);
void DEV_BindingTableReadHandler(Device *device, const Event &event);
void DEV_BindingTableVerifyHandler(Device *device, const Event &event);
void DEV_BindingCreateHandler(Device *device, const Event &event);
void DEV_BindingRemoveHandler(Device *device, const Event &event);
void DEV_ReadReportConfigurationHandler(Device *device, const Event &event);
void DEV_ReadNextReportConfigurationHandler(Device *device, const Event &event);
void DEV_ConfigureNextReportConfigurationHandler(Device *device, const Event &event);
void DEV_ConfigureReportingHandler(Device *device, const Event &event);
void DEV_BindingIdleHandler(Device *device, const Event &event);
void DEV_PollIdleStateHandler(Device *device, const Event &event);
void DEV_PollNextStateHandler(Device *device, const Event &event);
void DEV_PollBusyStateHandler(Device *device, const Event &event);
void DEV_DeadStateHandler(Device *device, const Event &event);
void DEV_ZgpStateHandler(Device *device, const Event &event);
using namespace deCONZ::literals;
constexpr int RxOnWhenIdleResponseTime = 2000;
constexpr int RxOffWhenIdleResponseTime = 8000;
constexpr int MaxConfirmTimeout = 20000;
constexpr int BindingAutoCheckInterval = 1000 * 60 * 60;
constexpr int MaxPollItemRetries = 3;
constexpr int MaxIdleApsConfirmErrors = 16;
constexpr int MaxSubResources = 8;
static int devManaged = -1;
struct DEV_PollItem
{
explicit DEV_PollItem(const Resource *r, const ResourceItem *i, const QVariant &p) :
resource(r), item(i), readParameters(p) {}
size_t retry = 0;
const Resource *resource = nullptr;
const ResourceItem *item = nullptr;
QVariant readParameters;
};
constexpr int64_t MarkZclConfigureBusy = 21;
struct ReportTracker
{
deCONZ::SteadyTimeRef lastReport;
deCONZ::SteadyTimeRef lastConfigureCheck;
uint16_t clusterId = 0;
uint16_t attributeId = 0;
uint8_t endpoint = 0;
};
struct BindingTracker
{
deCONZ::SteadyTimeRef tBound;
};
struct BindingContext
{
size_t bindingCheckRound = 0;
size_t bindingIter = 0;
size_t reportIter = 0;
size_t configIter = 0;
int mgmtBindSupported = MGMT_BIND_SUPPORT_UNKNOWN;
uint8_t mgmtBindStartIndex = 0;
std::vector<BindingTracker> bindingTrackers;
std::vector<DDF_Binding> bindings;
std::vector<ReportTracker> reportTrackers;
ZCL_ReadReportConfigurationParam readReportParam;
ZCL_Result zclResult;
ZDP_Result zdpResult;
};
static ReportTracker &DEV_GetOrCreateReportTracker(Device *device, uint16_t clusterId, uint16_t attrId, uint8_t endpoint);
class DevicePrivate
{
public:
void setState(DeviceStateHandler newState, DEV_StateLevel level = StateLevel0);
void startStateTimer(int IntervalMs, DEV_StateLevel level);
void stopStateTimer(DEV_StateLevel level);
bool hasRxOnWhenIdle() const;
Device *q = nullptr;
deCONZ::ApsController *apsCtrl = nullptr;
This is a helper to query the actual sub-device Resource* on demand via Resource::Handle.
*/
std::array<Resource::Handle, MaxSubResources> subResourceHandles;
std::vector<Resource*> subResources;
const deCONZ::Node *node = nullptr;
int deviceId = DEV_INVALID_DEVICE_ID;
int64_t creationTime = -1;
DeviceKey deviceKey = 0;
Indexes >0 represent sub states of StateLevel0 running in parallel.
*/
std::array<DeviceStateHandler, StateLevelMax> state{};
std::array<QBasicTimer, StateLevelMax> timer;
QElapsedTimer awake;
BindingContext binding;
std::vector<DEV_PollItem> pollItems;
int idleApsConfirmErrors = 0;
This is for debug asserting that the order of events is valid - it doesn't drive logic. */
bool stateEnterLock[StateLevelMax] = {};
bool managed = false;
ZDP_Result zdpResult;
DA_ReadResult readResult;
uint8_t zdpNeedFetchEndpointIndex = 0xFF;
int maxResponseTime = RxOffWhenIdleResponseTime;
struct
{
unsigned char hasDdf : 1;
unsigned char initialRun : 1;
unsigned char needZDPMaintenanceOnce : 1;
unsigned char needReadActiveEndpoints : 1;
unsigned char needReadSimpleDescriptors : 1;
unsigned char reserved : 3;
} flags{};
};
Device *DEV_ParentDevice(Resource *r)
{
if (r && r->parentResource() && r->parentResource()->prefix() == RDevices)
{
return static_cast<Device*>(r->parentResource());
}
return nullptr;
}
void DEV_ForwardNodeChange(Device *device, const QString &key, const QString &value)
{
if (device)
{
QMetaObject::invokeMethod(device->d->apsCtrl, "onRestNodeUpdated", Qt::DirectConnection,
Q_ARG(quint64, device->key()), Q_ARG(QString, key), Q_ARG(QString, value));
}
}
void DEV_EnqueueEvent(Device *device, const char *event)
{
Q_ASSERT(device);
Q_ASSERT(event);
emit device->eventNotify(Event(device->prefix(), event, 0, device->key()));
}
Resource *DEV_GetSubDevice(Device *device, const char *prefix, const QString &identifier)
{
if (!device)
{
return nullptr;
}
for (auto &sub : device->subDevices())
{
if (prefix && sub->prefix() != prefix)
{
continue;
}
if (sub->item(RAttrUniqueId)->toString() == identifier || sub->item(RAttrId)->toString() == identifier)
{
return sub;
}
}
return nullptr;
}
void DEV_InitStateHandler(Device *device, const Event &event)
{
DevicePrivate *d = device->d;
if (event.what() == REventStateEnter)
{
d->zdpResult = { };
d->node = DEV_GetCoreNode(device->key());
if ((event.deviceKey() & 0x00212E0000000000LLU) == 0x00212E0000000000LLU)
{
if (d->node && d->node->isCoordinator())
{
d->setState(DEV_DeadStateHandler);
return;
}
}
}
else if (event.what() == REventStateLeave)
{
return;
}
if (event.what() == REventPoll ||
event.what() == REventAwake ||
event.what() == RConfigReachable ||
event.what() == RStateReachable ||
event.what() == REventStateTimeout ||
event.what() == RStateLastUpdated ||
d->flags.initialRun == 1)
{
d->flags.initialRun = 0;
d->binding.bindingCheckRound = 0;
if (!device->node())
{
d->node = DEV_GetCoreNode(device->key());
}
if (device->node())
{
{
const deCONZ::Address a = device->node()->address();
ResourceItem *ext = device->item(RAttrExtAddress);
if (!ext->lastSet().isValid() || ext->toNumber() != a.ext())
{
ext->setValue(a.ext());
}
ResourceItem *nwk = device->item(RAttrNwkAddress);
if (!nwk->lastSet().isValid() || nwk->toNumber() != a.nwk())
{
nwk->setValue(a.nwk());
}
}
if (!device->node()->nodeDescriptor().isNull() || device->reachable())
{
d->setState(DEV_NodeDescriptorStateHandler);
}
}
else
{
DBG_Printf(DBG_DEV, "DEV Init no node found: " FMT_MAC "\n", FMT_MAC_CAST(event.deviceKey()));
if ((device->key() & 0xffffffff00000000LLU) == 0)
{
d->setState(DEV_ZgpStateHandler);
return;
}
}
}
}
void DEV_CheckItemChanges(Device *device, const Event &event)
{
DevicePrivate *d = device->d;
std::vector<Resource*> subDevices;
if (event.what() == REventAwake || event.what() == REventPoll)
{
subDevices = device->subDevices();
}
else
{
auto *sub = DEV_GetSubDevice(device, event.resource(), event.id());
if (sub)
{
subDevices.push_back(sub);
}
}
int apsEnqueued = 0;
for (auto *sub : subDevices)
{
if (sub && !sub->stateChanges().empty())
{
auto *item = sub->item(event.what());
for (auto &change : sub->stateChanges())
{
if (item)
{
change.verifyItemChange(item);
}
if (device->reachable() && apsEnqueued == 0 && change.tick(d->deviceKey, sub, d->apsCtrl) == 1)
{
apsEnqueued++;
}
}
sub->cleanupStateChanges();
}
}
}
*/
void DEV_NodeDescriptorStateHandler(Device *device, const Event &event)
{
DevicePrivate *d = device->d;
if (event.what() == REventStateEnter)
{
if (!device->node()->nodeDescriptor().isNull())
{
DBG_Printf(DBG_DEV, "DEV ZDP node descriptor verified: " FMT_MAC "\n", FMT_MAC_CAST(device->key()));
d->maxResponseTime = d->hasRxOnWhenIdle() ? RxOnWhenIdleResponseTime
: RxOffWhenIdleResponseTime;
bool isSleeper = !d->hasRxOnWhenIdle();
ResourceItem *capSleeper = device->item(RCapSleeper);
if (!capSleeper->lastSet().isValid() || capSleeper->toBool() != isSleeper)
{
capSleeper->setValue(isSleeper);
}
d->setState(DEV_ActiveEndpointsStateHandler);
}
else if (!device->reachable())
{
d->setState(DEV_InitStateHandler);
}
else
{
d->zdpResult = ZDP_NodeDescriptorReq(d->node->address(), d->apsCtrl);
if (d->zdpResult.isEnqueued)
{
d->startStateTimer(MaxConfirmTimeout, StateLevel0);
}
else
{
d->setState(DEV_InitStateHandler);
}
}
}
else if (event.what() == REventStateLeave)
{
d->stopStateTimer(StateLevel0);
}
else if (event.what() == REventApsConfirm)
{
if (d->zdpResult.apsReqId == EventApsConfirmId(event))
{
if (EventApsConfirmStatus(event) == deCONZ::ApsSuccessStatus)
{
d->stopStateTimer(StateLevel0);
d->startStateTimer(d->maxResponseTime, StateLevel0);
}
else
{
d->setState(DEV_InitStateHandler);
}
}
}
else if (event.what() == REventNodeDescriptor)
{
d->setState(DEV_InitStateHandler);
DEV_EnqueueEvent(device, REventAwake);
}
else if (event.what() == REventStateTimeout)
{
DBG_Printf(DBG_DEV, "DEV read ZDP node descriptor timeout: " FMT_MAC "\n", FMT_MAC_CAST(device->key()));
d->setState(DEV_InitStateHandler);
}
}
*/
void DEV_ActiveEndpointsStateHandler(Device *device, const Event &event)
{
DevicePrivate *d = device->d;
if (event.what() == REventStateEnter)
{
if (!device->node()->endpoints().empty() && !d->flags.needReadActiveEndpoints)
{
DBG_Printf(DBG_DEV, "DEV ZDP active endpoints verified: " FMT_MAC "\n", FMT_MAC_CAST(device->key()));
d->setState(DEV_SimpleDescriptorStateHandler);
}
else if (!device->reachable())
{
d->setState(DEV_InitStateHandler);
}
else
{
d->zdpResult = ZDP_ActiveEndpointsReq(d->node->address(), d->apsCtrl);
if (d->zdpResult.isEnqueued)
{
d->startStateTimer(MaxConfirmTimeout, StateLevel0);
}
else
{
d->setState(DEV_InitStateHandler);
}
}
}
else if (event.what() == REventStateLeave)
{
d->stopStateTimer(StateLevel0);
}
else if (event.what() == REventApsConfirm)
{
if (d->zdpResult.apsReqId == EventApsConfirmId(event))
{
if (EventApsConfirmStatus(event) == deCONZ::ApsSuccessStatus)
{
d->stopStateTimer(StateLevel0);
d->startStateTimer(d->maxResponseTime, StateLevel0);
}
else
{
d->setState(DEV_InitStateHandler);
}
}
}
else if (event.what() == REventActiveEndpoints)
{
d->flags.needReadActiveEndpoints = 0;
d->setState(DEV_InitStateHandler);
DEV_EnqueueEvent(device, REventAwake);
}
else if (event.what() == REventStateTimeout)
{
DBG_Printf(DBG_DEV, "DEV read ZDP active endpoints timeout: " FMT_MAC "\n", FMT_MAC_CAST(device->key()));
d->setState(DEV_InitStateHandler);
}
}
*/
void DEV_SimpleDescriptorStateHandler(Device *device, const Event &event)
{
DevicePrivate *d = device->d;
if (event.what() == REventStateEnter)
{
quint8 needFetchEp = 0x00;
if (d->flags.needReadSimpleDescriptors)
{
if (d->zdpNeedFetchEndpointIndex < device->node()->endpoints().size())
{
needFetchEp = device->node()->endpoints()[d->zdpNeedFetchEndpointIndex];
}
}
else
{
for (uint8_t ep : device->node()->endpoints())
{
bool ok = false;
for (size_t i = 0; i < device->node()->simpleDescriptors().size(); i++)
{
const deCONZ::SimpleDescriptor &sd = device->node()->simpleDescriptors()[i];
if (sd.endpoint() == ep && sd.deviceId() != 0xffff)
{
ok = true;
break;
}
}
if (!ok)
{
needFetchEp = ep;
break;
}
}
}
if (needFetchEp == 0x00)
{
DBG_Printf(DBG_DEV, "DEV ZDP simple descriptors verified: " FMT_MAC "\n", FMT_MAC_CAST(device->key()));
d->flags.needReadSimpleDescriptors = 0;
d->zdpNeedFetchEndpointIndex = 0xFF;
d->setState(DEV_BasicClusterStateHandler);
}
else if (!device->reachable())
{
d->setState(DEV_InitStateHandler);
}
else
{
d->zdpResult = ZDP_SimpleDescriptorReq(d->node->address(), needFetchEp, d->apsCtrl);
if (d->zdpResult.isEnqueued)
{
d->startStateTimer(MaxConfirmTimeout, StateLevel0);
}
else
{
d->setState(DEV_InitStateHandler);
}
}
}
else if (event.what() == REventStateLeave)
{
d->stopStateTimer(StateLevel0);
}
else if (event.what() == REventApsConfirm)
{
if (d->zdpResult.apsReqId == EventApsConfirmId(event))
{
if (EventApsConfirmStatus(event) == deCONZ::ApsSuccessStatus)
{
d->stopStateTimer(StateLevel0);
d->startStateTimer(d->maxResponseTime, StateLevel0);
}
else
{
d->setState(DEV_InitStateHandler);
}
}
}
else if (event.what() == REventSimpleDescriptor)
{
if (d->flags.needReadSimpleDescriptors)
{
if (d->zdpNeedFetchEndpointIndex < device->node()->endpoints().size())
{
d->zdpNeedFetchEndpointIndex += 1;
}
}
d->setState(DEV_InitStateHandler);
DEV_EnqueueEvent(device, REventAwake);
}
else if (event.what() == REventStateTimeout)
{
DBG_Printf(DBG_DEV, "DEV read ZDP simple descriptor timeout: " FMT_MAC "\n", FMT_MAC_CAST(device->key()));
d->setState(DEV_InitStateHandler);
}
}
*/
static const deCONZ::SimpleDescriptor *DEV_GetSimpleDescriptorForServerCluster(const Device *device, deCONZ::ZclClusterId_t clusterId)
{
for (const auto &sd : device->node()->simpleDescriptors())
{
const auto cluster = std::find_if(sd.inClusters().cbegin(), sd.inClusters().cend(), [clusterId](const deCONZ::ZclCluster &cl)
{
return cl.id_t() == clusterId;
});
if (cluster != sd.inClusters().cend())
{
return &sd;
}
}
return nullptr;
}
*/
bool DEV_FillItemFromSubdevices(Device *device, const char *itemSuffix, const std::vector<Resource*> &subDevices)
{
auto *ditem = device->item(itemSuffix);
Q_ASSERT(ditem);
if (ditem->lastSet().isValid())
{
return true;
}
for (const auto rsub : subDevices)
{
auto *sitem = rsub->item(itemSuffix);
if (sitem && sitem->lastSet().isValid())
{
if (ditem->setValue(sitem->toVariant()))
{
return true;
}
}
}
return false;
}
*/
bool DEV_FillItemFromBasicCluster(Device *device, const char *itemSuffix, deCONZ::ZclClusterId_t clusterId, deCONZ::ZclAttributeId_t attrId)
{
ResourceItem *ditem = device->item(itemSuffix);
if (!ditem || !device->node())
{
return false;
}
if (ditem->lastSet().isValid())
{
return true;
}
for (const auto &sd : device->node()->simpleDescriptors())
{
const auto cl = std::find_if(sd.inClusters().cbegin(), sd.inClusters().cend(),
[clusterId](const auto &x) { return x.id_t() == clusterId; });
if (cl == sd.inClusters().cend()) { continue; }
const auto at = std::find_if(cl->attributes().cbegin(), cl->attributes().cend(),
[attrId](const auto &x){ return x.id_t() == attrId; });
if (at == cl->attributes().cend()) { continue; }
const QVariant v = at->toVariant();
if (!v.isNull() && ditem->setValue(v))
{
return true;
}
}
return false;
}
This also configures generic read and parse handlers for an \p item if not already set.
*/
bool DEV_ZclRead(Device *device, ResourceItem *item, deCONZ::ZclClusterId_t clusterId, deCONZ::ZclAttributeId_t attrId)
{
Q_ASSERT(device);
Q_ASSERT(item);
DevicePrivate *d = device->d;
if (!device->reachable())
{
DBG_Printf(DBG_DEV, "DEV not reachable, skip read %s: " FMT_MAC "\n", item->descriptor().suffix, FMT_MAC_CAST(device->key()));
return false;
}
const auto *sd = DEV_GetSimpleDescriptorForServerCluster(device, clusterId);
if (!sd)
{
DBG_Printf(DBG_DEV, "DEV TODO cluster 0x%04X not found: " FMT_MAC "\n", static_cast<quint16>(clusterId), FMT_MAC_CAST(device->key()));
return false;
}
ZCL_Param param{};
param.valid = 1;
param.endpoint = sd->endpoint();
param.clusterId = static_cast<quint16>(clusterId);
param.attributes[0] = static_cast<quint16>(attrId);
param.attributeCount = 1;
const auto zclResult = ZCL_ReadAttributes(param, device->item(RAttrExtAddress)->toNumber(), device->item(RAttrNwkAddress)->toNumber(), d->apsCtrl);
d->readResult.isEnqueued = zclResult.isEnqueued;
d->readResult.apsReqId = zclResult.apsReqId;
d->readResult.sequenceNumber = zclResult.sequenceNumber;
return d->readResult.isEnqueued;
}
e.g. modelId, manufacturer name, application version, etc.
*/
void DEV_BasicClusterStateHandler(Device *device, const Event &event)
{
DevicePrivate *d = device->d;
if (event.what() == REventStateEnter)
{
struct _item {
const char *suffix;
deCONZ::ZclClusterId_t clusterId;
deCONZ::ZclAttributeId_t attrId;
};
const std::array<_item, 2> items = {
_item{ RAttrManufacturerName, 0x0000_clid, 0x0004_atid },
_item{ RAttrModelId, 0x0000_clid, 0x0005_atid }
};
size_t okCount = 0;
const auto &subDevices = device->subDevices();
for (const auto &it : items)
{
if (DEV_FillItemFromSubdevices(device, it.suffix, subDevices))
{
okCount++;
continue;
}
else if (DEV_FillItemFromBasicCluster(device, it.suffix, it.clusterId, it.attrId))
{
okCount++;
continue;
}
if (DEV_ZclRead(device, device->item(it.suffix), it.clusterId, it.attrId))
{
d->startStateTimer(MaxConfirmTimeout, StateLevel0);
return;
}
DBG_Printf(DBG_DEV, "DEV failed to read %s: " FMT_MAC "\n", it.suffix, FMT_MAC_CAST(device->key()));
break;
}
if (okCount != items.size())
{
d->setState(DEV_InitStateHandler);
}
else
{
DBG_Printf(DBG_DEV, "DEV modelId: %s, " FMT_MAC "\n", qPrintable(device->item(RAttrModelId)->toString()), FMT_MAC_CAST(device->key()));
d->setState(DEV_GetDeviceDescriptionHandler);
}
}
else if (event.what() == REventStateLeave)
{
d->stopStateTimer(StateLevel0);
}
else if (event.what() == REventApsConfirm)
{
if (d->readResult.apsReqId == EventApsConfirmId(event))
{
if (EventApsConfirmStatus(event) == deCONZ::ApsSuccessStatus)
{
d->stopStateTimer(StateLevel0);
d->startStateTimer(d->maxResponseTime, StateLevel0);
}
else
{
d->setState(DEV_InitStateHandler);
}
}
}
else if (event.what() == RAttrManufacturerName || event.what() == RAttrModelId)
{
DBG_Printf(DBG_DEV, "DEV received %s: " FMT_MAC "\n", event.what(), FMT_MAC_CAST(device->key()));
d->setState(DEV_InitStateHandler);
DEV_EnqueueEvent(device, REventAwake);
}
else if (event.what() == REventStateTimeout)
{
DBG_Printf(DBG_DEV, "DEV read basic cluster timeout: " FMT_MAC "\n", FMT_MAC_CAST(device->key()));
d->setState(DEV_InitStateHandler);
}
}
*/
void DEV_PublishToCore(Device *device)
{
struct CoreItem
{
const char *suffix;
const char *mapped;
};
std::array<CoreItem, 4> coreItems = {
{
{ RAttrName, "name" },
{ RAttrModelId, "modelid" },
{ RAttrManufacturerName, "vendor" },
{ RAttrSwVersion, "version" }
}
};
const auto subDevices = device->subDevices();
if (!subDevices.empty())
{
for (const CoreItem &i : coreItems)
{
const auto *item = subDevices.front()->item(i.suffix);
if (item && !item->toString().isEmpty())
{
DEV_ForwardNodeChange(device, QLatin1String(i.mapped), item->toString());
}
}
}
}
In that case the device is initialised (or updated) based on the JSON description.
The actual processing is delegated to \c DeviceDescriptions class. This is done async
so thousands of DDF files can be lazy loaded.
*/
void DEV_GetDeviceDescriptionHandler(Device *device, const Event &event)
{
DevicePrivate *d = device->d;
if (event.what() == REventStateEnter)
{
if (DEV_GetSimpleDescriptorForServerCluster(device, 0x0500_clid))
{
ResourceItem *item = device->addItem(DataTypeUInt16, RAttrZoneType);
if (item)
item->setIsPublic(false);
}
DEV_EnqueueEvent(device, REventDDFInitRequest);
}
else if (event.what() == REventDDFInitResponse)
{
DEV_PublishToCore(device);
if (event.num() == 1 || event.num() == 3)
{
d->managed = true;
d->flags.hasDdf = 1;
d->setState(DEV_IdleStateHandler);
if (event.num() == 1)
{
DEV_ForwardNodeChange(device, QLatin1String("hasddf"), QLatin1String("1"));
}
else if (event.num() == 3)
{
DEV_ForwardNodeChange(device, QLatin1String("hasddf"), QLatin1String("2"));
}
}
else
{
d->managed = false;
d->flags.hasDdf = 0;
d->setState(DEV_DeadStateHandler);
}
}
}
void DEV_CheckReachable(Device *device)
{
DevicePrivate *d = device->d;
bool devReachable = device->reachable();
for (Resource *r : d->subResources)
{
ResourceItem *item = r->item(RConfigReachable);
if (!item)
{
item = r->item(RStateReachable);
}
if (item && ((item->toBool() != devReachable) || !item->lastSet().isValid()))
{
r->setValue(item->descriptor().suffix, devReachable);
item->setNeedStore();
}
}
}
In parallel.
IdleState : Bindings | Polling | ItemChange
*/
void DEV_IdleStateHandler(Device *device, const Event &event)
{
DevicePrivate *d = device->d;
if (event.what() == REventStateEnter)
{
DEV_CheckReachable(device);
d->binding.bindingIter = 0;
d->setState(DEV_BindingHandler, STATE_LEVEL_BINDING);
d->setState(DEV_PollIdleStateHandler, STATE_LEVEL_POLL);
return;
}
else if (event.what() == REventStateLeave)
{
d->setState(nullptr, STATE_LEVEL_BINDING);
d->setState(nullptr, STATE_LEVEL_POLL);
d->stopStateTimer(STATE_LEVEL_BINDING);
d->stopStateTimer(STATE_LEVEL_POLL);
return;
}
else if (event.what() == REventApsConfirm)
{
if (EventApsConfirmStatus(event) == deCONZ::ApsSuccessStatus)
{
d->idleApsConfirmErrors = 0;
}
else
{
d->idleApsConfirmErrors++;
if (d->idleApsConfirmErrors > MaxIdleApsConfirmErrors && device->item(RStateReachable)->toBool())
{
d->idleApsConfirmErrors = 0;
DBG_Printf(DBG_DEV, "DEV Idle max APS confirm errors: " FMT_MAC "\n", FMT_MAC_CAST(device->key()));
device->item(RStateReachable)->setValue(false);
DEV_CheckReachable(device);
}
}
}
else if (event.what() != RAttrLastSeen && event.what() != REventPoll)
{
if (event.what() == RAttrSwVersion || event.what() == RAttrName)
{
DEV_PublishToCore(device);
}
}
if (!device->reachable() && !device->item(RCapSleeper)->toBool())
{
DBG_Printf(DBG_DEV, "DEV (NOT reachable) Idle event %s/" FMT_MAC "/%s\n", event.resource(), FMT_MAC_CAST(event.deviceKey()), event.what());
}
DEV_CheckItemChanges(device, event);
for (int i = StateLevel1; i < StateLevelMax; i++)
{
device->handleEvent(event, DEV_StateLevel(i));
}
}
https://github.com/dresden-elektronik/deconz-rest-plugin-v2/wiki/Device-Class#bindings-sub-state-machine
*/
void DEV_BindingHandler(Device *device, const Event &event)
{
DevicePrivate *d = device->d;
if (event.what() == REventStateEnter)
{
DBG_Printf(DBG_DEV, "DEV Binding enter %s/" FMT_MAC "\n", event.resource(), FMT_MAC_CAST(event.deviceKey()));
}
else if (event.what() == REventPoll || event.what() == REventAwake || event.what() == REventBindingTick)
{
if (d->binding.bindings.empty())
{
}
else if (DA_ApsUnconfirmedRequests() > 4)
{
}
else
{
d->binding.bindingIter = 0;
if (d->binding.mgmtBindSupported == MGMT_BIND_NOT_SUPPORTED)
{
d->setState(DEV_BindingTableVerifyHandler, STATE_LEVEL_BINDING);
}
else
{
d->setState(DEV_BindingTableReadHandler, STATE_LEVEL_BINDING);
}
}
}
else if (event.what() == REventBindingTable)
{
if (event.num() == deCONZ::ZdpSuccess)
{
d->binding.mgmtBindSupported = MGMT_BIND_SUPPORTED;
}
else if (event.num() == deCONZ::ZdpNotSupported)
{
d->binding.mgmtBindSupported = MGMT_BIND_NOT_SUPPORTED;
}
}
}
deCONZ::Binding DEV_ToCoreBinding(const DDF_Binding &bnd, quint64 srcAddress)
{
if (bnd.isUnicastBinding)
{
return deCONZ::Binding(srcAddress, bnd.dstExtAddress, bnd.clusterId, bnd.srcEndpoint, bnd.dstEndpoint);
}
else if (bnd.isGroupBinding)
{
return deCONZ::Binding(srcAddress, bnd.dstGroup, bnd.clusterId, bnd.srcEndpoint);
}
Q_ASSERT(0);
return {};
}
void DEV_BindingTableReadHandler(Device *device, const Event &event)
{
DevicePrivate *d = device->d;
if (event.what() == REventStateEnter)
{
DBG_Printf(DBG_DEV, "DEV Binding read bindings %s/" FMT_MAC "\n", event.resource(), FMT_MAC_CAST(event.deviceKey()));
d->binding.mgmtBindStartIndex = 0;
DEV_EnqueueEvent(device, REventBindingTick);
}
else if (event.what() == REventBindingTick)
{
d->zdpResult = ZDP_MgmtBindReq(d->binding.mgmtBindStartIndex, d->node->address(), d->apsCtrl);
if (d->zdpResult.isEnqueued)
{
d->startStateTimer(MaxConfirmTimeout, STATE_LEVEL_BINDING);
}
else
{
d->setState(DEV_BindingHandler, STATE_LEVEL_BINDING);
}
}
else if (event.what() == REventStateLeave)
{
d->stopStateTimer(STATE_LEVEL_BINDING);
}
else if (event.what() == REventApsConfirm)
{
if (d->zdpResult.apsReqId == EventApsConfirmId(event))
{
if (EventApsConfirmStatus(event) == deCONZ::ApsSuccessStatus)
{
d->stopStateTimer(STATE_LEVEL_BINDING);
d->startStateTimer(d->maxResponseTime, STATE_LEVEL_BINDING);
}
else
{
d->setState(DEV_BindingHandler, STATE_LEVEL_BINDING);
}
}
}
else if (event.what() == REventZdpMgmtBindResponse)
{
uint8_t buf[128];
if (event.hasData() && event.dataSize() >= 2 && event.dataSize() < sizeof(buf))
{
if (event.getData(buf, event.dataSize()))
{
const uint8_t seq = buf[0];
const uint8_t status = buf[1];
if (seq != d->zdpResult.zdpSeq)
{
return;
}
if (status == deCONZ::ZdpSuccess)
{
d->stopStateTimer(STATE_LEVEL_BINDING);
d->binding.mgmtBindSupported = MGMT_BIND_SUPPORTED;
uint8_t size = 0;
uint8_t index = 0;
uint8_t count = 0;
if (event.dataSize() >= 5)
{
size = buf[2];
index = buf[3];
count = buf[4];
}
if (size > index + count)
{
d->binding.mgmtBindStartIndex = index + count;
DEV_EnqueueEvent(device, REventBindingTick);
}
else
{
d->binding.bindingIter = 0;
d->setState(DEV_BindingTableVerifyHandler, STATE_LEVEL_BINDING);
}
}
else
{
if (status == deCONZ::ZdpNotSupported || status == deCONZ::ZdpNotPermitted)
{
d->binding.mgmtBindSupported = MGMT_BIND_NOT_SUPPORTED;
}
else
{
DBG_Printf(DBG_DEV, "DEV ZDP read binding table error: " FMT_MAC ", status: 0x%02X (TODO handle?)\n", FMT_MAC_CAST(device->key()), status);
}
d->setState(DEV_BindingHandler, STATE_LEVEL_BINDING);
}
}
}
}
else if (event.what() == REventStateTimeout)
{
DBG_Printf(DBG_DEV, "DEV ZDP read binding table timeout: " FMT_MAC "\n", FMT_MAC_CAST(device->key()));
d->setState(DEV_BindingHandler, STATE_LEVEL_BINDING);
}
}
void DEV_BindingTableVerifyHandler(Device *device, const Event &event)
{
DevicePrivate *d = device->d;
if (event.what() == REventStateEnter)
{
DBG_Printf(DBG_DEV, "DEV Binding verify bindings %s/" FMT_MAC "\n", event.resource(), FMT_MAC_CAST(event.deviceKey()));
DEV_EnqueueEvent(device, REventBindingTick);
}
else if (event.what() != REventBindingTick)
{
}
else if (d->binding.bindingIter >= d->binding.bindings.size())
{
d->binding.bindingCheckRound++;
d->setState(DEV_BindingRemoveHandler, STATE_LEVEL_BINDING);
}
else
{
auto &ddfBinding = d->binding.bindings[d->binding.bindingIter];
auto &tracker = d->binding.bindingTrackers[d->binding.bindingIter];
if (ddfBinding.dstExtAddress == 0 && ddfBinding.isUnicastBinding)
{
ddfBinding.dstExtAddress = d->apsCtrl->getParameter(deCONZ::ParamMacAddress);
DBG_Assert(ddfBinding.dstExtAddress != 0);
if (ddfBinding.dstExtAddress == 0)
{
d->setState(DEV_BindingHandler, STATE_LEVEL_BINDING);
return;
}
}
else if (ddfBinding.isGroupBinding)
{
bool ok = false;
for (const auto &sub : device->subDevices())
{
ResourceItem *configGroup = sub->item(RConfigGroup);
if (!configGroup)
{
continue;
}
const auto ls = configGroup->toString().split(',', SKIP_EMPTY_PARTS);
if (ddfBinding.configGroup >= ls.size())
{
ddfBinding.dstGroup = 0;
break;
}
uint group = ls[ddfBinding.configGroup].toUShort(&ok, 0);
if (ok && group != 0)
{
ddfBinding.dstGroup = group;
}
break;
}
if (!ok)
{
d->binding.bindingIter++;
DEV_EnqueueEvent(device, REventBindingTick);
return;
}
}
const auto &bindingTable = device->node()->bindingTable();
const auto bnd = DEV_ToCoreBinding(ddfBinding, d->deviceKey);
const auto i = std::find(bindingTable.const_begin(), bindingTable.const_end(), bnd);
bool needBind = false;
if (i == bindingTable.const_end())
{
needBind = true;
}
else
{
if (tracker.tBound < i->confirmedTimeRef())
{
tracker.tBound = i->confirmedTimeRef();
}
const auto now = deCONZ::steadyTimeRef();
const auto dt = isValid(tracker.tBound) ? (now - tracker.tBound).val / 1000 : -1;
if (i->dstAddressMode() == deCONZ::ApsExtAddress)
{
DBG_Printf(DBG_DEV, "DEV BND " FMT_MAC " cl: 0x%04X, dstAddrmode: %u, dst: " FMT_MAC ", dstEp: 0x%02X, dt: %d seconds\n",
FMT_MAC_CAST(i->srcAddress()), i->clusterId(), i->dstAddressMode(), FMT_MAC_CAST(i->dstAddress().ext()), i->dstEndpoint(), (int)dt);
}
else if (i->dstAddressMode() == deCONZ::ApsGroupAddress)
{
DBG_Printf(DBG_DEV, "DEV BND " FMT_MAC " cl: 0x%04X, dstAddrmode: %u, group: 0x%04X, dstEp: 0x%02X, dt: %d seconds\n",
FMT_MAC_CAST(i->srcAddress()), i->clusterId(), i->dstAddressMode(), i->dstAddress().group(), i->dstEndpoint(), (int)dt);
}
if (dt < 0 || dt > 1800)
{
needBind = true;
}
}
if (needBind)
{
d->setState(DEV_BindingCreateHandler, STATE_LEVEL_BINDING);
}
else if (i->dstAddressMode() == deCONZ::ApsExtAddress)
{
d->binding.configIter = 0;
d->binding.reportIter = 0;
d->setState(DEV_ReadReportConfigurationHandler, STATE_LEVEL_BINDING);
}
else if (i->dstAddressMode() == deCONZ::ApsGroupAddress)
{
d->binding.bindingIter++;
DEV_EnqueueEvent(device, REventBindingTick);
}
}
}
static void DEV_ProcessNextBinding(Device *device)
{
DevicePrivate *d = device->d;
d->binding.bindingIter++;
d->setState(DEV_BindingTableVerifyHandler, STATE_LEVEL_BINDING);
}
void DEV_BindingCreateHandler(Device *device, const Event &event)
{
DevicePrivate *d = device->d;
if (event.what() == REventStateEnter)
{
const auto &ddfBinding = d->binding.bindings[d->binding.bindingIter];
auto &tracker = d->binding.bindingTrackers[d->binding.bindingIter];
tracker.tBound = {};
const auto bnd = DEV_ToCoreBinding(ddfBinding, d->deviceKey);
d->zdpResult = ZDP_BindReq(bnd, d->apsCtrl);
if (d->zdpResult.isEnqueued)
{
d->startStateTimer(MaxConfirmTimeout, STATE_LEVEL_BINDING);
}
else
{
d->setState(DEV_BindingHandler, STATE_LEVEL_BINDING);
}
}
else if (event.what() == REventStateLeave)
{
d->stopStateTimer(STATE_LEVEL_BINDING);
}
else if (event.what() == REventApsConfirm)
{
if (d->zdpResult.apsReqId == EventApsConfirmId(event))
{
if (EventApsConfirmStatus(event) == deCONZ::ApsSuccessStatus)
{
d->stopStateTimer(STATE_LEVEL_BINDING);
d->startStateTimer(d->maxResponseTime, STATE_LEVEL_BINDING);
}
else
{
d->setState(DEV_BindingHandler, STATE_LEVEL_BINDING);
}
}
}
else if (event.what() == REventZdpResponse)
{
if (EventZdpResponseSequenceNumber(event) == d->zdpResult.zdpSeq)
{
if (EventZdpResponseStatus(event) == deCONZ::ZdpSuccess)
{
BindingTracker &tracker = d->binding.bindingTrackers[d->binding.bindingIter];
tracker.tBound = deCONZ::steadyTimeRef();
d->setState(DEV_BindingTableVerifyHandler, STATE_LEVEL_BINDING);
}
else
{
d->setState(DEV_BindingHandler, STATE_LEVEL_BINDING);
}
}
}
else if (event.what() == REventStateTimeout)
{
DBG_Printf(DBG_DEV, "DEV ZDP create binding timeout: " FMT_MAC "\n", FMT_MAC_CAST(device->key()));
d->setState(DEV_BindingHandler, STATE_LEVEL_BINDING);
}
}
void DEV_BindingRemoveHandler(Device *device, const Event &event)
{
DevicePrivate *d = device->d;
if (event.what() == REventStateEnter)
{
const auto &bindingTable = device->node()->bindingTable();
auto i = bindingTable.const_begin();
auto end = bindingTable.const_end();
for (; i != end; ++i)
{
if (i->dstAddressMode() == deCONZ::ApsGroupAddress)
{
bool hasDdfBinding = false;
bool hasDdfGroup = false;
for (const auto &ddfBinding : d->binding.bindings)
{
if (ddfBinding.isGroupBinding &&
i->clusterId() == ddfBinding.clusterId &&
i->srcEndpoint() == ddfBinding.srcEndpoint)
{
hasDdfBinding = true;
if (i->dstAddress().group() == ddfBinding.dstGroup)
{
hasDdfGroup = true;
break;
}
}
}
if (hasDdfBinding && !hasDdfGroup)
{
break;
}
}
else if (i->dstAddressMode() == deCONZ::ApsExtAddress)
{
const deCONZ::Node *dstNode = DEV_GetCoreNode(i->dstAddress().ext());
if (!dstNode)
{
DBG_Printf(DBG_DEV, "DEV ZDP remove binding to non existing node: " FMT_MAC "\n", FMT_MAC_CAST(i->dstAddress().ext()));
break;
}
}
}
if (i == bindingTable.const_end())
{
d->setState(DEV_BindingIdleHandler, STATE_LEVEL_BINDING);
return;
}
d->zdpResult = ZDP_UnbindReq(*i, d->apsCtrl);
if (d->zdpResult.isEnqueued)
{
d->startStateTimer(MaxConfirmTimeout, STATE_LEVEL_BINDING);
}
else
{
d->setState(DEV_BindingHandler, STATE_LEVEL_BINDING);
}
}
else if (event.what() == REventStateLeave)
{
d->stopStateTimer(STATE_LEVEL_BINDING);
}
else if (event.what() == REventApsConfirm)
{
if (d->zdpResult.apsReqId == EventApsConfirmId(event))
{
if (EventApsConfirmStatus(event) == deCONZ::ApsSuccessStatus)
{
d->stopStateTimer(STATE_LEVEL_BINDING);
d->startStateTimer(d->maxResponseTime, STATE_LEVEL_BINDING);
}
else
{
d->setState(DEV_BindingHandler, STATE_LEVEL_BINDING);
}
}
}
else if (event.what() == REventZdpResponse)
{
if (EventZdpResponseSequenceNumber(event) == d->zdpResult.zdpSeq)
{
d->setState(DEV_BindingHandler, STATE_LEVEL_BINDING);
DEV_EnqueueEvent(device, REventBindingTick);
}
}
else if (event.what() == REventStateTimeout)
{
DBG_Printf(DBG_DEV, "DEV ZDP remove binding timeout: " FMT_MAC "\n", FMT_MAC_CAST(device->key()));
d->setState(DEV_BindingHandler, STATE_LEVEL_BINDING);
}
}
static bool reportingConfigurationValid(const Device *device, const Event &event)
{
DevicePrivate *d = device->d;
ZCL_ReadReportConfigurationRsp rsp{};
if (!event.getData(&rsp, sizeof(rsp)))
{
return false;
}
const auto &bnd = d->binding.bindings[d->binding.bindingIter];
size_t okCount = 0;
for (size_t i = 0; i < rsp.recordCount; i++)
{
const auto &record = rsp.records[i];
for (const auto &report : bnd.reporting)
{
bool found = false;
for (auto &rec : d->binding.readReportParam.records)
{
if (rec.attributeId == report.attributeId && rec.direction == report.direction)
{
found = true;
break;
}
}
if (!found || record.status != deCONZ::ZclSuccessStatus) { continue; }
if (report.manufacturerCode != rsp.manufacturerCode) { continue; }
if (report.attributeId != record.attributeId) { continue; }
if (report.minInterval != record.minInterval) { continue; }
if (report.maxInterval != record.maxInterval) { continue; }
if (report.reportableChange != record.reportableChange) { continue; }
okCount++;
DBG_Printf(DBG_DEV, "DEV ZCL report configuration cl: 0x%04X, at: 0x%04X OK " FMT_MAC "\n", rsp.clusterId, record.attributeId, FMT_MAC_CAST(device->key()));
}
}
if (okCount == d->binding.readReportParam.records.size())
{
DBG_Printf(DBG_DEV, "DEV ZCL report configuration cl: 0x%04X, mfcode: 0x%04X verified " FMT_MAC "\n", rsp.clusterId, rsp.manufacturerCode, FMT_MAC_CAST(device->key()));
return true;
}
else
{
DBG_Printf(DBG_DEV, "DEV ZCL report configuration cl: 0x%04X, mfcode: 0x%04X needs update " FMT_MAC "\n", rsp.clusterId, rsp.manufacturerCode, FMT_MAC_CAST(device->key()));
return false;
}
}
void DEV_ReadReportConfigurationHandler(Device *device, const Event &event)
{
DevicePrivate *d = device->d;
if (event.what() == REventStateEnter)
{
Q_ASSERT(d->binding.bindingIter < d->binding.bindings.size());
const auto &bnd = d->binding.bindings[d->binding.bindingIter];
if (bnd.reporting.empty())
{
DEV_ProcessNextBinding(device);
return;
}
ZCL_ReadReportConfigurationParam ¶m = d->binding.readReportParam;
param = {};
param.extAddress = device->node()->address().ext();
param.nwkAddress = device->node()->address().nwk();
param.clusterId = bnd.clusterId;
param.manufacturerCode = 0;
param.endpoint = bnd.srcEndpoint;
auto tnow = deCONZ::steadyTimeRef();
for (; d->binding.reportIter < bnd.reporting.size(); d->binding.reportIter++)
{
const DDF_ZclReport &report = bnd.reporting[d->binding.reportIter];
ReportTracker &tracker = DEV_GetOrCreateReportTracker(device, bnd.clusterId, report.attributeId, bnd.srcEndpoint);
if (d->binding.bindingCheckRound == 0)
{
}
else if ((tnow - tracker.lastConfigureCheck) < deCONZ::TimeSeconds{3600})
{
DBG_Printf(DBG_DEV, "DEV " FMT_MAC " skip read ZCL report config for 0x%04X / 0x%04X\n", FMT_MAC_CAST(d->deviceKey), bnd.clusterId, report.attributeId);
continue;
}
if (param.records.empty())
{
param.manufacturerCode = report.manufacturerCode;
}
else if (param.manufacturerCode != report.manufacturerCode)
{
break;
}
tracker.lastConfigureCheck.ref = MarkZclConfigureBusy;
ZCL_ReadReportConfigurationParam::Record record{};
record.attributeId = report.attributeId;
record.direction = report.direction;
param.records.push_back(record);
if (param.records.size() == ZCL_ReadReportConfigurationParam::MaxRecords)
{
break;
}
}
if (param.records.empty())
{
DEV_ProcessNextBinding(device);
return;
}
d->binding.zclResult = ZCL_ReadReportConfiguration(param, d->apsCtrl);
if (d->binding.zclResult.isEnqueued)
{
d->startStateTimer(MaxConfirmTimeout, STATE_LEVEL_BINDING);
}
else
{
d->setState(DEV_BindingHandler, STATE_LEVEL_BINDING);
}
}
else if (event.what() == REventStateLeave)
{
d->stopStateTimer(STATE_LEVEL_BINDING);
}
else if (event.what() == REventApsConfirm)
{
if (d->binding.zclResult.apsReqId == EventApsConfirmId(event))
{
if (EventApsConfirmStatus(event) == deCONZ::ApsSuccessStatus)
{
d->stopStateTimer(STATE_LEVEL_BINDING);
d->startStateTimer(d->maxResponseTime, STATE_LEVEL_BINDING);
}
else
{
d->setState(DEV_BindingHandler, STATE_LEVEL_BINDING);
}
}
}
else if (event.what() == REventZclReadReportConfigResponse)
{
if (reportingConfigurationValid(device, event))
{
const auto tnow = deCONZ::steadyTimeRef();
for (ReportTracker &tracker : d->binding.reportTrackers)
{
if (tracker.lastConfigureCheck.ref == MarkZclConfigureBusy)
{
tracker.lastConfigureCheck = tnow;
}
}
auto &bnd = d->binding.bindings[d->binding.bindingIter];
if (d->binding.reportIter < bnd.reporting.size())
{
d->setState(DEV_ReadNextReportConfigurationHandler, STATE_LEVEL_BINDING);
}
else
{
DEV_ProcessNextBinding(device);
}
}
else
{
d->setState(DEV_ConfigureReportingHandler, STATE_LEVEL_BINDING);
}
}
else if (event.what() == REventStateTimeout)
{
DBG_Printf(DBG_DEV, "DEV ZCL read report configuration timeout: " FMT_MAC "\n", FMT_MAC_CAST(device->key()));
d->setState(DEV_BindingHandler, STATE_LEVEL_BINDING);
}
}
void DEV_ReadNextReportConfigurationHandler(Device *device, const Event &event)
{
DevicePrivate *d = device->d;
if (event.what() == REventStateEnter)
{
d->setState(DEV_ReadReportConfigurationHandler, STATE_LEVEL_BINDING);
}
}
void DEV_ConfigureNextReportConfigurationHandler(Device *device, const Event &event)
{
DevicePrivate *d = device->d;
if (event.what() == REventStateEnter)
{
d->setState(DEV_ConfigureReportingHandler, STATE_LEVEL_BINDING);
}
}
void DEV_ConfigureReportingHandler(Device *device, const Event &event)
{
DevicePrivate *d = device->d;
if (event.what() == REventStateEnter)
{
const auto &bnd = d->binding.bindings[d->binding.bindingIter];
Q_ASSERT(!bnd.reporting.empty());
ZCL_ConfigureReportingParam param{};
param.extAddress = device->node()->address().ext();
param.nwkAddress = device->node()->address().nwk();
param.clusterId = bnd.clusterId;
param.manufacturerCode = d->binding.readReportParam.manufacturerCode;
param.endpoint = bnd.srcEndpoint;
for (size_t i = d->binding.configIter; i < d->binding.reportIter && i < bnd.reporting.size(); i++)
{
const DDF_ZclReport &report = bnd.reporting[i];
d->binding.configIter++;
if (report.manufacturerCode != param.manufacturerCode)
{
continue;
}
ZCL_ConfigureReportingParam::Record record{};
record.attributeId = report.attributeId;
record.direction = report.direction;
record.dataType = report.dataType;
record.minInterval = report.minInterval;
record.maxInterval = report.maxInterval;
record.reportableChange = report.reportableChange;
record.timeout = 0;
param.records.push_back(record);
if (param.records.size() == ZCL_ConfigureReportingParam::MaxRecords)
{
break;
}
}
d->binding.zclResult.isEnqueued = false;
if (!param.records.empty())
{
d->binding.zclResult = ZCL_ConfigureReporting(param, d->apsCtrl);
}
if (d->binding.zclResult.isEnqueued)
{
d->startStateTimer(MaxConfirmTimeout, STATE_LEVEL_BINDING);
}
else
{
d->setState(DEV_BindingHandler, STATE_LEVEL_BINDING);
}
}
else if (event.what() == REventStateLeave)
{
d->stopStateTimer(STATE_LEVEL_BINDING);
}
else if (event.what() == REventApsConfirm)
{
if (d->binding.zclResult.apsReqId == EventApsConfirmId(event))
{
if (EventApsConfirmStatus(event) == deCONZ::ApsSuccessStatus)
{
d->stopStateTimer(STATE_LEVEL_BINDING);
d->startStateTimer(d->maxResponseTime, STATE_LEVEL_BINDING);
}
else
{
d->setState(DEV_BindingHandler, STATE_LEVEL_BINDING);
}
}
}
else if (event.what() == REventZclResponse)
{
if (d->binding.zclResult.sequenceNumber == EventZclSequenceNumber(event))
{
DBG_Printf(DBG_DEV, "DEV configure reporting %s/" FMT_MAC " ZCL response seq: %u, status: 0x%02X\n",
event.resource(), FMT_MAC_CAST(event.deviceKey()), d->binding.zclResult.sequenceNumber, EventZclStatus(event));
if (EventZclStatus(event) == deCONZ::ZclSuccessStatus)
{
auto &bnd = d->binding.bindings[d->binding.bindingIter];
if (d->binding.configIter < d->binding.reportIter)
{
d->setState(DEV_ConfigureNextReportConfigurationHandler, STATE_LEVEL_BINDING);
}
else if (d->binding.reportIter < bnd.reporting.size())
{
d->setState(DEV_ReadNextReportConfigurationHandler, STATE_LEVEL_BINDING);
}
else
{
DEV_ProcessNextBinding(device);
}
}
else
{
d->setState(DEV_BindingHandler, STATE_LEVEL_BINDING);
}
}
}
else if (event.what() == REventStateTimeout)
{
DBG_Printf(DBG_DEV, "DEV ZCL configure reporting timeout: " FMT_MAC "\n", FMT_MAC_CAST(device->key()));
d->setState(DEV_BindingHandler, STATE_LEVEL_BINDING);
}
}
void DEV_BindingIdleHandler(Device *device, const Event &event)
{
DevicePrivate *d = device->d;
if (event.what() == REventStateEnter)
{
DBG_Printf(DBG_DEV, "DEV Binding idle enter %s/" FMT_MAC "\n", event.resource(), FMT_MAC_CAST(event.deviceKey()));
d->startStateTimer(BindingAutoCheckInterval, STATE_LEVEL_BINDING);
}
else if (event.what() == REventStateLeave)
{
d->stopStateTimer(STATE_LEVEL_BINDING);
}
else if (event.what() == REventStateTimeout)
{
d->setState(DEV_BindingHandler, STATE_LEVEL_BINDING);
}
}
static ReportTracker &DEV_GetOrCreateReportTracker(Device *device, uint16_t clusterId, uint16_t attrId, uint8_t endpoint)
{
DevicePrivate *d = device->d;
auto i = std::find_if(d->binding.reportTrackers.begin(), d->binding.reportTrackers.end(), [&](ReportTracker &tracker) {
return tracker.endpoint == endpoint &&
tracker.clusterId == clusterId &&
tracker.attributeId == attrId;
});
if (i != d->binding.reportTrackers.end())
{
return *i;
}
ReportTracker tracker;
tracker.endpoint = endpoint;
tracker.clusterId = clusterId;
tracker.attributeId = attrId;
d->binding.reportTrackers.push_back(tracker);
return d->binding.reportTrackers.back();
}
static void DEV_UpdateReportTracker(Device *device, const ResourceItem *item)
{
if (!isValid(item->lastZclReport()))
{
return;
}
const ZCL_Param &zclParam = item->zclParam();
if (!isValid(zclParam) || zclParam.attributeCount == 0)
{
return;
}
Q_ASSERT(zclParam.attributeCount < zclParam.attributes.size());
for (size_t i = 0; i < zclParam.attributeCount && i < zclParam.attributes.size(); i++)
{
ReportTracker &tracker = DEV_GetOrCreateReportTracker(device, zclParam.clusterId, zclParam.attributes[i], zclParam.endpoint);
tracker.lastReport = item->lastZclReport();
}
}
The returned vector is reversed to use std::vector::pop_back() when processing the queue.
*/
std::vector<DEV_PollItem> DEV_GetPollItems(Device *device)
{
DevicePrivate *d = device->d;
std::vector<DEV_PollItem> result;
const auto now = QDateTime::currentDateTime();
const auto tnow = deCONZ::steadyTimeRef();
for (const auto *r : device->subDevices())
{
for (int i = 0; i < r->itemCount(); i++)
{
const auto *item = r->itemForIndex(size_t(i));
if (item->zclUnsupportedAttribute())
{
continue;
}
DEV_UpdateReportTracker(device, item);
const auto &ddfItem = DDF_GetItem(item);
if (ddfItem.readParameters.isNull())
{
continue;
}
int64_t dt = -1;
if (item->refreshInterval().val == 0)
{
}
else
{
if (isValid(item->lastZclReport()))
{
dt = (tnow - item->lastZclReport()).val / 1000;
if (dt < item->refreshInterval().val)
{
continue;
}
}
if (item->lastSet().isValid() && (item->valueSource() == ResourceItem::SourceDevice || item->valueSource() == ResourceItem::SourceUnknown))
{
const auto dt2 = item->lastSet().secsTo(now);
if (dt2 < item->refreshInterval().val)
{
continue;
}
dt = dt2;
}
}
const auto m = ddfItem.readParameters.toMap();
if (m.empty())
{
continue;
}
if (m.contains(QLatin1String("fn")) && m.value(QLatin1String("fn")).toString() == QLatin1String("none"))
{
continue;
}
DBG_Printf(DBG_DEV, "DEV " FMT_MAC " read %s, dt %d sec\n", FMT_MAC_CAST(d->deviceKey), item->descriptor().suffix, int(dt));
result.emplace_back(DEV_PollItem{r, item, ddfItem.readParameters});
}
}
std::reverse(result.begin(), result.end());
return result;
}
It collects all poll worthy items in a queue and moves to the PollNext state.
*/
void DEV_PollIdleStateHandler(Device *device, const Event &event)
{
DevicePrivate *d = device->d;
if (event.what() == REventStateEnter)
{
DBG_Printf(DBG_DEV, "DEV Poll Idle enter %s/" FMT_MAC "\n", event.resource(), FMT_MAC_CAST(event.deviceKey()));
}
else if (event.what() == REventPoll || event.what() == REventAwake)
{
if (DA_ApsUnconfirmedRequests() > 4)
{
return;
}
if (device->node())
{
const auto &addr = device->node()->address();
if (addr.hasNwk() && addr.nwk() != device->item(RAttrNwkAddress)->toNumber())
{
device->item(RAttrNwkAddress)->setValue(addr.nwk());
}
}
if (d->flags.needZDPMaintenanceOnce)
{
static int randomDelay = 0;
randomDelay++;
if (randomDelay > (d->deviceKey & 0xFF))
{
randomDelay = 0;
d->flags.needZDPMaintenanceOnce = 0;
if (!device->item(RCapSleeper)->toBool() && device->reachable())
{
d->flags.needReadActiveEndpoints = 1;
d->flags.needReadSimpleDescriptors = 1;
d->zdpNeedFetchEndpointIndex = 0;
DEV_EnqueueEvent(device, REventZdpReload);
return;
}
}
}
d->pollItems = DEV_GetPollItems(device);
if (!d->pollItems.empty())
{
d->setState(DEV_PollNextStateHandler, STATE_LEVEL_POLL);
return;
}
else
{
if (event.what() == REventPoll)
{
DBG_Printf(DBG_DEV, "DEV Poll Idle nothing to poll %s/" FMT_MAC "\n", event.resource(), FMT_MAC_CAST(event.deviceKey()));
DEV_EnqueueEvent(device, REventPollDone);
}
}
}
}
If no more items are in the queue it moves back to PollIdle state.
*/
void DEV_PollNextStateHandler(Device *device, const Event &event)
{
DevicePrivate *d = device->d;
if (event.what() == REventStateEnter || event.what() == REventStateTimeout)
{
Q_ASSERT(event.num() == STATE_LEVEL_POLL);
if (!device->reachable())
{
d->pollItems.clear();
}
if (d->pollItems.empty())
{
d->setState(DEV_PollIdleStateHandler, STATE_LEVEL_POLL);
DEV_EnqueueEvent(device, REventPollDone);
return;
}
auto &poll = d->pollItems.back();
const auto readFunction = DA_GetReadFunction(poll.readParameters);
d->readResult = { };
if (readFunction)
{
d->readResult = readFunction(poll.resource, poll.item, d->apsCtrl, poll.readParameters);
}
else
{
DBG_Printf(DBG_DEV, "DEV Poll Next no read function for item: %s / " FMT_MAC "\n", poll.item->descriptor().suffix, FMT_MAC_CAST(device->key()));
d->pollItems.pop_back();
d->startStateTimer(5, STATE_LEVEL_POLL);
return;
}
if (d->readResult.isEnqueued)
{
d->setState(DEV_PollBusyStateHandler, STATE_LEVEL_POLL);
}
else
{
poll.retry++;
DBG_Printf(DBG_DEV, "DEV Poll Next failed to enqueue read item: %s / " FMT_MAC "\n", poll.item->descriptor().suffix, FMT_MAC_CAST(device->key()));
if (poll.retry >= MaxPollItemRetries)
{
d->pollItems.pop_back();
}
d->startStateTimer(d->maxResponseTime, STATE_LEVEL_POLL);
}
}
else if (event.what() == REventStateLeave)
{
d->stopStateTimer(STATE_LEVEL_POLL);
}
}
static void checkPollItemRetry(std::vector<DEV_PollItem> &pollItems)
{
if (!pollItems.empty())
{
auto &pollItem = pollItems.back();
pollItem.retry++;
if (pollItem.retry >= MaxPollItemRetries)
{
pollItems.pop_back();
}
}
}
In any case it moves back to PollNext state.
If the request is successful the DEV_PollItem will be removed from the queue.
*/
void DEV_PollBusyStateHandler(Device *device, const Event &event)
{
DevicePrivate *d = device->d;
if (event.what() == REventStateEnter)
{
d->startStateTimer(MaxConfirmTimeout, STATE_LEVEL_POLL);
}
else if (event.what() == REventStateLeave)
{
d->stopStateTimer(STATE_LEVEL_POLL);
}
else if (event.what() == REventApsConfirm && EventApsConfirmId(event) == d->readResult.apsReqId)
{
DBG_Printf(DBG_DEV, "DEV Poll Busy %s/" FMT_MAC " APS-DATA.confirm id: %u, ZCL seq: %u, status: 0x%02X\n",
event.resource(), FMT_MAC_CAST(event.deviceKey()), d->readResult.apsReqId, d->readResult.sequenceNumber, EventApsConfirmStatus(event));
if (EventApsConfirmStatus(event) == deCONZ::ApsSuccessStatus)
{
d->idleApsConfirmErrors = 0;
d->stopStateTimer(StateLevel0);
d->startStateTimer(d->maxResponseTime, STATE_LEVEL_POLL);
}
else
{
checkPollItemRetry(d->pollItems);
d->setState(DEV_PollNextStateHandler, STATE_LEVEL_POLL);
}
}
else if (event.what() == REventZclResponse)
{
if (d->readResult.clusterId != EventZclClusterId(event))
{ }
else if (d->readResult.sequenceNumber == EventZclSequenceNumber(event) || d->readResult.ignoreResponseSequenceNumber)
{
uint8_t status = EventZclStatus(event);
DBG_Printf(DBG_DEV, "DEV Poll Busy %s/" FMT_MAC " ZCL response seq: %u, status: 0x%02X, cluster: 0x%04X\n",
event.resource(), FMT_MAC_CAST(event.deviceKey()), d->readResult.sequenceNumber, status, d->readResult.clusterId);
DBG_Assert(!d->pollItems.empty());
if (!d->pollItems.empty())
{
if (status == deCONZ::ZclUnsupportedAttributeStatus)
{
const auto &pi = d->pollItems.back();
Resource *r = DEV_GetResource(pi.resource->handle());
ResourceItem *item = r ? r->item(pi.item->descriptor().suffix) : nullptr;
if (item)
{
item->setZclUnsupportedAttribute();
}
}
d->pollItems.pop_back();
}
d->setState(DEV_PollNextStateHandler, STATE_LEVEL_POLL);
}
}
else if (event.what() == REventStateTimeout)
{
DBG_Printf(DBG_DEV, "DEV Poll Busy %s/" FMT_MAC " timeout seq: %u, cluster: 0x%04X\n",
event.resource(), FMT_MAC_CAST(event.deviceKey()), d->readResult.sequenceNumber, d->readResult.clusterId);
checkPollItemRetry(d->pollItems);
d->setState(DEV_PollNextStateHandler, STATE_LEVEL_POLL);
}
}
*/
void DEV_DeadStateHandler(Device *device, const Event &event)
{
if (event.what() == REventStateEnter)
{
DBG_Printf(DBG_DEV, "DEV enter passive state " FMT_MAC "\n", FMT_MAC_CAST(event.deviceKey()));
}
else if (event.what() == REventStateLeave)
{
}
else
{
DevicePrivate *d = device->d;
if (device->managed())
{
d->setState(DEV_InitStateHandler);
}
else
{
if (event.what() == REventPoll || event.what() == REventAwake)
{
if (device->node())
{
const deCONZ::Address &a = device->node()->address();
if (a.hasExt())
{
ResourceItem *ext = device->item(RAttrExtAddress);
if (!ext->lastSet().isValid() || ext->toNumber() != a.ext())
{
ext->setValue(a.ext());
}
}
ResourceItem *nwk = device->item(RAttrNwkAddress);
if (a.hasNwk())
{
nwk->setIsPublic(true);
if (!nwk->lastSet().isValid() || nwk->toNumber() != a.nwk())
{
nwk->setValue(a.nwk());
}
}
else if (!nwk->lastSet().isValid())
{
nwk->setIsPublic(false);
}
}
extern void DEV_PollLegacy(Device *device);
if (d->node && d->node->isCoordinator())
{
return;
}
DEV_PollLegacy(device);
}
}
}
}
void DEV_ZgpStateHandler(Device *device, const Event &event)
{
if (event.what() == REventStateEnter)
{
DBG_Printf(DBG_DEV, "DEV enter ZGP passive state " FMT_MAC "\n", FMT_MAC_CAST(event.deviceKey()));
ResourceItem *item;
item = device->item(RAttrNwkAddress);
if (item)
item->setIsPublic(false);
item = device->item(RCapSleeper);
if (item)
item->setValue(true);
}
else if (event.what() == REventStateLeave)
{
}
}
Device::Device(DeviceKey key, deCONZ::ApsController *apsCtrl, QObject *parent) :
QObject(parent),
Resource(RDevices),
d(new DevicePrivate)
{
Q_ASSERT(parent);
d->q = this;
d->apsCtrl = apsCtrl;
d->deviceKey = key;
d->flags.initialRun = 1;
d->flags.hasDdf = 0;
d->flags.needZDPMaintenanceOnce = 1;
d->flags.needReadActiveEndpoints = 0;
d->flags.needReadSimpleDescriptors = 0;
addItem(DataTypeBool, RStateReachable);
addItem(DataTypeBool, RCapSleeper);
addItem(DataTypeUInt64, RAttrExtAddress)->setIsPublic(false);
addItem(DataTypeUInt16, RAttrNwkAddress);
addItem(DataTypeString, RAttrUniqueId)->setValue(generateUniqueId(key, 0, 0));
addItem(DataTypeString, RAttrManufacturerName);
addItem(DataTypeString, RAttrModelId);
addItem(DataTypeString, RAttrDdfPolicy);
addItem(DataTypeString, RAttrDdfHash);
addItem(DataTypeUInt32, RAttrOtaVersion)->setIsPublic(false);
QTimer::singleShot(0, this, [this]()
{
d->setState(DEV_InitStateHandler);
});
}
Device::~Device()
{
for (auto *r : subDevices())
{
r->setParentResource(nullptr);
}
Q_ASSERT(d);
delete d;
d = nullptr;
}
void Device::setDeviceId(int id)
{
if (id >= 0)
{
d->deviceId = id;
}
}
void Device::setCreationTime(int64_t creationTime)
{
if (0 < creationTime)
{
d->creationTime = creationTime;
}
}
int64_t Device::creationTime() const
{
return d->creationTime;
}
int Device::deviceId() const
{
return d->deviceId;
}
void Device::addSubDevice(Resource *sub)
{
Q_ASSERT(sub);
Q_ASSERT(sub->item(RAttrUniqueId));
sub->setParentResource(this);
Q_ASSERT(isValid(sub->handle()));
for (auto &hnd : d->subResourceHandles)
{
if (hnd == sub->handle())
{
hnd = sub->handle();
return;
}
}
for (auto &hnd : d->subResourceHandles)
{
if (!isValid(hnd))
{
hnd = sub->handle();
DEV_CheckReachable(this);
std::sort(d->subResourceHandles.begin(), d->subResourceHandles.end(), [](const auto &a, const auto &b)
{
if (a.order == 0) { return false; }
return a.order < b.order;
});
return;
}
}
Q_ASSERT(0);
}
DeviceKey Device::key() const
{
return d->deviceKey;
}
const deCONZ::Node *Device::node() const
{
return d->node;
}
bool Device::managed() const
{
return d->managed && d->flags.hasDdf;
}
void Device::setManaged(bool managed)
{
d->managed = managed;
}
void Device::setSupportsMgmtBind(bool supported)
{
if (supported)
{
d->binding.mgmtBindSupported = MGMT_BIND_SUPPORTED;
}
else
{
d->binding.mgmtBindSupported = MGMT_BIND_NOT_SUPPORTED;
}
}
void Device::handleEvent(const Event &event, DEV_StateLevel level)
{
if (event.what() == REventStateEnter || event.what() == REventStateLeave)
{
if (event.num() < StateLevel0 || event.num() >= StateLevelMax)
{
return;
}
const auto level1 = static_cast<unsigned>(event.num());
const auto fn = d->state[level1];
if (d->stateEnterLock[level1] && event.what() == REventStateEnter)
{
d->stateEnterLock[level1] = false;
}
if (fn)
{
fn(this, event);
}
}
else if (d->stateEnterLock[level])
{
DBG_Printf(DBG_DEV, "DEV event before REventStateEnter: " FMT_MAC ", skip: %s\n", FMT_MAC_CAST(d->deviceKey), event.what());
}
else if (event.what() == REventDDFReload)
{
d->setState(DEV_InitStateHandler);
d->binding.bindingCheckRound = 0;
d->startStateTimer(50, StateLevel0);
}
else if (event.what() == REventZdpReload)
{
d->setState(DEV_ActiveEndpointsStateHandler);
d->startStateTimer(50, StateLevel0);
}
else if (d->state[level])
{
if (event.what() == REventAwake && level == StateLevel0)
{
d->awake.start();
}
else if (event.what() == RStateReachable && event.resource() == RDevices)
{
DEV_CheckReachable(this);
}
d->state[level](this, event);
}
}
void DevicePrivate::setState(DeviceStateHandler newState, DEV_StateLevel level)
{
if (state[level] != newState)
{
if (state[level])
{
state[level](q, Event(q->prefix(), REventStateLeave, level, q->key()));
stateEnterLock[level] = false;
}
state[level] = newState;
if (state[level])
{
stateEnterLock[level] = true;
Event e(q->prefix(), REventStateEnter, level, q->key());
e.setUrgent(true);
emit q->eventNotify(e);
}
}
}
void DevicePrivate::startStateTimer(int IntervalMs, DEV_StateLevel level)
{
timer[level].start(IntervalMs, q);
}
void DevicePrivate::stopStateTimer(DEV_StateLevel level)
{
if (timer[level].isActive())
{
timer[level].stop();
}
}
bool DevicePrivate::hasRxOnWhenIdle() const
{
return q->node()->nodeDescriptor().receiverOnWhenIdle();
}
void Device::timerEvent(QTimerEvent *event)
{
for (int i = 0; i < StateLevelMax; i++)
{
if (event->timerId() == d->timer[i].timerId())
{
d->timer[i].stop();
if (d->state[i])
{
d->state[i](this, Event(prefix(), REventStateTimeout, i, key()));
}
break;
}
}
}
qint64 Device::lastAwakeMs() const
{
return d->awake.isValid() ? d->awake.elapsed() : 8640000;
}
bool Device::reachable() const
{
if (lastAwakeMs() < RxOffWhenIdleResponseTime)
{
return true;
}
else if (node() && !node()->nodeDescriptor().isNull() && node()->nodeDescriptor().receiverOnWhenIdle())
{
return item(RStateReachable)->toBool();
}
else if (!item(RCapSleeper)->toBool())
{
return item(RStateReachable)->toBool();
}
return false;
}
const std::vector<Resource *> &Device::subDevices()
{
d->subResources.clear();
for (const auto hnd : d->subResourceHandles)
{
if (!isValid(hnd))
{
continue;
}
auto *r = DEV_GetResource(hnd);
if (r)
{
d->subResources.push_back(r);
}
}
return d->subResources;
}
void Device::clearBindings()
{
d->binding.bindings.clear();
d->binding.bindingTrackers.clear();
if (d->state[STATE_LEVEL_BINDING])
{
d->setState(DEV_BindingHandler, STATE_LEVEL_BINDING);
}
}
bool isSame(const DDF_Binding &a, const DDF_Binding &b)
{
return a.clusterId == b.clusterId &&
a.srcEndpoint == b.srcEndpoint &&
(
(a.isGroupBinding && b.isGroupBinding && a.configGroup == b.configGroup) ||
(a.isUnicastBinding && b.isUnicastBinding && a.dstExtAddress == b.dstExtAddress)
);
}
*/
void mergeBindingReportConfigs(DDF_Binding &a, const DDF_Binding &b)
{
for (const DDF_ZclReport &br : b.reporting)
{
const auto i = std::find_if(a.reporting.cbegin(), a.reporting.cend(),
[&br](const DDF_ZclReport &ar) { return ar.attributeId == br.attributeId; });
if (i == a.reporting.cend())
{
DBG_Printf(DBG_DEV, "DEV add reporting cluster: 0x%04X, attr: 0x%04X\n", b.clusterId, br.attributeId);
a.reporting.push_back(br);
}
}
}
void Device::addBinding(const DDF_Binding &bnd)
{
auto i = std::find_if(d->binding.bindings.begin(), d->binding.bindings.end(),
[&bnd](const auto &i) { return isSame(i, bnd); });
if (i != d->binding.bindings.end())
{
mergeBindingReportConfigs(*i, bnd);
}
else
{
DBG_Printf(DBG_DEV, "DEV add binding cluster: 0x%04X, " FMT_MAC "\n", bnd.clusterId, FMT_MAC_CAST(d->deviceKey));
BindingTracker tracker{};
d->binding.bindings.push_back(bnd);
d->binding.bindingTrackers.push_back(tracker);
Q_ASSERT(d->binding.bindings.size() == d->binding.bindingTrackers.size());
if (bnd.dstEndpoint == 0 && bnd.isUnicastBinding)
{
d->binding.bindings.back().dstEndpoint = 0x01;
}
}
}
const std::vector<DDF_Binding> &Device::bindings() const
{
return d->binding.bindings;
}
Device *DEV_GetDevice(DeviceContainer &devices, DeviceKey key)
{
auto d = std::find_if(devices.begin(), devices.end(),
[key](const std::unique_ptr<Device> &device) { return device->key() == key; });
if (d != devices.end())
{
return d->get();
}
return nullptr;
}
Device *DEV_GetOrCreateDevice(QObject *parent, deCONZ::ApsController *apsCtrl, EventEmitter *eventEmitter, DeviceContainer &devices, DeviceKey key)
{
Q_ASSERT(key != 0);
Q_ASSERT(apsCtrl);
auto d = std::find_if(devices.begin(), devices.end(),
[key](const std::unique_ptr<Device> &device) { return device->key() == key; });
if (d == devices.end())
{
devices.emplace_back(new Device(key, apsCtrl, parent));
Device *device = devices.back().get();
QObject::connect(device, SIGNAL(eventNotify(Event)), eventEmitter, SLOT(enqueueEvent(Event)));
device->setHandle(R_CreateResourceHandle(device, devices.size() - 1));
return device;
}
Q_ASSERT(d != devices.end());
return d->get();
}
bool DEV_RemoveDevice(DeviceContainer &devices, DeviceKey key)
{
const auto i = std::find_if(devices.cbegin(), devices.cend(),
[key](const std::unique_ptr<Device> &device) { return device->key() == key; });
if (i != devices.cend())
{
devices.erase(i);
}
return false;
}
void DEV_SetTestManaged(int enabled)
{
if (enabled >= 0 && enabled <= 2)
{
devManaged = enabled ? enabled : 0;
}
}
*/
bool DEV_TestManaged()
{
return devManaged > 0;
}
In addition legacy code for these tasks is disabled.
*/
bool DEV_TestStrict()
{
return devManaged > 1;
}