blob: 8ff29185f74b24c08ec5f79381111bb9a11c4712 [file] [log] [blame]
/**
*
* Copyright (c) 2021 Silicon Labs
*
* 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.
*/
/**
* This module provides the API to access various zcl utilities.
*
* @module REST API: various zcl utilities
*/
const toposort = require('toposort')
const queryZcl = require('../db/query-zcl')
const queryEvent = require('../db/query-event')
const dbEnum = require('../../src-shared/db-enum')
const env = require('./env')
const types = require('./types')
/**
* Comparator for sorting clusters.
*
* @param {*} a
* @param {*} b
* @returns -1, 0 or 1
*/
function clusterComparator(a, b) {
if (a.code < b.code) return -1
if (a.code > b.code) return 1
if (a.side < b.side) return -1
if (a.side > b.side) return 1
return 0
}
/**
* Comparator for sorting attribute.
*
* @param {*} a
* @param {*} b
* @returns -1, 0 or 1
*/
function attributeComparator(a, b) {
if (a.hexCode < b.hexCode) return -1
if (a.hexCode > b.hexCode) return 1
return 0
}
/**
* Comparator for sorting commands.
*
* @param {*} a
* @param {*} b
* @returns -1, 0 or 1
*/
function commandComparator(a, b) {
if (a.manufacturerCode < b.manufacturerCode) return -1
if (a.manufacturerCode > b.manufacturerCode) return 1
if (a.hexCode < b.hexCode) return -1
if (a.hexCode > b.hexCode) return 1
return 0
}
/**
* Comparator for sorting events.
*
* @param {*} a
* @param {*} b
* @returns -1, 0 or 1
*/
function eventComparator(a, b) {
if (a.manufacturerCode < b.manufacturerCode) return -1
if (a.manufacturerCode > b.manufacturerCode) return 1
if (a.code < b.code) return -1
if (a.code > b.code) return 1
return 0
}
/**
* Find struct by name from the given list of structs.
*
* @param {*} structs
* @param {*} name
* @returns struct
*/
function findStructByName(structs, name) {
for (const s of structs) {
if (s.name == name) {
return s
}
}
return null
}
/**
* Non-exported helper for sortStructsByDependency.
*/
function sortStructsByDependencyHelper(structs) {
let allStructNames = structs.map((s) => s.name)
let edges = []
// Add edges
structs.forEach((s) => {
s.items.forEach((i) => {
const type = i.type
if (allStructNames.includes(type)) {
edges.push([s.name, type])
}
})
})
let sortedEdges = toposort(edges).reverse()
let finalSort = []
sortedEdges.forEach((s) => {
finalSort.push(findStructByName(structs, s))
})
allStructNames.forEach((s) => {
if (!sortedEdges.includes(s)) finalSort.push(findStructByName(structs, s))
})
return finalSort
}
/**
* This method retrieves a bunch of structs sorted
* alphabetically. It's expected to resort the structs into a list
* where they are sorted in a way where dependency is observed.
*
* It uses the DFS-based topological sort algorithm.
*
* @param {*} structs
* @returns sorted structs according to topological search.
*/
async function sortStructsByDependency(structs) {
// Given global and non-global structs, the way dependencies work is this:
//
// 1) Global structs can only depend on other global structs.
// 2) Non-global structs can depend on either non-global or global structs.
//
// So we can output all global structs first (sorted by dependency), and then
// the non-global structs, again sorted by dependency.
//
// NOTE: the topological sort we use is not a stable sort, so adding some
// structs to the list can entirely change the order of all the structs.
let sortedGlobalStructs = sortStructsByDependencyHelper(
structs.filter((s) => s.struct_cluster_count == 0)
)
let sortedNonGlobalStructs = sortStructsByDependencyHelper(
structs.filter((s) => s.struct_cluster_count != 0)
)
return sortedGlobalStructs.concat(sortedNonGlobalStructs)
}
/**
* This function calculates the number of bytes in the data type and based on
* that returns the option specified in the template.
* for eg: Given that options are as follows:
* options.hash.array="b"
* options.hash.one_byte="u"
* options.hash.two_byte="v"
* options.hash.three_byte="x"
* options.hash.four_byte="w"
* options.hash.short_string="s"
* options.hash.long_string="l"
* options.hash.default="b"
*
* calculateBytes("char_string", options)
* will return 's'
*
* @param {*} res
* @param {*} options
* @param {*} db
* @param {*} packageIds
* @param {*} isStructType
*/
function calculateBytes(res, options, db, packageIds, isStructType) {
if (!isStructType) {
return calculateBytesForTypes(res, options, db, packageIds)
} else {
return calculateBytesForStructs(res, options, db, packageIds)
}
}
/**
*
* @param options
* @param optionsKey
* @param defaultValue
* Given the values determine to give the user defined value or the calculated value
*/
function optionsHashOrDefault(options, optionsKey, defaultValue) {
if (optionsKey in options.hash) {
return options.hash[optionsKey]
} else {
return defaultValue
}
}
/**
* Get the size of the type given.
*
* @param {*} res
* @param {*} options
* @param {*} db
* @param {*} packageIds
* @returns size of type
*/
function calculateBytesForTypes(res, options, db, packageIds) {
return queryZcl
.selectSizeFromType(db, packageIds, res.toLowerCase())
.then((x) => {
return new Promise((resolve, reject) => {
let result = 0
switch (x) {
case 1:
result = optionsHashOrDefault(options, 'one_byte', x)
break
case 2:
result = optionsHashOrDefault(options, 'two_byte', x)
break
case 3:
result = optionsHashOrDefault(options, 'three_byte', x)
break
case 4:
result = optionsHashOrDefault(options, 'four_byte', x)
break
case 5:
result = optionsHashOrDefault(options, 'five_byte', x)
break
case 6:
result = optionsHashOrDefault(options, 'six_byte', x)
break
case 7:
result = optionsHashOrDefault(options, 'seven_byte', x)
break
case 8:
result = optionsHashOrDefault(options, 'eight_byte', x)
break
case 9:
result = optionsHashOrDefault(options, 'nine_byte', x)
break
case 10:
result = optionsHashOrDefault(options, 'ten_byte', x)
break
case 11:
result = optionsHashOrDefault(options, 'eleven_byte', x)
break
case 12:
result = optionsHashOrDefault(options, 'twelve_byte', x)
break
case 13:
result = optionsHashOrDefault(options, 'thirteen_byte', x)
break
case 14:
result = optionsHashOrDefault(options, 'fourteen_byte', x)
break
case 15:
result = optionsHashOrDefault(options, 'fifteen_byte', x)
break
case 16:
result = optionsHashOrDefault(options, 'sixteen_byte', x)
break
default:
if (
res != null &&
res.includes('long') &&
res.includes(dbEnum.zclType.string)
) {
result = optionsHashOrDefault(options, 'long_string', 'l')
} else if (
res != null &&
!res.includes('long') &&
res.includes(dbEnum.zclType.string)
) {
result = optionsHashOrDefault(options, 'short_string', 's')
} else if ('default' in options.hash) {
result = options.hash.default
}
break
}
resolve(result)
})
})
.catch((err) => {
env.logError(
'Could not find size of the given type in' +
' calculateBytesForTypes: ' +
err
)
})
}
/**
* Get size of struct. Also allow user to specifiy a default if calculation is not needed.
*
* @param {*} res
* @param {*} options
* @param {*} db
* @param {*} packageIds
* @returns size of struct
*/
async function calculateBytesForStructs(res, options, db, packageIds) {
if ('struct' in options.hash) {
return options.hash.struct
} else {
return queryZcl
.selectAllStructItemsByStructName(db, res, packageIds)
.then((items) => {
let promises = []
items.forEach((item) =>
promises.push(
dataTypeCharacterFormatter(
db,
packageIds,
item,
options,
item.discriminatorName.toLowerCase()
)
)
)
return Promise.all(promises)
})
.then((resolvedPromises) =>
resolvedPromises.reduce((acc, cur) => acc + cur, 0)
)
.catch((err) => {
env.logError(
'Could not find size of struct ' +
res +
' in' +
' calculate_size_for_structs: ' +
err
)
return 0
})
}
}
/**
* Get user defined values from the template for a given size or else return defaults specified.
*
* @param {*} size
* @param {*} res
* @param {*} options
* @returns user defined or default value based on size
*/
function returnOptionsForTypes(size, res, options) {
return new Promise((resolve, reject) => {
let result = 0
switch (size) {
case 1:
result = optionsHashOrDefault(options, 'one_byte', size)
break
case 2:
result = optionsHashOrDefault(options, 'two_byte', size)
break
case 3:
result = optionsHashOrDefault(options, 'three_byte', size)
break
case 4:
result = optionsHashOrDefault(options, 'four_byte', size)
break
case 5:
result = optionsHashOrDefault(options, 'five_byte', size)
break
case 6:
result = optionsHashOrDefault(options, 'six_byte', size)
break
case 7:
result = optionsHashOrDefault(options, 'seven_byte', size)
break
case 8:
result = optionsHashOrDefault(options, 'eight_byte', size)
break
case 9:
result = optionsHashOrDefault(options, 'nine_byte', size)
break
case 10:
result = optionsHashOrDefault(options, 'ten_byte', size)
break
case 11:
result = optionsHashOrDefault(options, 'eleven_byte', size)
break
case 12:
result = optionsHashOrDefault(options, 'twelve_byte', size)
break
case 13:
result = optionsHashOrDefault(options, 'thirteen_byte', size)
break
case 14:
result = optionsHashOrDefault(options, 'fourteen_byte', size)
break
case 15:
result = optionsHashOrDefault(options, 'fifteen_byte', size)
break
case 16:
result = optionsHashOrDefault(options, 'sixteen_byte', size)
break
default:
if (
res != null &&
res.includes('long') &&
res.includes(dbEnum.zclType.string)
) {
result = optionsHashOrDefault(options, 'long_string', 'l')
} else if (
res != null &&
!res.includes('long') &&
res.includes(dbEnum.zclType.string)
) {
result = optionsHashOrDefault(options, 'short_string', 's')
} else if ('default' in options.hash) {
result = options.hash.default
}
break
}
resolve(result)
}).catch((err) => {
env.logError(
'Could not find size of the given type in' +
' returnOptionsForTypes: ' +
err
)
})
}
/**
*
* @param {*} db
* @param {*} packageIds
* @param {*} type
* @param {*} options
* @param {*} resType
* Character associated to a zcl/c data type.
*/
async function dataTypeCharacterFormatter(
db,
packageIds,
type,
options,
resType
) {
switch (resType) {
case dbEnum.zclType.array:
if (dbEnum.zclType.array in options.hash) {
return options.hash.array
} else {
return 'b'
}
case dbEnum.zclType.bitmap:
return queryZcl
.selectBitmapByName(db, packageIds, type)
.then((bitmapRec) => bitmapRec.size)
.then((size) => {
return returnOptionsForTypes(size, null, options)
})
case dbEnum.zclType.enum:
return queryZcl
.selectEnumByName(db, type, packageIds)
.then((enumRec) => enumRec.size)
.then((size) => {
return returnOptionsForTypes(size, null, options)
})
case dbEnum.zclType.struct:
if (dbEnum.zclType.struct in options.hash) {
return options.hash.struct
} else {
return calculateBytes(type, options, db, packageIds, true)
}
case dbEnum.zclType.atomic:
case dbEnum.zclType.unknown:
default:
return queryZcl
.selectAtomicType(db, packageIds, type)
.then((atomic) => {
if (
atomic &&
(atomic.name == 'char_string' ||
atomic.name == 'octet_string' ||
atomic.name == 'long_octet_string' ||
atomic.name == 'long_char_string')
) {
return atomic.name
} else {
return type
}
})
.then((res) => calculateBytes(res, options, db, packageIds, false))
}
}
/**
* Local function that checks if an enum by the name exists
*
* @param {*} db
* @param {*} enum_name
* @param {*} packageIds
* @returns Promise of content.
*/
function isEnum(db, enum_name, packageIds) {
return queryZcl
.selectEnumByName(db, enum_name, packageIds)
.then((enums) => (enums ? dbEnum.zclType.enum : dbEnum.zclType.unknown))
}
/**
* Local function that checks if a struct by the name exists
*
* @param {*} db
* @param {*} struct_name
* @param {*} packageIds
* @returns Promise of content.
*/
function isStruct(db, struct_name, packageIds) {
return queryZcl
.selectStructByName(db, struct_name, packageIds)
.then((st) => (st ? dbEnum.zclType.struct : dbEnum.zclType.unknown))
}
/**
* Function that checks if a given thing is an avent.
* @param {*} db
* @param {*} event_name
* @param {*} packageId
* @returns Promise of content.
*/
function isEvent(db, event_name, packageIds) {
return queryEvent
.selectAllEvents(db, packageIds)
.then((events) => events.find((event) => event.name == event_name))
.then((events) => (events ? 'event' : dbEnum.zclType.unknown))
}
/**
* Local function that checks if a bitmap by the name exists
*
* @param {*} db
* @param {*} bitmap_name
* @param {*} packageIds
* @returns Promise of content.
*/
function isBitmap(db, bitmap_name, packageIds) {
return queryZcl
.selectBitmapByName(db, packageIds, bitmap_name)
.then((st) => (st ? dbEnum.zclType.bitmap : dbEnum.zclType.unknown))
}
/**
*
* @param {*} fromType
* @param {*} toType
* @param {*} noWarning
*
* Type warning message. If noWarning is set to true then the warning message
* will not be shown.
*/
function defaultMessageForTypeConversion(fromType, toType, noWarning) {
if (!noWarning) {
return `/* TYPE WARNING: ${fromType} defaults to */ ` + toType
} else {
return toType
}
}
/**
*
*
* @param {*} type
* @param {*} options
* @param {*} packageIds
* @param {*} db
* @param {*} resolvedType
* @param {*} overridable
* @returns the data type associated with the resolvedType
*/
function dataTypeHelper(
type,
options,
packageIds,
db,
resolvedType,
overridable
) {
let no_warning = { no_warning: 0 }
if ('no_warning' in options.hash) {
no_warning = { no_warning: options.hash.no_warning }
}
switch (resolvedType) {
case dbEnum.zclType.array:
if ('array' in options.hash) {
return defaultMessageForTypeConversion(
`${type} array`,
options.hash.array,
options.hash.no_warning
)
} else {
return queryZcl
.selectAtomicType(db, packageIds, dbEnum.zclType.array)
.then((atomic) => overridable.atomicType(atomic))
}
case dbEnum.zclType.bitmap:
if ('bitmap' in options.hash) {
return defaultMessageForTypeConversion(
`${type}`,
options.hash.bitmap,
options.hash.no_warning
)
} else {
return queryZcl
.selectBitmapByName(db, packageIds, type)
.then((bitmapRec) => overridable.bitmapType(bitmapRec.size))
}
case dbEnum.zclType.enum:
if ('enum' in options.hash) {
return defaultMessageForTypeConversion(
`${type}`,
options.hash.enum,
options.hash.no_warning
)
} else {
return queryZcl
.selectEnumByName(db, type, packageIds)
.then((enumRec) => overridable.enumType(enumRec.size))
}
case dbEnum.zclType.struct:
if ('struct' in options.hash) {
return defaultMessageForTypeConversion(
`${type}`,
options.hash.struct,
options.hash.no_warning
)
} else {
return type
}
case dbEnum.zclType.atomic:
case dbEnum.zclType.unknown:
default:
return queryZcl
.selectAtomicType(db, packageIds, type)
.then((atomic) => overridable.atomicType({ ...atomic, ...no_warning }))
}
}
/**
*
* @param type
* @param options
* @param packageIds
* @param currentInstance
*
* Note: If the options has zclCharFormatter set to true then the function will
* return the user defined data associated with the zcl data type and not the
* actual data type. It can also be used to calculate the size of the data types
*
* This is a utility function which is called from other helper functions using ut current
* instance. See comments in asUnderlyingZclType for usage instructions.
*/
async function asUnderlyingZclTypeWithPackageId(
type,
options,
packageIds,
currentInstance
) {
let actualType = type
if (typeof type === 'number') {
let numberType = await queryZcl.selectDataTypeById(
currentInstance.global.db,
type
)
actualType = numberType.name
}
return Promise.all([
new Promise((resolve, reject) => {
if ('isArray' in currentInstance && currentInstance.isArray)
resolve(dbEnum.zclType.array)
else resolve(dbEnum.zclType.unknown)
}),
isEnum(currentInstance.global.db, actualType, packageIds),
isStruct(currentInstance.global.db, actualType, packageIds),
isBitmap(currentInstance.global.db, actualType, packageIds)
])
.then(
(res) =>
new Promise((resolve, reject) => {
for (let i = 0; i < res.length; i++) {
if (res[i] != 'unknown') {
resolve(res[i])
return
}
}
resolve(dbEnum.zclType.unknown)
})
)
.then((resType) => {
if (dbEnum.zclType.zclCharFormatter in options.hash) {
return dataTypeCharacterFormatter(
currentInstance.global.db,
packageIds,
actualType,
options,
resType
)
} else {
return dataTypeHelper(
actualType,
options,
packageIds,
currentInstance.global.db,
resType,
currentInstance.global.overridable
)
}
})
.catch((err) => {
env.logError(err)
throw err
})
}
/**
* Returns a promise that resolves into an object containing:
* type:
* atomicType:
* Base type for struct is a null.
*
* @param {*} db
* @param {*} type
* @param {*} packageIds
*/
async function determineType(db, type, packageIds) {
let atomic = await queryZcl.selectAtomicType(db, packageIds, type)
if (atomic != null)
return {
type: dbEnum.zclType.atomic,
atomicType: atomic.name
}
let theEnum = await queryZcl.selectEnumByName(db, type, packageIds)
if (theEnum != null) {
let size = theEnum.size
if (size == 3) {
size = 4
} else if (size == 6) {
size = 8
}
return {
type: dbEnum.zclType.enum,
atomicType: 'enum' + size * 8,
size: theEnum.size
}
}
let struct = await queryZcl.selectStructByName(db, type, packageIds)
if (struct != null)
return {
type: dbEnum.zclType.struct,
atomicType: null
}
let theBitmap = await queryZcl.selectBitmapByName(db, packageIds, type)
if (theBitmap != null) {
let size = theBitmap.size
if (size == 3) {
size = 4
} else if (size == 6) {
size = 8
}
return {
type: dbEnum.zclType.bitmap,
atomicType: 'bitmap' + size * 8,
size: theBitmap.size
}
}
return {
type: dbEnum.zclType.unknown,
atomicType: null
}
}
/**
* Get command signature of a command.
*
* @param {*} db
* @param {*} packageId
* @param {*} cmd
* @returns object
*/
async function createCommandSignature(db, packageId, cmd) {
let sig = []
let isSimple = true
let index = 0
for (const arg of cmd.commandArgs) {
let single = ''
let t = await determineType(db, arg.type, packageId)
let recordedType
if (t.type === dbEnum.zclType.enum) {
recordedType = dbEnum.zclType.enum + (t.size * 8).toString()
} else if (t.type === dbEnum.zclType.bitmap) {
recordedType = dbEnum.zclType.bitmap + (t.size * 8).toString()
} else {
if (t.atomicType == null) {
// if it's not a last arg, we call it not simple.
if (index < cmd.commandArgs.length - 1) {
isSimple = false
recordedType = 'NULL'
} else {
recordedType = 'POINTER'
}
} else {
recordedType = t.atomicType.toLowerCase()
}
}
arg.baseType = recordedType
single += `${recordedType}`
// Deal with arrays
if (arg.isArray) {
single += '[]'
arg.baseType = 'ARRAY'
if (index < cmd.commandArgs.length - 1) isSimple = false
}
// Deal with optionality
arg.isOptional = false
if (
arg.removedIn != null ||
arg.introducedIn != null ||
arg.presentIf != null
) {
single += '?'
arg.isOptional = true
if (arg.presentIf != null) isSimple = false
}
sig.push(single)
index++
}
return {
signature: sig.toString(),
isSimple: isSimple
}
}
/**
*
* @param {*} type
* @param {*} dataType
* @param {*} clusterId
* @param {*} packageIds
* @param {*} context
* @returns The size and sign of a zcl data type
*/
async function zcl_data_type_size_and_sign(
type,
dataType,
clusterId,
packageIds,
context
) {
let result = 0
let isSigned = false
if (dataType.discriminatorName.toLowerCase() == dbEnum.zclType.bitmap) {
let bitmap = await queryZcl.selectBitmapByNameAndClusterId(
context.global.db,
dataType.name,
clusterId,
packageIds
)
result = bitmap.size
} else if (dataType.discriminatorName.toLowerCase() == dbEnum.zclType.enum) {
let en = await queryZcl.selectEnumByNameAndClusterId(
context.global.db,
dataType.name,
clusterId,
packageIds
)
result = en.size
} else if (
dataType.discriminatorName.toLowerCase() == dbEnum.zclType.number
) {
let number = await queryZcl.selectNumberByNameAndClusterId(
context.global.db,
dataType.name,
clusterId,
packageIds
)
isSigned = number.isSigned
result = number.size
} else {
env.logWarning(type + ' is a complex type and size could not be determined')
}
return { size: result, isSigned: isSigned }
}
exports.clusterComparator = clusterComparator
exports.attributeComparator = attributeComparator
exports.commandComparator = commandComparator
exports.eventComparator = eventComparator
exports.sortStructsByDependency = sortStructsByDependency
exports.isEnum = isEnum
exports.isBitmap = isBitmap
exports.isStruct = isStruct
exports.isEvent = isEvent
exports.asUnderlyingZclTypeWithPackageId = asUnderlyingZclTypeWithPackageId
exports.determineType = determineType
exports.dataTypeCharacterFormatter = dataTypeCharacterFormatter
exports.calculateBytes = calculateBytes
exports.createCommandSignature = createCommandSignature
exports.zcl_data_type_size_and_sign = zcl_data_type_size_and_sign