blob: e800fab0a17ba5b4c2935977f7d3e20ca59f5c76 [file] [log] [blame]
/*
*
* Copyright (c) 2021 Project CHIP Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
// Import helpers from zap core
const zapPath = '../../../../third_party/zap/repo/dist/src-electron/';
const queryConfig = require(zapPath + 'db/query-config.js')
const queryCommand = require(zapPath + 'db/query-command.js')
const queryEndpoint = require(zapPath + 'db/query-endpoint.js')
const queryEndpointType = require(zapPath + 'db/query-endpoint-type.js')
const templateUtil = require(zapPath + 'generator/template-util.js')
const zclHelper = require(zapPath + 'generator/helper-zcl.js')
const zclQuery = require(zapPath + 'db/query-zcl.js')
const { Deferred } = require('./Deferred.js');
const ListHelper = require('./ListHelper.js');
const StringHelper = require('./StringHelper.js');
const StructHelper = require('./StructHelper.js');
const ChipTypesHelper = require('./ChipTypesHelper.js');
//
// Load Step 1
//
function loadAtomics(packageId)
{
const { db, sessionId } = this.global;
const options = { 'hash' : {} };
const resolveZclTypes = atomics => Promise.all(atomics.map(atomic => {
return zclHelper.asUnderlyingZclType.call(this, atomic.name, options).then(zclType => {
atomic.chipType = zclType;
return atomic;
});
}));
return zclQuery.selectAllAtomics(db, packageId).then(resolveZclTypes);
}
function loadBitmaps(packageId)
{
const { db, sessionId } = this.global;
return zclQuery.selectAllBitmaps(db, packageId);
}
function loadEnums(packageId)
{
const { db, sessionId } = this.global;
return zclQuery.selectAllEnums(db, packageId);
}
function loadStructItems(struct, packageId)
{
const { db, sessionId } = this.global;
return zclQuery.selectAllStructItemsById(db, struct.id).then(structItems => {
struct.items = structItems;
return struct;
});
}
function loadStructs(packageId)
{
const { db, sessionId } = this.global;
return zclQuery.selectAllStructsWithItemCount(db, packageId)
.then(structs => Promise.all(structs.map(struct => loadStructItems.call(this, struct, packageId))));
}
function loadClusters()
{
const { db, sessionId } = this.global;
return queryEndpointType.selectEndpointTypeIds(db, sessionId)
.then(endpointTypes => queryEndpointType.selectAllClustersDetailsFromEndpointTypes(db, endpointTypes))
.then(clusters => clusters.filter(cluster => cluster.enabled == 1));
}
function loadCommandResponse(command, packageId)
{
const { db, sessionId } = this.global;
return queryCommand.selectCommandById(db, command.id, packageId).then(commandDetails => {
if (commandDetails.responseRef == null) {
command.response = null;
return command;
}
return queryCommand.selectCommandById(db, commandDetails.responseRef, packageId).then(response => {
command.response = response;
return command;
});
});
}
function loadCommandArguments(command, packageId)
{
const { db, sessionId } = this.global;
return queryCommand.selectCommandArgumentsByCommandId(db, command.id, packageId).then(commandArguments => {
command.arguments = commandArguments;
return command;
});
}
function loadCommands(packageId)
{
const { db, sessionId } = this.global;
return queryEndpointType.selectEndpointTypeIds(db, sessionId)
.then(endpointTypes => queryEndpointType.selectClustersAndEndpointDetailsFromEndpointTypes(db, endpointTypes))
.then(endpointTypesAndClusters => queryCommand.selectCommandDetailsFromAllEndpointTypesAndClusters(
db, endpointTypesAndClusters, true))
.then(commands => Promise.all(commands.map(command => loadCommandResponse.call(this, command, packageId))))
.then(commands => Promise.all(commands.map(command => loadCommandArguments.call(this, command, packageId))));
}
function loadAttributes(packageId)
{
// The 'server' side is enforced here, because the list of attributes is used to generate client global
// commands to retrieve server side attributes.
const { db, sessionId } = this.global;
return queryEndpointType.selectEndpointTypeIds(db, sessionId)
.then(endpointTypes => Promise.all(
endpointTypes.map(({ endpointTypeId }) => queryEndpoint.selectEndpointClusters(db, endpointTypeId))))
.then(clusters => clusters.flat())
.then(clusters => Promise.all(
clusters.map(({ clusterId, side, endpointTypeId }) => queryEndpoint.selectEndpointClusterAttributes(
db, clusterId, 'server', endpointTypeId))))
.then(attributes => attributes.flat())
.then(attributes => attributes.filter(attribute => attribute.isIncluded))
.then(attributes => attributes.sort((a, b) => a.code - b.code));
//.then(attributes => Promise.all(attributes.map(attribute => types.typeSizeAttribute(db, packageId, attribute))
}
function loadGlobalAttributes(packageId)
{
const { db, sessionId } = this.global;
return zclQuery.selectAllAttributes(db, packageId)
.then(attributes => attributes.filter(attribute => attribute.clusterRef == null))
.then(attributes => attributes.map(attribute => attribute.code));
}
//
// Load step 2
//
/**
* This method converts a ZCL type to the length expected for the
* BufferWriter.Put method.
* TODO
* Not all types are supported at the moment, so if there is any unsupported type
* that we are trying to convert, it will throw an error.
*/
function asPutLength(zclType)
{
const type = ChipTypesHelper.asBasicType(zclType);
switch (type) {
case 'bool':
return '8';
case 'int8_t':
case 'int16_t':
case 'int32_t':
case 'int64_t':
case 'uint8_t':
case 'uint16_t':
case 'uint32_t':
case 'uint64_t':
return type.replace(/[^0-9]/g, '');
default:
throw error = 'asPutLength: Unhandled type: ' + zclType;
}
}
function asPutCastType(zclType)
{
const type = ChipTypesHelper.asBasicType(zclType);
switch (type) {
case 'bool':
return 'uint8_t';
case 'int8_t':
case 'int16_t':
case 'int32_t':
case 'int64_t':
return 'u' + type;
case 'uint8_t':
case 'uint16_t':
case 'uint32_t':
case 'uint64_t':
return type;
default:
throw error = 'asPutCastType: Unhandled type: ' + zclType;
}
}
function asChipCallback(item)
{
if (StringHelper.isOctetString(item.type)) {
return { name : 'OctetString', type : 'const chip::ByteSpan' };
}
if (StringHelper.isCharString(item.type)) {
return { name : 'CharString', type : 'const chip::CharSpan' };
}
if (ListHelper.isList(item.type)) {
return { name : 'List', type : null };
}
const basicType = ChipTypesHelper.asBasicType(item.chipType);
switch (basicType) {
case 'int8_t':
case 'int16_t':
case 'int32_t':
case 'int64_t':
return { name : 'Int' + basicType.replace(/[^0-9]/g, '') + 's', type : basicType };
case 'uint8_t':
case 'uint16_t':
case 'uint32_t':
case 'uint64_t':
return { name : 'Int' + basicType.replace(/[^0-9]/g, '') + 'u', type : basicType };
case 'bool':
return { name : 'Boolean', type : 'bool' };
default:
return { name : 'Unsupported', type : null };
}
}
function getAtomic(atomics, type)
{
return atomics.find(atomic => atomic.name == type.toLowerCase());
}
function getBitmap(bitmaps, type)
{
return bitmaps.find(bitmap => bitmap.label == type);
}
function getEnum(enums, type)
{
return enums.find(enumItem => enumItem.label == type);
}
function getStruct(structs, type)
{
return structs.find(struct => struct.label == type);
}
function handleString(item, [ atomics, enums, bitmaps, structs ])
{
if (!StringHelper.isString(item.type)) {
return false;
}
const atomic = getAtomic(atomics, item.type);
if (!atomic) {
return false;
}
const kLengthSizeInBytes = 2;
item.atomicTypeId = atomic.atomicId;
if (StringHelper.isOctetString(item.type)) {
item.chipType = 'chip::ByteSpan';
} else {
item.chipType = 'chip::CharSpan';
}
item.size = kLengthSizeInBytes + item.maxLength;
item.name = item.name || item.label;
return true;
}
function handleList(item, [ atomics, enums, bitmaps, structs ])
{
if (!ListHelper.isList(item.type)) {
return false;
}
const entryType = item.entryType;
if (!entryType) {
console.log(item);
throw new Error(item.label, 'List[T] is missing type "T" information');
}
item.isList = true;
item.type = entryType;
enhancedItem(item, [ atomics, enums, bitmaps, structs ]);
return true;
}
function handleStruct(item, [ atomics, enums, bitmaps, structs ])
{
const struct = getStruct(structs, item.type);
if (!struct) {
return false;
}
// Add a leading `_` before the name of struct to match what is done in the af-structs.zapt template.
// For instance structs are declared as "typedef struct _{{asType label}}".
item.chipType = '_' + item.type;
item.isStruct = true;
struct.items.map(structItem => enhancedItem(structItem, [ atomics, enums, bitmaps, structs ]));
item.items = struct.items;
item.size = struct.items.map(type => type.size).reduce((accumulator, currentValue) => accumulator + currentValue, 0);
return true;
}
function handleBasic(item, [ atomics, enums, bitmaps, structs ])
{
let itemType = item.type;
const enumItem = getEnum(enums, itemType);
if (enumItem) {
item.isEnum = true;
itemType = enumItem.type;
}
const bitmap = getBitmap(bitmaps, itemType);
if (bitmap) {
item.isBitmap = true;
itemType = bitmap.type;
}
const atomic = getAtomic(atomics, itemType);
if (atomic) {
item.name = item.name || item.label;
item.isStruct = false;
item.atomicTypeId = atomic.atomicId;
item.size = atomic.size;
item.chipType = atomic.chipType;
item.chipTypePutLength = asPutLength(atomic.chipType);
item.chipTypePutCastType = asPutCastType(atomic.chipType);
return true;
}
return false;
}
function enhancedItem(item, types)
{
if (handleString(item, types)) {
return;
}
if (handleList(item, types)) {
return;
}
if (handleStruct(item, types)) {
return;
}
if (handleBasic(item, types)) {
return;
}
console.log(item);
throw new Error(item.type + ' not found.');
}
function inlineStructItems(args)
{
const arguments = [];
args.forEach(argument => {
if (!argument.isStruct) {
arguments.push(argument);
return;
}
argument.items.forEach(item => {
arguments.push(item);
});
});
return arguments;
}
function enhancedCommands(commands, types)
{
commands.forEach(command => {
command.arguments.forEach(argument => {
enhancedItem(argument, types);
});
});
commands.forEach(command => {
command.isResponse = command.name.includes('Response');
command.isManufacturerSpecificCommand = !!this.mfgCode;
command.hasSpecificResponse = !!command.response;
if (command.response) {
const responseName = command.response.name;
command.responseName = responseName;
// The 'response' property contains the response returned by the `selectCommandById`
// helper. But this one does not contains all the metadata informations added by
// `enhancedItem`, so instead of using the one from ZAP, retrieve the enhanced version.
command.response = commands.find(command => command.name == responseName);
} else {
command.responseName = 'DefaultSuccess';
command.response = { arguments : [] };
}
});
// Filter unused responses
commands = commands.filter(command => {
if (!command.isResponse) {
return true;
}
const responseName = command.name;
return commands.find(command => command.responseName == responseName);
});
// At this stage, 'command.arguments' may contains 'struct'. But some controllers does not know (yet) how
// to handle them. So those needs to be inlined.
commands.forEach(command => {
if (command.isResponse) {
return;
}
command.expandedArguments = inlineStructItems(command.arguments);
});
return commands;
}
function enhancedAttributes(attributes, globalAttributes, types)
{
attributes.forEach(attribute => {
enhancedItem(attribute, types);
attribute.isGlobalAttribute = globalAttributes.includes(attribute.code);
attribute.isWritableAttribute = attribute.isWritable === 1;
attribute.isReportableAttribute = attribute.includedReportable === 1;
attribute.chipCallback = asChipCallback(attribute);
});
attributes.forEach(attribute => {
const argument = {
name : attribute.name,
type : attribute.type,
size : attribute.size,
isList : attribute.isList,
isArray : attribute.isList,
isNullable : attribute.isNullable,
chipType : attribute.chipType,
chipCallback : attribute.chipCallback,
label : attribute.name,
};
attribute.arguments = [ argument ];
attribute.response = { arguments : [ argument ] };
});
// At this stage, the 'attributes' array contains all attributes enabled for all endpoints. It means
// that a lot of attributes are duplicated if a cluster is enabled on multiple endpoints but that's
// not what the templates expect. So let's deduplicate them.
const compare = (a, b) => (a.name == b.name && a.clusterId == b.clusterId && a.side == b.side);
return attributes.filter((att, index) => attributes.findIndex(att2 => compare(att, att2)) == index);
}
const Clusters = {
ready : new Deferred()
};
Clusters.init = function(context, packageId) {
if (this.ready.running)
{
return this.ready;
}
this.ready.running = true;
const loadTypes = [
loadAtomics.call(context, packageId),
loadEnums.call(context, packageId),
loadBitmaps.call(context, packageId),
loadStructs.call(context, packageId),
];
const promises = [
Promise.all(loadTypes),
loadClusters.call(context),
loadCommands.call(context, packageId),
loadAttributes.call(context, packageId),
loadGlobalAttributes.call(context, packageId),
];
return Promise.all(promises).then(([types, clusters, commands, attributes, globalAttributes]) => {
this._clusters = clusters;
this._commands = enhancedCommands(commands, types);
this._attributes = enhancedAttributes(attributes, globalAttributes, types);
return this.ready.resolve();
}, err => this.ready.reject(err));
}
//
// Helpers: All
//
function asBlocks(promise, options)
{
const fn = pkgId => Clusters.init(this, pkgId).then(() => promise.then(data => templateUtil.collectBlocks(data, options, this)));
return templateUtil.ensureZclPackageId(this).then(fn).catch(err => { console.log(err); throw err; });
}
function asPromise(promise)
{
const fn = pkgId => Clusters.init(this, pkgId).then(() => promise);
return templateUtil.ensureZclPackageId(this).then(fn).catch(err => { console.log(err); throw err; });
}
//
// Helpers: Get all clusters/commands/responses/attributes.
//
const kResponseFilter = (isResponse, item) => isResponse == item.isResponse;
Clusters.getClusters = function()
{
return this.ready.then(() => this._clusters);
}
Clusters.getCommands = function()
{
return this.ready.then(() => this._commands.filter(kResponseFilter.bind(null, false)));
}
Clusters.getResponses = function()
{
return this.ready.then(() => this._commands.filter(kResponseFilter.bind(null, true)));
}
Clusters.getAttributes = function()
{
return this.ready.then(() => this._attributes);
}
//
// Helpers: Get by Cluster Name
//
const kNameFilter = (name, item) => name.toLowerCase() == (item.clusterName || item.name).toLowerCase();
Clusters.getCommandsByClusterName = function(name)
{
return this.getCommands().then(items => items.filter(kNameFilter.bind(null, name)));
}
Clusters.getResponsesByClusterName = function(name)
{
return this.getResponses().then(items => items.filter(kNameFilter.bind(null, name)));
}
Clusters.getAttributesByClusterName = function(name)
{
return this.ready.then(() => {
const clusterId = this._clusters.find(kNameFilter.bind(null, name)).id;
const filter = attribute => attribute.clusterId == clusterId;
return this.getAttributes().then(items => items.filter(filter));
});
}
//
// Helpers: Get by Cluster Side
//
const kSideFilter = (side, item) => item.source ? ((item.source == side && item.outgoing) || (item.source != side && item.incoming))
: item.side == side;
Clusters.getCommandsByClusterSide = function(side)
{
return this.getCommands().then(items => items.filter(kSideFilter.bind(null, side)));
}
Clusters.getResponsesByClusterSide = function(side)
{
return this.getResponses().then(items => items.filter(kSideFilter.bind(null, side)));
}
Clusters.getAttributesByClusterSide = function(side)
{
return this.getAttributes().then(items => items.filter(kSideFilter.bind(null, side)));
}
//
// Helpers: Client
//
const kClientSideFilter = kSideFilter.bind(null, 'client');
Clusters.getClientClusters = function()
{
return this.getClusters().then(items => items.filter(kClientSideFilter));
}
Clusters.getClientCommands = function(name)
{
return this.getCommandsByClusterName(name).then(items => items.filter(kClientSideFilter));
}
Clusters.getClientResponses = function(name)
{
return this.getResponsesByClusterName(name).then(items => items.filter(kClientSideFilter));
}
Clusters.getClientAttributes = function(name)
{
return this.getAttributesByClusterName(name).then(items => items.filter(kClientSideFilter));
}
//
// Helpers: Server
//
const kServerSideFilter = kSideFilter.bind(null, 'server');
Clusters.getServerClusters = function()
{
return this.getClusters().then(items => items.filter(kServerSideFilter));
}
Clusters.getServerCommands = function(name)
{
return this.getCommandsByClusterName(name).then(items => items.filter(kServerSideFilter));
}
Clusters.getServerResponses = function(name)
{
return this.getResponsesByClusterName(name).then(items => items.filter(kServerSideFilter));
}
Clusters.getServerAttributes = function(name)
{
return this.getAttributesByClusterName(name).then(items => items.filter(kServerSideFilter));
}
//
// Module exports
//
exports.Clusters = Clusters;
exports.asBlocks = asBlocks;
exports.asPromise = asPromise;