* Copyright (c) 2022-2024 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 <array>
#include <QDirIterator>
#include <QFile>
#include <QJsonArray>
#include <QJsonDocument>
#include <QJsonObject>
#include <QJsonValue>
#include <QSettings>
#include <QTimer>
#include <deconz/atom_table.h>
#include <deconz/dbg_trace.h>
#include <deconz/file.h>
#include <deconz/u_assert.h>
#include <deconz/u_sstream_ex.h>
#include <deconz/buffer_pool.h>
#include <deconz/u_memory.h>
#include <deconz/u_time.h>
#include <deconz/u_ecc.h>
#include "database.h"
#include "device_ddf_bundle.h"
#include "device_ddf_init.h"
#include "device_descriptions.h"
#include "device_js/device_js.h"
#include "utils/scratchmem.h"
#include "json.h"
#include "event.h"
#include "resource.h"
#define DDF_MAX_PATH_LENGTH 1024
#define DDF_MAX_PUBLIC_KEYS 64
#define HND_MIN_LOAD_COUNTER 1
#define HND_MAX_LOAD_COUNTER 15
#define HND_MAX_DESCRIPTIONS 16383
#define HND_MAX_ITEMS 1023
#define HND_MAX_SUB_DEVS 15
Packs location to an DDF item into a opaque 32-bit unsigned int handle.
The DDF item lookup complexity is O(1) via DDF_GetItem() function.
*/
union ItemHandlePack
{
struct {
unsigned int loadCounter : 4;
unsigned int description : 14;
unsigned int subDevice: 4;
unsigned int item : 10;
};
uint32_t handle;
};
static unsigned atDDFPolicyLatestPreferStable;
static unsigned atDDFPolicyLatest;
static unsigned atDDFPolicyPin;
static unsigned atDDFPolicyRawJson;
static DeviceDescriptions *_instance = nullptr;
static DeviceDescriptionsPrivate *_priv = nullptr;
class DDF_ParseContext
{
public:
deCONZ::StorageLocation fileLocation;
char filePath[DDF_MAX_PATH_LENGTH];
unsigned filePathLength = 0;
uint8_t *fileData = nullptr;
unsigned fileDataSize = 0;
std::array<cj_token, 8192> tokens;
DDFB_ExtfChunk *extChunks;
int64_t bundleLastModified;
uint64_t signatures;
uint32_t scratchPos = 0;
std::array<unsigned char, 1 << 20> scratchMem;
int n_rawDDF = 0;
int n_devIdentifiers = 0;
};
struct ConstantEntry
{
AT_AtomIndex key;
AT_AtomIndex value;
};
* Lookup which DDFs already have been queried.
*
*/
enum DDF_LoadState
{
DDF_LoadStateScheduled,
DDF_LoadStateLoaded
};
enum DDF_ReloadWhat
{
DDF_ReloadIdle,
DDF_ReloadBundles,
DDF_ReloadAll
};
struct DDF_LoadRecord
{
AT_AtomIndex modelid;
AT_AtomIndex mfname;
uint32_t mfnameLowerCaseHash;
DDF_LoadState loadState;
};
class DeviceDescriptionsPrivate
{
public:
uint loadCounter = HND_MIN_LOAD_COUNTER;
std::vector<ConstantEntry> constants2;
std::vector<DeviceDescription::Item> genericItems;
std::vector<DeviceDescription> descriptions;
DeviceDescription invalidDescription;
DeviceDescription::Item invalidItem;
DeviceDescription::SubDevice invalidSubDevice;
QStringList enabledStatusFilter;
std::vector<DDF_SubDeviceDescriptor> subDevices;
std::vector<DDF_FunctionDescriptor> readFunctions;
std::vector<DDF_FunctionDescriptor> writeFunctions;
std::vector<DDF_FunctionDescriptor> parseFunctions;
std::vector<DDF_LoadRecord> ddfLoadRecords;
std::vector<U_ECC_PublicKeySecp256k1> publicKeys;
DDF_ReloadWhat ddfReloadWhat = DDF_ReloadIdle;
QTimer *ddfReloadTimer = nullptr;
};
static int DDF_ReadFileInMemory(DDF_ParseContext *pctx);
static int DDF_ReadConstantsJson(DDF_ParseContext *pctx, std::vector<ConstantEntry> & constants);
static DeviceDescription::Item DDF_ReadItemFile(DDF_ParseContext *pctx);
static DeviceDescription DDF_ReadDeviceFile(DDF_ParseContext *pctx);
static DDF_SubDeviceDescriptor DDF_ReadSubDeviceFile(DDF_ParseContext *pctx);
static DeviceDescription DDF_MergeGenericItems(const std::vector<DeviceDescription::Item> &genericItems, const DeviceDescription &ddf);
static int DDF_MergeGenericBundleItems(DeviceDescription &ddf, DDF_ParseContext *pctx);
static int DDF_ProcessSignatures(DDF_ParseContext *pctx, std::vector<U_ECC_PublicKeySecp256k1> &publicKeys, U_BStream *bs, uint32_t *bundleHash);
static DeviceDescription::Item *DDF_GetItemMutable(const ResourceItem *item);
static void DDF_UpdateItemHandlesForIndex(std::vector<DeviceDescription> &descriptions, uint loadCounter, size_t index);
static void DDF_TryCompileAndFixJavascript(QString *expr, const QString &path);
DeviceDescription DDF_LoadScripts(const DeviceDescription &ddf);
* https://maskray.me/blog/2023-04-12-elf-hash-function
*
* PJW hash adapted from musl libc.
*
* TODO(mpi): make this a own module U_StringHash()
*/
static uint32_t DDF_StringHash(const void *s0, unsigned size)
{
uint32_t h;
const unsigned char *s;
h = 0;
s = (const unsigned char*)s0;
while (size--)
{
h = 16 * h + *s++;
h ^= h >> 24 & 0xF0;
}
return h & 0xfffffff;
}
This is mainly used to get a unique number to compare case insensitive manufacturer names.
For example for "HEIMAN", "heiman" and "Heiman" atoms this function returns the same hash.
*/
static uint32_t DDF_AtomLowerCaseStringHash(AT_AtomIndex ati)
{
unsigned len;
AT_Atom atom;
char str[192];
str[0] = '\0';
atom = AT_GetAtomByIndex(ati);
if (atom.len == 0)
return 0;
if (sizeof(str) <= atom.len)
return DDF_StringHash(atom.data, atom.len);
for (len = 0; len < atom.len; len++)
{
uint8_t ch = atom.data[len];
if (ch & 0x80)
return DDF_StringHash(atom.data, atom.len);
if (ch >= 'A' && ch <= 'Z')
ch += (unsigned char)('a' - 'A');
str[len] = (char)ch;
}
str[len] = '\0';
return DDF_StringHash(str, len);
}
DeviceDescriptions::DeviceDescriptions(QObject *parent) :
QObject(parent),
d_ptr2(new DeviceDescriptionsPrivate)
{
_instance = this;
_priv = d_ptr2;
d_ptr2->ddfReloadTimer = new QTimer(this);
d_ptr2->ddfReloadTimer->setSingleShot(true);
connect(d_ptr2->ddfReloadTimer, &QTimer::timeout, this, &DeviceDescriptions::ddfReloadTimerFired);
{
AT_AtomIndex ati;
const char *str;
str = "latest_prefer_stable";
AT_AddAtom(str, U_strlen(str), &ati);
atDDFPolicyLatestPreferStable = ati.index;
str = "latest";
AT_AddAtom(str, U_strlen(str), &ati);
atDDFPolicyLatest = ati.index;
str = "pin";
AT_AddAtom(str, U_strlen(str), &ati);
atDDFPolicyPin = ati.index;
str = "raw_json";
AT_AddAtom(str, U_strlen(str), &ati);
atDDFPolicyRawJson = ati.index;
}
{
* Register offical public keys for beta and stable signed bundles.
* These are used to to select bundles according to the attr/ddf_policy
*/
U_ECC_PublicKeySecp256k1 pk;
uint8_t stable_key[33] = {
0x03, 0x93, 0x2D, 0x60, 0xA3, 0x35, 0x44, 0xFD, 0xB9, 0x20, 0x2B, 0x41, 0xA7, 0x68, 0xCD, 0xD8,
0x70, 0x90, 0x82, 0xBD, 0xE8, 0xCD, 0x85, 0x47, 0x21, 0x68, 0xC5, 0x2A, 0xD8, 0xC3, 0xE5, 0x76, 0xF6 };
uint8_t beta_key[33] = {
0x02, 0xAB, 0x93, 0x42, 0x38, 0x60, 0xD3, 0x9D, 0x2C, 0xDC, 0xBC, 0xA0, 0xF9, 0x04, 0x2B, 0xD1,
0xA2, 0x45, 0xED, 0xB6, 0xDC, 0xC1, 0x0C, 0x4C, 0xFF, 0x1B, 0x78, 0xE9, 0xF2, 0x43, 0xF5, 0x3F, 0x1E };
U_memcpy(pk.key, stable_key, sizeof(pk.key));
d_ptr2->publicKeys.push_back(pk);
U_memcpy(pk.key, beta_key, sizeof(pk.key));
d_ptr2->publicKeys.push_back(pk);
}
{
DDF_FunctionDescriptor fn;
fn.name = "zcl:attr";
fn.description = "Generic function to parse ZCL attributes.";
DDF_FunctionDescriptor::Parameter param;
param.name = "Endpoint";
param.key = "ep";
param.description = "255 means any endpoint, 0 means auto selected from subdevice.";
param.dataType = DataTypeUInt8;
param.defaultValue = 0;
param.isOptional = 1;
param.isHexString = 0;
param.supportsArray = 0;
fn.parameters.push_back(param);
param.name = "Cluster ID";
param.key = "cl";
param.description = "As string hex value";
param.dataType = DataTypeUInt16;
param.defaultValue = 0;
param.isOptional = 0;
param.isHexString = 1;
param.supportsArray = 0;
fn.parameters.push_back(param);
param.name = "Attribute ID";
param.key = "at";
param.description = "As string hex value";
param.dataType = DataTypeUInt16;
param.defaultValue = 0;
param.isOptional = 0;
param.isHexString = 1;
param.supportsArray = 0;
fn.parameters.push_back(param);
param.name = "Manufacturer code";
param.key = "mf";
param.description = "As string hex value.";
param.dataType = DataTypeUInt16;
param.defaultValue = 0;
param.isOptional = 1;
param.isHexString = 1;
param.supportsArray = 0;
fn.parameters.push_back(param);
param.name = "Javascript file";
param.key = "script";
param.description = "Relative path of a Javascript .js file.";
param.dataType = DataTypeString;
param.defaultValue = {};
param.isOptional = 1;
param.isHexString = 0;
param.supportsArray = 0;
fn.parameters.push_back(param);
param.name = "Expression";
param.key = "eval";
param.description = "Javascript expression to transform the raw value.";
param.dataType = DataTypeString;
param.defaultValue = QLatin1String("Item.val = Attr.val");
param.isOptional = 1;
param.isHexString = 0;
param.supportsArray = 0;
fn.parameters.push_back(param);
d_ptr2->parseFunctions.push_back(fn);
}
{
DDF_FunctionDescriptor fn;
fn.name = "zcl:attr";
fn.description = "Generic function to read ZCL attributes.";
DDF_FunctionDescriptor::Parameter param;
param.name = "Endpoint";
param.key = "ep";
param.description = "255 means any endpoint, 0 means auto selected from subdevice.";
param.dataType = DataTypeUInt8;
param.defaultValue = 255;
param.isOptional = 0;
param.isHexString = 0;
param.supportsArray = 0;
fn.parameters.push_back(param);
param.name = "Cluster ID";
param.key = "cl";
param.description = "As string hex value";
param.dataType = DataTypeUInt16;
param.defaultValue = 0;
param.isOptional = 0;
param.isHexString = 1;
param.supportsArray = 0;
fn.parameters.push_back(param);
param.name = "Attribute ID";
param.key = "at";
param.description = "As string hex value";
param.dataType = DataTypeUInt16;
param.defaultValue = 0;
param.isOptional = 0;
param.isHexString = 1;
param.supportsArray = 1;
fn.parameters.push_back(param);
param.name = "Manufacturer code";
param.key = "mf";
param.description = "As string hex value.";
param.dataType = DataTypeUInt16;
param.defaultValue = 0;
param.isOptional = 1;
param.isHexString = 1;
param.supportsArray = 0;
fn.parameters.push_back(param);
d_ptr2->readFunctions.push_back(fn);
}
{
DDF_FunctionDescriptor fn;
fn.name = "zcl:attr";
fn.description = "Generic function to write ZCL attributes.";
DDF_FunctionDescriptor::Parameter param;
param.name = "Endpoint";
param.key = "ep";
param.description = "255 means any endpoint, 0 means auto selected from subdevice.";
param.dataType = DataTypeUInt8;
param.defaultValue = 0;
param.isOptional = 1;
param.isHexString = 0;
param.supportsArray = 0;
fn.parameters.push_back(param);
param.name = "Cluster ID";
param.key = "cl";
param.description = "As string hex value";
param.dataType = DataTypeUInt16;
param.defaultValue = 0;
param.isOptional = 0;
param.isHexString = 1;
param.supportsArray = 0;
fn.parameters.push_back(param);
param.name = "Attribute ID";
param.key = "at";
param.description = "As string hex value";
param.dataType = DataTypeUInt16;
param.defaultValue = 0;
param.isOptional = 0;
param.isHexString = 1;
param.supportsArray = 0;
fn.parameters.push_back(param);
param.name = "Datatype";
param.key = "dt";
param.description = "Datatype of the data to be written.";
param.dataType = DataTypeUInt8;
param.defaultValue = 0;
param.isOptional = 0;
param.isHexString = 1;
param.supportsArray = 0;
fn.parameters.push_back(param);
param.name = "Manufacturer code";
param.key = "mf";
param.description = "As string hex value.";
param.dataType = DataTypeUInt16;
param.defaultValue = 0;
param.isOptional = 1;
param.isHexString = 1;
param.supportsArray = 0;
fn.parameters.push_back(param);
param.name = "Javascript file";
param.key = "script";
param.description = "Relative path of a Javascript .js file.";
param.dataType = DataTypeString;
param.defaultValue = {};
param.isOptional = 1;
param.isHexString = 0;
param.supportsArray = 0;
fn.parameters.push_back(param);
param.name = "Expression";
param.key = "eval";
param.description = "Javascript expression to transform the raw value.";
param.dataType = DataTypeString;
param.defaultValue = QLatin1String("Item.val;");
param.isOptional = 1;
param.isHexString = 0;
param.supportsArray = 0;
fn.parameters.push_back(param);
d_ptr2->writeFunctions.push_back(fn);
}
{
DDF_FunctionDescriptor fn;
fn.name = "zcl:cmd";
fn.description = "Generic function to parse ZCL commands.";
DDF_FunctionDescriptor::Parameter param;
param.name = "Endpoint";
param.key = "ep";
param.description = "255 means any endpoint, 0 means auto selected from subdevice.";
param.dataType = DataTypeUInt8;
param.defaultValue = 0;
param.isOptional = 1;
param.isHexString = 0;
param.supportsArray = 0;
fn.parameters.push_back(param);
param.name = "Cluster ID";
param.key = "cl";
param.description = "As string hex value.";
param.dataType = DataTypeUInt16;
param.defaultValue = 0;
param.isOptional = 0;
param.isHexString = 1;
param.supportsArray = 0;
fn.parameters.push_back(param);
param.name = "Command ID";
param.key = "cmd";
param.description = "As string hex value.";
param.dataType = DataTypeUInt8;
param.defaultValue = 0;
param.isOptional = 0;
param.isHexString = 1;
param.supportsArray = 0;
fn.parameters.push_back(param);
param.name = "Manufacturer code";
param.key = "mf";
param.description = "As string hex value.";
param.dataType = DataTypeUInt16;
param.defaultValue = 0;
param.isOptional = 1;
param.isHexString = 1;
param.supportsArray = 0;
fn.parameters.push_back(param);
param.name = "Javascript file";
param.key = "script";
param.description = "Relative path of a Javascript .js file.";
param.dataType = DataTypeString;
param.defaultValue = {};
param.isOptional = 1;
param.isHexString = 0;
param.supportsArray = 0;
fn.parameters.push_back(param);
param.name = "Expression";
param.key = "eval";
param.description = "Javascript expression to transform the raw value.";
param.dataType = DataTypeString;
param.defaultValue = 0;
param.isOptional = 1;
param.isHexString = 0;
param.supportsArray = 0;
fn.parameters.push_back(param);
d_ptr2->parseFunctions.push_back(fn);
}
{
DDF_FunctionDescriptor fn;
fn.name = "zcl:cmd";
fn.description = "Generic function to read ZCL commands.";
DDF_FunctionDescriptor::Parameter param;
param.name = "Endpoint";
param.key = "ep";
param.description = "255 means any endpoint, 0 means auto selected from subdevice.";
param.dataType = DataTypeUInt8;
param.defaultValue = 255;
param.isOptional = 0;
param.isHexString = 0;
param.supportsArray = 0;
fn.parameters.push_back(param);
param.name = "Cluster ID";
param.key = "cl";
param.description = "As string hex value.";
param.dataType = DataTypeUInt16;
param.defaultValue = 0;
param.isOptional = 0;
param.isHexString = 1;
param.supportsArray = 0;
fn.parameters.push_back(param);
param.name = "Command ID";
param.key = "cmd";
param.description = "As string hex value.";
param.dataType = DataTypeUInt8;
param.defaultValue = 0;
param.isOptional = 0;
param.isHexString = 1;
param.supportsArray = 0;
fn.parameters.push_back(param);
param.name = "Manufacturer code";
param.key = "mf";
param.description = "As string hex value.";
param.dataType = DataTypeUInt16;
param.defaultValue = 0;
param.isOptional = 1;
param.isHexString = 1;
param.supportsArray = 0;
fn.parameters.push_back(param);
param.name = "Javascript file";
param.key = "script";
param.description = "Relative path of a Javascript .js file.";
param.dataType = DataTypeString;
param.defaultValue = {};
param.isOptional = 1;
param.isHexString = 0;
param.supportsArray = 0;
fn.parameters.push_back(param);
param.name = "Expression";
param.key = "eval";
param.description = "Javascript expression to transform the raw value.";
param.dataType = DataTypeString;
param.defaultValue = 0;
param.isOptional = 1;
param.isHexString = 0;
param.supportsArray = 0;
fn.parameters.push_back(param);
d_ptr2->readFunctions.push_back(fn);
}
{
DDF_FunctionDescriptor fn;
fn.name = "zcl:cmd";
fn.description = "Generic function to send ZCL commands.";
DDF_FunctionDescriptor::Parameter param;
param.name = "Endpoint";
param.key = "ep";
param.description = "255 means any endpoint, 0 means auto selected from subdevice.";
param.dataType = DataTypeUInt8;
param.defaultValue = 0;
param.isOptional = 1;
param.isHexString = 0;
param.supportsArray = 0;
fn.parameters.push_back(param);
param.name = "Cluster ID";
param.key = "cl";
param.description = "As string hex value.";
param.dataType = DataTypeUInt16;
param.defaultValue = 0;
param.isOptional = 0;
param.isHexString = 1;
param.supportsArray = 0;
fn.parameters.push_back(param);
param.name = "Command ID";
param.key = "cmd";
param.description = "As string hex value.";
param.dataType = DataTypeUInt8;
param.defaultValue = 0;
param.isOptional = 0;
param.isHexString = 1;
param.supportsArray = 0;
fn.parameters.push_back(param);
param.name = "Manufacturer code";
param.key = "mf";
param.description = "As string hex value.";
param.dataType = DataTypeUInt16;
param.defaultValue = 0;
param.isOptional = 1;
param.isHexString = 1;
param.supportsArray = 0;
fn.parameters.push_back(param);
param.name = "Javascript file";
param.key = "script";
param.description = "Relative path of a Javascript .js file.";
param.dataType = DataTypeString;
param.defaultValue = {};
param.isOptional = 1;
param.isHexString = 0;
param.supportsArray = 0;
fn.parameters.push_back(param);
param.name = "Expression";
param.key = "eval";
param.description = "Javascript expression to transform the raw value.";
param.dataType = DataTypeString;
param.defaultValue = 0;
param.isOptional = 1;
param.isHexString = 0;
param.supportsArray = 0;
fn.parameters.push_back(param);
d_ptr2->writeFunctions.push_back(fn);
}
{
DDF_FunctionDescriptor fn;
fn.name = "tuya";
fn.description = "Generic function to parse Tuya data.";
DDF_FunctionDescriptor::Parameter param;
param.name = "Datapoint";
param.key = "dpid";
param.description = "1-255 the datapoint ID.";
param.dataType = DataTypeUInt8;
param.defaultValue = 0;
param.isOptional = 0;
param.isHexString = 0;
param.supportsArray = 0;
fn.parameters.push_back(param);
param.name = "Javascript file";
param.key = "script";
param.description = "Relative path of a Javascript .js file.";
param.dataType = DataTypeString;
param.defaultValue = {};
param.isOptional = 1;
param.isHexString = 0;
param.supportsArray = 0;
fn.parameters.push_back(param);
param.name = "Expression";
param.key = "eval";
param.description = "Javascript expression to transform the raw value.";
param.dataType = DataTypeString;
param.defaultValue = QLatin1String("Item.val = Attr.val");
param.isOptional = 1;
param.isHexString = 0;
param.supportsArray = 0;
fn.parameters.push_back(param);
d_ptr2->parseFunctions.push_back(fn);
}
{
DDF_FunctionDescriptor fn;
fn.name = "tuya";
fn.description = "Generic function to read all Tuya datapoints. It has no parameters.";
d_ptr2->readFunctions.push_back(fn);
}
{
DDF_FunctionDescriptor fn;
fn.name = "tuya";
fn.description = "Generic function to write Tuya data.";
DDF_FunctionDescriptor::Parameter param;
param.name = "Datapoint";
param.key = "dpid";
param.description = "1-255 the datapoint ID.";
param.dataType = DataTypeUInt8;
param.defaultValue = 0;
param.isOptional = 0;
param.isHexString = 0;
param.supportsArray = 0;
fn.parameters.push_back(param);
param.name = "Datatype";
param.key = "dt";
param.description = "Datatype of the data to be written.";
param.dataType = DataTypeUInt8;
param.defaultValue = 0;
param.isOptional = 0;
param.isHexString = 1;
param.supportsArray = 0;
fn.parameters.push_back(param);
param.name = "Expression";
param.key = "eval";
param.description = "Javascript expression to transform the raw value.";
param.dataType = DataTypeString;
param.defaultValue = QLatin1String("Item.val;");
param.isOptional = 1;
param.isHexString = 0;
param.supportsArray = 0;
fn.parameters.push_back(param);
d_ptr2->writeFunctions.push_back(fn);
}
{
DDF_FunctionDescriptor fn;
fn.name = "ias:zonestatus";
fn.description = "Generic function to parse IAS ZONE status change notifications or zone status from read/report command.";
DDF_FunctionDescriptor::Parameter param;
param.name = "IAS Zone status mask";
param.key = "mask";
param.description = "Sets the bitmask for Alert1 and Alert2 item of the IAS Zone status.";
param.dataType = DataTypeString;
param.defaultValue = QLatin1String("alarm1,alarm2");
param.isOptional = 1;
param.isHexString = 0;
param.supportsArray = 0;
fn.parameters.push_back(param);
d_ptr2->parseFunctions.push_back(fn);
}
{
DDF_FunctionDescriptor fn;
fn.name = "numtostr";
fn.description = "Generic function to to convert number to string.";
DDF_FunctionDescriptor::Parameter param;
param.name = "Source item";
param.key = "srcitem";
param.description = "The source item holding the number.";
param.dataType = DataTypeString;
param.defaultValue = 0;
param.isOptional = 0;
param.isHexString = 0;
param.supportsArray = 0;
fn.parameters.push_back(param);
param.name = "Operator";
param.key = "op";
param.description = "Comparison operator (lt | le | eq | gt | ge)";
param.dataType = DataTypeString;
param.defaultValue = 0;
param.isOptional = 0;
param.isHexString = 0;
param.supportsArray = 0;
fn.parameters.push_back(param);
param.name = "Mapping";
param.key = "to";
param.description = "Array of (num, string) mappings";
param.dataType = DataTypeString;
param.defaultValue = 0;
param.isOptional = 0;
param.isHexString = 0;
param.supportsArray = 1;
fn.parameters.push_back(param);
d_ptr2->parseFunctions.push_back(fn);
}
{
DDF_FunctionDescriptor fn;
fn.name = "time";
fn.description = "Specialized function to parse time, local and last set time from read/report commands of the time cluster and auto-sync time if needed.";
d_ptr2->parseFunctions.push_back(fn);
}
{
DDF_FunctionDescriptor fn;
fn.name = "xiaomi:special";
fn.description = "Generic function to parse custom Xiaomi attributes and commands.";
DDF_FunctionDescriptor::Parameter param;
param.name = "Endpoint";
param.key = "ep";
param.description = "Source endpoint of the incoming command, default value 255 means any endpoint.";
param.dataType = DataTypeUInt8;
param.defaultValue = 255;
param.isOptional = 1;
param.isHexString = 0;
param.supportsArray = 0;
fn.parameters.push_back(param);
param.name = "Attribute ID";
param.key = "at";
param.description = "The attribute to parse, shall be 0xff01, 0xff02 or 0x00f7";
param.dataType = DataTypeUInt16;
param.defaultValue = 0;
param.isOptional = 0;
param.isHexString = 1;
param.supportsArray = 0;
fn.parameters.push_back(param);
param.name = "Index";
param.key = "idx";
param.description = "A 8-bit string hex value.";
param.dataType = DataTypeUInt8;
param.defaultValue = 0;
param.isOptional = 0;
param.isHexString = 1;
param.supportsArray = 0;
fn.parameters.push_back(param);
param.name = "Expression";
param.key = "eval";
param.description = "Javascript expression to transform the raw value.";
param.dataType = DataTypeString;
param.defaultValue = {};
param.isOptional = 0;
param.isHexString = 0;
param.supportsArray = 0;
fn.parameters.push_back(param);
d_ptr2->parseFunctions.push_back(fn);
}
}
Use these to load only DDFs and bundles which are in use.
*/
void DeviceDescriptions::prepare()
{
auto &records = _priv->ddfLoadRecords;
const auto res = DB_LoadIdentifierPairs();
for (size_t i = 0; i < res.size(); i++)
{
size_t j = 0;
for (j = 0; j < records.size(); j++)
{
if (records[j].mfname.index == res[i].mfnameAtomIndex &&
records[j].modelid.index == res[i].modelIdAtomIndex)
{
break;
}
}
if (j == records.size())
{
DDF_LoadRecord rec;
rec.mfname.index = res[i].mfnameAtomIndex;
rec.mfnameLowerCaseHash = DDF_AtomLowerCaseStringHash(rec.mfname);
rec.modelid.index = res[i].modelIdAtomIndex;
rec.loadState = DDF_LoadStateScheduled;
records.push_back(rec);
}
}
}
DeviceDescriptions::~DeviceDescriptions()
{
Q_ASSERT(_instance == this);
_instance = nullptr;
_priv = nullptr;
Q_ASSERT(d_ptr2);
delete d_ptr2;
d_ptr2 = nullptr;
}
void DeviceDescriptions::setEnabledStatusFilter(const QStringList &filter)
{
if (d_ptr2->enabledStatusFilter != filter)
{
d_ptr2->enabledStatusFilter = filter;
DBG_Printf(DBG_INFO, "DDF enabled for %s status\n", qPrintable(filter.join(QLatin1String(", "))));
}
}
const QStringList &DeviceDescriptions::enabledStatusFilter() const
{
return d_ptr2->enabledStatusFilter;
}
*/
DeviceDescriptions *DeviceDescriptions::instance()
{
Q_ASSERT(_instance);
return _instance;
}
bool DDF_IsStatusEnabled(const QString &status)
{
if (_priv)
{
return _priv->enabledStatusFilter.contains(status, Qt::CaseInsensitive);
}
return false;
}
*/
void DDF_AnnoteZclParse1(int line, const char *file, const Resource *resource, ResourceItem *item, quint8 ep, quint16 clusterId, quint16 attributeId, const char *eval)
{
DBG_Assert(resource);
DBG_Assert(item);
DBG_Assert(eval);
if (!_instance || !resource || !item || !eval)
{
return;
}
if (item->ddfItemHandle() == DeviceDescription::Item::InvalidItemHandle)
{
const Device *device = nullptr;
if (resource->parentResource())
{
device = static_cast<const Device*>(resource->parentResource());
}
if (!device)
{
return;
}
const auto *uniqueId = resource->item(RAttrUniqueId);
if (!uniqueId)
{
return;
}
auto &ddf = _instance->get(device);
if (!ddf.isValid())
{
return;
}
const QStringList u = uniqueId->toString().split(QLatin1Char('-'), SKIP_EMPTY_PARTS);
for (const auto &sub : ddf.subDevices)
{
if (u.size() != sub.uniqueId.size())
{
continue;
}
bool ok = true;
for (int i = 1; i < qMin(u.size(), sub.uniqueId.size()); i++)
{
if (u[i].toUInt(0, 16) != sub.uniqueId[i].toUInt(0, 16))
{
ok = false;
}
}
if (!ok)
{
continue;
}
for (const auto &ddfItem : sub.items)
{
if (ddfItem.name == item->descriptor().suffix)
{
item->setDdfItemHandle(ddfItem.handle);
break;
}
}
break;
}
}
if (item->ddfItemHandle() != DeviceDescription::Item::InvalidItemHandle)
{
DeviceDescription::Item *ddfItem = DDF_GetItemMutable(item);
if (ddfItem && ddfItem->isValid())
{
if (ddfItem->parseParameters.isNull())
{
char buf[255];
QVariantMap param;
param[QLatin1String("ep")] = int(ep);
snprintf(buf, sizeof(buf), "0x%04X", clusterId);
param[QLatin1String("cl")] = QLatin1String(buf);
snprintf(buf, sizeof(buf), "0x%04X", attributeId);
param[QLatin1String("at")] = QLatin1String(buf);
param[QLatin1String("eval")] = QLatin1String(eval);
size_t fileLen = strlen(file);
const char *fileName = file + fileLen;
for (size_t i = fileLen; i > 0; i--, fileName--)
{
if (*fileName == '/')
{
fileName++;
break;
}
}
snprintf(buf, sizeof(buf), "%s:%d", fileName, line);
param[QLatin1String("cppsrc")] = QLatin1String(buf);
ddfItem->parseParameters = param;
DBG_Printf(DBG_DDF, "DDF %s:%d: %s updated ZCL function cl: 0x%04X, at: 0x%04X, eval: %s\n", fileName, line, qPrintable(resource->item(RAttrUniqueId)->toString()), clusterId, attributeId, eval);
}
}
}
}
void DeviceDescriptions::handleEvent(const Event &event)
{
if (event.what() == REventDDFInitRequest)
{
handleDDFInitRequest(event);
}
else if (event.what() == REventDDFReload)
{
if (event.num() == 0)
{
}
}
}
\returns The DDF object, DeviceDescription::isValid() to check for success.
*/
const DeviceDescription &DeviceDescriptions::get(const Resource *resource, DDF_MatchControl match)
{
U_ASSERT(resource);
Q_D(const DeviceDescriptions);
const ResourceItem *modelidItem = resource->item(RAttrModelId);
const ResourceItem *mfnameItem = resource->item(RAttrManufacturerName);
const ResourceItem *typeItem = resource->item(RAttrType);
* Collect all matching DDFs.
* The result is than sorted and according to the attr/ddf_policy the best candidate
* will be selected.
*/
unsigned matchedCount = 0;
static std::array<int, 16> matchedIndices;
U_ASSERT(modelidItem);
U_ASSERT(mfnameItem);
if (typeItem)
{
const char *type = typeItem->toCString();
if (type[0] == 'Z' && type[1] == 'G')
{
return d->invalidDescription;
}
if (type[0] == 'C' && type[1] == 'o' && type[2] == 'n')
{
return d->invalidDescription;
}
}
unsigned modelidAtomIndex = modelidItem->atomIndex();
unsigned mfnameAtomIndex = mfnameItem->atomIndex();
if (modelidAtomIndex == 0 || mfnameAtomIndex == 0)
{
return d->invalidDescription;
}
U_ASSERT(modelidAtomIndex != 0);
U_ASSERT(mfnameAtomIndex != 0);
uint32_t mfnameLowerCaseHash = DDF_AtomLowerCaseStringHash(AT_AtomIndex{mfnameAtomIndex});
* Filter matching DDFs, there can be multiple entries for the same modelid and manufacturer name.
* Further sorting for the 'best' match according to attr/ddf_policy is done afterwards.
*/
{
auto i = d->descriptions.begin();
for (;matchedCount < matchedIndices.size();)
{
i = std::find_if(i, d->descriptions.end(), [modelidAtomIndex, mfnameAtomIndex, mfnameLowerCaseHash](const DeviceDescription &ddf)
{
if (ddf.mfnameAtomIndices.size() != ddf.modelidAtomIndices.size())
{
return false;
}
for (size_t j = 0; j < ddf.modelidAtomIndices.size(); j++)
{
if (ddf.modelidAtomIndices[j] == modelidAtomIndex)
{
if (ddf.mfnameAtomIndices[j] == mfnameAtomIndex)
return true;
uint32_t mfnameLowerCaseHash2 = DDF_AtomLowerCaseStringHash(AT_AtomIndex{ddf.mfnameAtomIndices[j]});
if (mfnameLowerCaseHash == mfnameLowerCaseHash2)
return true;
}
}
return false;
});
if (i == d->descriptions.end())
{
if (loadDDFAndBundlesFromDisc(resource))
{
i = d->descriptions.begin();
continue;
}
break;
}
if (!i->matchExpr.isEmpty() && match == DDF_EvalMatchExpr)
{
DeviceJs *djs = DeviceJs::instance();
djs->reset();
djs->setResource(resource->parentResource() ? resource->parentResource() : resource);
if (djs->evaluate(i->matchExpr) == JsEvalResult::Ok)
{
const auto res = djs->result();
DBG_Printf(DBG_DDF, "matchexpr: %s --> %s\n", qPrintable(i->matchExpr), qPrintable(res.toString()));
if (res.toBool())
{
matchedIndices[matchedCount] = i->handle;
matchedCount++;
}
}
else
{
DBG_Printf(DBG_DDF, "failed to evaluate matchexpr for %s: %s, err: %s\n", qPrintable(resource->item(RAttrUniqueId)->toString()), qPrintable(i->matchExpr), qPrintable(djs->errorString()));
}
}
else
{
matchedIndices[matchedCount] = i->handle;
matchedCount++;
}
i++;
}
}
if (matchedCount != 0)
{
* Now split the matches up in categories sorted by latest timestamp.
*/
unsigned invalidIndex = 0xFFFFFFFF;
unsigned rawJsonIndex = invalidIndex;
unsigned latestStableBundleIndex = invalidIndex;
unsigned latestBetaBundleIndex = invalidIndex;
unsigned latestUserBundleIndex = invalidIndex;
for (size_t i = 0; i < matchedCount; i++)
{
const DeviceDescription &ddf1 = d->descriptions[matchedIndices[i]];
if (ddf1.storageLocation == deCONZ::DdfLocation || ddf1.storageLocation == deCONZ::DdfUserLocation)
{
if (rawJsonIndex == invalidIndex)
{
rawJsonIndex = matchedIndices[i];
}
else if (d->descriptions[rawJsonIndex].status == QLatin1String("Draft"))
{
rawJsonIndex = matchedIndices[i];
}
else if (ddf1.storageLocation == deCONZ::DdfUserLocation && d->descriptions[rawJsonIndex].storageLocation == deCONZ::DdfLocation)
{
rawJsonIndex = matchedIndices[i];
}
continue;
}
if (ddf1.storageLocation == deCONZ::DdfBundleUserLocation || ddf1.storageLocation == deCONZ::DdfBundleLocation)
{
if (ddf1.signedBy & 1)
{
if (latestStableBundleIndex == invalidIndex)
{
latestStableBundleIndex = matchedIndices[i];
}
else
{
const DeviceDescription &ddf0 = d->descriptions[latestStableBundleIndex];
if (ddf0.lastModified < ddf1.lastModified)
{
latestStableBundleIndex = matchedIndices[i];
}
}
}
if (ddf1.signedBy & 2)
{
if (latestBetaBundleIndex == invalidIndex)
{
latestBetaBundleIndex = matchedIndices[i];
}
else
{
const DeviceDescription &ddf0 = d->descriptions[latestBetaBundleIndex];
if (ddf0.lastModified < ddf1.lastModified)
{
latestBetaBundleIndex = matchedIndices[i];
}
}
}
if ((ddf1.signedBy & 3) == 0)
{
if (latestUserBundleIndex == invalidIndex)
{
latestUserBundleIndex = matchedIndices[i];
}
else
{
const DeviceDescription &ddf0 = d->descriptions[latestUserBundleIndex];
if (ddf0.lastModified < ddf1.lastModified)
{
latestUserBundleIndex = matchedIndices[i];
}
}
}
}
}
unsigned policy = atDDFPolicyLatestPreferStable;
{
const Resource *rParent = resource->parentResource() ? resource->parentResource() : resource;
const ResourceItem *ddfPolicyItem = rParent->item(RAttrDdfPolicy);
if (ddfPolicyItem)
{
policy = ddfPolicyItem->atomIndex();
}
}
if (policy == atDDFPolicyRawJson && rawJsonIndex != invalidIndex)
{
return d->descriptions[rawJsonIndex];
}
if (policy == atDDFPolicyLatestPreferStable)
{
if (latestStableBundleIndex != invalidIndex)
return d->descriptions[latestStableBundleIndex];
}
if (policy == atDDFPolicyLatest || policy == atDDFPolicyLatestPreferStable)
{
unsigned bundleCount = 0;
std::array<unsigned, 3> bundleIndices;
if (latestStableBundleIndex != invalidIndex)
bundleIndices[bundleCount++] = latestStableBundleIndex;
if (latestBetaBundleIndex != invalidIndex)
bundleIndices[bundleCount++] = latestBetaBundleIndex;
if (latestUserBundleIndex != invalidIndex)
bundleIndices[bundleCount++] = latestUserBundleIndex;
if (bundleCount != 0)
{
unsigned bestMatch = bundleIndices[0];
for (unsigned i = 1; i < bundleCount; i++)
{
const DeviceDescription &ddf0 = d->descriptions[bestMatch];
const DeviceDescription &ddf1 = d->descriptions[i];
if (ddf0.lastModified < ddf1.lastModified)
bestMatch = i;
}
return d->descriptions[bestMatch];
}
}
if (policy == atDDFPolicyPin)
{
* Lookup a matching bundle by its hash. This is finicky but ensures no bugus is matched.
*/
const Resource *rParent = resource->parentResource() ? resource->parentResource() : resource;
const ResourceItem *ddfHashItem = rParent->item(RAttrDdfHash);
if (ddfHashItem)
{
unsigned len = U_strlen(ddfHashItem->toCString());
if (len == 64)
{
uint32_t hash[8] = {0};
{
U_SStream ss;
uint8_t *byte = reinterpret_cast<uint8_t*>(&hash[0]);
U_sstream_init(&ss, (void*)ddfHashItem->toCString(), len);
for (int i = 0; i < 32; i++)
byte[i] = U_sstream_get_hex_byte(&ss);
}
for (unsigned i = 0; i < matchedCount; i++)
{
const DeviceDescription &ddf = d->descriptions[matchedIndices[i]];
int j = 0;
for (; j < 8; j++)
{
if (ddf.sha256Hash[j] != hash[j])
break;
}
if (j == 8)
return ddf;
}
}
}
}
* Fallback: If none of above matches pick your poison.
*/
if (latestStableBundleIndex != invalidIndex)
return d->descriptions[latestStableBundleIndex];
if (latestBetaBundleIndex != invalidIndex)
return d->descriptions[latestBetaBundleIndex];
if (latestUserBundleIndex != invalidIndex)
return d->descriptions[latestUserBundleIndex];
if (rawJsonIndex != invalidIndex)
return d->descriptions[rawJsonIndex];
}
return d->invalidDescription;
}
bool DeviceDescriptions::loadDDFAndBundlesFromDisc(const Resource *resource)
{
Q_D(DeviceDescriptions);
const ResourceItem *modelidItem = resource->item(RAttrModelId);
const ResourceItem *mfnameItem = resource->item(RAttrManufacturerName);
U_ASSERT(modelidItem);
U_ASSERT(mfnameItem);
unsigned modelidAtomIndex = modelidItem->atomIndex();
unsigned mfnameAtomIndex = mfnameItem->atomIndex();
U_ASSERT(modelidAtomIndex != 0);
U_ASSERT(mfnameAtomIndex != 0);
if (modelidAtomIndex == 0 || mfnameAtomIndex == 0)
{
return false;
}
uint32_t mfnameLowerCaseHash = DDF_AtomLowerCaseStringHash(AT_AtomIndex{mfnameAtomIndex});
for (const DDF_LoadRecord &loadRecord : d->ddfLoadRecords)
{
if (loadRecord.mfnameLowerCaseHash == mfnameLowerCaseHash && loadRecord.modelid.index == modelidAtomIndex)
{
return false;
}
if (loadRecord.mfname.index == mfnameAtomIndex && loadRecord.modelid.index == modelidAtomIndex)
{
return false;
}
}
DBG_Printf(DBG_DDF, "try load DDF from disc for %s -- %s\n", mfnameItem->toCString(), modelidItem->toCString());
DDF_LoadRecord loadRecord;
loadRecord.modelid.index = modelidAtomIndex;
loadRecord.mfname.index = mfnameAtomIndex;
loadRecord.mfnameLowerCaseHash = mfnameLowerCaseHash;
loadRecord.loadState = DDF_LoadStateScheduled;
d->ddfLoadRecords.push_back(loadRecord);
unsigned countBefore = d->descriptions.size();
readAll();
return countBefore < d->descriptions.size();
}
const DeviceDescription &DeviceDescriptions::getFromHandle(DeviceDescription::Item::Handle hnd) const
{
Q_D(const DeviceDescriptions);
ItemHandlePack h;
h.handle = hnd;
if (h.handle != DeviceDescription::Item::InvalidItemHandle)
{
if (h.description < d->descriptions.size())
{
return d->descriptions[h.description];
}
}
return d->invalidDescription;
}
void DeviceDescriptions::put(const DeviceDescription &ddf)
{
if (!ddf.isValid())
{
return;
}
Q_D(DeviceDescriptions);
if (ddf.handle >= 0 && ddf.handle <= int(d->descriptions.size()))
{
DeviceDescription &ddf0 = d->descriptions[ddf.handle];
DBG_Assert(ddf0.handle == ddf.handle);
if (ddf.handle == ddf0.handle)
{
DBG_Printf(DBG_DDF, "update ddf %s index %d\n", qPrintable(ddf0.modelIds.front()), ddf.handle);
ddf0 = ddf;
DDF_UpdateItemHandlesForIndex(d->descriptions, d->loadCounter, static_cast<size_t>(ddf.handle));
return;
}
}
}
const DeviceDescription &DeviceDescriptions::load(const QString &path)
{
Q_UNUSED(path)
Q_D(DeviceDescriptions);
#if 0
auto i = std::find_if(d->descriptions.begin(), d->descriptions.end(), [&path](const auto &ddf){ return ddf.path == path; });
if (i != d->descriptions.end())
{
return *i;
}
auto result = DDF_ReadDeviceFile(path);
if (!result.empty())
{
for (auto &ddf : result)
{
ddf = DDF_MergeGenericItems(d->genericItems, ddf);
ddf = DDF_LoadScripts(ddf);
i = std::find_if(d->descriptions.begin(), d->descriptions.end(), [&ddf](const DeviceDescription &b)
{
return ddf.modelIds == b.modelIds && ddf.manufacturerNames == b.manufacturerNames;
});
if (i != d->descriptions.end())
{
*i = ddf;
}
else
{
d->descriptions.push_back(ddf);
}
}
DDF_UpdateItemHandles(d->descriptions, d->loadCounter);
i = std::find_if(d->descriptions.begin(), d->descriptions.end(), [&path](const auto &ddf){ return ddf.path == path; });
if (i != d->descriptions.end())
{
return *i;
}
}
#endif
return d->invalidDescription;
}
const DeviceDescription::SubDevice &DeviceDescriptions::getSubDevice(const Resource *resource) const
{
Q_D(const DeviceDescriptions);
if (resource)
{
ItemHandlePack h;
for (int i = 0; i < resource->itemCount(); i++)
{
const ResourceItem *item = resource->itemForIndex(size_t(i));
U_ASSERT(item);
h.handle = item->ddfItemHandle();
if (h.handle == DeviceDescription::Item::InvalidItemHandle)
{
continue;
}
if (h.description < d->descriptions.size())
{
const DeviceDescription &ddf = d->descriptions[h.description];
if (h.subDevice < ddf.subDevices.size())
{
const DeviceDescription::SubDevice &sub = ddf.subDevices[h.subDevice];
if (h.item < sub.items.size())
{
const DeviceDescription::Item &ddfItem = sub.items[h.item];
ItemHandlePack h2;
h2.handle = ddfItem.handle;
if (h.loadCounter == h2.loadCounter)
{
return sub;
}
}
}
}
}
}
return d->invalidSubDevice;
}
\returns The constant value on success, or the constant itself on error.
*/
QString DeviceDescriptions::constantToString(const QString &constant) const
{
Q_D(const DeviceDescriptions);
if (constant.startsWith('$'))
{
char buf[128];
AT_AtomIndex key;
int len;
for (len = 0; len < constant.size() && len < 127; len++)
{
buf[len] = constant.at(len).toLatin1();
}
buf[len] = '\0';
if (AT_GetAtomIndex(buf, (unsigned)len, &key))
{
for (size_t i = 0; i < d->constants2.size(); i++)
{
if (d->constants2[i].key.index == key.index)
{
AT_Atom a = AT_GetAtomByIndex(d->constants2[i].value);
if (a.len)
{
return QString::fromUtf8((const char*)a.data, a.len);
}
}
}
}
}
return constant;
}
QString DeviceDescriptions::stringToConstant(const QString &str) const
{
Q_D(const DeviceDescriptions);
if (str.startsWith('$'))
{
return str;
}
char buf[128];
AT_AtomIndex val;
int len;
for (len = 0; len < str.size() && len < 127; len++)
{
buf[len] = str.at(len).toLatin1();
}
buf[len] = '\0';
if (len)
{
if (AT_GetAtomIndex(buf, (unsigned)len, &val))
{
for (size_t i = 0; i < d->constants2.size(); i++)
{
if (d->constants2[i].value.index == val.index)
{
AT_Atom a = AT_GetAtomByIndex(d->constants2[i].key);
if (a.len)
{
return QString::fromUtf8((const char*)a.data, a.len);
}
break;
}
}
}
}
return str;
}
static DeviceDescription::Item *DDF_GetItemMutable(const ResourceItem *item)
{
if (!_priv || !item)
{
return nullptr;
}
DeviceDescriptionsPrivate *d = _priv;
ItemHandlePack h;
h.handle = item->ddfItemHandle();
if (h.handle == DeviceDescription::Item::InvalidItemHandle)
{
return nullptr;
}
DBG_Assert(h.description < d->descriptions.size());
if (h.description >= d->descriptions.size())
{
return nullptr;
}
auto &ddf = d->descriptions[h.description];
DBG_Assert(h.subDevice < ddf.subDevices.size());
if (h.subDevice >= ddf.subDevices.size())
{
return nullptr;
}
auto &sub = ddf.subDevices[h.subDevice];
DBG_Assert(h.item < sub.items.size());
if (h.item < sub.items.size())
{
DeviceDescription::Item *ddfItem = &sub.items[h.item];
ItemHandlePack h2;
h2.handle = ddfItem->handle;
if (h.handle == h2.handle)
{
return ddfItem;
}
}
return nullptr;
}
If \p item has a valid DDF item handle the respective entry is returned.
Otherwise the generic item list is searched based on the item.suffix.
The returned entry can be check with DeviceDescription::Item::isValid().
*/
const DeviceDescription::Item &DDF_GetItem(const ResourceItem *item)
{
Q_ASSERT(_instance);
return _instance->getItem(item);
}
*/
const DeviceDescription::Item &DeviceDescriptions::getItem(const ResourceItem *item) const
{
Q_D(const DeviceDescriptions);
ItemHandlePack h;
h.handle = item->ddfItemHandle();
if (h.handle == DeviceDescription::Item::InvalidItemHandle)
{
return getGenericItem(item->descriptor().suffix);
}
if (h.description < d->descriptions.size())
{
const auto &ddf = d->descriptions[h.description];
if (h.subDevice < ddf.subDevices.size())
{
const auto &sub = ddf.subDevices[h.subDevice];
if (h.item < sub.items.size())
{
const DeviceDescription::Item &ddfItem = sub.items[h.item];
ItemHandlePack h2;
h2.handle = ddfItem.handle;
if (h.loadCounter == h2.loadCounter)
{
return ddfItem;
}
}
}
}
return d->invalidItem;
}
const DDF_Items &DeviceDescriptions::genericItems() const
{
return d_ptr2->genericItems;
}
const DeviceDescription::Item &DeviceDescriptions::getGenericItem(const char *suffix) const
{
Q_D(const DeviceDescriptions);
for (const auto &item : d->genericItems)
{
if (item.name == QLatin1String(suffix))
{
return item;
}
}
return d->invalidItem;
}
const std::vector<DDF_FunctionDescriptor> &DeviceDescriptions::getParseFunctions() const
{
return d_ptr2->parseFunctions;
}
const std::vector<DDF_FunctionDescriptor> &DeviceDescriptions::getReadFunctions() const
{
return d_ptr2->readFunctions;
}
const std::vector<DDF_FunctionDescriptor> &DeviceDescriptions::getWriteFunctions() const
{
return d_ptr2->writeFunctions;
}
const std::vector<DDF_SubDeviceDescriptor> &DeviceDescriptions::getSubDevices() const
{
return d_ptr2->subDevices;
}
static void DDF_UpdateItemHandlesForIndex(std::vector<DeviceDescription> &descriptions, uint loadCounter, size_t index)
{
U_ASSERT(index < descriptions.size());
if (descriptions.size() <= index)
{
return;
}
U_ASSERT(index < HND_MAX_DESCRIPTIONS);
U_ASSERT(loadCounter >= HND_MIN_LOAD_COUNTER);
U_ASSERT(loadCounter <= HND_MAX_LOAD_COUNTER);
ItemHandlePack handle;
DeviceDescription &ddf = descriptions[index];
ddf.handle = static_cast<int>(index);
handle.description = static_cast<unsigned>(index);
handle.loadCounter = loadCounter;
handle.subDevice = 0;
for (DeviceDescription::SubDevice &sub : ddf.subDevices)
{
handle.item = 0;
for (DeviceDescription::Item &item : sub.items)
{
item.handle = handle.handle;
U_ASSERT(handle.item < HND_MAX_ITEMS);
handle.item++;
}
U_ASSERT(handle.subDevice < HND_MAX_SUB_DEVS);
handle.subDevice++;
}
}
The fix only applies if the JS doesn't compile and after the modified version successfully
compiles. Can be removed onces all DDFs have been updated.
Following cases are fixed:
'^let ' // let at begin of the expression
' let '
'\nlet '
'\tlet '
'(let ' // let within a scope like: for (let i=0; i < 3; i++) {...}
*/
static void DDF_TryCompileAndFixJavascript(QString *expr, const QString &path)
{
#ifdef USE_DUKTAPE_JS_ENGINE
if (DeviceJs::instance()->testCompile(*expr) == JsEvalResult::Ok)
{
return;
}
int idx = 0;
int nfixes = 0;
QString fix = *expr;
const QString letSearch("let");
for ( ; idx != -1; )
{
idx = fix.indexOf(letSearch, idx);
if (idx < 0)
{
break;
}
if (idx == 0 || fix.at(idx - 1).isSpace() || fix.at(idx - 1) == '(')
{
fix[idx + 0] = 'v';
fix[idx + 1] = 'a';
fix[idx + 2] = 'r';
idx += 4;
nfixes++;
}
}
if (nfixes > 0 && DeviceJs::instance()->testCompile(fix) == JsEvalResult::Ok)
{
*expr = fix;
return;
}
DBG_Printf(DBG_DDF, "DDF failed to compile JS: %s\n%s\n", qPrintable(path), qPrintable(DeviceJs::instance()->errorString()));
#else
Q_UNUSED(expr)
Q_UNUSED(path)
#endif
}
enum JSON_Schema
{
JSON_SCHEMA_UNKNOWN,
JSON_SCHEMA_CONSTANTS_1,
JSON_SCHEMA_CONSTANTS_2,
JSON_SCHEMA_RESOURCE_ITEM_1,
JSON_SCHEMA_SUB_DEVICE_1,
JSON_SCHEMA_DEV_CAP_1
};
The function doesn't actually parse the full JSON document
but instead just extracts the "schema": "<SCHEMA>" content.
*/
JSON_Schema DDF_GetJsonSchema(uint8_t *data, unsigned dataSize)
{
U_SStream ss[1];
unsigned beg = 0;
unsigned end = 0;
unsigned len;
U_ASSERT(data);
U_ASSERT(dataSize > 0);
if (*data != '{' && *data != '[')
{
return JSON_SCHEMA_UNKNOWN;
}
U_sstream_init(ss, data, dataSize);
if (U_sstream_find(ss, "\"schema\""))
{
U_sstream_seek(ss, ss[0].pos + 8);
if (U_sstream_find(ss, "\""))
{
U_sstream_seek(ss, ss[0].pos + 1);
beg = ss[0].pos;
if (U_sstream_find(ss, "\""))
{
end = ss[0].pos;
}
}
}
if (beg < end)
{
len = end - beg;
U_sstream_init(ss, &data[beg], len);
if (len == 19 && U_sstream_starts_with(ss, "devcap1.schema.json"))
{
return JSON_SCHEMA_DEV_CAP_1;
}
else if (len == 25 && U_sstream_starts_with(ss, "resourceitem1.schema.json"))
{
return JSON_SCHEMA_RESOURCE_ITEM_1;
}
else if (len == 22 && U_sstream_starts_with(ss, "constants1.schema.json"))
{
return JSON_SCHEMA_CONSTANTS_1;
}
else if (len == 22 && U_sstream_starts_with(ss, "constants2.schema.json"))
{
return JSON_SCHEMA_CONSTANTS_2;
}
else if (len == 22 && U_sstream_starts_with(ss, "subdevice1.schema.json"))
{
return JSON_SCHEMA_SUB_DEVICE_1;
}
}
return JSON_SCHEMA_UNKNOWN;
}
*/
void DeviceDescriptions::readAll()
{
readAllRawJson();
readAllBundles();
}
*/
void DeviceDescriptions::readAllRawJson()
{
Q_D(DeviceDescriptions);
d->loadCounter = (d->loadCounter + 1) % HND_MAX_LOAD_COUNTER;
if (d->loadCounter <= HND_MIN_LOAD_COUNTER)
{
d->loadCounter = HND_MIN_LOAD_COUNTER;
}
ScratchMemWaypoint swp;
uint8_t *ctx_mem = SCRATCH_ALLOC(uint8_t*, sizeof(DDF_ParseContext) + 64);
U_ASSERT(ctx_mem);
if (!ctx_mem)
{
DBG_Printf(DBG_ERROR, "DDF not enough memory to create DDF_ParseContext\n");
return;
}
DBG_Printf(DBG_DDF, "DDF try to find raw JSON DDFs for %u identifier pairs\n", (unsigned)d->ddfLoadRecords.size());
DDF_ParseContext *pctx = new(ctx_mem)DDF_ParseContext;
U_ASSERT(pctx);
pctx->extChunks = nullptr;
DBG_MEASURE_START(DDF_ReadRawJson);
std::vector<DDF_SubDeviceDescriptor> subDevices;
std::array<deCONZ::StorageLocation, 2> locations = { deCONZ::DdfLocation, deCONZ::DdfUserLocation};
bool hasConstants = false;
for (size_t dit = 0; dit < locations.size(); dit++)
{
const QString filePath = deCONZ::getStorageLocation(locations[dit]) + "/generic/constants.json";
pctx->filePath[0] = '\0';
pctx->filePathLength = 0;
pctx->scratchPos = 0;
{
U_SStream ss;
U_sstream_init(&ss, pctx->filePath, sizeof(pctx->filePath));
U_sstream_put_str(&ss, filePath.toUtf8().data());
pctx->filePathLength = ss.pos;
}
if (DDF_ReadFileInMemory(pctx))
{
if (DDF_ReadConstantsJson(pctx, d->constants2))
{
DBG_Printf(DBG_DDF, "DDF loaded %d string constants from %s\n", (int)d->constants2.size(), pctx->filePath);
hasConstants = true;
}
}
}
if (d->constants2.empty() || !hasConstants)
{
DBG_Printf(DBG_DDF, "DDF failed to load string constants\n");
}
U_ASSERT(hasConstants);
for (size_t dit = 0; dit < locations.size(); dit++)
{
const QString dirpath = deCONZ::getStorageLocation(locations[dit]);
QDirIterator it(dirpath, QDirIterator::Subdirectories | QDirIterator::FollowSymlinks);
while (it.hasNext())
{
it.next();
pctx->filePath[0] = '\0';
pctx->filePathLength = 0;
pctx->scratchPos = 0;
QString filePath = it.filePath();
{
U_SStream ss;
U_sstream_init(&ss, pctx->filePath, sizeof(pctx->filePath));
U_sstream_put_str(&ss, filePath.toUtf8().data());
pctx->filePathLength = ss.pos;
}
if (it.filePath().endsWith(QLatin1String("generic/constants.json")))
{
}
else if (it.fileName() == QLatin1String("button_maps.json"))
{ }
else if (it.fileName().endsWith(QLatin1String(".json")))
{
if (it.filePath().contains(QLatin1String("generic/items/")))
{
if (DDF_ReadFileInMemory(pctx))
{
DeviceDescription::Item result = DDF_ReadItemFile(pctx);
if (result.isValid())
{
result.isGenericRead = !result.readParameters.isNull() ? 1 : 0;
result.isGenericWrite = !result.writeParameters.isNull() ? 1 : 0;
result.isGenericParse = !result.parseParameters.isNull() ? 1 : 0;
size_t j = 0;
for (j = 0; j < d->genericItems.size(); j++)
{
DeviceDescription::Item &genItem = d->genericItems[j];
if (genItem.name == result.name)
{
genItem = result;
break;
}
}
if (j == d->genericItems.size())
{
d->genericItems.push_back(result);
}
}
}
}
else if (it.filePath().contains(QLatin1String("generic/subdevices/")))
{
if (DDF_ReadFileInMemory(pctx))
{
DDF_SubDeviceDescriptor result = DDF_ReadSubDeviceFile(pctx);
if (isValid(result))
{
subDevices.push_back(result);
}
}
}
else
{
if (DDF_ReadFileInMemory(pctx))
{
DeviceDescription result = DDF_ReadDeviceFile(pctx);
if (result.isValid())
{
result.storageLocation = locations[dit];
if (U_Sha256(pctx->fileData, pctx->fileDataSize, (unsigned char*)&result.sha256Hash[0]) == 0)
{
DBG_Printf(DBG_DDF, "DDF failed to create SHA-256 hash of DDF\n");
}
unsigned j = 0;
unsigned k = 0;
bool found = false;
* Check if this DDF is already loaded.
*/
for (j = 0; j < d->descriptions.size(); j++)
{
const DeviceDescription &ddf = d->descriptions[j];
for (k = 0; k < 8; k++)
{
if (ddf.sha256Hash[k] != result.sha256Hash[k])
{
break;
}
}
if (k == 8)
{
found = true;
break;
}
}
if (!found)
{
* Further check if the DDF is scheduled for loading.
* That is when an actual possibly matching device exists in the setup.
*/
bool scheduled = false;
if (result.manufacturerNames.size() == result.modelIds.size())
{
for (j = 0; j < result.manufacturerNames.size(); j++)
{
AT_AtomIndex mfnameIndex;
AT_AtomIndex modelidIndex;
uint32_t mfnameLowerCaseHash = 0;
mfnameIndex.index = 0;
modelidIndex.index = 0;
* Try to get atoms for the mfname/modelid pair.
* Note: If they don't exist, this isn't the pair we are looking for!
* We don't add atoms for all strings found in DDFs to safe memory.
*/
{
const QByteArray m = constantToString(result.manufacturerNames[j]).toUtf8();
if (AT_GetAtomIndex(m.constData(), (unsigned)m.size(), &mfnameIndex) != 1)
{
if (m.startsWith('$'))
{
DBG_Printf(DBG_DDF, "DDF failed to resolve constant %s\n", m.data());
}
else
{
continue;
}
}
else
{
mfnameLowerCaseHash = DDF_AtomLowerCaseStringHash(mfnameIndex);
}
}
{
const QByteArray m = constantToString(result.modelIds[j]).toUtf8();
if (AT_GetAtomIndex(m.constData(), (unsigned)m.size(), &modelidIndex) != 1)
{
continue;
}
}
for (k = 0; k < d->ddfLoadRecords.size(); k++)
{
if (modelidIndex.index == d->ddfLoadRecords[k].modelid.index)
{
if (mfnameLowerCaseHash == 0)
{
U_ASSERT(0);
}
else if (mfnameLowerCaseHash != d->ddfLoadRecords[k].mfnameLowerCaseHash)
{
continue;
}
scheduled = true;
break;
}
}
if (scheduled)
{
break;
}
}
}
else
{
DBG_Printf(DBG_DDF, "DDF ignore %s due unequal manufacturername/modelid array sizes\n", pctx->filePath);
}
if (scheduled)
{
* The DDF is of interest, now register all atoms for faster lookups.
*/
for (const auto &mfname : result.manufacturerNames)
{
const QString m = DeviceDescriptions::instance()->constantToString(mfname);
AT_AtomIndex ati;
if (AT_AddAtom(m.toUtf8().data(), m.size(), &ati) && ati.index != 0)
{
result.mfnameAtomIndices.push_back(ati.index);
}
}
for (const auto &modelId : result.modelIds)
{
const QString m = DeviceDescriptions::instance()->constantToString(modelId);
AT_AtomIndex ati;
if (AT_AddAtom(m.toUtf8().data(), m.size(), &ati) && ati.index != 0)
{
result.modelidAtomIndices.push_back(ati.index);
}
}
DBG_Printf(DBG_DDF, "DDF cache raw JSON DDF %s\n", pctx->filePath);
d->descriptions.push_back(std::move(result));
DDF_UpdateItemHandlesForIndex(d->descriptions, d->loadCounter, d->descriptions.size() - 1);
}
}
}
}
}
}
}
}
if (!subDevices.empty())
{
std::sort(subDevices.begin(), subDevices.end(), [](const auto &a, const auto &b){
return a.name < b.name;
});
d->subDevices = std::move(subDevices);
}
if (!d->descriptions.empty())
{
for (auto &ddf : d->descriptions)
{
ddf = DDF_MergeGenericItems(d->genericItems, ddf);
ddf = DDF_LoadScripts(ddf);
}
DBG_Printf(DBG_DDF, "DDF loaded %d raw JSON DDFs\n", (int)d->descriptions.size());
}
DBG_MEASURE_END(DDF_ReadRawJson);
}
#if 0
{
U_ECC_PrivateKeySecp256k1 privkey = {0};
U_ECC_PublicKeySecp256k1 pubkey = {0};
U_ECC_SignatureSecp256k1 sig = {0};
unsigned char hash[U_SHA256_HASH_SIZE] = {0};
if (U_ECC_CreateKeyPairSecp256k1(&privkey, &pubkey) == 1)
{
DBG_Printf(DBG_INFO, "created keypair\n");
U_Sha256(&privkey, sizeof(privkey), hash);
if (U_ECC_SignSecp256K1(&privkey, hash, sizeof(hash), &sig))
{
DBG_Printf(DBG_INFO, "created signature\n");
if (U_ECC_VerifySignatureSecp256k1(&pubkey, &sig, hash, sizeof(hash)))
{
DBG_Printf(DBG_INFO, "verified signature\n");
}
sig.sig[6] = sig.sig[6] + 9;
if (U_ECC_VerifySignatureSecp256k1(&pubkey, &sig, hash, sizeof(hash)) == 0)
{
DBG_Printf(DBG_INFO, "invalid signature [OK]\n");
}
}
}
}
{
U_HmacSha256Test();
}
#endif
*/
static int DDF_ReloadBundleDevices(const char *desc, unsigned descSize, std::vector<DDF_LoadRecord> &ddfLoadRecords)
{
cj_ctx cj[1];
char buf[96];
cj_token_ref ref;
cj_token_ref parent_ref;
cj_token_ref deviceids_ref;
AT_AtomIndex modelid_ati;
AT_AtomIndex mfname_ati;
cj_token *tokens;
unsigned n_tokens = 1024;
int n_marked = 0;
ScratchMemWaypoint swp;
tokens = SCRATCH_ALLOC(cj_token*, n_tokens * sizeof(*tokens));
U_ASSERT(tokens);
if (!tokens)
return 0;
cj_parse_init(cj, desc, descSize, tokens, n_tokens);
cj_parse(cj);
if (cj->status != CJ_OK)
return 0;
parent_ref = 0;
deviceids_ref = cj_value_ref(cj, parent_ref, "device_identifiers");
if (deviceids_ref == CJ_INVALID_TOKEN_INDEX)
return 0;
if (tokens[deviceids_ref].type != CJ_TOKEN_ARRAY_BEG)
return 0;
for (ref = deviceids_ref + 1; tokens[ref].type != CJ_TOKEN_ARRAY_END && ref < cj[0].tokens_pos; )
{
if (tokens[ref].type == CJ_TOKEN_ITEM_SEP)
{
ref++;
continue;
}
if (tokens[ref].type != CJ_TOKEN_ARRAY_BEG)
break;
if (tokens[ref + 1].type != CJ_TOKEN_STRING)
break;
if (tokens[ref + 2].type != CJ_TOKEN_ITEM_SEP)
break;
if (tokens[ref + 3].type != CJ_TOKEN_STRING)
break;
if (tokens[ref + 4].type != CJ_TOKEN_ARRAY_END)
break;
* Lookup if the manufacturername and modelid pair has registered atoms.
* If not this can't be a bundle of interest.
*/
bool foundAtoms = true;
if (cj_copy_ref_utf8(cj, buf, sizeof(buf), ref + 1) == 0)
break;
if (AT_GetAtomIndex(buf, U_strlen(buf), &mfname_ati) == 0)
foundAtoms = false;
if (cj_copy_ref_utf8(cj, buf, sizeof(buf), ref + 3) == 0)
break;
if (AT_GetAtomIndex(buf, U_strlen(buf), &modelid_ati) == 0)
foundAtoms = false;
ref += 5;
if (!foundAtoms)
continue;
for (size_t j = 0; j < ddfLoadRecords.size(); j++)
{
DDF_LoadRecord &rec = ddfLoadRecords[j];
if (rec.mfname.index != mfname_ati.index)
continue;
if (rec.modelid.index != modelid_ati.index)
continue;
rec.loadState = DDF_LoadStateScheduled;
n_marked++;
}
}
return n_marked > 0;
}
static int DDF_IsBundleScheduled(DDF_ParseContext *pctx, const char *desc, unsigned descSize, const std::vector<DDF_LoadRecord> &ddfLoadRecords)
{
cj_ctx cj[1];
char buf[96];
cj_token_ref ref;
cj_token_ref parent_ref;
cj_token_ref deviceids_ref;
cj_token_ref last_modified_ref;
AT_AtomIndex modelid_ati;
AT_AtomIndex mfname_ati;
cj_token *tokens = pctx->tokens.data();
cj_parse_init(cj, desc, descSize, pctx->tokens.data(), (cj_size)pctx->tokens.size());
cj_parse(cj);
if (cj->status != CJ_OK)
return 0;
parent_ref = 0;
deviceids_ref = cj_value_ref(cj, parent_ref, "device_identifiers");
last_modified_ref = cj_value_ref(cj, parent_ref, "last_modified");
if (last_modified_ref == CJ_INVALID_TOKEN_INDEX)
return 0;
if (tokens[last_modified_ref].type != CJ_TOKEN_STRING)
return 0;
{
cj_token *tok = &tokens[last_modified_ref];
pctx->bundleLastModified = U_TimeFromISO8601(&desc[tok->pos], tok->len);
}
if (deviceids_ref == CJ_INVALID_TOKEN_INDEX)
return 0;
if (tokens[deviceids_ref].type != CJ_TOKEN_ARRAY_BEG)
return 0;
for (ref = deviceids_ref + 1; tokens[ref].type != CJ_TOKEN_ARRAY_END && ref < cj[0].tokens_pos; )
{
if (tokens[ref].type == CJ_TOKEN_ITEM_SEP)
{
ref++;
continue;
}
if (tokens[ref].type != CJ_TOKEN_ARRAY_BEG)
break;
if (tokens[ref + 1].type != CJ_TOKEN_STRING)
break;
if (tokens[ref + 2].type != CJ_TOKEN_ITEM_SEP)
break;
if (tokens[ref + 3].type != CJ_TOKEN_STRING)
break;
if (tokens[ref + 4].type != CJ_TOKEN_ARRAY_END)
break;
* Lookup if the manufacturername and modelid pair has registered atoms.
* If not this can't be a bundle of interest.
*/
bool foundAtoms = true;
if (cj_copy_ref_utf8(cj, buf, sizeof(buf), ref + 1) == 0)
break;
if (AT_GetAtomIndex(buf, U_strlen(buf), &mfname_ati) == 0)
foundAtoms = false;
if (cj_copy_ref_utf8(cj, buf, sizeof(buf), ref + 3) == 0)
break;
if (AT_GetAtomIndex(buf, U_strlen(buf), &modelid_ati) == 0)
foundAtoms = false;
ref += 5;
if (!foundAtoms)
continue;
uint32_t mfnameLowerCaseHash = DDF_AtomLowerCaseStringHash(mfname_ati);
for (size_t j = 0; j < ddfLoadRecords.size(); j++)
{
const DDF_LoadRecord &rec = ddfLoadRecords[j];
if (rec.modelid.index != modelid_ati.index)
continue;
if (rec.mfnameLowerCaseHash != mfnameLowerCaseHash)
continue;
return 1;
}
}
return 0;
}
void DEV_DDF_BundleUpdated(unsigned char *data, unsigned dataSize)
{
U_BStream bs;
unsigned chunkSize;
U_bstream_init(&bs, data, dataSize);
if (DDFB_FindChunk(&bs, "RIFF", &chunkSize) == 0)
return;
if (DDFB_FindChunk(&bs, "DDFB", &chunkSize) == 0)
return;
if (DDFB_FindChunk(&bs, "DESC", &chunkSize) == 0)
return;
if (DDF_ReloadBundleDevices((char*)&bs.data[bs.pos], chunkSize, _priv->ddfLoadRecords) != 0)
{
_priv->ddfReloadWhat = DDF_ReloadBundles;
_priv->ddfReloadTimer->stop();
_priv->ddfReloadTimer->start(2000);
}
}
void DeviceDescriptions::reloadAllRawJsonAndBundles(const Resource *resource)
{
const ResourceItem *mfnameItem = resource->item(RAttrManufacturerName);
const ResourceItem *modelidItem = resource->item(RAttrModelId);
unsigned mfnameAtomIndex = mfnameItem->atomIndex();
unsigned modelidAtomIndex = modelidItem->atomIndex();
uint32_t mfnameLowerCaseHash = DDF_AtomLowerCaseStringHash(AT_AtomIndex{mfnameAtomIndex});
for (size_t j = 0; j < d_ptr2->ddfLoadRecords.size(); j++)
{
DDF_LoadRecord &rec = d_ptr2->ddfLoadRecords[j];
if (rec.mfnameLowerCaseHash != mfnameLowerCaseHash)
continue;
if (rec.modelid.index != modelidAtomIndex)
continue;
if (rec.loadState != DDF_LoadStateScheduled)
{
rec.loadState = DDF_LoadStateScheduled;
}
}
d_ptr2->ddfReloadWhat = DDF_ReloadAll;
d_ptr2->ddfReloadTimer->stop();
d_ptr2->ddfReloadTimer->start(1000);
}
void DeviceDescriptions::ddfReloadTimerFired()
{
if (d_ptr2->ddfReloadWhat == DDF_ReloadAll)
{
readAll();
}
else if (d_ptr2->ddfReloadWhat == DDF_ReloadBundles)
{
readAllBundles();
}
d_ptr2->ddfReloadWhat = DDF_ReloadIdle;
for (DDF_LoadRecord &rec : d_ptr2->ddfLoadRecords)
{
if (rec.loadState == DDF_LoadStateScheduled)
{
rec.loadState = DDF_LoadStateLoaded;
DEV_ReloadDeviceIdendifier(rec.mfname.index, rec.modelid.index);
}
}
}
*/
void DeviceDescriptions::readAllBundles()
{
Q_D(DeviceDescriptions);
ScratchMemWaypoint swp;
uint8_t *ctx_mem = SCRATCH_ALLOC(uint8_t*, sizeof(DDF_ParseContext) + 64);
U_ASSERT(ctx_mem);
if (!ctx_mem)
{
DBG_Printf(DBG_ERROR, "DDF not enough memory to create DDF_ParseContext\n");
return;
}
DDF_ParseContext *pctx = new(ctx_mem)DDF_ParseContext;
U_ASSERT(pctx);
DBG_MEASURE_START(DDF_ReadBundles);
FS_Dir dir;
FS_File fp;
U_SStream ss;
U_BStream bs;
unsigned chunkSize;
unsigned basePathLength;
unsigned scratchPosPerBundle;
deCONZ::StorageLocation locations[2] = { deCONZ::DdfBundleUserLocation, deCONZ::DdfBundleLocation };
for (int dit = 0; dit < 2; dit++)
{
{
QByteArray loc = deCONZ::getStorageLocation(locations[dit]).toUtf8();
U_sstream_init(&ss, pctx->filePath, sizeof(pctx->filePath));
U_sstream_put_str(&ss, loc.data());
basePathLength = ss.pos;
}
scratchPosPerBundle = ScratchMemPos();
if (FS_OpenDir(&dir, pctx->filePath))
{
for (;FS_ReadDir(&dir);)
{
if (dir.entry.type != FS_TYPE_FILE)
continue;
U_sstream_init(&ss, dir.entry.name, strlen(dir.entry.name));
if (U_sstream_find(&ss, ".ddf") == 0 && U_sstream_find(&ss, ".ddb") == 0)
continue;
ScratchMemRewind(scratchPosPerBundle);
U_sstream_init(&ss, pctx->filePath, sizeof(pctx->filePath));
ss.pos = basePathLength;
U_sstream_put_str(&ss, "/");
U_sstream_put_str(&ss, dir.entry.name);
pctx->filePathLength = ss.pos;
pctx->bundleLastModified = 0;
pctx->extChunks = nullptr;
pctx->signatures = 0;
if (DDF_ReadFileInMemory(pctx) == 0)
continue;
unsigned ddfbChunkOffset;
unsigned ddfbChunkSize;
uint32_t ddfbHash[8];
unsigned char *fileData;
unsigned fileDataSize;
fileData = pctx->fileData;
fileDataSize = pctx->fileDataSize;
U_bstream_init(&bs, pctx->fileData, pctx->fileDataSize);
if (DDFB_FindChunk(&bs, "RIFF", &chunkSize) == 0)
continue;
if (DDFB_FindChunk(&bs, "DDFB", &chunkSize) == 0)
continue;
ddfbChunkOffset = bs.pos;
ddfbChunkSize = chunkSize;
{
U_Sha256(&pctx->fileData[ddfbChunkOffset - 8], ddfbChunkSize + 8, (uint8_t*)&ddfbHash[0]);
unsigned i;
unsigned k;
for (i = 0; i < d->descriptions.size(); i++)
{
uint32_t *hash0 = d->descriptions[i].sha256Hash;
for (k = 0; k < 8; k++)
{
if (hash0[k] != ddfbHash[k])
break;
}
if (k == 8)
break;
}
if (i < d->descriptions.size())
continue;
}
if (DDFB_FindChunk(&bs, "DESC", &chunkSize) == 0)
continue;
* Only load bundles into memory for devices which are present.
*/
if (DDF_IsBundleScheduled(pctx, (char*)&bs.data[bs.pos], chunkSize, d->ddfLoadRecords) == 0)
continue;
U_bstream_init(&bs, &fileData[ddfbChunkOffset], ddfbChunkSize);
for (;bs.status == U_BSTREAM_OK;)
{
if (DDFB_IsChunk(&bs, "EXTF"))
{
DDFB_ExtfChunk *extf = SCRATCH_ALLOC(DDFB_ExtfChunk*, sizeof (*extf));
if (extf && DDFB_ReadExtfChunk(&bs, extf))
{
extf->next = pctx->extChunks;
pctx->extChunks = extf;
continue;
}
}
DDFB_SkipChunk(&bs);
}
if (!pctx->extChunks)
continue;
if (DDF_ReadConstantsJson(pctx, d->constants2))
{
}
* Now process the actual DDF content which is in ETXF chunk with type DDFC.
*/
DDFB_ExtfChunk *extfDDFC = nullptr;
for (DDFB_ExtfChunk *extf = pctx->extChunks; extf; extf = extf->next)
{
if (extf->fileType[0] != 'D' || extf->fileType[1] != 'D' || extf->fileType[2] != 'F' || extf->fileType[3] != 'C')
continue;
JSON_Schema schema = DDF_GetJsonSchema(extf->fileData, extf->fileSize);
if (schema == JSON_SCHEMA_DEV_CAP_1)
{
extfDDFC = extf;
break;
}
}
if (!extfDDFC)
continue;
pctx->fileData = extfDDFC->fileData;
pctx->fileDataSize = extfDDFC->fileSize;
DeviceDescription ddf = DDF_ReadDeviceFile(pctx);
if (!ddf.isValid())
{
continue;
}
U_bstream_init(&bs, &fileData[8], fileDataSize - 8);
if (DDFB_FindChunk(&bs, "SIGN", &chunkSize) == 1)
{
U_bstream_init(&bs, &bs.data[bs.pos], chunkSize);
DDF_ProcessSignatures(pctx, d->publicKeys, &bs, ddfbHash);
}
ddf.storageLocation = locations[dit];
ddf.lastModified = pctx->bundleLastModified;
ddf.signedBy = pctx->signatures;
if (DDF_MergeGenericBundleItems(ddf, pctx) == 0)
{
continue;
}
for (unsigned i = 0; i < 8; i++)
ddf.sha256Hash[i] = ddfbHash[i];
{
* The DDF is of interest, now register all atoms for faster lookups.
*/
for (const auto &mfname : ddf.manufacturerNames)
{
const QString m = constantToString(mfname);
AT_AtomIndex ati;
if (AT_AddAtom(m.toUtf8().data(), m.size(), &ati) && ati.index != 0)
{
ddf.mfnameAtomIndices.push_back(ati.index);
}
}
for (const auto &modelId : ddf.modelIds)
{
const QString m = constantToString(modelId);
AT_AtomIndex ati;
if (AT_AddAtom(m.toUtf8().data(), m.size(), &ati) && ati.index != 0)
{
ddf.modelidAtomIndices.push_back(ati.index);
}
}
d->descriptions.push_back(std::move(ddf));
DDF_UpdateItemHandlesForIndex(d->descriptions, d->loadCounter, d->descriptions.size() - 1);
}
DBG_Printf(DBG_DDF, "DDF bundle: %s, size: %u bytes\n", ss.str, pctx->fileDataSize);
}
FS_CloseDir(&dir);
}
}
DBG_MEASURE_END(DDF_ReadBundles);
}
Currently this is done syncronously later on it will be async to not block
the main thread while loading DDF files.
*/
void DeviceDescriptions::handleDDFInitRequest(const Event &event)
{
Q_D(DeviceDescriptions);
auto *resource = DEV_GetResource(RDevices, QString::number(event.deviceKey()));
int result = -1;
if (resource)
{
const DeviceDescription &ddf = get(resource, DDF_EvalMatchExpr);
if (ddf.isValid())
{
result = 0;
if (!DEV_TestManaged() && !DDF_IsStatusEnabled(ddf.status))
{
result = 2;
}
else if (DEV_InitDeviceFromDescription(static_cast<Device*>(resource), ddf))
{
result = 1;
if (ddf.status == QLatin1String("Draft"))
{
result = 2;
}
else if (ddf.storageLocation == deCONZ::DdfBundleLocation || ddf.storageLocation == deCONZ::DdfBundleUserLocation)
{
result = 3;
}
}
}
if (result >= 0)
{
DBG_Printf(DBG_INFO, "DEV found DDF for " FMT_MAC ", path: %s, result: %d\n", FMT_MAC_CAST(event.deviceKey()), qPrintable(ddf.path), result);
}
if (result == 0)
{
DBG_Printf(DBG_INFO, "DEV init Device from DDF for " FMT_MAC " failed\n", FMT_MAC_CAST(event.deviceKey()));
}
else if (result == -1)
{
DBG_Printf(DBG_INFO, "DEV no DDF for " FMT_MAC ", modelId: %s\n", FMT_MAC_CAST(event.deviceKey()), resource->item(RAttrModelId)->toCString());
DBG_Printf(DBG_INFO, "DEV create on-the-fly DDF for " FMT_MAC "\n", FMT_MAC_CAST(event.deviceKey()));
DeviceDescription ddf1;
Device *device = static_cast<Device*>(resource);
if (DEV_InitBaseDescriptionForDevice(device, ddf1))
{
* Register all atoms for faster lookups.
*/
for (const auto &mfname : ddf1.manufacturerNames)
{
const QString m = constantToString(mfname);
AT_AtomIndex ati;
if (AT_AddAtom(m.toUtf8().data(), m.size(), &ati) && ati.index != 0)
{
ddf1.mfnameAtomIndices.push_back(ati.index);
}
}
for (const auto &modelId : ddf1.modelIds)
{
const QString m = constantToString(modelId);
AT_AtomIndex ati;
if (AT_AddAtom(m.toUtf8().data(), m.size(), &ati) && ati.index != 0)
{
ddf1.modelidAtomIndices.push_back(ati.index);
}
}
ddf1.storageLocation = deCONZ::DdfUserLocation;
d->descriptions.push_back(std::move(ddf1));
DDF_UpdateItemHandlesForIndex(d->descriptions, d->loadCounter, d->descriptions.size() - 1);
}
}
}
emit eventNotify(Event(RDevices, REventDDFInitResponse, result, event.deviceKey()));
}
*/
static int DDF_ReadConstantsJson(DDF_ParseContext *pctx, std::vector<ConstantEntry> &constants)
{
cj_ctx ctx;
cj_ctx *cj;
cj_token *tok;
cj_token_ref ref;
ConstantEntry constEntry;
const char *fileData = (const char*)pctx->fileData;
unsigned fileDataSize = pctx->fileDataSize;
if (pctx->extChunks)
{
fileData = nullptr;
fileDataSize = 0;
for (DDFB_ExtfChunk *extf = pctx->extChunks; extf; extf = extf->next)
{
if (extf->fileType[0] != 'J' || extf->fileType[1] != 'S' || extf->fileType[2] != 'O' || extf->fileType[3] != 'N')
continue;
JSON_Schema schema = DDF_GetJsonSchema(extf->fileData, extf->fileSize);
if (schema == JSON_SCHEMA_CONSTANTS_2)
{
fileData = (const char*)extf->fileData;
fileDataSize = extf->fileSize;
break;
}
}
}
if (!fileData || fileDataSize == 0)
{
return 0;
}
auto &tokens = pctx->tokens;
cj = &ctx;
cj_parse_init(cj, fileData, fileDataSize, tokens.data(), tokens.size());
cj_parse(cj);
if (cj->status == CJ_OK)
{
for (ref = 0; ref < cj->tokens_pos; ref++)
{
tok = &cj->tokens[ref];
if (tok->type == CJ_TOKEN_STRING && (ref + 2) < cj->tokens_pos)
{
if (cj->buf[tok->pos] == '$' && tok[1].type == CJ_TOKEN_NAME_SEP && tok[2].type == CJ_TOKEN_STRING)
{
if (tok[0].len < 2 || tok[2].len < 2)
{
}
else if (tok[0].len > AT_MAX_ATOM_SIZE || tok[2].len > AT_MAX_ATOM_SIZE)
{
}
else if (AT_AddAtom(&cj->buf[tok[0].pos], tok[0].len, &constEntry.key) == 1 &&
AT_AddAtom(&cj->buf[tok[2].pos], tok[2].len, &constEntry.value) == 1)
{
for (size_t i = 0; i < constants.size(); i++)
{
if (constants[i].key.index == constEntry.key.index && constants[i].value.index == constEntry.value.index)
{
constEntry.key.index = 0;
constEntry.value.index = 0;
break;
}
}
if (constEntry.key.index != 0 && constEntry.value.index != 0)
{
constants.push_back(constEntry);
}
}
}
}
}
return 1;
}
return 0;
}
ApiDataType API_DataTypeFromString(const QString &str)
{
if (str == QLatin1String("bool")) return DataTypeBool;
if (str == QLatin1String("uint8")) return DataTypeUInt8;
if (str == QLatin1String("uint16")) return DataTypeUInt16;
if (str == QLatin1String("uint32")) return DataTypeUInt32;
if (str == QLatin1String("uint64")) return DataTypeUInt64;
if (str == QLatin1String("int8")) return DataTypeInt8;
if (str == QLatin1String("int16")) return DataTypeInt16;
if (str == QLatin1String("int32")) return DataTypeInt32;
if (str == QLatin1String("int64")) return DataTypeInt64;
if (str == QLatin1String("string")) return DataTypeString;
if (str == QLatin1String("double")) return DataTypeReal;
if (str == QLatin1String("time")) return DataTypeTime;
if (str == QLatin1String("timepattern")) return DataTypeTimePattern;
return DataTypeUnknown;
}
static int DDF_ReadFileInMemory(DDF_ParseContext *pctx)
{
FS_File f;
long remaining = (pctx->scratchPos < pctx->scratchMem.size()) ? pctx->scratchMem.size() - pctx->scratchPos : 0;
pctx->scratchPos = 0;
pctx->fileData = nullptr;
pctx->fileDataSize = 0;
if (FS_OpenFile(&f, FS_MODE_R, pctx->filePath))
{
long fsize = FS_GetFileSize(&f);
if (fsize + 1 > remaining)
{
}
else if (fsize > 0)
{
unsigned char *data = pctx->scratchMem.data() + pctx->scratchPos;
long n = FS_ReadFile(&f, data, remaining);
if (n == fsize)
{
FS_CloseFile(&f);
data[n] = '\0';
pctx->scratchPos += (n + 1);
pctx->fileData = data;
pctx->fileDataSize = n;
return 1;
}
}
FS_CloseFile(&f);
}
return 0;
}
\returns A parsed item, use DeviceDescription::Item::isValid() to check for success.
*/
static DeviceDescription::Item DDF_ParseItem(DDF_ParseContext *pctx, const QJsonObject &obj)
{
DeviceDescription::Item result{};
bool hasSchema = obj.contains(QLatin1String("schema"));
if (obj.contains(QLatin1String("name")))
{
result.name = obj.value(QLatin1String("name")).toString().toUtf8().constData();
}
else if (obj.contains(QLatin1String("id")))
{
result.name = obj.value(QLatin1String("id")).toString().toUtf8().constData();
}
if (!hasSchema)
{
if (result.name == RConfigColorCapabilities) { result.name = RCapColorCapabilities; }
if (result.name == RConfigCtMax) { result.name = RCapColorCtMax; }
if (result.name == RConfigCtMin) { result.name = RCapColorCtMin; }
}
if (obj.contains(QLatin1String("description")))
{
result.description = obj.value(QLatin1String("description")).toString();
}
if (result.name.empty())
{
return {};
}
if (!getResourceItemDescriptor(result.name, result.descriptor))
{
QString schema;
if (hasSchema)
{
schema = obj.value(QLatin1String("schema")).toString();
}
if (schema == QLatin1String("resourceitem1.schema.json"))
{
QString dataType;
ResourceItemDescriptor rid{};
if (obj.contains(QLatin1String("access")))
{
const auto access = obj.value(QLatin1String("access")).toString();
if (access == QLatin1String("R"))
{
rid.access = ResourceItemDescriptor::Access::ReadOnly;
}
else if (access == QLatin1String("RW"))
{
rid.access = ResourceItemDescriptor::Access::ReadWrite;
}
}
if (obj.contains(QLatin1String("datatype")))
{
QString dataType = obj.value(QLatin1String("datatype")).toString().toLower();
rid.type = API_DataTypeFromString(dataType);
if (dataType.startsWith("uint") || dataType.startsWith("int"))
{
rid.qVariantType = QVariant::Double;
}
else if (rid.type == DataTypeReal)
{
rid.qVariantType = QVariant::Double;
}
else if (rid.type == DataTypeBool)
{
rid.qVariantType = QVariant::Bool;
}
else
{
DBG_Assert(rid.type == DataTypeString || rid.type == DataTypeTime || rid.type == DataTypeTimePattern);
rid.qVariantType = QVariant::String;
}
}
if (obj.contains(QLatin1String("range")))
{
const auto range = obj.value(QLatin1String("range")).toArray();
if (range.count() == 2)
{
bool ok1 = false;
bool ok2 = false;
double rangeMin = range.at(0).toString().toDouble(&ok1);
double rangeMax = range.at(1).toString().toDouble(&ok2);
if (ok1 && ok2)
{
rid.validMin = rangeMin;
rid.validMax = rangeMax;
}
}
}
if (rid.isValid())
{
rid.flags = ResourceItem::FlagDynamicDescriptor;
size_t len = result.name.size();
char *dynSuffix = new char[len + 1];
memcpy(dynSuffix, result.name.c_str(), len);
dynSuffix[len] = '\0';
rid.suffix = dynSuffix;
if (R_AddResourceItemDescriptor(rid))
{
DBG_Printf(DBG_DDF, "DDF added dynamic ResourceItemDescriptor %s\n", result.name.c_str());
}
}
}
else
{
DBG_Printf(DBG_DDF, "DDF unsupported ResourceItem schema: %s\n", qPrintable(schema));
}
}
if (getResourceItemDescriptor(result.name, result.descriptor))
{
if (obj.contains(QLatin1String("access")))
{
const auto access = obj.value(QLatin1String("access")).toString();
if (access == "R")
{
result.descriptor.access = ResourceItemDescriptor::Access::ReadOnly;
}
else if (access == "RW")
{
result.descriptor.access = ResourceItemDescriptor::Access::ReadWrite;
}
}
if (obj.contains(QLatin1String("public")))
{
result.isPublic = obj.value(QLatin1String("public")).toBool() ? 1 : 0;
result.hasIsPublic = 1;
}
if (obj.contains(QLatin1String("implicit")))
{
result.isImplicit = obj.value(QLatin1String("implicit")).toBool() ? 1 : 0;
}
if (obj.contains(QLatin1String("awake")))
{
result.awake = obj.value(QLatin1String("awake")).toBool() ? 1 : 0;
}
if (obj.contains(QLatin1String("managed")))
{
result.isManaged = obj.value(QLatin1String("managed")).toBool() ? 1 : 0;
}
if (obj.contains(QLatin1String("static")))
{
result.isStatic = 1;
result.defaultValue = obj.value(QLatin1String("static")).toVariant();
}
else
{
if (obj.contains(QLatin1String("default")))
{
result.defaultValue = obj.value(QLatin1String("default")).toVariant();
}
const auto parse = obj.value(QLatin1String("parse"));
if (parse.isObject())
{
result.parseParameters = parse.toVariant();
}
const auto read = obj.value(QLatin1String("read"));
if (read.isObject())
{
result.readParameters = read.toVariant();
}
if (obj.contains(QLatin1String("refresh.interval")))
{
result.refreshInterval = obj.value(QLatin1String("refresh.interval")).toInt(0);
}
const auto write = obj.value(QLatin1String("write"));
if (write.isObject())
{
result.writeParameters = write.toVariant();
}
}
if (DBG_IsEnabled(DBG_INFO_L2))
{
DBG_Printf(DBG_DDF, "DDF loaded resource item descriptor: %s, public: %u\n", result.descriptor.suffix, (result.isPublic ? 1 : 0));
}
}
else
{
DBG_Printf(DBG_DDF, "DDF failed to load resource item descriptor: %s\n", result.name.c_str());
}
return result;
}
\returns The sub device object, use DeviceDescription::SubDevice::isValid() to check for success.
*/
static DeviceDescription::SubDevice DDF_ParseSubDevice(DDF_ParseContext *pctx, const QJsonObject &obj)
{
DeviceDescription::SubDevice result;
result.type = obj.value(QLatin1String("type")).toString();
if (result.type.isEmpty())
{
return result;
}
result.restApi = obj.value(QLatin1String("restapi")).toString();
if (result.restApi.isEmpty())
{
return result;
}
if (obj.contains(QLatin1String("meta")))
{
auto meta = obj.value(QLatin1String("meta"));
if (meta.isObject())
{
result.meta = meta.toVariant().toMap();
}
}
const auto uniqueId = obj.value(QLatin1String("uuid"));
if (uniqueId.isArray())
{
const auto arr = uniqueId.toArray();
for (const auto &i : arr)
{
result.uniqueId.push_back(i.toString());
}
}
const auto fingerPrint = obj.value(QLatin1String("fingerprint"));
if (fingerPrint.isObject())
{
bool ok;
const auto fp = fingerPrint.toObject();
result.fingerPrint.endpoint = fp.value(QLatin1String("endpoint")).toString().toUInt(&ok, 0);
result.fingerPrint.profileId = ok ? fp.value(QLatin1String("profile")).toString().toUInt(&ok, 0) : 0;
result.fingerPrint.deviceId = ok ? fp.value(QLatin1String("device")).toString().toUInt(&ok, 0) : 0;
if (fp.value(QLatin1String("in")).isArray())
{
const auto arr = fp.value(QLatin1String("in")).toArray();
for (const auto &cl : arr)
{
const auto clusterId = ok ? cl.toString().toUInt(&ok, 0) : 0;
if (ok)
{
result.fingerPrint.inClusters.push_back(clusterId);
}
}
}
if (fp.value(QLatin1String("out")).isArray())
{
const auto arr = fp.value(QLatin1String("out")).toArray();
for (const auto &cl : arr)
{
const auto clusterId = ok ? cl.toString().toUInt(&ok, 0) : 0;
if (ok)
{
result.fingerPrint.outClusters.push_back(clusterId);
}
}
}
if (!ok)
{
result.fingerPrint = { };
}
}
{
#if 0
if (obj.value(QLatin1String("buttons")).isObject())
{
const auto buttons = obj.value(QLatin1String("buttons")).toObject();
for (auto bi = buttons.constBegin(); bi != buttons.constEnd(); bi++)
{
DBG_Printf(DBG_INFO, "BUTTON: %s\n", qPrintable(bi.key()));
}
DBG_Flush();
}
#endif
if (obj.value(QLatin1String("buttonevents")).isObject())
{
const auto buttonEvents = obj.value(QLatin1String("buttonevents")).toObject();
for (auto bi = buttonEvents.constBegin(); bi != buttonEvents.constEnd(); bi++)
{
bool ok;
unsigned buttonEvent = bi.key().toUInt(&ok);
if (ok)
{
auto i = std::find(result.buttonEvents.cbegin(), result.buttonEvents.cend(), buttonEvent);
if (i == result.buttonEvents.cend())
{
result.buttonEvents.push_back(buttonEvent);
}
}
}
}
}
const auto items = obj.value(QLatin1String("items"));
if (!items.isArray())
{
return result;
}
{
const auto arr = items.toArray();
for (const auto &i : arr)
{
if (i.isObject())
{
const auto item = DDF_ParseItem(pctx, i.toObject());
if (item.isValid())
{
result.items.push_back(item);
}
else
{
}
}
}
}
return result;
}
\returns The ZCL report, use DDF_ZclReport::isValid() to check for success.
*/
static DDF_ZclReport DDF_ParseZclReport(const QJsonObject &obj)
{
DDF_ZclReport result{};
if (!obj.contains(QLatin1String("at")) ||
!obj.contains(QLatin1String("dt")) ||
!obj.contains(QLatin1String("min")) ||
!obj.contains(QLatin1String("max")))
{
return {};
}
bool ok = false;
result.attributeId = obj.value(QLatin1String("at")).toString().toUShort(&ok, 0);
if (!ok)
{
return {};
}
{
auto dataType = obj.value(QLatin1String("dt")).toString().toUShort(&ok, 0);
if (!ok || dataType > 0xFF)
{
return {};
}
result.dataType = dataType;
}
{
const auto minInterval = obj.value(QLatin1String("min")).toInt(-1);
if (minInterval < 0 || minInterval > UINT16_MAX)
{
return {};
}
result.minInterval = minInterval;
}
{
const auto maxInterval = obj.value(QLatin1String("max")).toInt(-1);
if (maxInterval < 0 || maxInterval > UINT16_MAX)
{
return {};
}
result.maxInterval = maxInterval;
}
if (obj.contains(QLatin1String("change")))
{
result.reportableChange = obj.value(QLatin1String("change")).toString().toUInt(&ok, 0);
if (!ok)
{
return {};
}
}
if (obj.contains(QLatin1String("mf")))
{
result.manufacturerCode = obj.value(QLatin1String("mf")).toString().toUShort(&ok, 0);
if (!ok)
{
return {};
}
}
result.valid = true;
return result;
}
\returns The binding, use DDF_Binding::isValid() to check for success.
*/
static DDF_Binding DDF_ParseBinding(const QJsonObject &obj)
{
DDF_Binding result{};
if (!obj.contains(QLatin1String("bind")) ||
!obj.contains(QLatin1String("src.ep")) ||
!obj.contains(QLatin1String("cl")))
{
return {};
}
const auto type = obj.value(QLatin1String("bind")).toString();
if (type == QLatin1String("unicast"))
{
result.isUnicastBinding = 1;
}
else if (type == QLatin1String("groupcast"))
{
result.isGroupBinding = 1;
}
else
{
return {};
}
bool ok = false;
{
const auto srcEndpoint = obj.value(QLatin1String("src.ep")).toInt(-1);
if (srcEndpoint < 0 || srcEndpoint > UINT8_MAX)
{
return {};
}
result.srcEndpoint = srcEndpoint;
}
{
result.clusterId = obj.value(QLatin1String("cl")).toString().toUShort(&ok, 0);
if (!ok)
{
return {};
}
}
if (obj.contains(QLatin1String("dst.ep")))
{
const auto dstEndpoint = obj.value(QLatin1String("dst.ep")).toInt(-1);
if (dstEndpoint < 0 || dstEndpoint >= 255)
{
return {};
}
result.dstEndpoint = dstEndpoint;
}
else
{
result.dstEndpoint = 0;
}
if (result.isGroupBinding && obj.contains(QLatin1String("config.group")))
{
const auto configGroup = obj.value(QLatin1String("config.group")).toInt(-1);
if (configGroup < 0 || configGroup >= 255)
{
return {};
}
result.configGroup = configGroup;
}
else
{
result.configGroup = 0;
}
const auto report = obj.value(QLatin1String("report"));
if (report.isArray())
{
const auto reportArr = report.toArray();
for (const auto &i : reportArr)
{
if (i.isObject())
{
const auto rep = DDF_ParseZclReport(i.toObject());
if (isValid(rep))
{
result.reporting.push_back(rep);
}
}
}
}
return result;
}
The obj[key] value can be a string or array of strings.
\returns List of parsed strings.
*/
static QStringList DDF_ParseStringOrList(const QJsonObject &obj, QLatin1String key)
{
QStringList result;
const auto val = obj.value(key);
if (val.isString())
{
result.push_back(val.toString());
}
else if (val.isArray())
{
const auto arr = val.toArray();
for (const auto &i : arr)
{
if (i.isString())
{
result.push_back(i.toString());
}
}
}
return result;
}
\returns DDF object, use DeviceDescription::isValid() to check for success.
*/
static DeviceDescription DDF_ParseDeviceObject(DDF_ParseContext *pctx, const QJsonObject &obj)
{
DeviceDescription result;
const auto schema = obj.value(QLatin1String("schema")).toString();
if (schema != QLatin1String("devcap1.schema.json"))
{
return result;
}
const auto subDevices = obj.value(QLatin1String("subdevices"));
if (!subDevices.isArray())
{
return result;
}
U_ASSERT(pctx->filePathLength != 0);
U_ASSERT(pctx->filePath[pctx->filePathLength] == '\0');
result.path = &pctx->filePath[0];
result.manufacturerNames = DDF_ParseStringOrList(obj, QLatin1String("manufacturername"));
result.modelIds = DDF_ParseStringOrList(obj, QLatin1String("modelid"));
result.product = obj.value(QLatin1String("product")).toString();
if (obj.contains(QLatin1String("status")))
{
result.status = obj.value(QLatin1String("status")).toString();
}
if (obj.contains(QLatin1String("vendor")))
{
result.vendor = obj.value(QLatin1String("vendor")).toString();
}
if (obj.contains(QLatin1String("sleeper")))
{
result.sleeper = obj.value(QLatin1String("sleeper")).toBool() ? 1 : 0;
}
if (obj.contains(QLatin1String("supportsMgmtBind")))
{
result.supportsMgmtBind = obj.value(QLatin1String("supportsMgmtBind")).toBool() ? 1 : 0;
}
if (obj.contains(QLatin1String("matchexpr")))
{
result.matchExpr = obj.value(QLatin1String("matchexpr")).toString();
}
const auto keys = obj.keys();
for (const auto &key : keys)
{
DBG_Printf(DBG_DDF, "DDF %s: %s\n", qPrintable(key), qPrintable(obj.value(key).toString()));
}
const auto subDevicesArr = subDevices.toArray();
for (const auto &i : subDevicesArr)
{
if (i.isObject())
{
const auto sub = DDF_ParseSubDevice(pctx, i.toObject());
if (sub.isValid())
{
result.subDevices.push_back(sub);
}
}
}
const auto bindings = obj.value(QLatin1String("bindings"));
if (bindings.isArray())
{
const auto bindingsArr = bindings.toArray();
for (const auto &i : bindingsArr)
{
if (i.isObject())
{
const auto bnd = DDF_ParseBinding(i.toObject());
if (isValid(bnd))
{
result.bindings.push_back(bnd);
}
}
}
}
return result;
}
\returns A parsed item, use DeviceDescription::Item::isValid() to check for success.
*/
static DeviceDescription::Item DDF_ReadItemFile(DDF_ParseContext *pctx)
{
if (!pctx->fileData || pctx->fileDataSize < 16)
{
return { };
}
const QByteArray data = QByteArray::fromRawData((const char*)pctx->fileData, pctx->fileDataSize);
QJsonParseError error;
QJsonDocument doc = QJsonDocument::fromJson(data, &error);
if (error.error != QJsonParseError::NoError)
{
DBG_Printf(DBG_DDF, "DDF failed to read %s, err: %s, offset: %d\n", pctx->filePath, qPrintable(error.errorString()), error.offset);
return { };
}
if (doc.isObject())
{
return DDF_ParseItem(pctx, doc.object());
}
return { };
}
\returns A parsed subdevice, use isValid(DDF_SubDeviceDescriptor) to check for success.
*/
static DDF_SubDeviceDescriptor DDF_ReadSubDeviceFile(DDF_ParseContext *pctx)
{
DDF_SubDeviceDescriptor result = { };
if (!pctx->fileData || pctx->fileDataSize < 16)
{
return result;
}
const QByteArray data = QByteArray::fromRawData((const char*)pctx->fileData, pctx->fileDataSize);
QJsonParseError error;
QJsonDocument doc = QJsonDocument::fromJson(data, &error);
if (error.error != QJsonParseError::NoError)
{
DBG_Printf(DBG_DDF, "DDF failed to read %s, err: %s, offset: %d\n", pctx->filePath, qPrintable(error.errorString()), error.offset);
return result;
}
if (doc.isObject())
{
const auto obj = doc.object();
QString schema;
if (obj.contains(QLatin1String("schema")))
{
schema = obj.value(QLatin1String("schema")).toString();
}
if (schema != QLatin1String("subdevice1.schema.json"))
{
return result;
}
if (obj.contains(QLatin1String("name")))
{
result.name = obj.value(QLatin1String("name")).toString();
}
if (obj.contains(QLatin1String("type")))
{
result.type = obj.value(QLatin1String("type")).toString();
}
if (obj.contains(QLatin1String("restapi")))
{
result.restApi = obj.value(QLatin1String("restapi")).toString();
}
result.order = obj.value(QLatin1String("order")).toInt(SUBDEVICE_DEFAULT_ORDER);
if (obj.contains(QLatin1String("uuid")))
{
const auto uniqueId = obj.value(QLatin1String("uuid"));
if (uniqueId.isArray())
{
const auto arr = uniqueId.toArray();
for (const auto &i : arr)
{
DBG_Assert(i.isString());
result.uniqueId.push_back(i.toString());
}
}
}
if (obj.contains(QLatin1String("items")))
{
const auto items = obj.value(QLatin1String("items"));
if (items.isArray())
{
const auto arr = items.toArray();
for (const auto &i : arr)
{
DBG_Assert(i.isString());
ResourceItemDescriptor rid;
if (getResourceItemDescriptor(i.toString(), rid))
{
result.items.push_back(rid.suffix);
}
}
}
}
}
return result;
}
static QVariant DDF_ResolveParamScript(const QVariant ¶m, const QString &path)
{
auto result = param;
if (param.type() != QVariant::Map)
{
return result;
}
auto map = param.toMap();
if (map.contains(QLatin1String("script")))
{
const auto script = map["script"].toString();
const QFileInfo fi(path);
QFile f(fi.canonicalPath() + "/" + script);
if (f.exists() && f.open(QFile::ReadOnly))
{
QString content = f.readAll();
if (!content.isEmpty())
{
DDF_TryCompileAndFixJavascript(&content, path);
map["eval"] = content;
result = std::move(map);
}
}
}
else if (map.contains(QLatin1String("eval")))
{
QString content = map[QLatin1String("eval")].toString();
if (!content.isEmpty())
{
DDF_TryCompileAndFixJavascript(&content, path);
map[QLatin1String("eval")] = content;
result = std::move(map);
}
}
return result;
}
static QVariant DDF_ResolveBundleParamScript(const QVariant ¶m, DDF_ParseContext *pctx)
{
auto result = param;
if (param.type() != QVariant::Map)
{
return result;
}
auto map = param.toMap();
unsigned fnameStart;
if (map.contains(QLatin1String("script")))
{
const std::string script = map["script"].toString().toStdString();
for (DDFB_ExtfChunk *extf = pctx->extChunks; extf; extf = extf->next)
{
if (extf->fileType[0] != 'S' || extf->fileType[1] != 'C' || extf->fileType[2] != 'J' || extf->fileType[3] != 'S')
continue;
if (extf->pathLength < 4)
continue;
* Lookup just the filename of the Javascript file in the bundle.
* While this could be wrong in theory it's unlikely.
* If needed we can resolve relative paths later on to be more strict.
*/
fnameStart = extf->pathLength;
for (;fnameStart; fnameStart--)
{
if (extf->path[fnameStart] == '/')
break;
}
unsigned fnameLength = extf->pathLength - fnameStart;
if (script.size() < fnameLength)
continue;
if (U_memcmp(script.c_str() + (script.size() - fnameLength), &extf->path[fnameStart], fnameLength) != 0)
continue;
QString content = QString::fromUtf8((const char*)extf->fileData, extf->fileSize);
if (!content.isEmpty())
{
map["eval"] = content;
}
break;
}
}
if (map.contains(QLatin1String("eval")))
{
QString content = map[QLatin1String("eval")].toString();
if (!content.isEmpty())
{
QString path;
DDF_TryCompileAndFixJavascript(&content, path);
map[QLatin1String("eval")] = content;
result = std::move(map);
}
}
return result;
}
DeviceDescription DDF_LoadScripts(const DeviceDescription &ddf)
{
auto result = ddf;
for (auto &sub : result.subDevices)
{
for (auto &item : sub.items)
{
item.parseParameters = DDF_ResolveParamScript(item.parseParameters, ddf.path);
item.readParameters = DDF_ResolveParamScript(item.readParameters, ddf.path);
item.writeParameters = DDF_ResolveParamScript(item.writeParameters, ddf.path);
}
}
return result;
}
\returns Vector of parsed DDF objects.
*/
static DeviceDescription DDF_ReadDeviceFile(DDF_ParseContext *pctx)
{
U_ASSERT(pctx->fileData);
U_ASSERT(pctx->fileDataSize > 64);
const QByteArray data = QByteArray::fromRawData((const char*)pctx->fileData, pctx->fileDataSize);
QJsonParseError error;
QJsonDocument doc = QJsonDocument::fromJson(data, &error);
if (error.error != QJsonParseError::NoError)
{
DBG_Printf(DBG_DDF, "DDF failed to read %s, err: %s, offset: %d\n", pctx->filePath, qPrintable(error.errorString()), error.offset);
return { };
}
if (doc.isObject())
{
DeviceDescription ddf = DDF_ParseDeviceObject(pctx, doc.object());
if (ddf.isValid())
{
return ddf;
}
}
return { };
}
static int DDF_ProcessSignatures(DDF_ParseContext *pctx, std::vector<U_ECC_PublicKeySecp256k1> &publicKeys, U_BStream *bs, uint32_t *bundleHash)
{
unsigned i;
unsigned count;
uint16_t pubkeyLen;
uint16_t sigLen;
U_ECC_PublicKeySecp256k1 pubkey;
U_ECC_SignatureSecp256k1 sig;
count = 0;
for (;bs->status == U_BSTREAM_OK && bs->pos < bs->size;)
{
pubkeyLen = U_bstream_get_u16_le(bs);
if (sizeof(pubkey.key) < pubkeyLen)
{
return 0;
}
for (i = 0; i < pubkeyLen; i++)
{
pubkey.key[i] = U_bstream_get_u8(bs);
}
sigLen = U_bstream_get_u16_le(bs);
if (sizeof(sig.sig) < sigLen)
{
return 0;
}
for (i = 0; i < sigLen; i++)
{
sig.sig[i] = U_bstream_get_u8(bs);
}
if (U_ECC_VerifySignatureSecp256k1(&pubkey, &sig, (uint8_t*)bundleHash, U_SHA256_HASH_SIZE))
{
for (i = 0; i < publicKeys.size(); i++)
{
U_ECC_PublicKeySecp256k1 &pk = publicKeys[i];
if (U_memcmp(pk.key, pubkey.key, sizeof(pk.key)) == 0)
break;
}
if (i == publicKeys.size() && publicKeys.size() < DDF_MAX_PUBLIC_KEYS)
{
publicKeys.push_back(pubkey);
}
pctx->signatures |= (1 << i);
count++;
}
}
if (count)
return 1;
return 0;
}
Only properties which are already defined in the DDF file won't be overwritten.
\param genericItems - generic items used as source
\param ddf - DDF object with unmerged items
\returns The merged DDF object.
*/
static DeviceDescription DDF_MergeGenericItems(const std::vector<DeviceDescription::Item> &genericItems, const DeviceDescription &ddf)
{
auto result = ddf;
for (auto &sub : result.subDevices)
{
for (auto &item : sub.items)
{
const auto genItem = std::find_if(genericItems.cbegin(), genericItems.cend(),
[&item](const DeviceDescription::Item &i){ return i.descriptor.suffix == item.descriptor.suffix; });
if (genItem == genericItems.cend())
{
continue;
}
item.isImplicit = genItem->isImplicit;
item.isManaged = genItem->isManaged;
item.isGenericRead = 0;
item.isGenericWrite = 0;
item.isGenericParse = 0;
if (!item.isStatic)
{
if (item.readParameters.isNull()) { item.readParameters = genItem->readParameters; item.isGenericRead = 1; }
if (item.writeParameters.isNull()) { item.writeParameters = genItem->writeParameters; item.isGenericWrite = 1; }
if (item.parseParameters.isNull()) { item.parseParameters = genItem->parseParameters; item.isGenericParse = 1; }
if (item.refreshInterval == DeviceDescription::Item::NoRefreshInterval && genItem->refreshInterval != item.refreshInterval)
{
item.refreshInterval = genItem->refreshInterval;
}
}
if (item.descriptor.access == ResourceItemDescriptor::Access::Unknown)
{
item.descriptor.access = genItem->descriptor.access;
}
if (!item.hasIsPublic)
{
item.isPublic = genItem->isPublic;
}
if (!item.defaultValue.isValid() && genItem->defaultValue.isValid())
{
item.defaultValue = genItem->defaultValue;
}
}
}
return result;
}
static int DDF_MergeGenericBundleItems(DeviceDescription &ddf, DDF_ParseContext *pctx)
{
std::vector<DeviceDescription::Item> genericItems;
{
uint8_t *fileData = pctx->fileData;
unsigned fileDataSize = pctx->fileDataSize;
* Load generic items from bundle.
*/
for (DDFB_ExtfChunk *extf = pctx->extChunks; extf; extf = extf->next)
{
U_SStream ss;
U_sstream_init(&ss, (void*)extf->path, extf->pathLength);
if (U_sstream_starts_with(&ss, "generic/items") == 0)
continue;
pctx->fileData = extf->fileData;
pctx->fileDataSize = extf->fileSize;
DeviceDescription::Item item = DDF_ReadItemFile(pctx);
if (item.isValid())
{
genericItems.push_back(std::move(item));
}
else
{
U_ASSERT(0 && "failed to read bundle item file");
}
}
pctx->fileData = fileData;
pctx->fileDataSize = fileDataSize;
}
for (DeviceDescription::SubDevice &sub : ddf.subDevices)
{
for (DeviceDescription::Item &item : sub.items)
{
const auto genItem = std::find_if(genericItems.cbegin(), genericItems.cend(),
[&item](const DeviceDescription::Item &i){ return i.descriptor.suffix == item.descriptor.suffix; });
if (genItem == genericItems.cend())
{
continue;
}
item.isImplicit = genItem->isImplicit;
item.isManaged = genItem->isManaged;
item.isGenericRead = 0;
item.isGenericWrite = 0;
item.isGenericParse = 0;
if (!item.isStatic)
{
if (item.readParameters.isNull()) { item.readParameters = genItem->readParameters; item.isGenericRead = 1; }
if (item.writeParameters.isNull()) { item.writeParameters = genItem->writeParameters; item.isGenericWrite = 1; }
if (item.parseParameters.isNull()) { item.parseParameters = genItem->parseParameters; item.isGenericParse = 1; }
if (item.refreshInterval == DeviceDescription::Item::NoRefreshInterval && genItem->refreshInterval != item.refreshInterval)
{
item.refreshInterval = genItem->refreshInterval;
}
item.parseParameters = DDF_ResolveBundleParamScript(item.parseParameters, pctx);
item.readParameters = DDF_ResolveBundleParamScript(item.readParameters, pctx);
item.writeParameters = DDF_ResolveBundleParamScript(item.writeParameters, pctx);
}
if (item.descriptor.access == ResourceItemDescriptor::Access::Unknown)
{
item.descriptor.access = genItem->descriptor.access;
}
if (!item.hasIsPublic)
{
item.isPublic = genItem->isPublic;
}
if (!item.defaultValue.isValid() && genItem->defaultValue.isValid())
{
item.defaultValue = genItem->defaultValue;
}
}
}
return 1;
}
uint8_t DDF_GetSubDeviceOrder(const QString &type)
{
if (type.isEmpty() || type.startsWith(QLatin1String("CLIP")))
{
return SUBDEVICE_DEFAULT_ORDER;
}
if (_priv)
{
auto i = std::find_if(_priv->subDevices.cbegin(), _priv->subDevices.cend(), [&](const auto &sub)
{ return sub.name == type; });
if (i != _priv->subDevices.cend())
{
return i->order;
}
}
#ifdef QT_DEBUG
DBG_Printf(DBG_DDF, "DDF No subdevice for type: %s\n", qPrintable(type));
#endif
return SUBDEVICE_DEFAULT_ORDER;
}
*/
Resource::Handle R_CreateResourceHandle(const Resource *r, size_t containerIndex)
{
Q_ASSERT(r->prefix() != nullptr);
if (r->item(RAttrUniqueId)->toString().isEmpty())
{
return {};
}
Resource::Handle result;
result.hash = qHash(r->item(RAttrUniqueId)->toString());
result.index = containerIndex;
result.type = r->prefix()[1];
result.order = 0;
Q_ASSERT(result.type == 's' || result.type == 'l' || result.type == 'd' || result.type == 'g');
Q_ASSERT(isValid(result));
if (result.type == 's' || result.type == 'l')
{
const ResourceItem *type = r->item(RAttrType);
if (type)
{
result.order = DDF_GetSubDeviceOrder(type->toString());
}
}
return result;
}