diff --git a/src-electron/generator/helper-endpointconfig.js b/src-electron/generator/helper-endpointconfig.js index 5a3d35852f..44dcf567a1 100644 --- a/src-electron/generator/helper-endpointconfig.js +++ b/src-electron/generator/helper-endpointconfig.js @@ -1033,6 +1033,11 @@ async function collectAttributes( for (let attrIdx = 0; attrIdx < c.attributes.length; attrIdx++) { let a = c.attributes[attrIdx] let attributeDefaultValue = attributeDefaultValues[attrIdx] + // Whether the default value that ends up being generated is one the + // configuration asked for. An attribute that was never given a default + // value is blank, which is also what the UI shows for it. + let hasConfiguredDefaultValue = + a.defaultValue != null && a.defaultValue !== '' // typeSize is the size of a buffer needed to hold the attribute, if // that's known. let typeSize = a.typeSize @@ -1083,6 +1088,7 @@ async function collectAttributes( ) { defaultSize = 0 attributeDefaultValue = undefined + hasConfiguredDefaultValue = false } } @@ -1242,6 +1248,19 @@ async function collectAttributes( } if (a.isNullable) mask.push('nullable') if (a.mustUseTimedWrite) mask.push('must_use_timed_write') + // The generated default value cannot say whether it came from the + // configuration: an attribute left blank is generated as + // ZAP_EMPTY_DEFAULT(), which is indistinguishable from a configured + // zero, and a blank nullable attribute is generated as the encoding of + // null, which is indistinguishable from a configured null. This mask + // records the difference. It is only generated when the template asks + // for it, so that older code generation is unaffected. + if ( + options.isNoDefaultValueMaskGenerationEnabled && + !hasConfiguredDefaultValue + ) { + mask.push('no_default_value') + } let zap_type = 'UNKNOWN ATTRIBUTE TYPE' if (a.typeInfo.atomicType) { zap_type = a.typeInfo.atomicType @@ -1478,7 +1497,9 @@ function endpoint_config(options) { options.hash.allowUnknownStorageOption !== 'false', spaceForDefaultValue: options.hash.spaceForDefaultValue, isReadableMaskGenerationEnabled: - options.hash.isReadableMaskGenerationEnabled === 'true' + options.hash.isReadableMaskGenerationEnabled === 'true', + isNoDefaultValueMaskGenerationEnabled: + options.hash.isNoDefaultValueMaskGenerationEnabled === 'true' } let promise = templateUtil .ensureZclPackageIds(newContext) diff --git a/test/gen-matter-3-1.test.js b/test/gen-matter-3-1.test.js index df5d4c80e9..74e428955b 100644 --- a/test/gen-matter-3-1.test.js +++ b/test/gen-matter-3-1.test.js @@ -327,19 +327,19 @@ test( let ept = genResult.content['endpoint_config_v2.h'] expect(ept).toContain( - `{ 0x00000005, ZAP_TYPE(ENUM8), 1, ZAP_ATTRIBUTE_MASK(EXTERNAL_STORAGE) | ZAP_ATTRIBUTE_MASK(READABLE) | ZAP_ATTRIBUTE_MASK(NULLABLE), ZAP_EMPTY_DEFAULT() }, /* LastNetworkingStatus */` + `{ 0x00000005, ZAP_TYPE(ENUM8), 1, ZAP_ATTRIBUTE_MASK(EXTERNAL_STORAGE) | ZAP_ATTRIBUTE_MASK(READABLE) | ZAP_ATTRIBUTE_MASK(NULLABLE) | ZAP_ATTRIBUTE_MASK(NO_DEFAULT_VALUE), ZAP_EMPTY_DEFAULT() }, /* LastNetworkingStatus */` ) expect(ept).toContain( - ' { 0x00000000, ZAP_TYPE(TEMPERATURE), 2, ZAP_ATTRIBUTE_MASK(READABLE) | ZAP_ATTRIBUTE_MASK(NULLABLE), ZAP_SIMPLE_DEFAULT(0x8000) },' + ' { 0x00000000, ZAP_TYPE(TEMPERATURE), 2, ZAP_ATTRIBUTE_MASK(READABLE) | ZAP_ATTRIBUTE_MASK(NULLABLE) | ZAP_ATTRIBUTE_MASK(NO_DEFAULT_VALUE), ZAP_SIMPLE_DEFAULT(0x8000) },' ) let eptOld = genResult.content['endpoint_config.h'] expect(eptOld).not.toContain( - `{ 0x00000005, ZAP_TYPE(ENUM8), 1, ZAP_ATTRIBUTE_MASK(EXTERNAL_STORAGE) | ZAP_ATTRIBUTE_MASK(READABLE) | ZAP_ATTRIBUTE_MASK(NULLABLE), ZAP_EMPTY_DEFAULT() }, /* LastNetworkingStatus */` + `{ 0x00000005, ZAP_TYPE(ENUM8), 1, ZAP_ATTRIBUTE_MASK(EXTERNAL_STORAGE) | ZAP_ATTRIBUTE_MASK(READABLE) | ZAP_ATTRIBUTE_MASK(NULLABLE) | ZAP_ATTRIBUTE_MASK(NO_DEFAULT_VALUE), ZAP_EMPTY_DEFAULT() }, /* LastNetworkingStatus */` ) expect(eptOld).not.toContain( - ' { 0x00000000, ZAP_TYPE(TEMPERATURE), 2, ZAP_ATTRIBUTE_MASK(READABLE) | ZAP_ATTRIBUTE_MASK(NULLABLE), ZAP_SIMPLE_DEFAULT(0x8000) },' + ' { 0x00000000, ZAP_TYPE(TEMPERATURE), 2, ZAP_ATTRIBUTE_MASK(READABLE) | ZAP_ATTRIBUTE_MASK(NULLABLE) | ZAP_ATTRIBUTE_MASK(NO_DEFAULT_VALUE), ZAP_SIMPLE_DEFAULT(0x8000) },' ) }, testUtil.timeout.long() diff --git a/test/gen-template/matter3/endpoint_config_v2.zapt b/test/gen-template/matter3/endpoint_config_v2.zapt index 09da721e13..6add286a86 100644 --- a/test/gen-template/matter3/endpoint_config_v2.zapt +++ b/test/gen-template/matter3/endpoint_config_v2.zapt @@ -5,7 +5,7 @@ #include -{{#endpoint_config allowUnknownStorageOption="false" spaceForDefaultValue=4 isReadableMaskGenerationEnabled="true"}} +{{#endpoint_config allowUnknownStorageOption="false" spaceForDefaultValue=4 isReadableMaskGenerationEnabled="true" isNoDefaultValueMaskGenerationEnabled="true"}} // Default values for the attributes longer than a pointer, // in a form of a binary blob diff --git a/test/no-default-value-mask.test.js b/test/no-default-value-mask.test.js new file mode 100644 index 0000000000..71b6ee16f4 --- /dev/null +++ b/test/no-default-value-mask.test.js @@ -0,0 +1,155 @@ +/** + * + * Copyright (c) 2025 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. + * + * + * @jest-environment node + */ + +const genEngine = require('../src-electron/generator/generation-engine') +const env = require('../src-electron/util/env') +const dbApi = require('../src-electron/db/db-api') +const zclLoader = require('../src-electron/zcl/zcl-loader') +const importJs = require('../src-electron/importexport/import') +const querySession = require('../src-electron/db/query-session') +const testUtil = require('./test-util') + +let db +// endpoint_config_v2.h asks for the mask, endpoint_config.h does not. +let withMask +let withoutMask + +beforeAll(async () => { + env.setDevelopmentEnv() + let file = env.sqliteTestFile('no-default-value-mask') + db = await dbApi.initDatabaseAndLoadSchema( + file, + env.schemaFile(), + env.zapVersion() + ) + await zclLoader.loadZcl(db, env.builtinMatterZclMetafile()) + let templateContext = await genEngine.loadTemplates( + db, + testUtil.testTemplate.matter3 + ) + let sessionId = await querySession.createBlankSession(db) + await importJs.importDataFromFile( + db, + testUtil.matterTestFile.allClustersFileFormat2, + { sessionId: sessionId } + ) + let genResult = await genEngine.generate( + db, + sessionId, + templateContext.packageId, + {}, + { disableDeprecationWarnings: true } + ) + expect(genResult.hasErrors).toEqual(false) + withMask = genResult.content['endpoint_config_v2.h'] + withoutMask = genResult.content['endpoint_config.h'] +}, testUtil.timeout.long()) + +afterAll(() => dbApi.closeDatabase(db), testUtil.timeout.short()) + +test( + 'A blank attribute is flagged, so its generated zero is not read as configured', + () => { + expect(withMask).toContain( + '{ 0x00000005, ZAP_TYPE(ENUM8), 1, ZAP_ATTRIBUTE_MASK(READABLE) | ZAP_ATTRIBUTE_MASK(NULLABLE) | ZAP_ATTRIBUTE_MASK(NO_DEFAULT_VALUE), ZAP_EMPTY_DEFAULT() }, /* LastNetworkingStatus */' + ) + }, + testUtil.timeout.short() +) + +test( + 'A blank nullable attribute is flagged, even though null is generated for it', + () => { + // This is the case the mask exists for: ZAP fills in the encoding of null + // for a nullable attribute that was left blank, and that is otherwise + // indistinguishable from a configured null. + expect(withMask).toContain( + '{ 0x00000000, ZAP_TYPE(TEMPERATURE), 2, ZAP_ATTRIBUTE_MASK(READABLE) | ZAP_ATTRIBUTE_MASK(NULLABLE) | ZAP_ATTRIBUTE_MASK(NO_DEFAULT_VALUE), ZAP_SIMPLE_DEFAULT(0x8000) }, /* LocalTemperature */' + ) + // A configured null, on the other hand, is a value: 0xFF is the encoding of + // null for this type and it carries no mask. + expect(withMask).toContain( + '{ 0x00000011, ZAP_TYPE(INT8U), 1, ZAP_ATTRIBUTE_MASK(WRITABLE) | ZAP_ATTRIBUTE_MASK(READABLE) | ZAP_ATTRIBUTE_MASK(NULLABLE), ZAP_SIMPLE_DEFAULT(0xFF) }, /* OnLevel */' + ) + }, + testUtil.timeout.short() +) + +test( + 'A blank attribute with a min and a max is flagged too', + () => { + // The default value is behind a pointer to a min/max/value triplet here, + // which is the other place a generated zero could be mistaken for a + // configured one. + expect(withMask).toContain( + '{ 0x00000002, ZAP_TYPE(ENUM8), 1, ZAP_ATTRIBUTE_MASK(MIN_MAX) | ZAP_ATTRIBUTE_MASK(WRITABLE) | ZAP_ATTRIBUTE_MASK(READABLE) | ZAP_ATTRIBUTE_MASK(NO_DEFAULT_VALUE), ZAP_MIN_MAX_DEFAULTS_INDEX(28) }, /* ScheduleProgrammingVisibility */' + ) + }, + testUtil.timeout.short() +) + +test( + 'An attribute whose default value is dropped is flagged', + () => { + // CurrentHeapFree is answered by the attribute access interface, so its + // default value is not generated at all and nothing should read the zero + // that is left behind. + expect(withMask).toContain( + '{ 0x00000001, ZAP_TYPE(INT64U), 8, ZAP_ATTRIBUTE_MASK(EXTERNAL_STORAGE) | ZAP_ATTRIBUTE_MASK(READABLE) | ZAP_ATTRIBUTE_MASK(NO_DEFAULT_VALUE), ZAP_EMPTY_DEFAULT() }, /* CurrentHeapFree */' + ) + }, + testUtil.timeout.short() +) + +test( + 'A configured value of zero is not flagged', + () => { + expect(withMask).toContain( + '{ 0x00000000, ZAP_TYPE(BOOLEAN), 1, ZAP_ATTRIBUTE_MASK(READABLE), ZAP_SIMPLE_DEFAULT(0x00) }, /* OnOff */' + ) + }, + testUtil.timeout.short() +) + +test( + 'Every generated empty default value is flagged', + () => { + // The mask is only useful if it is exhaustive: an unflagged attribute has + // to be one whose value was configured. + let unflagged = withMask + .split('\n') + .filter( + (line) => + line.includes('ZAP_EMPTY_DEFAULT()') && + line.includes('ZAP_ATTRIBUTE_MASK') && + !line.includes('NO_DEFAULT_VALUE') + ) + expect(unflagged).toEqual([]) + }, + testUtil.timeout.short() +) + +test( + 'A template that does not ask for the mask never gets it', + () => { + expect(withoutMask).not.toContain('NO_DEFAULT_VALUE') + }, + testUtil.timeout.short() +)