Decode TLV payload data and present it in a human readable format (#27638)

* Start with a flat tree library for a human TLV format

* Temp change for test

* Switch to a flat list and more flexible finding ... expect I want to find by id AND name eventually

* Clang-format

* Add tests for searching by name in a flat tree

* Provide non-array find-entry

* Have a good tree position that works for navigating and descend/ascend

* Added more documentation

* Add more unit tests

* Fix naming

* Support current path for flat tree positions

* Restyle

* Add IM message encoding, to have pretty-print of data once available

* Added secure channel message formats

* Add UDC defintions

* Rename things

* make the matter file parseable

* Attempt to start a codegen for tlv meta mapping

* Restyle

* Add missing files

* Restyle

* Have some codegen working, start defining types and names

* Start implementing a bit of a table generation. not done, but tables start to exist

* Support events for tables

* Add support for commands (untested though)

* More work, all except lists and constants are code-generated

* Restyle

* List support and better tag support including anonymous support

* Make tags specific: many tags are NOT context tags currently

* Restyle

* Add some test data for development tests

* Start adding some test support ... to be removed later

* Code compiles

* Add a unit test that compiles and runs

* Starting some decoding support. Still very much broken

* A bit more decoding, this time we handle lists. TLV interface is VERY bad

* Better decoding, we now show data

* Add some item information, to prepare for enum and bitmap decoding

* Restyle

* Add error messages on usage of command line

* remove a non yes/no argument

* Update error syntax for 2 more arguments

* update the help. using both true/false and yes/no is a mess

* Update logic for decoding

* Do not allow restyle

* Better StringBuilder formatting

* Test adjustment

* Naming update

* Restyle

* Add Format option for buffer writers and string builders

* Update comments

* Updated to only have printf inside stringbuilder and NOT bufferwriter

* Restyle

* Add missing file

* remove cpp file comments

* Fix cast to make clang happy

* Much better formatting and make the compile clang-friendly

* minor const correctness change. TLVReader has non-const getters

* Add special tags for payloads of things

* Added logic for binary data and payloads, to process things

* Start adding clusters metadata, make everything const-correct

* Minor update

* Start updating formats

* Never pass null pointer in vsnprintf, since our size available is never 0

* Restyle

* Restyle

* Better decoding

* Clean up some printfs

* Iterator decoding seems to work, including getting sub-data types

* Better organization of code ... protocols decoder is actually a class now

* Allow passing in decode trees for protocol understanding

* Better arg parsing - was able to test for invalid data

* Restyle

* Add back reset call to stringbuilder

* Restyle and make protocol decoding actually work

* Unformatted protocols/cluster meta

* Add more trace data for testing, fix SEGFAULT in decoder

* Support non-struct list entries

* Switch list decoding logic to be inside generated metadata

* Restyle

* Fix compilation and generation

* Start having codegen support for protocols metadata

* Move clusters meta to compile time codegen as well

* Restyle

* Cleanup dependencies a bit

* Start making TestDecoding be actual unit tests

* more unit tests, without protocol decoding

* Slightly better formatting

* More unit tests ... although invalid data looks odd

* Better formatting of unknown attributes

* Updated tests

* Restyle

* Better messaging, test overflows

* Removed unused file

* Undo submodule update

* Add some tests for invoke. Command list is NOT complete

* Restyle

* Yield commands that have no request structure

* Fix comment

* Start adding some unit tests for cpp-tlvmeta codegen

* Add tests for real

* Allow both hex and json at the same time for output

* Rename log_json to just json

* Add file output option for json tracing

* make the output look like a json array when outputing to file

* Restyle

* Fix support of "json:log"

* Fix support of "json:log"

* make things compile

* Rename open/close to openfile/closefile to avoid override errors

* Restyle

* StartsWith should be available now globally as it is always used

* StartsWith should be available now globally as it is always used

* Forward declare json to make arm cross compile pass

* Forward declare json to make arm cross compile pass

* Restyle

* Add some support for formatting enums and bitmaps

* Restyle

* Add json_tracing exceptions for includes checks

* Proper bitmap support with tests, status codes are bitmaps now

* Restyle

* Update test for overflow to have more unique values

* Restyle

* Fix decoding of command inputs and names

* Add a fuzz test for payload decoder

* Handle invalid TLV

* Add more error handling. Fuzzing runs longer now

* Restyle

* Make clang happy

* Fix efr32 unit test compilation

* Fix python lint

* Restyle

* Fix typo and restyle

* Restyle

* Add dependencies to flat-tree for generated code: they are needed

* make tests use uppercase for unknown tags as this is the code update I made recently

* Undo submodule update

* Make clang-tidy happy

* Fix subscribe response message indexing to match spec

---------

Co-authored-by: Andrei Litvin <andreilitvin@google.com>
diff --git a/BUILD.gn b/BUILD.gn
index 6cc0d55..5b3a9e4 100644
--- a/BUILD.gn
+++ b/BUILD.gn
@@ -54,6 +54,7 @@
         "${chip_root}/src/credentials/tests:fuzz-chip-cert",
         "${chip_root}/src/lib/core/tests:fuzz-tlv-reader",
         "${chip_root}/src/lib/dnssd/minimal_mdns/tests:fuzz-minmdns-packet-parsing",
+        "${chip_root}/src/lib/format/tests:fuzz-payload-decoder",
       ]
     }
   }
diff --git a/build/chip/chip_codegen.gni b/build/chip/chip_codegen.gni
index 64c0642..1a6927b 100644
--- a/build/chip/chip_codegen.gni
+++ b/build/chip/chip_codegen.gni
@@ -53,9 +53,19 @@
       rebase_path(target_gen_dir, root_build_dir),
       "--expected-outputs",
       rebase_path(_expected_outputs, root_build_dir),
-      rebase_path(_idl_file, root_build_dir),
     ]
 
+    if (defined(invoker.options)) {
+      foreach(option, invoker.options) {
+        args += [
+          "--option",
+          option,
+        ]
+      }
+    }
+
+    args += [ rebase_path(_idl_file, root_build_dir) ]
+
     inputs = [
       _idl_file,
       _expected_outputs,
@@ -313,12 +323,15 @@
                                "generator",
                                "input",
                                "outputs",
+                               "options",
                                "public_configs",
                              ])
     }
   } else {
     _name = target_name
 
+    not_needed(invoker, [ "options" ])
+
     # This constructs a path like:
     #  FROM all-clusters-app.matter (inside examples/all-clusters-app/all-clusters-common/)
     #  USING "cpp-app" for generator:
diff --git a/examples/chip-tool/args.gni b/examples/chip-tool/args.gni
index a6f6dce..96dd37f 100644
--- a/examples/chip-tool/args.gni
+++ b/examples/chip-tool/args.gni
@@ -27,3 +27,6 @@
 
 # Perfetto requires C++17
 cpp_standard = "gnu++17"
+
+matter_log_json_payload_hex = true
+matter_log_json_payload_decode_full = true
diff --git a/scripts/pregenerate/__init__.py b/scripts/pregenerate/__init__.py
index 53c1e65..a6f759b 100644
--- a/scripts/pregenerate/__init__.py
+++ b/scripts/pregenerate/__init__.py
@@ -20,7 +20,8 @@
 from typing import Iterator, List, Optional
 
 from .types import IdlFileType, InputIdlFile
-from .using_codegen import CodegenCppAppPregenerator, CodegenJavaClassPregenerator, CodegenJavaJNIPregenerator
+from .using_codegen import (CodegenCppAppPregenerator, CodegenCppClustersTLVMetaPregenerator,
+                            CodegenCppProtocolsTLVMetaPregenerator, CodegenJavaClassPregenerator, CodegenJavaJNIPregenerator)
 from .using_zap import ZapApplicationPregenerator
 
 
@@ -94,6 +95,8 @@
         CodegenJavaJNIPregenerator(sdk_root),
         CodegenJavaClassPregenerator(sdk_root),
         CodegenCppAppPregenerator(sdk_root),
+        CodegenCppClustersTLVMetaPregenerator(sdk_root),
+        CodegenCppProtocolsTLVMetaPregenerator(sdk_root),
 
         # ZAP codegen
         ZapApplicationPregenerator(sdk_root),
diff --git a/scripts/pregenerate/using_codegen.py b/scripts/pregenerate/using_codegen.py
index 5d790d8..a48f187 100644
--- a/scripts/pregenerate/using_codegen.py
+++ b/scripts/pregenerate/using_codegen.py
@@ -27,11 +27,12 @@
 class CodegenTarget:
     """A target that uses `scripts/codegen.py` to generate files."""
 
-    def __init__(self, idl: InputIdlFile, generator: str, sdk_root: str, runner):
+    def __init__(self, idl: InputIdlFile, generator: str, sdk_root: str, runner, options=[]):
         self.idl = idl
         self.generator = generator
         self.sdk_root = sdk_root
         self.runner = runner
+        self.options = options
 
         if idl.file_type != IdlFileType.MATTER:
             raise Exception(
@@ -51,8 +52,12 @@
             '--log-level', 'fatal',
             '--generator', self.generator,
             '--output-dir', output_dir,
-            self.idl.full_path
         ]
+        for option in self.options:
+            cmd.append("--option")
+            cmd.append(option)
+
+        cmd.append(self.idl.full_path)
 
         logging.debug(f"Executing {cmd}")
         self.runner.run(cmd)
@@ -97,6 +102,9 @@
         if idl.file_type != IdlFileType.MATTER:
             return False
 
+        if '/lib/format/' in idl.relative_path:
+            return False
+
         # we should not be checked for these, but verify just in case
         if '/tests/' in idl.relative_path:
             return False
@@ -105,3 +113,29 @@
 
     def CreateTarget(self, idl: InputIdlFile, runner):
         return CodegenTarget(sdk_root=self.sdk_root, idl=idl, generator="cpp-app", runner=runner)
+
+
+class CodegenCppProtocolsTLVMetaPregenerator:
+    """Pregeneration logic for "cpp-app" codegen.py outputs"""
+
+    def __init__(self, sdk_root):
+        self.sdk_root = sdk_root
+
+    def Accept(self, idl: InputIdlFile):
+        return (idl.file_type == IdlFileType.MATTER) and idl.relative_path.endswith('/protocol_messages.matter')
+
+    def CreateTarget(self, idl: InputIdlFile, runner):
+        return CodegenTarget(sdk_root=self.sdk_root, idl=idl, generator="cpp-tlvmeta", options=["table_name:protocols_meta"], runner=runner)
+
+
+class CodegenCppClustersTLVMetaPregenerator:
+    """Pregeneration logic for "cpp-app" codegen.py outputs"""
+
+    def __init__(self, sdk_root):
+        self.sdk_root = sdk_root
+
+    def Accept(self, idl: InputIdlFile):
+        return (idl.file_type == IdlFileType.MATTER) and idl.relative_path.endswith('/controller-clusters.matter')
+
+    def CreateTarget(self, idl: InputIdlFile, runner):
+        return CodegenTarget(sdk_root=self.sdk_root, idl=idl, generator="cpp-tlvmeta", options=["table_name:clusters_meta"], runner=runner)
diff --git a/scripts/py_matter_idl/files.gni b/scripts/py_matter_idl/files.gni
index 0118c1a..1f7a7b0 100644
--- a/scripts/py_matter_idl/files.gni
+++ b/scripts/py_matter_idl/files.gni
@@ -3,12 +3,18 @@
 
 # Templates used for generation
 matter_idl_generator_templates = [
-  "${chip_root}/scripts/py_matter_idl/matter_idl/generators/java/ChipClustersCpp.jinja",
-  "${chip_root}/scripts/py_matter_idl/matter_idl/generators/java/ChipClustersRead.jinja",
-  "${chip_root}/scripts/py_matter_idl/matter_idl/generators/java/ClusterWriteMapping.jinja",
-  "${chip_root}/scripts/py_matter_idl/matter_idl/generators/java/ClusterIDMapping.jinja",
   "${chip_root}/scripts/py_matter_idl/matter_idl/generators/cpp/application/CallbackStubSource.jinja",
   "${chip_root}/scripts/py_matter_idl/matter_idl/generators/cpp/application/PluginApplicationCallbacksHeader.jinja",
+  "${chip_root}/scripts/py_matter_idl/matter_idl/generators/cpp/tlvmeta/TLVMetaData_cpp.jinja",
+  "${chip_root}/scripts/py_matter_idl/matter_idl/generators/cpp/tlvmeta/TLVMetaData_h.jinja",
+  "${chip_root}/scripts/py_matter_idl/matter_idl/generators/java/CHIPCallbackTypes.jinja",
+  "${chip_root}/scripts/py_matter_idl/matter_idl/generators/java/ChipClustersCpp.jinja",
+  "${chip_root}/scripts/py_matter_idl/matter_idl/generators/java/ChipClustersRead.jinja",
+  "${chip_root}/scripts/py_matter_idl/matter_idl/generators/java/CHIPGlobalCallbacks_cpp.jinja",
+  "${chip_root}/scripts/py_matter_idl/matter_idl/generators/java/CHIPReadCallbacks_h.jinja",
+  "${chip_root}/scripts/py_matter_idl/matter_idl/generators/java/ClusterIDMapping.jinja",
+  "${chip_root}/scripts/py_matter_idl/matter_idl/generators/java/ClusterReadMapping.jinja",
+  "${chip_root}/scripts/py_matter_idl/matter_idl/generators/java/ClusterWriteMapping.jinja",
 ]
 
 matter_idl_generator_sources = [
@@ -16,11 +22,19 @@
   "${chip_root}/scripts/py_matter_idl/matter_idl/generators/__init__.py",
   "${chip_root}/scripts/py_matter_idl/matter_idl/generators/cpp/__init__.py",
   "${chip_root}/scripts/py_matter_idl/matter_idl/generators/cpp/application/__init__.py",
+  "${chip_root}/scripts/py_matter_idl/matter_idl/generators/cpp/tlvmeta/__init__.py",
   "${chip_root}/scripts/py_matter_idl/matter_idl/generators/filters.py",
   "${chip_root}/scripts/py_matter_idl/matter_idl/generators/java/__init__.py",
+  "${chip_root}/scripts/py_matter_idl/matter_idl/generators/registry.py",
   "${chip_root}/scripts/py_matter_idl/matter_idl/generators/types.py",
+  "${chip_root}/scripts/py_matter_idl/matter_idl/lint/__init__.py",
+  "${chip_root}/scripts/py_matter_idl/matter_idl/lint/lint_rules_parser.py",
+  "${chip_root}/scripts/py_matter_idl/matter_idl/lint/types.py",
   "${chip_root}/scripts/py_matter_idl/matter_idl/matter_idl_parser.py",
   "${chip_root}/scripts/py_matter_idl/matter_idl/matter_idl_types.py",
+  "${chip_root}/scripts/py_matter_idl/matter_idl/test_generators.py",
+  "${chip_root}/scripts/py_matter_idl/matter_idl/test_matter_idl_parser.py",
+  "${chip_root}/scripts/py_matter_idl/matter_idl/test_xml_parser.py",
   "${chip_root}/scripts/py_matter_idl/matter_idl/xml_parser.py",
   "${chip_root}/scripts/py_matter_idl/matter_idl/zapxml/__init__.py",
   "${chip_root}/scripts/py_matter_idl/matter_idl/zapxml/handlers/__init__.py",
diff --git a/scripts/py_matter_idl/matter_idl/generators/cpp/tlvmeta/TLVMetaData_cpp.jinja b/scripts/py_matter_idl/matter_idl/generators/cpp/tlvmeta/TLVMetaData_cpp.jinja
new file mode 100644
index 0000000..f748875
--- /dev/null
+++ b/scripts/py_matter_idl/matter_idl/generators/cpp/tlvmeta/TLVMetaData_cpp.jinja
@@ -0,0 +1,44 @@
+#include <tlv/meta/{{table_name}}.h>
+
+namespace chip {
+namespace TLVMeta {
+namespace {
+
+using namespace chip::FlatTree;
+using namespace chip::TLV;
+
+{%- for table in sub_tables %}
+
+const Entry<ItemInfo> _{{table.full_name}}[] = {
+  {%-  for entry in table.entries %}
+  { { {{entry.code}}, "{{entry.name}}", ItemType::{{entry.item_type}} }, {{entry.reference | indexInTable(sub_tables)}} }, // {{entry.real_type}}
+  {%- endfor %}
+};
+{%- endfor %}
+
+const Entry<ItemInfo> _all_clusters[] = {
+{%- for cluster in clusters | sort(attribute='code') %}
+  { { ClusterTag({{"0x%02X" | format(cluster.code)}}), "{{cluster.name}}", ItemType::kDefault }, {{cluster.name | indexInTable(sub_tables)}} },
+{%- endfor %}
+
+};
+
+// For any non-structure list like u64[] or similar.
+const Entry<ItemInfo> _primitive_type_list[] = {
+  { { AnonymousTag(), "[]", ItemType::kDefault }, kInvalidNodeIndex },
+};
+
+} // namespace
+
+#define _ENTRY(n) { sizeof(n) / sizeof(n[0]), n}
+
+const std::array<const Node<ItemInfo>, {{ sub_tables | length }} + 2> {{table_name}} = { {
+  _ENTRY(_all_clusters), // 0
+  _ENTRY(_primitive_type_list), // 1
+{%- for table in sub_tables %}
+  _ENTRY(_{{table.full_name}}), // {{loop.index + 1}}
+{%- endfor %}
+} };
+
+} // namespace TLVMeta
+} // namespace chip
diff --git a/scripts/py_matter_idl/matter_idl/generators/cpp/tlvmeta/TLVMetaData_h.jinja b/scripts/py_matter_idl/matter_idl/generators/cpp/tlvmeta/TLVMetaData_h.jinja
new file mode 100644
index 0000000..f8d1e77
--- /dev/null
+++ b/scripts/py_matter_idl/matter_idl/generators/cpp/tlvmeta/TLVMetaData_h.jinja
@@ -0,0 +1,12 @@
+#include <lib/format/tlv_meta.h>
+#include <lib/format/FlatTree.h>
+
+#include <array>
+
+namespace chip {
+namespace TLVMeta {
+
+extern const std::array<const FlatTree::Node<ItemInfo>, {{ sub_tables | length }} + 2> {{table_name}};
+
+} // namespace TLVMeta
+} // namespace chip
diff --git a/scripts/py_matter_idl/matter_idl/generators/cpp/tlvmeta/__init__.py b/scripts/py_matter_idl/matter_idl/generators/cpp/tlvmeta/__init__.py
new file mode 100644
index 0000000..0bd57e2
--- /dev/null
+++ b/scripts/py_matter_idl/matter_idl/generators/cpp/tlvmeta/__init__.py
@@ -0,0 +1,299 @@
+#!/usr/bin/env python
+# Copyright (c) 2023 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 os
+from dataclasses import dataclass
+from typing import Generator, List, Optional
+
+from matter_idl.generators import CodeGenerator, GeneratorStorage
+from matter_idl.matter_idl_types import Cluster, ClusterSide, Field, Idl, StructTag
+
+
+@dataclass
+class TableEntry:
+    code: str                   # Encoding like ContextTag() or AnonymousTag() or similar
+    name: str                   # human friendly name
+    reference: Optional[str]    # reference to full name
+    real_type: str              # real type
+    item_type: str = 'kDefault'  # type flag for decoding
+
+
+@dataclass
+class Table:
+    # Usable variable fully qualified name (like <Cluster>_<name>)
+    full_name: str
+    entries: List[TableEntry]
+
+
+class ClusterTablesGenerator:
+    """Handles conversion from a cluster to tables."""
+
+    def __init__(self, cluster: Cluster):
+        self.cluster = cluster
+        self.known_types = set()  # all types where we create reference_to
+        self.list_types = set()  # all types that require a list entry
+        self.item_type_map = {
+            "protocol_cluster_id": "kProtocolClusterId",
+            "protocol_attribute_id": "kProtocolAttributeId",
+            "protocol_command_id": "kProtocolCommandId",
+            "protocol_event_id": "kProtocolEventId",
+
+            "cluster_attribute_payload": "kProtocolPayloadAttribute",
+            "cluster_command_payload": "kProtocolPayloadCommand",
+            "cluster_event_payload": "kProtocolPayloadEvent",
+
+            "protocol_binary_data": "kProtocolBinaryData",
+        }
+
+        for e in self.cluster.enums:
+            self.item_type_map[e.name] = "kEnum"
+
+        for b in self.cluster.bitmaps:
+            self.item_type_map[b.name] = "kBitmap"
+
+    def FieldEntry(self, field: Field, tag_type: str = 'ContextTag') -> TableEntry:
+        type_reference = "%s_%s" % (self.cluster.name, field.data_type.name)
+        if type_reference not in self.known_types:
+            type_reference = None
+
+        item_type = self.item_type_map.get(field.data_type.name, 'kDefault')
+
+        real_type = "%s::%s" % (self.cluster.name, field.data_type.name)
+        if field.is_list:
+            real_type = real_type + "[]"
+            item_type = "kList"
+
+            if type_reference:
+                self.list_types.add(type_reference)
+                type_reference = type_reference + "_list_"
+            else:
+                type_reference = "primitive_type_list_"
+
+        return TableEntry(
+            code=f'{tag_type}({field.code})',
+            name=field.name,
+            reference=type_reference,
+            real_type=real_type,
+            item_type=item_type,
+        )
+
+    def ComputeKnownTypes(self):
+        self.known_types.clear()
+
+        for s in self.cluster.structs:
+            self.known_types.add("%s_%s" % (self.cluster.name, s.name))
+
+        # Events are structures
+        for e in self.cluster.events:
+            self.known_types.add("%s_%s" % (self.cluster.name, e.name))
+
+        for e in self.cluster.enums:
+            self.known_types.add("%s_%s" % (self.cluster.name, e.name))
+
+        for b in self.cluster.bitmaps:
+            self.known_types.add("%s_%s" % (self.cluster.name, b.name))
+
+    def CommandEntries(self) -> Generator[TableEntry, None, None]:
+        # yield entries for every command input
+        for c in self.cluster.commands:
+            if c.input_param:
+                yield TableEntry(
+                    name=c.name,
+                    code=f'CommandTag({c.code})',
+                    reference="%s_%s" % (
+                        self.cluster.name, c.input_param),
+                    real_type="%s::%s::%s" % (
+                        self.cluster.name, c.name, c.input_param)
+                )
+            else:
+                yield TableEntry(
+                    name=c.name,
+                    code=f'CommandTag({c.code})',
+                    reference=None,
+                    real_type="%s::%s::()" % (
+                        self.cluster.name, c.name)
+                )
+
+        # yield entries for every command output. We use "respons struct"
+        # for this to figure out where to tag IDs from.
+        for c in self.cluster.structs:
+            if c.tag != StructTag.RESPONSE:
+                continue
+            yield TableEntry(
+                name=c.name,
+                code=f'CommandTag({c.code})',
+                reference="%s_%s" % (
+                    self.cluster.name, c.name),
+                real_type="%s::%s" % (self.cluster.name, c.name),
+            )
+
+    def GenerateTables(self) -> Generator[Table, None, None]:
+        self.ComputeKnownTypes()
+
+        # Clusters have attributes. They are direct descendants for
+        # attributes
+        cluster_entries = []
+        cluster_entries.extend([self.FieldEntry(
+            a.definition, tag_type='AttributeTag') for a in self.cluster.attributes])
+
+        cluster_entries.extend([
+            # events always reference an existing struct
+            TableEntry(
+                code=f'EventTag({e.code})',
+                name=e.name,
+                reference="%s_%s" % (self.cluster.name, e.name),
+                real_type='%s::%s' % (self.cluster.name, e.name)
+            )
+            for e in self.cluster.events
+        ])
+        cluster_entries.extend(
+            [entry for entry in self.CommandEntries()]
+        )
+
+        yield Table(
+            full_name=self.cluster.name,
+            entries=cluster_entries,
+        )
+
+        for s in self.cluster.structs:
+            yield Table(
+                full_name="%s_%s" % (self.cluster.name, s.name),
+                entries=[self.FieldEntry(field) for field in s.fields]
+            )
+
+        for e in self.cluster.events:
+            yield Table(
+                full_name="%s_%s" % (self.cluster.name, e.name),
+                entries=[self.FieldEntry(field) for field in e.fields]
+            )
+
+        # some items have lists, create an intermediate item for those
+        for name in self.list_types:
+            yield Table(
+                full_name="%s_list_" % name,
+                entries=[
+                    TableEntry(
+                        code="AnonymousTag()",
+                        name="[]",
+                        reference=name,
+                        real_type="%s[]" % name,
+                    )
+                ]
+            )
+
+        for e in self.cluster.enums:
+            yield Table(
+                full_name="%s_%s" % (self.cluster.name, e.name),
+                entries=[
+                    TableEntry(
+                        code="ConstantValueTag(0x%X)" % entry.code,
+                        name=entry.name,
+                        reference=None,
+                        real_type="%s::%s::%s" % (self.cluster.name, e.name, entry.name)
+                    )
+                    for entry in e.entries
+                ]
+            )
+
+        for e in self.cluster.bitmaps:
+            yield Table(
+                full_name="%s_%s" % (self.cluster.name, e.name),
+                entries=[
+                    TableEntry(
+                        code="ConstantValueTag(0x%X)" % entry.code,
+                        name=entry.name,
+                        reference=None,
+                        real_type="%s::%s::%s" % (self.cluster.name, e.name, entry.name)
+                    )
+                    for entry in e.entries
+                ]
+            )
+
+
+def CreateTables(idl: Idl) -> List[Table]:
+    result = []
+    for cluster in idl.clusters:
+        result.extend(
+            [table for table in ClusterTablesGenerator(cluster).GenerateTables()])
+
+    return result
+
+
+def IndexInTable(name: Optional[str], table: List[Table]) -> str:
+    """Find the index of the given name in the table.
+
+    The index is 1-based (to allow for a first entry containing a
+    starting point for the app)
+    """
+    if not name:
+        return "kInvalidNodeIndex"
+
+    if name == "primitive_type_list_":
+        return "1"
+
+    for idx, t in enumerate(table):
+        if t.full_name == name:
+            # Index skipping hard-coded items
+            return idx + 2
+
+    raise Exception("Name %r not found in table" % name)
+
+
+class TLVMetaDataGenerator(CodeGenerator):
+    """
+    Generation of cpp code containing TLV metadata information.
+
+    Epecting extra option for constant naming
+
+    Example execution via codegen.py:
+
+      ./scripts/codegen.py                        \
+          --output-dir out/metaexample            \
+          --generator cpp-tlvmeta                 \
+          --option table_name:protocols_meta      \
+          src/lib/format/protocol_messages.matter
+    """
+
+    def __init__(self, storage: GeneratorStorage, idl: Idl, table_name: str = "clusters_meta", **kargs):
+        super().__init__(storage, idl, fs_loader_searchpath=os.path.dirname(__file__))
+        self.table_name = table_name
+        self.jinja_env.filters['indexInTable'] = IndexInTable
+
+    def internal_render_all(self):
+        """
+        Renders the cpp and header files required for applications
+        """
+
+        tables = CreateTables(self.idl)
+
+        self.internal_render_one_output(
+            template_path="TLVMetaData_cpp.jinja",
+            output_file_name=f"tlv/meta/{self.table_name}.cpp",
+            vars={
+                'clusters': [c for c in self.idl.clusters if c.side == ClusterSide.CLIENT],
+                'table_name': self.table_name,
+                'sub_tables': tables,
+
+            }
+        )
+
+        self.internal_render_one_output(
+            template_path="TLVMetaData_h.jinja",
+            output_file_name=f"tlv/meta/{self.table_name}.h",
+            vars={
+                'clusters': [c for c in self.idl.clusters if c.side == ClusterSide.CLIENT],
+                'table_name': self.table_name,
+                'sub_tables': tables,
+            }
+        )
diff --git a/scripts/py_matter_idl/matter_idl/generators/registry.py b/scripts/py_matter_idl/matter_idl/generators/registry.py
index 7f1e17b..e3c8e1e 100644
--- a/scripts/py_matter_idl/matter_idl/generators/registry.py
+++ b/scripts/py_matter_idl/matter_idl/generators/registry.py
@@ -16,6 +16,7 @@
 import importlib
 
 from matter_idl.generators.cpp.application import CppApplicationGenerator
+from matter_idl.generators.cpp.tlvmeta import TLVMetaDataGenerator
 from matter_idl.generators.java import JavaClassGenerator, JavaJNIGenerator
 
 
@@ -28,6 +29,7 @@
     JAVA_JNI = enum.auto()
     JAVA_CLASS = enum.auto()
     CPP_APPLICATION = enum.auto()
+    CPP_TLVMETA = enum.auto()
     CUSTOM = enum.auto()
 
     def Create(self, *args, **kargs):
@@ -37,6 +39,8 @@
             return JavaClassGenerator(*args, **kargs)
         elif self == CodeGenerator.CPP_APPLICATION:
             return CppApplicationGenerator(*args, **kargs)
+        elif self == CodeGenerator.CPP_TLVMETA:
+            return TLVMetaDataGenerator(*args, **kargs)
         elif self == CodeGenerator.CUSTOM:
             # Use a package naming convention to find the custom generator:
             # ./matter_idl_plugin/__init__.py defines a subclass of CodeGenerator named CustomGenerator.
@@ -65,5 +69,6 @@
     'java-jni': CodeGenerator.JAVA_JNI,
     'java-class': CodeGenerator.JAVA_CLASS,
     'cpp-app': CodeGenerator.CPP_APPLICATION,
+    'cpp-tlvmeta': CodeGenerator.CPP_TLVMETA,
     'custom': CodeGenerator.CUSTOM,
 }
diff --git a/scripts/py_matter_idl/matter_idl/test_generators.py b/scripts/py_matter_idl/matter_idl/test_generators.py
index 146b624..0576f32 100755
--- a/scripts/py_matter_idl/matter_idl/test_generators.py
+++ b/scripts/py_matter_idl/matter_idl/test_generators.py
@@ -32,6 +32,7 @@
 
 from matter_idl.generators import GeneratorStorage
 from matter_idl.generators.cpp.application import CppApplicationGenerator
+from matter_idl.generators.cpp.tlvmeta import TLVMetaDataGenerator
 from matter_idl.generators.java import JavaClassGenerator, JavaJNIGenerator
 from matter_idl.matter_idl_types import Idl
 
@@ -121,6 +122,8 @@
             return JavaClassGenerator(storage, idl)
         if self.generator_name.lower() == 'cpp-app':
             return CppApplicationGenerator(storage, idl)
+        if self.generator_name.lower() == 'cpp-tlvmeta':
+            return TLVMetaDataGenerator(storage, idl, table_name="clusters_meta")
         if self.generator_name.lower() == 'custom-example-proto':
             sys.path.append(os.path.abspath(
                 os.path.join(os.path.dirname(__file__), '../examples')))
diff --git a/scripts/py_matter_idl/matter_idl/tests/available_tests.yaml b/scripts/py_matter_idl/matter_idl/tests/available_tests.yaml
index f8f757b..3a50ebc 100644
--- a/scripts/py_matter_idl/matter_idl/tests/available_tests.yaml
+++ b/scripts/py_matter_idl/matter_idl/tests/available_tests.yaml
@@ -69,6 +69,14 @@
         app/PluginApplicationCallbacks.h: outputs/large_lighting_app/cpp-app/PluginApplicationCallbacks.h
         app/callback-stub.cpp: outputs/large_lighting_app/cpp-app/callback-stub.cpp
 
+cpp-tlvmeta:
+    inputs/cluster_with_commands.matter:
+        tlv/meta/clusters_meta.cpp: outputs/cluster_with_commands/cpp-tlvmeta/clusters_meta.cpp
+        tlv/meta/clusters_meta.h: outputs/cluster_with_commands/cpp-tlvmeta/clusters_meta.h
+    inputs/cluster_struct_attribute.matter:
+        tlv/meta/clusters_meta.cpp: outputs/cluster_struct_attribute/cpp-tlvmeta/clusters_meta.cpp
+        tlv/meta/clusters_meta.h: outputs/cluster_struct_attribute/cpp-tlvmeta/clusters_meta.h
+
 custom-example-proto:
     inputs/several_clusters.matter:
         proto/first_cluster.proto: outputs/proto/first_cluster.proto
diff --git a/scripts/py_matter_idl/matter_idl/tests/outputs/cluster_struct_attribute/cpp-tlvmeta/clusters_meta.cpp b/scripts/py_matter_idl/matter_idl/tests/outputs/cluster_struct_attribute/cpp-tlvmeta/clusters_meta.cpp
new file mode 100644
index 0000000..4985c3e
--- /dev/null
+++ b/scripts/py_matter_idl/matter_idl/tests/outputs/cluster_struct_attribute/cpp-tlvmeta/clusters_meta.cpp
@@ -0,0 +1,48 @@
+#include <tlv/meta/clusters_meta.h>
+
+namespace chip {
+namespace TLVMeta {
+namespace {
+
+using namespace chip::FlatTree;
+using namespace chip::TLV;
+
+const Entry<ItemInfo> _DemoCluster[] = {
+  { { AttributeTag(5), "singleFailSafe", ItemType::kDefault }, 3 }, // DemoCluster::ArmFailSafeRequest
+  { { AttributeTag(100), "armFailsafes", ItemType::kList }, 4 }, // DemoCluster::ArmFailSafeRequest[]
+};
+
+const Entry<ItemInfo> _DemoCluster_ArmFailSafeRequest[] = {
+  { { ContextTag(0), "expiryLengthSeconds", ItemType::kDefault }, kInvalidNodeIndex }, // DemoCluster::INT16U
+  { { ContextTag(1), "breadcrumb", ItemType::kDefault }, kInvalidNodeIndex }, // DemoCluster::INT64U
+  { { ContextTag(2), "timeoutMs", ItemType::kDefault }, kInvalidNodeIndex }, // DemoCluster::INT32U
+};
+
+const Entry<ItemInfo> _DemoCluster_ArmFailSafeRequest_list_[] = {
+  { { AnonymousTag(), "[]", ItemType::kDefault }, 3 }, // DemoCluster_ArmFailSafeRequest[]
+};
+
+const Entry<ItemInfo> _all_clusters[] = {
+  { { ClusterTag(0x0A), "DemoCluster", ItemType::kDefault }, 2 },
+
+};
+
+// For any non-structure list like u64[] or similar.
+const Entry<ItemInfo> _primitive_type_list[] = {
+  { { AnonymousTag(), "[]", ItemType::kDefault }, kInvalidNodeIndex },
+};
+
+} // namespace
+
+#define _ENTRY(n) { sizeof(n) / sizeof(n[0]), n}
+
+const std::array<const Node<ItemInfo>, 3 + 2> clusters_meta = { {
+  _ENTRY(_all_clusters), // 0
+  _ENTRY(_primitive_type_list), // 1
+  _ENTRY(_DemoCluster), // 2
+  _ENTRY(_DemoCluster_ArmFailSafeRequest), // 3
+  _ENTRY(_DemoCluster_ArmFailSafeRequest_list_), // 4
+} };
+
+} // namespace TLVMeta
+} // namespace chip
diff --git a/scripts/py_matter_idl/matter_idl/tests/outputs/cluster_struct_attribute/cpp-tlvmeta/clusters_meta.h b/scripts/py_matter_idl/matter_idl/tests/outputs/cluster_struct_attribute/cpp-tlvmeta/clusters_meta.h
new file mode 100644
index 0000000..c8a3aa6
--- /dev/null
+++ b/scripts/py_matter_idl/matter_idl/tests/outputs/cluster_struct_attribute/cpp-tlvmeta/clusters_meta.h
@@ -0,0 +1,12 @@
+#include <lib/format/tlv_meta.h>
+#include <lib/format/FlatTree.h>
+
+#include <array>
+
+namespace chip {
+namespace TLVMeta {
+
+extern const std::array<const FlatTree::Node<ItemInfo>, 3 + 2> clusters_meta;
+
+} // namespace TLVMeta
+} // namespace chip
diff --git a/scripts/py_matter_idl/matter_idl/tests/outputs/cluster_with_commands/cpp-tlvmeta/clusters_meta.cpp b/scripts/py_matter_idl/matter_idl/tests/outputs/cluster_with_commands/cpp-tlvmeta/clusters_meta.cpp
new file mode 100644
index 0000000..05d3085
--- /dev/null
+++ b/scripts/py_matter_idl/matter_idl/tests/outputs/cluster_with_commands/cpp-tlvmeta/clusters_meta.cpp
@@ -0,0 +1,79 @@
+#include <tlv/meta/clusters_meta.h>
+
+namespace chip {
+namespace TLVMeta {
+namespace {
+
+using namespace chip::FlatTree;
+using namespace chip::TLV;
+
+const Entry<ItemInfo> _OnOff[] = {
+  { { AttributeTag(0), "onOff", ItemType::kDefault }, kInvalidNodeIndex }, // OnOff::boolean
+  { { AttributeTag(65532), "featureMap", ItemType::kDefault }, kInvalidNodeIndex }, // OnOff::bitmap32
+  { { AttributeTag(65533), "clusterRevision", ItemType::kDefault }, kInvalidNodeIndex }, // OnOff::int16u
+  { { CommandTag(0), "Off", ItemType::kDefault }, kInvalidNodeIndex }, // OnOff::Off::()
+  { { CommandTag(1), "On", ItemType::kDefault }, kInvalidNodeIndex }, // OnOff::On::()
+  { { CommandTag(2), "Toggle", ItemType::kDefault }, kInvalidNodeIndex }, // OnOff::Toggle::()
+};
+
+const Entry<ItemInfo> _OnOff_OnOffDelayedAllOffEffectVariant[] = {
+  { { ConstantValueTag(0x0), "kFadeToOffIn0p8Seconds", ItemType::kDefault }, kInvalidNodeIndex }, // OnOff::OnOffDelayedAllOffEffectVariant::kFadeToOffIn0p8Seconds
+  { { ConstantValueTag(0x1), "kNoFade", ItemType::kDefault }, kInvalidNodeIndex }, // OnOff::OnOffDelayedAllOffEffectVariant::kNoFade
+  { { ConstantValueTag(0x2), "k50PercentDimDownIn0p8SecondsThenFadeToOffIn12Seconds", ItemType::kDefault }, kInvalidNodeIndex }, // OnOff::OnOffDelayedAllOffEffectVariant::k50PercentDimDownIn0p8SecondsThenFadeToOffIn12Seconds
+};
+
+const Entry<ItemInfo> _OnOff_OnOffDyingLightEffectVariant[] = {
+  { { ConstantValueTag(0x0), "k20PercenterDimUpIn0p5SecondsThenFadeToOffIn1Second", ItemType::kDefault }, kInvalidNodeIndex }, // OnOff::OnOffDyingLightEffectVariant::k20PercenterDimUpIn0p5SecondsThenFadeToOffIn1Second
+};
+
+const Entry<ItemInfo> _OnOff_OnOffEffectIdentifier[] = {
+  { { ConstantValueTag(0x0), "kDelayedAllOff", ItemType::kDefault }, kInvalidNodeIndex }, // OnOff::OnOffEffectIdentifier::kDelayedAllOff
+  { { ConstantValueTag(0x1), "kDyingLight", ItemType::kDefault }, kInvalidNodeIndex }, // OnOff::OnOffEffectIdentifier::kDyingLight
+};
+
+const Entry<ItemInfo> _OnOff_OnOffStartUpOnOff[] = {
+  { { ConstantValueTag(0x0), "kOff", ItemType::kDefault }, kInvalidNodeIndex }, // OnOff::OnOffStartUpOnOff::kOff
+  { { ConstantValueTag(0x1), "kOn", ItemType::kDefault }, kInvalidNodeIndex }, // OnOff::OnOffStartUpOnOff::kOn
+  { { ConstantValueTag(0x2), "kTogglePreviousOnOff", ItemType::kDefault }, kInvalidNodeIndex }, // OnOff::OnOffStartUpOnOff::kTogglePreviousOnOff
+};
+
+const Entry<ItemInfo> _OnOff_OnOffControl[] = {
+  { { ConstantValueTag(0x1), "kAcceptOnlyWhenOn", ItemType::kDefault }, kInvalidNodeIndex }, // OnOff::OnOffControl::kAcceptOnlyWhenOn
+};
+
+const Entry<ItemInfo> _OnOff_OnOffFeature[] = {
+  { { ConstantValueTag(0x1), "kLighting", ItemType::kDefault }, kInvalidNodeIndex }, // OnOff::OnOffFeature::kLighting
+};
+
+const Entry<ItemInfo> _OnOff_ScenesFeature[] = {
+  { { ConstantValueTag(0x1), "kSceneNames", ItemType::kDefault }, kInvalidNodeIndex }, // OnOff::ScenesFeature::kSceneNames
+};
+
+const Entry<ItemInfo> _all_clusters[] = {
+
+};
+
+// For any non-structure list like u64[] or similar.
+const Entry<ItemInfo> _primitive_type_list[] = {
+  { { AnonymousTag(), "[]", ItemType::kDefault }, kInvalidNodeIndex },
+};
+
+} // namespace
+
+#define _ENTRY(n) { sizeof(n) / sizeof(n[0]), n}
+
+const std::array<const Node<ItemInfo>, 8 + 2> clusters_meta = { {
+  _ENTRY(_all_clusters), // 0
+  _ENTRY(_primitive_type_list), // 1
+  _ENTRY(_OnOff), // 2
+  _ENTRY(_OnOff_OnOffDelayedAllOffEffectVariant), // 3
+  _ENTRY(_OnOff_OnOffDyingLightEffectVariant), // 4
+  _ENTRY(_OnOff_OnOffEffectIdentifier), // 5
+  _ENTRY(_OnOff_OnOffStartUpOnOff), // 6
+  _ENTRY(_OnOff_OnOffControl), // 7
+  _ENTRY(_OnOff_OnOffFeature), // 8
+  _ENTRY(_OnOff_ScenesFeature), // 9
+} };
+
+} // namespace TLVMeta
+} // namespace chip
diff --git a/scripts/py_matter_idl/matter_idl/tests/outputs/cluster_with_commands/cpp-tlvmeta/clusters_meta.h b/scripts/py_matter_idl/matter_idl/tests/outputs/cluster_with_commands/cpp-tlvmeta/clusters_meta.h
new file mode 100644
index 0000000..f2bd083
--- /dev/null
+++ b/scripts/py_matter_idl/matter_idl/tests/outputs/cluster_with_commands/cpp-tlvmeta/clusters_meta.h
@@ -0,0 +1,12 @@
+#include <lib/format/tlv_meta.h>
+#include <lib/format/FlatTree.h>
+
+#include <array>
+
+namespace chip {
+namespace TLVMeta {
+
+extern const std::array<const FlatTree::Node<ItemInfo>, 8 + 2> clusters_meta;
+
+} // namespace TLVMeta
+} // namespace chip
diff --git a/scripts/py_matter_idl/setup.cfg b/scripts/py_matter_idl/setup.cfg
index ae5b29a..2a7d386 100644
--- a/scripts/py_matter_idl/setup.cfg
+++ b/scripts/py_matter_idl/setup.cfg
@@ -28,6 +28,8 @@
     matter_grammar.lark
     generators/cpp/application/CallbackStubSource.jinja
     generators/cpp/application/PluginApplicationCallbacksHeader.jinja
+    generators/cpp/tlvmeta/TLVMetaData_cpp.jinja
+    generators/cpp/tlvmeta/TLVMetaData_h.jinja
     generators/java/CHIPCallbackTypes.jinja
     generators/java/ChipClustersCpp.jinja
     generators/java/ChipClustersRead.jinja
diff --git a/src/BUILD.gn b/src/BUILD.gn
index c36315f..531ee98 100644
--- a/src/BUILD.gn
+++ b/src/BUILD.gn
@@ -59,6 +59,7 @@
       "${chip_root}/src/lib/address_resolve/tests",
       "${chip_root}/src/lib/asn1/tests",
       "${chip_root}/src/lib/core/tests",
+      "${chip_root}/src/lib/format/tests",
       "${chip_root}/src/messaging/tests",
       "${chip_root}/src/protocols/bdx/tests",
       "${chip_root}/src/protocols/user_directed_commissioning/tests",
diff --git a/src/controller/data_model/BUILD.gn b/src/controller/data_model/BUILD.gn
index 72e981c..0bf3c15 100644
--- a/src/controller/data_model/BUILD.gn
+++ b/src/controller/data_model/BUILD.gn
@@ -21,6 +21,25 @@
 import("${chip_root}/build/chip/java/config.gni")
 import("${chip_root}/src/app/chip_data_model.gni")
 
+chip_codegen("cluster-tlv-metadata") {
+  input = "controller-clusters.matter"
+  generator = "cpp-tlvmeta"
+
+  options = [ "table_name:clusters_meta" ]
+
+  outputs = [
+    "tlv/meta/clusters_meta.cpp",
+    "tlv/meta/clusters_meta.h",
+  ]
+
+  deps = [
+    "${chip_root}/src/lib/format:flat-tree",
+    "${chip_root}/src/lib/format:tlv-metadata-headers",
+  ]
+
+  public_configs = [ "${chip_root}/src:includes" ]
+}
+
 chip_data_model("data_model") {
   zap_file = "controller-clusters.zap"
 
diff --git a/src/lib/core/TLVTags.h b/src/lib/core/TLVTags.h
index 4355076..fd2a815 100644
--- a/src/lib/core/TLVTags.h
+++ b/src/lib/core/TLVTags.h
@@ -46,6 +46,8 @@
     constexpr bool operator==(const Tag & other) const { return mVal == other.mVal; }
     constexpr bool operator!=(const Tag & other) const { return mVal != other.mVal; }
 
+    uint64_t RawValue() const { return mVal; }
+
 private:
     explicit constexpr Tag(uint64_t val) : mVal(val) {}
 
diff --git a/src/lib/format/BUILD.gn b/src/lib/format/BUILD.gn
new file mode 100644
index 0000000..c75ecd0
--- /dev/null
+++ b/src/lib/format/BUILD.gn
@@ -0,0 +1,70 @@
+# Copyright (c) 2023 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("//build_overrides/build.gni")
+import("//build_overrides/chip.gni")
+
+import("${chip_root}/build/chip/chip_codegen.gni")
+
+source_set("flat-tree") {
+  sources = [
+    "FlatTree.h",
+    "FlatTreePosition.h",
+  ]
+
+  public_deps = [ "${chip_root}/src/lib/core" ]
+
+  public_configs = [ "${chip_root}/src:includes" ]
+}
+
+source_set("tlv-metadata-headers") {
+  sources = [
+    "tlv_meta.h",  # TODO: move in separate source set
+  ]
+}
+
+chip_codegen("protocol-tlv-metadata") {
+  input = "protocol_messages.matter"
+  generator = "cpp-tlvmeta"
+
+  options = [ "table_name:protocols_meta" ]
+
+  outputs = [
+    "tlv/meta/protocols_meta.cpp",
+    "tlv/meta/protocols_meta.h",
+  ]
+
+  deps = [
+    ":flat-tree",
+    ":tlv-metadata-headers",
+  ]
+
+  public_configs = [ "${chip_root}/src:includes" ]
+}
+
+source_set("protocol-decoder") {
+  sources = [
+    "protocol_decoder.cpp",
+    "protocol_decoder.h",
+  ]
+
+  public_deps = [
+    ":flat-tree",
+    ":tlv-metadata-headers",
+    "${chip_root}/src/lib/core",
+    "${chip_root}/src/lib/support",
+  ]
+
+  public_configs = [ "${chip_root}/src:includes" ]
+}
diff --git a/src/lib/format/FlatTree.h b/src/lib/format/FlatTree.h
new file mode 100644
index 0000000..6c1eeb7
--- /dev/null
+++ b/src/lib/format/FlatTree.h
@@ -0,0 +1,99 @@
+/*
+ *
+ *    Copyright (c) 2023 Project CHIP Authors
+ *    All rights reserved.
+ *
+ *    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.
+ */
+#pragma once
+
+#include <stddef.h>
+
+#include <array>
+#include <limits>
+
+namespace chip {
+namespace FlatTree {
+
+// A flat tree allows for tree data to be stored in a single flat
+// array.
+
+/// Invalid indexes in a tree
+static constexpr size_t kInvalidNodeIndex = std::numeric_limits<size_t>::max();
+
+/// An entry represents a single element identified by a key and containing a
+/// value
+///
+/// In a tree representation, every entry may potentially have a child node,
+/// whose index is located in [node_index].
+template <typename CONTENT>
+struct Entry
+{
+    CONTENT data;
+
+    // Node index is a valid index inside a node array if a entry has
+    // child elements, it is kInvalidNodeIndex otherwise;
+    size_t node_index;
+};
+
+template <typename CONTENT>
+struct Node
+{
+    size_t entry_count;             // number of items in [entries]
+    const Entry<CONTENT> * entries; // child items of [entry_count] size
+
+    /// Attempt to find the entry with given matcher.
+    ///
+    /// Returns nullptr if no matches can be found.
+    template <typename MATCHER>
+    const Entry<CONTENT> * find_entry(MATCHER matcher) const
+    {
+        for (size_t i = 0; i < entry_count; i++)
+        {
+            if (matcher(entries[i].data))
+            {
+                return &entries[i];
+            }
+        }
+        return nullptr;
+    }
+};
+
+/// Search for a given entry in a sized array
+///
+/// [data] is the flat tree array
+/// [idx] is the index of the node to search. If out of bounds, nullptr is returned
+/// [matcher] is the match function.
+template <typename CONTENT, typename MATCHER>
+inline const Entry<CONTENT> * FindEntry(const Node<CONTENT> * content, size_t content_size, size_t idx, MATCHER matcher)
+{
+    if (idx >= content_size)
+    {
+        return nullptr;
+    }
+    return content[idx].find_entry(matcher);
+}
+
+/// Search for a given entry in an array (array will do bounds check)
+///
+/// [data] is the flat tree array
+/// [idx] is the index of the node to search. If out of bounds, nullptr is returned
+/// [matcher] is the match function.
+template <typename CONTENT, typename MATCHER, size_t N>
+inline const Entry<CONTENT> * FindEntry(const std::array<Node<CONTENT>, N> & data, size_t idx, MATCHER matcher)
+{
+    return FindEntry(data.data(), N, idx, matcher);
+}
+
+} // namespace FlatTree
+} // namespace chip
diff --git a/src/lib/format/FlatTreePosition.h b/src/lib/format/FlatTreePosition.h
new file mode 100644
index 0000000..e01f8b4
--- /dev/null
+++ b/src/lib/format/FlatTreePosition.h
@@ -0,0 +1,193 @@
+/*
+ *
+ *    Copyright (c) 2023 Project CHIP Authors
+ *    All rights reserved.
+ *
+ *    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.
+ */
+#pragma once
+
+#include <lib/format/FlatTree.h>
+#include <lib/support/Span.h>
+
+namespace chip {
+namespace FlatTree {
+
+/// Represents a position inside a given tree, allowing for descending
+/// and ascending.
+///
+/// A possition in the tree may be undefined if descending to a non-existing leaf,
+/// however the position still allows moving back again.
+///
+/// DESCEND_DEPTH is the maximum remembered depth for going back up.
+///
+/// General usage:
+///
+///     Position<DataType, 10> position(tree, tree_size);
+///
+///     position.Enter(ByName("foo"));
+///     position.Enter(ByName("bar"));
+///     position.Enter(ByName("baz"));
+///
+///     position.Get()  /// content of foo::bar::baz if it exists
+///
+///     position.Exit();
+///     position.Exit();
+///
+///     position.Get()  /// content of foo if it exists
+///
+///     position.Enter(ById(1234));
+///
+///     position.Get()  /// content of foo::1234
+///
+template <typename CONTENT, size_t DESCEND_DEPTH>
+class Position
+{
+public:
+    Position(const Node<CONTENT> * tree, size_t treeSize) : mTree(tree), mTreeSize(treeSize) {}
+
+    template <size_t N>
+    Position(const std::array<const Node<CONTENT>, N> & tree) : mTree(tree.data()), mTreeSize(N)
+    {}
+
+    // Move back to the top
+    void ResetToTop()
+    {
+        mDescendDepth        = 0;
+        mUnknownDescendDepth = 0;
+    }
+
+    /// Attempt to find a child of the current position that matches
+    /// the given matcher
+    template <typename MATCHER>
+    void Enter(MATCHER matcher);
+
+    /// Move up the tree, undoes an 'Enter' operation.
+    void Exit();
+
+    /// Fetch the value where the node is positioned on or nullptr if that
+    /// value is not available;
+    const CONTENT * Get() const;
+
+    /// Returns the entries visited so far
+    ///
+    /// WILL RETURN EMPTY if the descend depth has been
+    /// exceeded. Callers MUST handle empty return.
+    ///
+    /// Span valid until one of Enter/Exit functions are called
+    /// and as long as the Position is valid (points inside the object).
+    chip::Span<const Entry<CONTENT> *> CurrentPath();
+
+    bool HasValidTree() const { return mTree != nullptr; }
+
+    size_t DescendDepth() const { return mDescendDepth + mUnknownDescendDepth; }
+
+private:
+    // actual tree that we visit
+    const Node<CONTENT> * mTree = nullptr;
+    const size_t mTreeSize      = 0;
+
+    // Keeping track of descending into the tree, to be able to move back
+    // last element in the array is the "current" item
+    const Entry<CONTENT> * mPositions[DESCEND_DEPTH] = { nullptr };
+    size_t mDescendDepth                             = 0; // filled amount of mDescendPositions
+
+    // Descend past remembering memory or in not-found entries.
+    size_t mUnknownDescendDepth = 0; // depth in invalid positions
+};
+
+template <typename CONTENT, size_t DESCEND_DEPTH>
+const CONTENT * Position<CONTENT, DESCEND_DEPTH>::Get() const
+{
+    if (mUnknownDescendDepth > 0)
+    {
+        return nullptr;
+    }
+
+    if (mDescendDepth == 0)
+    {
+        return nullptr;
+    }
+
+    return &mPositions[mDescendDepth - 1]->data;
+}
+
+template <typename CONTENT, size_t DESCEND_DEPTH>
+template <typename MATCHER>
+void Position<CONTENT, DESCEND_DEPTH>::Enter(MATCHER matcher)
+{
+    if (mUnknownDescendDepth > 0)
+    {
+        // keep descending into the unknown
+        mUnknownDescendDepth++;
+        return;
+    }
+
+    // To be able to descend, we have to be able to remember
+    // the current position
+    if (mDescendDepth == DESCEND_DEPTH)
+    {
+        mUnknownDescendDepth = 1;
+        return;
+    }
+
+    size_t nodeIdx = 0; // assume root node
+    if (mDescendDepth > 0)
+    {
+        nodeIdx = mPositions[mDescendDepth - 1]->node_index;
+    }
+
+    const Entry<CONTENT> * child = FindEntry(mTree, mTreeSize, nodeIdx, matcher);
+
+    if (child == nullptr)
+    {
+        mUnknownDescendDepth = 1;
+        return;
+    }
+
+    mPositions[mDescendDepth++] = child;
+}
+
+template <typename CONTENT, size_t DESCEND_DEPTH>
+void Position<CONTENT, DESCEND_DEPTH>::Exit()
+{
+    if (mUnknownDescendDepth > 0)
+    {
+        mUnknownDescendDepth--;
+        return;
+    }
+
+    if (mDescendDepth > 0)
+    {
+        mDescendDepth--;
+    }
+}
+
+template <typename CONTENT, size_t DESCEND_DEPTH>
+chip::Span<const Entry<CONTENT> *> Position<CONTENT, DESCEND_DEPTH>::CurrentPath()
+{
+    if (mUnknownDescendDepth > 0)
+    {
+        return chip::Span<const Entry<CONTENT> *>();
+    }
+
+    // const chip::FlatTree::Entry<{anonymous}::NamedTag>* const* to
+    // const chip::FlatTree::Entry<{anonymous}::NamedTag>**
+    typename chip::Span<const Entry<CONTENT> *>::pointer p = mPositions;
+
+    // return chip::Span<const Entry<CONTENT> *>(mPositions, mDescendDepth);
+    return chip::Span<const Entry<CONTENT> *>(p, mDescendDepth);
+}
+
+} // namespace FlatTree
+} // namespace chip
diff --git a/src/lib/format/protocol_decoder.cpp b/src/lib/format/protocol_decoder.cpp
new file mode 100644
index 0000000..8c3b352
--- /dev/null
+++ b/src/lib/format/protocol_decoder.cpp
@@ -0,0 +1,657 @@
+/*
+ *
+ *    Copyright (c) 2023 Project CHIP Authors
+ *    All rights reserved.
+ *
+ *    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.
+ */
+#include <lib/format/protocol_decoder.h>
+
+#include <lib/format/FlatTree.h>
+#include <lib/format/FlatTreePosition.h>
+
+namespace chip {
+namespace Decoders {
+
+namespace {
+
+using namespace chip::TLV;
+
+using chip::StringBuilderBase;
+using chip::TLVMeta::AttributeTag;
+using chip::TLVMeta::ClusterTag;
+using chip::TLVMeta::CommandTag;
+using chip::TLVMeta::ConstantValueTag;
+using chip::TLVMeta::EventTag;
+using chip::TLVMeta::ItemType;
+
+class ByTag
+{
+public:
+    constexpr ByTag(Tag tag) : mTag(tag) {}
+    bool operator()(const chip::TLVMeta::ItemInfo & item) { return item.tag == mTag; }
+
+private:
+    const Tag mTag;
+};
+
+const char * DecodeTagControl(const TLVTagControl aTagControl)
+{
+    switch (aTagControl)
+    {
+    case TLVTagControl::Anonymous:
+        return "Anonymous";
+    case TLVTagControl::ContextSpecific:
+        return "ContextSpecific";
+    case TLVTagControl::CommonProfile_2Bytes:
+        return "Common2B";
+    case TLVTagControl::CommonProfile_4Bytes:
+        return "Common4B";
+    case TLVTagControl::ImplicitProfile_2Bytes:
+        return "Implicit2B";
+    case TLVTagControl::ImplicitProfile_4Bytes:
+        return "Implicit4B";
+    case TLVTagControl::FullyQualified_6Bytes:
+        return "FullyQualified6B";
+    case TLVTagControl::FullyQualified_8Bytes:
+        return "FullyQualified8";
+    default:
+        return "???";
+    }
+}
+
+void FormatCurrentTag(const TLVReader & reader, chip::StringBuilderBase & out)
+{
+    chip::TLV::TLVTagControl tagControl = static_cast<TLVTagControl>(reader.GetControlByte() & kTLVTagControlMask);
+    chip::TLV::Tag tag                  = reader.GetTag();
+
+    if (IsProfileTag(tag))
+    {
+        out.AddFormat("%s(0x%X::0x%X::0x%" PRIX32 ")", DecodeTagControl(tagControl), VendorIdFromTag(tag), ProfileNumFromTag(tag),
+                      TagNumFromTag(tag));
+    }
+    else if (IsContextTag(tag))
+    {
+        out.AddFormat("%s(0x%" PRIX32 ")", DecodeTagControl(tagControl), TagNumFromTag(tag));
+    }
+    else
+    {
+        out.AddFormat("UnknownTag(0x%" PRIX64 ")", tag.RawValue());
+    }
+}
+
+CHIP_ERROR FormatCurrentValue(TLVReader & reader, chip::StringBuilderBase & out)
+{
+    switch (reader.GetType())
+    {
+    case kTLVType_SignedInteger: {
+        int64_t sVal;
+        ReturnErrorOnFailure(reader.Get(sVal));
+        out.AddFormat("%" PRIi64, sVal);
+        break;
+    }
+    case kTLVType_UnsignedInteger: {
+        uint64_t uVal;
+        ReturnErrorOnFailure(reader.Get(uVal));
+        out.AddFormat("%" PRIu64, uVal);
+        break;
+    }
+    case kTLVType_Boolean: {
+        bool bVal;
+        ReturnErrorOnFailure(reader.Get(bVal));
+        out.Add(bVal ? "true" : "false");
+        break;
+    }
+    case kTLVType_FloatingPointNumber: {
+        double fpVal;
+        ReturnErrorOnFailure(reader.Get(fpVal));
+        out.AddFormat("%lf", fpVal);
+        break;
+    }
+    case kTLVType_UTF8String: {
+        const uint8_t * strbuf = nullptr;
+        ReturnErrorOnFailure(reader.GetDataPtr(strbuf));
+        out.AddFormat("\"%-.*s\"", static_cast<int>(reader.GetLength()), strbuf);
+        break;
+    }
+    case kTLVType_ByteString: {
+        const uint8_t * strbuf = nullptr;
+        ReturnErrorOnFailure(reader.GetDataPtr(strbuf));
+        out.Add("hex:");
+        for (uint32_t i = 0; i < reader.GetLength(); i++)
+        {
+            out.AddFormat("%02X", strbuf[i]);
+        }
+        break;
+    }
+    case kTLVType_Null:
+        out.Add("NULL");
+        break;
+
+    case kTLVType_NotSpecified:
+        out.Add("Not Specified");
+        break;
+    default:
+        out.Add("???");
+        break;
+    }
+
+    return CHIP_NO_ERROR;
+}
+
+// Returns a null terminated string containing the current reader value
+void PrettyPrintCurrentValue(TLVReader & reader, chip::StringBuilderBase & out, PayloadDecoderBase::DecodePosition & position)
+{
+    CHIP_ERROR err = FormatCurrentValue(reader, out);
+
+    if (err != CHIP_NO_ERROR)
+    {
+        out.AddFormat("ERR: %" CHIP_ERROR_FORMAT, err.Format());
+        return;
+    }
+
+    auto data = position.Get();
+    if (data == nullptr)
+    {
+        return;
+    }
+
+    // Report enum values in human readable form
+    if (data->type == ItemType::kEnum && (reader.GetType() == kTLVType_UnsignedInteger))
+    {
+        uint64_t value = 0;
+        VerifyOrReturn(reader.Get(value) == CHIP_NO_ERROR);
+
+        position.Enter(ByTag(ConstantValueTag(value)));
+        auto enum_data = position.Get();
+        if (enum_data != nullptr)
+        {
+            out.Add(" == ").Add(enum_data->name);
+        }
+        position.Exit();
+    }
+
+    if (data->type == ItemType::kBitmap && (reader.GetType() == kTLVType_UnsignedInteger))
+    {
+        uint64_t value = 0;
+        VerifyOrReturn(reader.Get(value) == CHIP_NO_ERROR);
+
+        uint64_t bit = 0x01;
+        bool first   = true;
+        for (unsigned i = 0; i < 64; i++, bit <<= 1)
+        {
+            if ((value & bit) == 0)
+            {
+                continue;
+            }
+
+            // NOTE: this only can select individual bits;
+            position.Enter(ByTag(ConstantValueTag(bit)));
+            auto bitmap_data = position.Get();
+            if (bitmap_data == nullptr)
+            {
+                position.Exit();
+                continue;
+            }
+
+            // Try to pretty print the value
+            if (first)
+            {
+                out.Add(" == ");
+                first = false;
+            }
+            else
+            {
+                out.Add(" | ");
+            }
+
+            out.Add(bitmap_data->name);
+            value = value & (~bit);
+
+            position.Exit();
+        }
+
+        if (!first && value)
+        {
+            // Only append if some constants were found.
+            out.AddFormat(" | 0x%08" PRIX64, value);
+        }
+    }
+}
+
+} // namespace
+
+PayloadDecoderBase::PayloadDecoderBase(const PayloadDecoderInitParams & params, StringBuilderBase & nameBuilder,
+                                       StringBuilderBase & valueBuilder) :
+    mProtocol(params.GetProtocol()),
+    mMessageType(params.GetMessageType()), mNameBuilder(nameBuilder), mValueBuilder(valueBuilder),
+
+    mPayloadPosition(params.GetProtocolDecodeTree(), params.GetProtocolDecodeTreeSize()),
+    mIMContentPosition(params.GetClusterDecodeTree(), params.GetClusterDecodeTreeSize())
+{}
+
+void PayloadDecoderBase::StartDecoding(const TLVReader & reader)
+{
+    mReader = reader;
+    mPayloadPosition.ResetToTop();
+    mIMContentPosition.ResetToTop();
+    mCurrentNesting = 0;
+    mClusterId      = 0;
+    mAttributeId    = 0;
+    mEventId        = 0;
+    mCommandId      = 0;
+    mState          = State::kStarting;
+}
+
+bool PayloadDecoderBase::Next(PayloadEntry & entry)
+{
+    switch (mState)
+    {
+    case State::kStarting:
+        NextFromStarting(entry);
+        return true;
+    case State::kValueRead:
+        NextFromValueRead(entry);
+        return true;
+    case State::kContentRead:
+        NextFromContentRead(entry);
+        return true;
+    case State::kDone:
+        return false;
+    }
+    // should never happen
+    return false;
+}
+
+void PayloadDecoderBase::NextFromStarting(PayloadEntry & entry)
+{
+    // Find the right protocol (fake cluster id)
+    mPayloadPosition.Enter(ByTag(ClusterTag(0xFFFF0000 | mProtocol.ToFullyQualifiedSpecForm())));
+    mPayloadPosition.Enter(ByTag(AttributeTag(mMessageType)));
+
+    auto data = mPayloadPosition.Get();
+    if (data == nullptr)
+    {
+        // do not try to decode unknown data. assume binary
+        mNameBuilder.Reset().AddFormat("PROTO(0x%" PRIX32 ", 0x%X)", mProtocol.ToFullyQualifiedSpecForm(), mMessageType);
+        mValueBuilder.Reset().Add("UNKNOWN");
+        entry  = PayloadEntry::SimpleValue(mNameBuilder.c_str(), mValueBuilder.c_str());
+        mState = State::kDone;
+        return;
+    }
+
+    // name is known (so valid protocol)
+    if (mReader.GetTotalLength() == 0)
+    {
+        mState = State::kDone;
+        entry  = PayloadEntry::SimpleValue(data->name, "");
+        return;
+    }
+
+    if (data->type == ItemType::kProtocolBinaryData)
+    {
+        mState = State::kDone;
+        entry  = PayloadEntry::SimpleValue(data->name, "BINARY DATA");
+        return;
+    }
+
+    CHIP_ERROR err = mReader.Next(kTLVType_Structure, AnonymousTag());
+    if (err != CHIP_NO_ERROR)
+    {
+        mValueBuilder.Reset().AddFormat("ERROR getting Anonymous Structure TLV: %" CHIP_ERROR_FORMAT, err.Format());
+        mState = State::kDone;
+        entry  = PayloadEntry::SimpleValue(data->name, mValueBuilder.c_str());
+        return;
+    }
+
+    EnterContainer(entry);
+}
+
+void PayloadDecoderBase::ExitContainer(PayloadEntry & entry)
+{
+    entry = PayloadEntry::NestingExit();
+
+    if (mCurrentNesting > 0)
+    {
+        if (mState == State::kContentRead)
+        {
+            mIMContentPosition.Exit();
+            if (mIMContentPosition.DescendDepth() <= 1)
+            {
+                // Lever for actual content is 2: cluster::attr/cmd/ev
+                mState = State::kValueRead;
+                mPayloadPosition.Exit();
+            }
+        }
+        else
+        {
+            mPayloadPosition.Exit();
+        }
+        mReader.ExitContainer(mNestingEnters[--mCurrentNesting]);
+    }
+
+    if (mCurrentNesting == 0)
+    {
+        mState = State::kDone;
+    }
+}
+
+bool PayloadDecoderBase::ReaderEnterContainer(PayloadEntry & entry)
+{
+    if (mCurrentNesting >= kMaxDecodeDepth)
+    {
+        mValueBuilder.AddFormat("NESTING DEPTH REACHED");
+        entry = PayloadEntry::SimpleValue(mNameBuilder.c_str(), mValueBuilder.c_str());
+        return false;
+    }
+
+    TLVType containerType;
+    CHIP_ERROR err = mReader.EnterContainer(containerType);
+    if (err != CHIP_NO_ERROR)
+    {
+        mValueBuilder.AddFormat("ERROR entering container: %" CHIP_ERROR_FORMAT, err.Format());
+        entry  = PayloadEntry::SimpleValue(mNameBuilder.c_str(), mValueBuilder.c_str());
+        mState = State::kDone;
+        return false;
+    }
+
+    mNestingEnters[mCurrentNesting++] = containerType;
+
+    return true;
+}
+
+void PayloadDecoderBase::EnterContainer(PayloadEntry & entry)
+{
+    // must be done BEFORE entering container
+    // to preserve the value and not get a 'container tag'
+    // below when data is not valid
+    //
+    // TODO: this formatting is wasteful, should really be done only
+    //       if data is NULLPTR.
+    FormatCurrentTag(mReader, mNameBuilder.Reset());
+
+    VerifyOrReturn(ReaderEnterContainer(entry));
+
+    const chip::TLVMeta::ItemInfo * data = nullptr;
+
+    if (mState == State::kContentRead)
+    {
+        data = mIMContentPosition.Get();
+    }
+    else
+    {
+        mState = State::kValueRead;
+        data   = mPayloadPosition.Get();
+    }
+
+    if (data == nullptr)
+    {
+        entry = PayloadEntry::NestingEnter(mNameBuilder.c_str());
+    }
+    else
+    {
+        entry = PayloadEntry::NestingEnter(data->name);
+    }
+}
+
+void PayloadDecoderBase::NextFromContentRead(PayloadEntry & entry)
+{
+    CHIP_ERROR err = mReader.Next();
+    if (err == CHIP_END_OF_TLV)
+    {
+        ExitContainer(entry);
+        return;
+    }
+
+    if (err != CHIP_NO_ERROR)
+    {
+        mValueBuilder.Reset().AddFormat("ERROR on TLV Next: %" CHIP_ERROR_FORMAT, err.Format());
+        entry  = PayloadEntry::SimpleValue("TLV_ERR", mValueBuilder.c_str());
+        mState = State::kDone;
+        return;
+    }
+
+    if (mCurrentNesting > 0 && mNestingEnters[mCurrentNesting - 1] == kTLVType_List)
+    {
+        // Spec A5.3: `The members of a list may be encoded with any form of tag, including an anonymous tag.`
+        // TLVMeta always uses Anonymous
+        mIMContentPosition.Enter(ByTag(AnonymousTag()));
+    }
+    else
+    {
+        mIMContentPosition.Enter(ByTag(mReader.GetTag()));
+    }
+    auto data = mIMContentPosition.Get();
+
+    if (data != nullptr)
+    {
+        if (data->type == ItemType::kProtocolBinaryData)
+        {
+            mIMContentPosition.Exit();
+            entry = PayloadEntry::SimpleValue(data->name, "BINARY DATA");
+            return;
+        }
+    }
+
+    if (TLVTypeIsContainer(mReader.GetType()))
+    {
+        EnterContainer(entry);
+        return;
+    }
+
+    PrettyPrintCurrentValue(mReader, mValueBuilder.Reset(), mIMContentPosition);
+    mIMContentPosition.Exit();
+
+    if (data == nullptr)
+    {
+        FormatCurrentTag(mReader, mNameBuilder.Reset());
+        entry = PayloadEntry::SimpleValue(mNameBuilder.c_str(), mValueBuilder.c_str());
+        return;
+    }
+
+    entry = PayloadEntry::SimpleValue(data->name, mValueBuilder.c_str());
+}
+
+void PayloadDecoderBase::MoveToContent(PayloadEntry & entry)
+{
+    if (!mIMContentPosition.HasValidTree())
+    {
+        mPayloadPosition.Exit();
+        return;
+    }
+
+    VerifyOrDie((entry.GetType() == PayloadEntry::IMPayloadType::kAttribute) ||
+                (entry.GetType() == PayloadEntry::IMPayloadType::kCommand) ||
+                (entry.GetType() == PayloadEntry::IMPayloadType::kEvent));
+
+    mNameBuilder.Reset();
+
+    mIMContentPosition.ResetToTop();
+    mIMContentPosition.Enter(ByTag(ClusterTag(entry.GetClusterId())));
+    auto data = mIMContentPosition.Get();
+    if (data != nullptr)
+    {
+        mNameBuilder.AddFormat("%s::", data->name);
+    }
+    else
+    {
+        mNameBuilder.AddFormat("0x%" PRIx32 "::", entry.GetClusterId());
+    }
+
+    uint32_t id          = 0;
+    const char * id_type = "UNKNOWN";
+
+    switch (entry.GetType())
+    {
+    case PayloadEntry::IMPayloadType::kAttribute:
+        mIMContentPosition.Enter(ByTag(AttributeTag(entry.GetAttributeId())));
+        id      = entry.GetAttributeId();
+        id_type = "ATTR";
+        break;
+    case PayloadEntry::IMPayloadType::kCommand:
+        mIMContentPosition.Enter(ByTag(CommandTag(entry.GetCommandId())));
+        id      = entry.GetCommandId();
+        id_type = "CMD";
+        break;
+    case PayloadEntry::IMPayloadType::kEvent:
+        mIMContentPosition.Enter(ByTag(EventTag(entry.GetEventId())));
+        id      = entry.GetEventId();
+        id_type = "EV";
+        break;
+    default:
+        // never happens: verified all case above covered.
+        break;
+    }
+
+    data = mIMContentPosition.Get();
+    if (data != nullptr)
+    {
+        mNameBuilder.AddFormat("%s", data->name);
+    }
+    else
+    {
+        mNameBuilder.AddFormat("%s(0x%" PRIx32 ")", id_type, id);
+    }
+
+    if (TLVTypeIsContainer(mReader.GetType()))
+    {
+        mState = State::kContentRead;
+        entry  = PayloadEntry::NestingEnter(mNameBuilder.c_str());
+        ReaderEnterContainer(entry);
+    }
+    else
+    {
+        PrettyPrintCurrentValue(mReader, mValueBuilder.Reset(), mIMContentPosition);
+        entry = PayloadEntry::SimpleValue(mNameBuilder.c_str(), mValueBuilder.c_str());
+
+        // Can simply exit, only one value to return
+        mPayloadPosition.Exit();
+    }
+}
+
+void PayloadDecoderBase::NextFromValueRead(PayloadEntry & entry)
+{
+    CHIP_ERROR err = mReader.Next();
+    if (err == CHIP_END_OF_TLV)
+    {
+        ExitContainer(entry);
+        return;
+    }
+
+    if (err != CHIP_NO_ERROR)
+    {
+        mValueBuilder.Reset().AddFormat("ERROR on TLV Next: %" CHIP_ERROR_FORMAT, err.Format());
+        entry  = PayloadEntry::SimpleValue("TLV_ERR", mValueBuilder.c_str());
+        mState = State::kDone;
+        return;
+    }
+
+    // Attempt to find information about the current tag
+    mPayloadPosition.Enter(ByTag(mReader.GetTag()));
+    auto data = mPayloadPosition.Get();
+
+    // handle special types
+    if (data != nullptr)
+    {
+        if (data->type == ItemType::kProtocolBinaryData)
+        {
+            mPayloadPosition.Exit();
+            entry = PayloadEntry::SimpleValue(data->name, "BINARY DATA");
+            return;
+        }
+
+        if (data->type == ItemType::kProtocolPayloadAttribute)
+        {
+            entry = PayloadEntry::AttributePayload(mClusterId, mAttributeId);
+            MoveToContent(entry);
+            return;
+        }
+
+        if (data->type == ItemType::kProtocolPayloadCommand)
+        {
+            entry = PayloadEntry::CommandPayload(mClusterId, mCommandId);
+            MoveToContent(entry);
+            return;
+        }
+
+        if (data->type == ItemType::kProtocolPayloadEvent)
+        {
+            entry = PayloadEntry::EventPayload(mClusterId, mEventId);
+            MoveToContent(entry);
+            return;
+        }
+    }
+
+    if (TLVTypeIsContainer(mReader.GetType()))
+    {
+        EnterContainer(entry);
+        return;
+    }
+
+    if (data == nullptr)
+    {
+        FormatCurrentTag(mReader, mNameBuilder.Reset());
+        PrettyPrintCurrentValue(mReader, mValueBuilder.Reset(), mPayloadPosition);
+        entry = PayloadEntry::SimpleValue(mNameBuilder.c_str(), mValueBuilder.c_str());
+        mPayloadPosition.Exit();
+        return;
+    }
+
+    // at this point, data is "simple data" or "simple data with meaning"
+
+    const chip::TLVMeta::ItemInfo * info = nullptr;
+    switch (data->type)
+    {
+    case ItemType::kProtocolClusterId:
+        mReader.Get(mClusterId);
+        mIMContentPosition.ResetToTop();
+        mIMContentPosition.Enter(ByTag(ClusterTag(mClusterId)));
+        info = mIMContentPosition.Get();
+        break;
+    case ItemType::kProtocolAttributeId:
+        mReader.Get(mAttributeId);
+        mIMContentPosition.ResetToTop();
+        mIMContentPosition.Enter(ByTag(ClusterTag(mClusterId)));
+        mIMContentPosition.Enter(ByTag(AttributeTag(mAttributeId)));
+        info = mIMContentPosition.Get();
+        break;
+    case ItemType::kProtocolCommandId:
+        mReader.Get(mCommandId);
+        mIMContentPosition.ResetToTop();
+        mIMContentPosition.Enter(ByTag(ClusterTag(mClusterId)));
+        mIMContentPosition.Enter(ByTag(CommandTag(mCommandId)));
+        info = mIMContentPosition.Get();
+        break;
+    case ItemType::kProtocolEventId:
+        mReader.Get(mEventId);
+        mIMContentPosition.ResetToTop();
+        mIMContentPosition.Enter(ByTag(ClusterTag(mClusterId)));
+        mIMContentPosition.Enter(ByTag(EventTag(mEventId)));
+        info = mIMContentPosition.Get();
+        break;
+    default:
+        break;
+    }
+
+    PrettyPrintCurrentValue(mReader, mValueBuilder.Reset(), mPayloadPosition);
+    if (info != nullptr)
+    {
+        mValueBuilder.Add(" == '").Add(info->name).Add("'");
+    }
+
+    mPayloadPosition.Exit();
+    entry = PayloadEntry::SimpleValue(data->name, mValueBuilder.c_str());
+}
+
+} // namespace Decoders
+} // namespace chip
diff --git a/src/lib/format/protocol_decoder.h b/src/lib/format/protocol_decoder.h
new file mode 100644
index 0000000..3cf3cdb
--- /dev/null
+++ b/src/lib/format/protocol_decoder.h
@@ -0,0 +1,289 @@
+/*
+ *
+ *    Copyright (c) 2023 Project CHIP Authors
+ *    All rights reserved.
+ *
+ *    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.
+ */
+#pragma once
+
+#include <lib/core/TLVReader.h>
+#include <lib/format/FlatTreePosition.h>
+#include <lib/format/tlv_meta.h>
+#include <lib/support/StringBuilder.h>
+#include <protocols/Protocols.h>
+
+namespace chip {
+namespace Decoders {
+
+/// Represents an individual decoded entry for IM Payloads
+/// Generally a name + value + metadata tuple, where name and value are never NULL.
+class PayloadEntry
+{
+public:
+    static constexpr uint32_t kInvalidId = 0xFFFFFFFF;
+    enum class IMPayloadType
+    {
+        kNone = 0, // generally should not be used except initial init
+        kValue,    // represents a simple value to output
+
+        kNestingEnter, // Nested struct enter. Has name, empty value
+        kNestingExit,  // Nested struct exit (has no name/value)
+
+        // Content payloads
+        kAttribute,
+        kCommand,
+        kEvent,
+    };
+    PayloadEntry(const PayloadEntry &) = default;
+    PayloadEntry & operator=(const PayloadEntry &) = default;
+
+    PayloadEntry() : mType(IMPayloadType::kNone), mName(""), mValue("") {}
+
+    IMPayloadType GetType() const { return mType; }
+
+    const char * GetName() const { return mName; }
+    const char * GetValueText() const { return mValue; }
+
+    /// valid only if payload is an IM Payload
+    uint32_t GetClusterId() const { return mClusterId; };
+
+    /// valid only if payload as an Attribute ID
+    uint32_t GetAttributeId() const
+    {
+        VerifyOrReturnValue(mType == IMPayloadType::kAttribute, kInvalidId);
+        return mSubId;
+    }
+
+    /// valid only if payload as a Command ID
+    uint32_t GetCommandId() const
+    {
+        VerifyOrReturnValue(mType == IMPayloadType::kCommand, kInvalidId);
+        return mSubId;
+    }
+
+    /// valid only if payload as a Command ID
+    uint32_t GetEventId() const
+    {
+        VerifyOrReturnValue(mType == IMPayloadType::kEvent, kInvalidId);
+        return mSubId;
+    }
+
+    static PayloadEntry SimpleValue(const char * name, const char * value)
+    {
+        return PayloadEntry(IMPayloadType::kValue, name, value);
+    }
+
+    static PayloadEntry NestingEnter(const char * name) { return PayloadEntry(IMPayloadType::kNestingEnter, name, ""); }
+
+    static PayloadEntry NestingExit() { return PayloadEntry(IMPayloadType::kNestingExit, "", ""); }
+
+    static PayloadEntry AttributePayload(uint32_t cluster_id, uint32_t attribute_id)
+    {
+        PayloadEntry result(IMPayloadType::kAttribute, "ATTR_DATA", "");
+        result.mClusterId = cluster_id;
+        result.mSubId     = attribute_id;
+
+        return result;
+    }
+
+    static PayloadEntry CommandPayload(uint32_t cluster_id, uint32_t command_id)
+    {
+        PayloadEntry result(IMPayloadType::kCommand, "COMMAND_DATA", "");
+        result.mClusterId = cluster_id;
+        result.mSubId     = command_id;
+        return result;
+    }
+
+    static PayloadEntry EventPayload(uint32_t cluster_id, uint32_t event_id)
+    {
+        PayloadEntry result(IMPayloadType::kEvent, "EVENT_DATA", "");
+        result.mClusterId = cluster_id;
+        result.mSubId     = event_id;
+        return result;
+    }
+
+private:
+    PayloadEntry(IMPayloadType type, const char * name, const char * value) : mType(type), mName(name), mValue(value) {}
+
+    IMPayloadType mType = IMPayloadType::kValue;
+    const char * mName  = nullptr;
+    const char * mValue = nullptr;
+
+    uint32_t mClusterId = 0;
+    uint32_t mSubId     = 0; // attribute, command or event id
+};
+
+/// Sets up decoding of some Matter data payload
+class PayloadDecoderInitParams
+{
+public:
+    using DecodeTree           = const FlatTree::Node<chip::TLVMeta::ItemInfo> *;
+    PayloadDecoderInitParams() = default;
+
+    PayloadDecoderInitParams & SetProtocol(Protocols::Id value)
+    {
+        mProtocol = value;
+        return *this;
+    }
+
+    PayloadDecoderInitParams & SetMessageType(uint8_t value)
+    {
+        mMessageType = value;
+        return *this;
+    }
+
+    PayloadDecoderInitParams & SetProtocolDecodeTree(DecodeTree tree, size_t s)
+    {
+        mProtocolTree     = tree;
+        mProtocolTreeSize = s;
+        return *this;
+    }
+
+    template <size_t N>
+    PayloadDecoderInitParams & SetProtocolDecodeTree(const std::array<const FlatTree::Node<chip::TLVMeta::ItemInfo>, N> & a)
+    {
+        return SetProtocolDecodeTree(a.data(), N);
+    }
+
+    PayloadDecoderInitParams & SetClusterDecodeTree(DecodeTree tree, size_t s)
+    {
+        mClusterTree     = tree;
+        mClusterTreeSize = s;
+        return *this;
+    }
+
+    template <size_t N>
+    PayloadDecoderInitParams & SetClusterDecodeTree(const std::array<const FlatTree::Node<chip::TLVMeta::ItemInfo>, N> & a)
+    {
+        return SetClusterDecodeTree(a.data(), N);
+    }
+
+    DecodeTree GetProtocolDecodeTree() const { return mProtocolTree; }
+    size_t GetProtocolDecodeTreeSize() const { return mProtocolTreeSize; }
+    DecodeTree GetClusterDecodeTree() const { return mClusterTree; }
+    size_t GetClusterDecodeTreeSize() const { return mClusterTreeSize; }
+
+    Protocols::Id GetProtocol() const { return mProtocol; }
+    uint8_t GetMessageType() const { return mMessageType; }
+
+private:
+    DecodeTree mProtocolTree = nullptr;
+    size_t mProtocolTreeSize = 0;
+
+    DecodeTree mClusterTree = nullptr;
+    size_t mClusterTreeSize = 0;
+
+    Protocols::Id mProtocol = Protocols::NotSpecified;
+    uint8_t mMessageType    = 0;
+};
+
+class PayloadDecoderBase
+{
+public:
+    static constexpr size_t kMaxDecodeDepth = 16;
+    using DecodePosition                    = chip::FlatTree::Position<chip::TLVMeta::ItemInfo, kMaxDecodeDepth>;
+
+    PayloadDecoderBase(const PayloadDecoderInitParams & params, StringBuilderBase & nameBuilder, StringBuilderBase & valueBuilder);
+
+    /// Initialize decoding from the given reader
+    /// Will create a copy of the reader, however the copy will contain
+    /// pointers in the original reader (so data must stay valid while Next is called)
+    void StartDecoding(const TLV::TLVReader & reader);
+
+    void StartDecoding(chip::ByteSpan data)
+    {
+        TLV::TLVReader reader;
+        reader.Init(data);
+        StartDecoding(reader);
+    }
+
+    /// Get the next decoded entry from the underlying TLV
+    ///
+    /// If a cluster decoder is set, then kAttribute/kCommand/kEvent are ALWAYS decoded
+    /// (even if unknown tags), otherwise they will be returned as separate PayloadEntry values.
+    ///
+    /// Returns false when decoding finished.
+    bool Next(PayloadEntry & entry);
+
+    const TLV::TLVReader & ReadState() const { return mReader; }
+
+private:
+    enum class State
+    {
+        kStarting,
+        kValueRead,   // reading value for Payload
+        kContentRead, // reading value for IMContent (i.e. decoded attr/cmd/event)
+        kDone,
+    };
+
+    /// Move to the given attribute/event/command entry.
+    ///
+    /// [entry] MUST be of type command/attribute/event.
+    ///
+    /// This call either moves to "ContentDecoding mode" if content tree is available
+    /// or leaves entry unchanged if content decoding tree is not available.
+    void MoveToContent(PayloadEntry & entry);
+
+    void NextFromStarting(PayloadEntry & entry);
+    void NextFromValueRead(PayloadEntry & entry);
+    void NextFromContentRead(PayloadEntry & entry);
+
+    /// Enter the container in mReader.
+    ///
+    /// May change entry/state in case of errors.
+    ///
+    /// Returns false on error (and entry is changed in that case)
+    bool ReaderEnterContainer(PayloadEntry & entry);
+
+    /// Returns a "nesting enter" value, making sure that
+    /// nesting depth is sufficient.
+    void EnterContainer(PayloadEntry & entry);
+
+    /// Returns a "nesting exit" value, making sure that
+    /// nesting depth is sufficient.
+    void ExitContainer(PayloadEntry & entry);
+
+    const chip::Protocols::Id mProtocol;
+    const uint8_t mMessageType;
+
+    StringBuilderBase & mNameBuilder;
+    StringBuilderBase & mValueBuilder;
+
+    State mState = State::kStarting;
+    TLV::TLVReader mReader;
+    DecodePosition mPayloadPosition;
+    DecodePosition mIMContentPosition;
+    TLV::TLVType mNestingEnters[kMaxDecodeDepth];
+    size_t mCurrentNesting = 0;
+
+    /// incremental state for parsing of paths
+    uint32_t mClusterId   = 0;
+    uint32_t mAttributeId = 0;
+    uint32_t mEventId     = 0;
+    uint32_t mCommandId   = 0;
+};
+
+template <size_t kNameBufferSize, size_t kValueBufferSize>
+class PayloadDecoder : public PayloadDecoderBase
+{
+public:
+    PayloadDecoder(const PayloadDecoderInitParams & params) : PayloadDecoderBase(std::move(params), mName, mValue) {}
+
+private:
+    chip::StringBuilder<kNameBufferSize> mName;
+    chip::StringBuilder<kValueBufferSize> mValue;
+};
+
+} // namespace Decoders
+} // namespace chip
diff --git a/src/lib/format/protocol_messages.matter b/src/lib/format/protocol_messages.matter
new file mode 100644
index 0000000..0346b13
--- /dev/null
+++ b/src/lib/format/protocol_messages.matter
@@ -0,0 +1,406 @@
+/// Fake cluster for defining structures for secure channel protocol
+///
+/// This follows the `4.10.1. Secure Channel Protocol Messages`
+/// defined in the Matter specification.
+///
+/// This is NOT a real cluster.
+///
+/// Since this is a protocol decoder, the following CUSTOM (non-zap) types
+/// Are used as markers for data:
+///
+/// Protocol decoding paths:
+///    protocol_cluster_id
+///    protocol_attribute_id
+///    protocol_command_id
+///    protocol_event_id
+///
+/// Payloads:
+///    cluster_attribute_payload
+///    cluster_command_payload
+///    cluster_event_payload
+///
+/// Special types:
+///    protocol_binary_data - structures without TLV encoded data
+///
+client cluster SecureChannelProtocol = 0xFFFF0000 {
+
+  struct ICDParameterStruct {
+    int32u sleepy_idle_interval = 1;
+    int32u sleepy_active_interval = 2;
+  }
+
+  struct PBKDFParamRequest {
+    octet_string<32> initiator_random = 1;
+    int16u initiator_session_id = 2;
+    int16u passcode_id = 3;
+    boolean has_pbkdf_parameters = 4;
+    optional ICDParameterStruct initiator_icd_params = 5;
+  }
+
+  struct CryptoPBKDFParameterSet {
+    int32u iterations = 1;
+    octet_string<32> salt = 2; // 16..32 in size
+  }
+
+  struct PBKDFParamResponse {
+    octet_string<32> initiator_random = 1;
+    octet_string<32> responder_random = 2;
+    int16u responder_session_id = 3;
+    CryptoPBKDFParameterSet pbkdf_parameters = 4;
+    optional IDCParameterStruct responder_icd_params = 5;
+  }
+
+  struct PasePake1 {
+    octet_string pA = 1;
+  }
+
+  struct PasePake2 {
+    octet_string pB = 1;
+    octet_string cB = 2;
+  }
+
+  struct PasePake3 {
+    octet_string cA = 1;
+  }
+
+  struct CaseSigma1 {
+    octet_string<32> initiator_random = 1;
+    int16u initiator_session_id = 2;
+    octet_string<32> destination_id = 3;
+    octet_string<65> initiator_eph_pub_key = 4;
+    optional ICDParameterStruct initiator_icd_params = 5;
+    optional octet_string<16> resumption_id = 6;
+    optional octet_string initiator_resume_mic = 7;
+  }
+
+  struct CaseSigma2 {
+    octet_string<32> responder_random = 1;
+    int16u responder_sessoion_id = 2;
+    octet_string<65> responder_eph_pub_key = 3;
+    octet_string encrypted2 = 4;
+    optional ICDParameterStruct responder_icd_params = 5;
+  }
+
+  struct CaseSigma3 {
+    octet_string encrypted = 1;
+  }
+
+  struct CaseSigma2Resume {
+    octet_string<16> resumption_id = 1;
+    octet_string<16> sigma2_resume_mic = 2;
+    int16u responder_sessoion_id = 3;
+    optional ICDParameterStruct responder_icd_params = 4;
+  }
+
+  // IDs here are based on Protocol opcodes
+  //
+  // Written here as "attributes" to force code generation
+  // to consider these active structures.
+  readonly attribute protocol_binary_data msg_counter_sync_request = 0x00;
+  readonly attribute protocol_binary_data msg_counter_sync_response = 0x01;
+  readonly attribute protocol_binary_data mrp_ack = 0x10;
+  readonly attribute PBKDFParamRequest pbkdf_param_request = 0x20;
+  readonly attribute PBKDFParamResponse pbkdf_param_response = 0x21;
+  readonly attribute PasePake1 pase_pake1 = 0x22;
+  readonly attribute PasePake2 pase_pake2 = 0x23;
+  readonly attribute PasePake3 pase_pake3 = 0x24;
+  readonly attribute CaseSigma1 case_sigma1 = 0x30;
+  readonly attribute CaseSigma2 case_sigma2 = 0x31;
+  readonly attribute CaseSigma3 case_sigma3 = 0x32;
+  readonly attribute CaseSigma2Resume case_sigma2_resume = 0x33;
+  readonly attribute protocol_binary_data status_report = 0x40;
+}
+
+/// Fake cluster for defining structures for IM data encoding.
+///
+/// This follows the `10. Interaction Model Encoding Specification`
+/// defined in the Matter specification.
+///
+/// This is NOT a real cluster.
+///
+client cluster IMProtocol = 0xFFFF0001 {
+  struct AttributePathIB {
+    boolean enable_tag_compression = 0;
+    optional int64u node_id = 1;
+    optional int16u endpoint_id = 2;
+    optional protocol_cluster_id cluster_id = 3;
+    optional protocol_attribute_id attribute_id = 4;
+    optional nullable int16u list_index = 5;
+  }
+
+  struct EventPathIB {
+    optional int64u node_id = 0;
+    optional int16u endpoint_id = 1;
+    optional protocol_cluster_id cluster_id = 2;
+    optional protocol_event_id event_id = 3;
+    boolean is_urgent = 4;
+  }
+
+  struct EventFilterIB {
+    optional int64u node_id = 0;
+    int64u event_min = 1;
+  }
+
+  struct ClusterPathIB {
+    optional int64u node_id = 0;
+    optional int16u endpoint_id = 1;
+    optional protocol_cluster_id cluster_id = 2;
+  }
+
+  struct DataVersionFilterIB {
+    ClusterPathIB path = 0;
+    int32u data_version = 1;
+  }
+
+  struct StatusResponseMessage {
+    int8u status = 0;
+
+    // 10.2.2.2. Context Tag Encoded Action Information
+    int8u interaction_model_revison = 0xFF;
+  }
+
+  struct ReadRequestMessage {
+    optional AttributePathIB attribute_requests[] = 0;
+    optional EventPathIB event_requests[] = 1;
+    optional EventFilterIB event_filters[] = 2;
+    optional boolean fabric_filtered = 3;
+    optional DataVersionFilterIB data_version_filters[] = 4;
+
+    // 10.2.2.2. Context Tag Encoded Action Information
+    int8u interaction_model_revison = 0xFF;
+  }
+
+  struct SubscribeRequestMessage {
+    boolean keep_subscriptions = 0;
+    int16u min_minterval_floor = 1;
+    int16u max_minterval_ceiling = 2;
+    optional AttributePathIB attribute_requests = 3;
+    optional EventPathIB event_requests = 4;
+    optional EventFilterIB event_filters = 5;
+    // NOTE: 6 is missing here ...
+    boolean fabric_filtered = 7;
+    optional DataVersionFilterIB data_version_filters = 8;
+
+    // 10.2.2.2. Context Tag Encoded Action Information
+    int8u interaction_model_revison = 0xFF;
+  }
+
+  struct SubscribeResponseMessage {
+    int32u subscription_id = 0;
+    // NOTE: 1 missing here
+    int16u max_interval = 2;
+
+    // 10.2.2.2. Context Tag Encoded Action Information
+    int8u interaction_model_revison = 0xFF;
+  }
+
+  enum StatusCodeEnum : ENUM8 {
+    kSuccess = 0x0;
+    kFailure = 0x01;
+    kInvalidSubscription = 0x7d;
+    kUnsupportedAccess = 0x7e;
+    kUnsupportedEndpoint = 0x7f;
+    kInvalidAction = 0x80;
+    kUnsupportedCommand = 0x81;
+    kInvalidCommand = 0x85;
+    kUnsupportedAttribute = 0x86;
+    kConstraintError = 0x87;
+    kUnsupportedWrite = 0x88;
+    kResourceExhausted = 0x89;
+    kNotFound = 0x8b;
+    kUnreportableAttribute = 0x8c;
+    kInvalidDataType = 0x8d;
+    kUnsupportedRead = 0x8f;
+    kDataVersionMismatch = 0x92;
+    kTimeout = 0x94;
+    kBusy = 0x9c;
+    kUnsupportedCluster = 0xc3;
+    kNoUpstreamSubscription = 0xc5;
+    kNeedsTimedInteraction = 0xc6;
+    kUnsupportedEvent = 0xc7;
+    kPathsExhausted = 0xc8;
+    kTimedRequestMismatch = 0xc9;
+    kFailsafeRequired = 0xca;
+    kWriteIgnored = 0xF0;
+  }
+
+  struct StatusIB {
+    StatusCodeEnum status = 0;
+    int8u cluster_status = 1;
+  }
+
+  struct AttributeStatus {
+    AttributePathIB path = 0;
+    StatusIB status = 1;
+  }
+
+  struct AttributeData {
+    optional int32u data_version = 0;
+    AttributePathIB path = 1;
+    cluster_attribute_payload data = 2;
+  }
+
+  struct AttributeReportIB {
+    AttributeStatus attribute_status = 0;
+    AttributeData attribute_data = 1;
+  }
+
+  struct EventStatusIB {
+    EventPathIB path = 0;
+    StatusIB status = 1;
+  }
+
+  struct EventDataIB {
+    EventPathIB path = 0;
+    int64u event_number = 1;
+    int8u priority = 2;
+
+    // oneof {
+    int64u epoch_timestamp = 3;
+    int64u system_timestamp = 4;
+    int64u delta_epoch_timestamp = 5;
+    int64u delta_system_timestamp = 6;
+    //  }
+
+    cluster_event_payload data = 2;
+  }
+
+  struct EventReportIB {
+    EventStatusIB event_status = 0;
+    EventDataIB event_data = 1;
+  }
+
+  struct ReportDataMessage {
+    optional int32u subscription_id = 0;
+    optional AttributeReportIB attribute_reports[] = 1;
+    optional EventReportIB event_reports[] = 2;
+    optional boolean more_cunked_messages = 3;
+    optional boolean suppress_response = 4;
+
+    // 10.2.2.2. Context Tag Encoded Action Information
+    int8u interaction_model_revison = 0xFF;
+  }
+
+  struct AttributeDataIB {
+    int32u data_version = 0;
+    AttributePathIB path = 1;
+    cluster_attribute_payload data = 2;
+  }
+
+  struct WriteRequestMessage {
+    optional boolean suppres_response = 0;
+    boolean timed_request = 1;
+    AttributeDataIB write_requests[] = 2;
+    optional boolean more_chunked_messages = 3;
+
+    // 10.2.2.2. Context Tag Encoded Action Information
+    int8u interaction_model_revison = 0xFF;
+  }
+
+  struct AttributeStatusIB {
+    AttributePathIB path = 0;
+    StatusIB status = 1;
+  }
+
+  struct WriteResponseMessage {
+    AttributeStatusIB write_responses[] = 0;
+
+    // 10.2.2.2. Context Tag Encoded Action Information
+    int8u interaction_model_revison = 0xFF;
+  }
+
+  struct CommandPathIB {
+    optional int16u endpoint_id = 0;
+    optional protocol_cluster_id cluster_id = 1;
+    optional protocol_command_id command_id = 2;
+  }
+
+  struct CommandDataIB {
+    CommandPathIB path = 0;
+    cluster_command_payload data = 1;
+  }
+
+  struct InvokeRequestMessage {
+    boolean suppress_response = 0;
+    boolean timed_request = 1;
+    CommandDataIB invoke_requests[] = 2;
+
+    // 10.2.2.2. Context Tag Encoded Action Information
+    int8u interaction_model_revison = 0xFF;
+  }
+
+  struct CommandStatusIB {
+    CommandPathIB path = 0;
+    StatusIB status = 1;
+  }
+
+  struct InvokeResponseIB {
+    CommandDataIB command = 0;
+    CommandStatusIB status = 1;
+  }
+
+  struct InvokeResponseMessage {
+    boolean suppress_response = 0;
+    InvokeResponseIB invoke_responses[] = 1;
+
+    // 10.2.2.2. Context Tag Encoded Action Information
+    int8u interaction_model_revison = 0xFF;
+  }
+
+  struct TimedRequestMessage {
+    int16u timeout = 0;
+
+    // 10.2.2.2. Context Tag Encoded Action Information
+    int8u interaction_model_revison = 0xFF;
+  }
+
+  // IDs here are based on Protocol opcodes, defined
+  // in 10.2.1 in the matter spec.
+  //
+  // Written here as "attributes" to force code generation
+  // to consider these active structures.
+  readonly attribute StatusResponseMessage status_response = 1;
+  readonly attribute ReadRequestMessage read_request = 2;
+  readonly attribute SubscribeRequestMessage subscribe_request = 3;
+  readonly attribute SubscribeResponseMessage subscribe_response = 4;
+  readonly attribute ReportDataMessage report_data = 5;
+  readonly attribute WriteRequestMessage write_request = 6;
+  readonly attribute WriteResponseMessage write_response = 7;
+  readonly attribute InvokeRequestMessage invoke_request = 8;
+  readonly attribute InvokeResponseMessage invoke_response = 9;
+  readonly attribute TimedRequestMessage timed_request = 10;
+}
+
+client cluster BdxProtocol = 0xFFFF0002 {
+  // IDs here are based on Protocol opcodes, defined
+  // in 11.21.3.1  in the matter spec.
+  //
+  // Written here as "attributes" to force code generation
+  // to consider these active structures.
+  readonly attribute octet_string send_init = 1;
+  readonly attribute octet_string send_accept = 2;
+
+  readonly attribute octet_string receive_init = 4;
+  readonly attribute octet_string receive_accept = 5;
+
+
+  readonly attribute octet_string block_query = 0x10;
+  readonly attribute octet_string block = 0x11;
+  readonly attribute octet_string block_eof = 0x12;
+  readonly attribute octet_string block_ack = 0x13;
+  readonly attribute octet_string block_ack_eof = 0x14;
+  readonly attribute octet_string block_query_with_skip = 0x15;
+}
+
+client cluster UserDirectedCommissioningProtocol = 0xFFFF0003 {
+  struct IdentificationDeclarationStruct {
+    octet_string<8> instance_name = 1;
+  }
+
+  // IDs here are based on Protocol opcodes, defined
+  // in 5.3.2 in the matter spec.
+  //
+  // Written here as "attributes" to force code generation
+  // to consider these active structures.
+  readonly attribute IdentificationDeclarationStruct identification_declaration = 0;
+}
\ No newline at end of file
diff --git a/src/lib/format/tests/BUILD.gn b/src/lib/format/tests/BUILD.gn
new file mode 100644
index 0000000..90428b5
--- /dev/null
+++ b/src/lib/format/tests/BUILD.gn
@@ -0,0 +1,61 @@
+# Copyright (c) 2023 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("//build_overrides/build.gni")
+import("//build_overrides/chip.gni")
+import("//build_overrides/nlunit_test.gni")
+
+import("${chip_root}/build/chip/chip_test_suite.gni")
+import("${chip_root}/build/chip/fuzz_test.gni")
+
+chip_test_suite("tests") {
+  output_name = "libFormatTests"
+
+  test_sources = [
+    "TestDecoding.cpp",
+    "TestFlatTree.cpp",
+    "TestFlatTreePosition.cpp",
+  ]
+
+  sources = [
+    "sample_data.cpp",
+    "sample_data.h",
+  ]
+  cflags = [ "-Wconversion" ]
+
+  public_deps = [
+    "${chip_root}/src/controller/data_model:cluster-tlv-metadata",
+    "${chip_root}/src/lib/core",
+    "${chip_root}/src/lib/format:flat-tree",
+    "${chip_root}/src/lib/format:protocol-decoder",
+    "${chip_root}/src/lib/format:protocol-tlv-metadata",
+    "${chip_root}/src/lib/support:testing",
+    "${nlunit_test_root}:nlunit-test",
+  ]
+}
+
+if (enable_fuzz_test_targets) {
+  chip_fuzz_target("fuzz-payload-decoder") {
+    sources = [ "FuzzPayloadDecoder.cpp" ]
+    public_deps = [
+      "${chip_root}/src/controller/data_model:cluster-tlv-metadata",
+      "${chip_root}/src/lib/core",
+      "${chip_root}/src/lib/format:flat-tree",
+      "${chip_root}/src/lib/format:protocol-decoder",
+      "${chip_root}/src/lib/format:protocol-tlv-metadata",
+      "${chip_root}/src/lib/support",
+      "${chip_root}/src/platform/logging:stdio",
+    ]
+  }
+}
diff --git a/src/lib/format/tests/FuzzPayloadDecoder.cpp b/src/lib/format/tests/FuzzPayloadDecoder.cpp
new file mode 100644
index 0000000..409c4cb
--- /dev/null
+++ b/src/lib/format/tests/FuzzPayloadDecoder.cpp
@@ -0,0 +1,79 @@
+/*
+ *
+ *    Copyright (c) 2020-2021 Project CHIP Authors
+ *    All rights reserved.
+ *
+ *    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.
+ */
+#include <cstddef>
+#include <cstdint>
+#include <lib/format/protocol_decoder.h>
+#include <lib/support/StringBuilder.h>
+
+#include <tlv/meta/clusters_meta.h>
+#include <tlv/meta/protocols_meta.h>
+
+namespace {
+
+using namespace chip::Decoders;
+using namespace chip::FlatTree;
+using namespace chip::TLV;
+using namespace chip::TLVMeta;
+
+void RunDecode(const PayloadDecoderInitParams & params, chip::ByteSpan payload)
+{
+    chip::Decoders::PayloadDecoder<64, 128> decoder(params);
+
+    decoder.StartDecoding(payload);
+
+    PayloadEntry entry;
+    while (decoder.Next(entry))
+    {
+        // Nothing to do ...
+    }
+}
+
+} // namespace
+
+extern "C" int LLVMFuzzerTestOneInput(const uint8_t * data, size_t len)
+{
+    PayloadDecoderInitParams params;
+    params.SetProtocolDecodeTree(chip::TLVMeta::protocols_meta).SetClusterDecodeTree(chip::TLVMeta::clusters_meta);
+
+    chip::ByteSpan payload(data, len);
+
+    // Try all possible IM types (including an invalid one)
+    params.SetProtocol(chip::Protocols::InteractionModel::Id);
+    for (uint8_t messageType = 0; messageType < 11; messageType++)
+    {
+        RunDecode(params.SetMessageType(messageType), payload);
+    }
+
+    // Try some SC variants
+    params.SetProtocol(chip::Protocols::SecureChannel::Id);
+    RunDecode(params.SetMessageType(0), payload);
+    RunDecode(params.SetMessageType(1), payload);
+    RunDecode(params.SetMessageType(2), payload);
+    RunDecode(params.SetMessageType(10), payload);
+    RunDecode(params.SetMessageType(11), payload);
+    RunDecode(params.SetMessageType(20), payload);
+    RunDecode(params.SetMessageType(32), payload);
+    RunDecode(params.SetMessageType(33), payload);
+
+    params.SetProtocol(chip::Protocols::BDX::Id);
+    RunDecode(params.SetMessageType(1), payload);
+    RunDecode(params.SetMessageType(2), payload);
+    RunDecode(params.SetMessageType(3), payload);
+
+    return 0;
+}
diff --git a/src/lib/format/tests/TestDecoding.cpp b/src/lib/format/tests/TestDecoding.cpp
new file mode 100644
index 0000000..6fcb154
--- /dev/null
+++ b/src/lib/format/tests/TestDecoding.cpp
@@ -0,0 +1,677 @@
+/*
+ *
+ *    Copyright (c) 2023 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.
+ */
+#include <lib/core/TLVWriter.h>
+#include <lib/format/protocol_decoder.h>
+#include <lib/support/StringBuilder.h>
+#include <lib/support/UnitTestRegistration.h>
+
+#include <tlv/meta/clusters_meta.h>
+#include <tlv/meta/protocols_meta.h>
+
+#include <nlunit-test.h>
+
+#include "sample_data.h"
+
+namespace {
+
+using namespace chip::Decoders;
+using namespace chip::FlatTree;
+using namespace chip::TLV;
+using namespace chip::TLVMeta;
+using namespace chip::TestData;
+
+const Entry<ItemInfo> _empty_item[0]                 = {};
+const std::array<const Node<ItemInfo>, 1> empty_meta = { { { 0, _empty_item } } };
+
+const Entry<ItemInfo> _FakeProtocolData[] = {
+    { { AttributeTag(5), "proto5", ItemType::kDefault }, kInvalidNodeIndex },
+    { { AttributeTag(16), "proto16", ItemType::kDefault }, kInvalidNodeIndex },
+};
+
+const Entry<ItemInfo> _FakeProtocols[] = {
+    { { ClusterTag(0xFFFF0000), "FakeSC", ItemType::kDefault }, 1 },
+    { { ClusterTag(0xFFFF0001), "FakeIM", ItemType::kDefault }, 1 },
+};
+
+const std::array<const Node<ItemInfo>, 53 + 2> fake_protocols_meta = { {
+    { 2, _FakeProtocols },
+    { 2, _FakeProtocolData },
+} };
+
+void TestSampleData(nlTestSuite * inSuite, const PayloadDecoderInitParams & params, const SamplePayload & data,
+                    const char * expectation)
+{
+    chip::Decoders::PayloadDecoder<64, 128> decoder(
+        PayloadDecoderInitParams(params).SetProtocol(data.protocolId).SetMessageType(data.messageType));
+
+    decoder.StartDecoding(data.payload);
+
+    chip::StringBuilder<4096> output_builder;
+
+    PayloadEntry entry;
+    int nesting = 0;
+    while (decoder.Next(entry))
+    {
+        switch (entry.GetType())
+        {
+        case PayloadEntry::IMPayloadType::kNestingExit:
+            nesting--;
+            continue;
+        case PayloadEntry::IMPayloadType::kAttribute:
+            output_builder.AddFormat("%*sATTRIBUTE: %" PRIi32 "/%" PRIi32 "\n", nesting * 2, "", entry.GetClusterId(),
+                                     entry.GetAttributeId());
+            continue;
+        case PayloadEntry::IMPayloadType::kCommand:
+            output_builder.AddFormat("%*sCOMMAND: %" PRIi32 "/%" PRIi32 "\n", nesting * 2, "", entry.GetClusterId(),
+                                     entry.GetCommandId());
+            continue;
+        case PayloadEntry::IMPayloadType::kEvent:
+            output_builder.AddFormat("%*sEVENT: %" PRIi32 "/%" PRIi32 "\n", nesting * 2, "", entry.GetClusterId(),
+                                     entry.GetEventId());
+            continue;
+        default:
+            break;
+        }
+
+        output_builder.AddFormat("%*s%s", nesting * 2, "", entry.GetName());
+
+        if (entry.GetType() == PayloadEntry::IMPayloadType::kNestingEnter)
+        {
+            nesting++;
+        }
+        else if (entry.GetValueText()[0] != '\0')
+        {
+            output_builder.AddFormat(": %s", entry.GetValueText());
+        }
+        else
+        {
+            output_builder.Add(": EMPTY");
+        }
+        output_builder.AddFormat("\n");
+    }
+    output_builder.AddMarkerIfOverflow();
+
+    if (strcmp(output_builder.c_str(), expectation) != 0)
+    {
+        printf("!!!!!!!!!!!!!!!!!!! EXPECTED OUTPUT !!!!!!!!!!!!!!!!!\n");
+        printf("%s\n", expectation);
+        printf("!!!!!!!!!!!!!!!!!!! ACTUAL OUTPUT   !!!!!!!!!!!!!!!!!\n");
+        printf("%s\n", output_builder.c_str());
+
+        unsigned idx = 0;
+        while (expectation[idx] == output_builder.c_str()[idx])
+        {
+            idx++;
+        }
+        printf("!!!!!!!!!!!!!!!!!!! DIFF LOCATION !!!!!!!!!!!!!!!!!\n");
+        printf("First diff at index %u\n", idx);
+
+        chip::StringBuilder<31> partial;
+        printf("EXPECT: '%s'\n", partial.Reset().Add(expectation + idx).AddMarkerIfOverflow().c_str());
+        printf("ACTUAL: '%s'\n", partial.Reset().Add(output_builder.c_str() + idx).AddMarkerIfOverflow().c_str());
+    }
+
+    NL_TEST_ASSERT(inSuite, strcmp(output_builder.c_str(), expectation) == 0);
+}
+
+void TestFullDataDecoding(nlTestSuite * inSuite, void * inContext)
+{
+    PayloadDecoderInitParams params;
+
+    params.SetProtocolDecodeTree(chip::TLVMeta::protocols_meta).SetClusterDecodeTree(chip::TLVMeta::clusters_meta);
+
+    TestSampleData(inSuite, params, secure_channel_mrp_ack, "mrp_ack: EMPTY\n");
+    TestSampleData(inSuite, params, secure_channel_pkbdf_param_request,
+                   "pbkdf_param_request\n"
+                   "  initiator_random: hex:7C8698755B8E9866BB4FFDC27B733F3B6EF7F83D43FBE0CA6AD2B8C52C8F4236\n"
+                   "  initiator_session_id: 37677\n"
+                   "  passcode_id: 0\n"
+                   "  has_pbkdf_parameters: false\n");
+    TestSampleData(inSuite, params, secure_channel_pkbdf_param_response,
+                   "pbkdf_param_response\n"
+                   "  initiator_random: hex:7C8698755B8E9866BB4FFDC27B733F3B6EF7F83D43FBE0CA6AD2B8C52C8F4236\n"
+                   "  responder_random: hex:A44EB3E1A751A88A32BAB59EF16EB9764C20E1A9DDBEF6EFE3F588C943C58424\n"
+                   "  responder_session_id: 40168\n"
+                   "  pbkdf_parameters\n"
+                   "    iterations: 1000\n"
+                   "    salt: hex:E8FC1E6FD0023422B3CA7ECEDD344444551C814D3D0B0EB9C096F00E8A8051B2\n");
+    TestSampleData(inSuite, params, secure_channel_pase_pake1,
+                   // clang-format off
+                   "pase_pake1\n"
+                   "  pA: hex:0422ABC7A84352850456BD4A510905FE6BB782A0863A9382550E1228020801B22EEC4102C60F80082842B9739705FCD37F134651442A41E3723DFFE0278\n"
+                   // clang-format on
+    );
+    TestSampleData(inSuite, params, secure_channel_pase_pake2,
+                   // clang-format off
+                   "pase_pake2\n"
+                   "  pB: hex:04B6A44A3347C6B77900A3674CA19F40F25F056F8CB344EC1B4FA7888B9E6B570B7010431C5D0BE4021FE74A96C40721765FDA6802BE8DFDF5624332275\n"
+                   "  cB: hex:40E7452275E38AEBAF0E0F6FAB33A1B0CB5AEB5E824230DD40D0071DC7E55C87\n"
+                   // clang-format on
+    );
+    TestSampleData(inSuite, params, secure_channel_pase_pake3,
+                   "pase_pake3\n"
+                   "  cA: hex:6008C72EDEC9D25D4A36522F0BF23058F9378EFE38CBBCCE8C6853900169BC38\n");
+    TestSampleData(inSuite, params, secure_channel_status_report, "status_report: BINARY DATA\n");
+    TestSampleData(inSuite, params, im_protocol_read_request,
+                   "read_request\n"
+                   "  attribute_requests\n"
+                   "    []\n"
+                   "      cluster_id: 49 == 'NetworkCommissioning'\n"
+                   "      attribute_id: 65532 == 'featureMap'\n"
+                   "    []\n"
+                   "      endpoint_id: 0\n"
+                   "      cluster_id: 48 == 'GeneralCommissioning'\n"
+                   "      attribute_id: 0 == 'breadcrumb'\n"
+                   "    []\n"
+                   "      endpoint_id: 0\n"
+                   "      cluster_id: 48 == 'GeneralCommissioning'\n"
+                   "      attribute_id: 1 == 'basicCommissioningInfo'\n"
+                   "    []\n"
+                   "      endpoint_id: 0\n"
+                   "      cluster_id: 48 == 'GeneralCommissioning'\n"
+                   "      attribute_id: 2 == 'regulatoryConfig'\n"
+                   "    []\n"
+                   "      endpoint_id: 0\n"
+                   "      cluster_id: 48 == 'GeneralCommissioning'\n"
+                   "      attribute_id: 3 == 'locationCapability'\n"
+                   "    []\n"
+                   "      endpoint_id: 0\n"
+                   "      cluster_id: 40 == 'BasicInformation'\n"
+                   "      attribute_id: 2 == 'vendorID'\n"
+                   "    []\n"
+                   "      endpoint_id: 0\n"
+                   "      cluster_id: 40 == 'BasicInformation'\n"
+                   "      attribute_id: 4 == 'productID'\n"
+                   "    []\n"
+                   "      cluster_id: 49 == 'NetworkCommissioning'\n"
+                   "      attribute_id: 3 == 'connectMaxTimeSeconds'\n"
+                   "  fabric_filtered: false\n"
+                   "  interaction_model_revison: 1\n");
+    TestSampleData(inSuite, params, im_protocol_report_data,
+                   "report_data\n"
+                   "  attribute_reports\n"
+                   "    []\n"
+                   "      attribute_data\n"
+                   "        data_version: 28559721\n"
+                   "        path\n"
+                   "          endpoint_id: 0\n"
+                   "          cluster_id: 49 == 'NetworkCommissioning'\n"
+                   "          attribute_id: 3 == 'connectMaxTimeSeconds'\n"
+                   "        NetworkCommissioning::connectMaxTimeSeconds: 0\n"
+                   "    []\n"
+                   "      attribute_data\n"
+                   "        data_version: 664978787\n"
+                   "        path\n"
+                   "          endpoint_id: 0\n"
+                   "          cluster_id: 40 == 'BasicInformation'\n"
+                   "          attribute_id: 4 == 'productID'\n"
+                   "        BasicInformation::productID: 32769\n"
+                   "    []\n"
+                   "      attribute_data\n"
+                   "        data_version: 664978787\n"
+                   "        path\n"
+                   "          endpoint_id: 0\n"
+                   "          cluster_id: 40 == 'BasicInformation'\n"
+                   "          attribute_id: 2 == 'vendorID'\n"
+                   "        BasicInformation::vendorID: 65521\n"
+                   "    []\n"
+                   "      attribute_data\n"
+                   "        data_version: 1414030794\n"
+                   "        path\n"
+                   "          endpoint_id: 0\n"
+                   "          cluster_id: 48 == 'GeneralCommissioning'\n"
+                   "          attribute_id: 3 == 'locationCapability'\n"
+                   "        GeneralCommissioning::locationCapability: 2 == kIndoorOutdoor\n"
+                   "    []\n"
+                   "      attribute_data\n"
+                   "        data_version: 1414030794\n"
+                   "        path\n"
+                   "          endpoint_id: 0\n"
+                   "          cluster_id: 48 == 'GeneralCommissioning'\n"
+                   "          attribute_id: 2 == 'regulatoryConfig'\n"
+                   "        GeneralCommissioning::regulatoryConfig: 0 == kIndoor\n"
+                   "    []\n"
+                   "      attribute_data\n"
+                   "        data_version: 1414030794\n"
+                   "        path\n"
+                   "          endpoint_id: 0\n"
+                   "          cluster_id: 48 == 'GeneralCommissioning'\n"
+                   "          attribute_id: 1 == 'basicCommissioningInfo'\n"
+                   "        GeneralCommissioning::basicCommissioningInfo\n"
+                   "          failSafeExpiryLengthSeconds: 60\n"
+                   "          maxCumulativeFailsafeSeconds: 900\n"
+                   "    []\n"
+                   "      attribute_data\n"
+                   "        data_version: 1414030794\n"
+                   "        path\n"
+                   "          endpoint_id: 0\n"
+                   "          cluster_id: 48 == 'GeneralCommissioning'\n"
+                   "          attribute_id: 0 == 'breadcrumb'\n"
+                   "        GeneralCommissioning::breadcrumb: 0\n"
+                   "    []\n"
+                   "      attribute_data\n"
+                   "        data_version: 28559721\n"
+                   "        path\n"
+                   "          endpoint_id: 0\n"
+                   "          cluster_id: 49 == 'NetworkCommissioning'\n"
+                   "          attribute_id: 65532 == 'featureMap'\n"
+                   "        NetworkCommissioning::featureMap: 4\n"
+                   "  suppress_response: true\n"
+                   "  interaction_model_revison: 1\n");
+
+    // Different content
+    TestSampleData(inSuite, params, im_protocol_report_data_acl,
+                   "report_data\n"
+                   "  attribute_reports\n"
+                   "    []\n"
+                   "      attribute_data\n"
+                   "        data_version: 3420147058\n"
+                   "        path\n"
+                   "          endpoint_id: 0\n"
+                   "          cluster_id: 31 == 'AccessControl'\n"
+                   "          attribute_id: 0 == 'acl'\n"
+                   "        AccessControl::acl\n"
+                   "          []\n"
+                   "            privilege: 5 == kAdminister\n"
+                   "            authMode: 2 == kCASE\n"
+                   "            subjects\n"
+                   "              []: 112233\n"
+                   "            targets: NULL\n"
+                   "            fabricIndex: 1\n"
+                   "  suppress_response: true\n"
+                   "  interaction_model_revison: 1\n");
+
+    TestSampleData(
+        inSuite, params, im_protocol_report_data_window_covering,
+        "report_data\n"
+        "  attribute_reports\n"
+        "    []\n"
+        "      attribute_data\n"
+        "        data_version: 2054986218\n"
+        "        path\n"
+        "          endpoint_id: 1\n"
+        "          cluster_id: 258 == 'WindowCovering'\n"
+        "          attribute_id: 7 == 'configStatus'\n"
+        "        WindowCovering::configStatus: 27 == kOperational | kOnlineReserved | kLiftPositionAware | kTiltPositionAware\n"
+        "  suppress_response: true\n"
+        "  interaction_model_revison: 1\n");
+
+    TestSampleData(inSuite, params, im_protocol_invoke_request,
+                   "invoke_request\n"
+                   "  suppress_response: false\n"
+                   "  timed_request: false\n"
+                   "  invoke_requests\n"
+                   "    []\n"
+                   "      path\n"
+                   "        endpoint_id: 1\n"
+                   "        cluster_id: 6 == 'OnOff'\n"
+                   "        command_id: 2 == 'Toggle'\n"
+                   "      OnOff::Toggle\n"
+                   "  interaction_model_revison: 1\n");
+
+    TestSampleData(inSuite, params, im_protocol_invoke_response,
+                   "invoke_response\n"
+                   "  suppress_response: false\n"
+                   "  invoke_responses\n"
+                   "    []\n"
+                   "      status\n"
+                   "        path\n"
+                   "          endpoint_id: 1\n"
+                   "          cluster_id: 6 == 'OnOff'\n"
+                   "          command_id: 2 == 'Toggle'\n"
+                   "        status\n"
+                   "          status: 0 == kSuccess\n"
+                   "  interaction_model_revison: 1\n");
+
+    TestSampleData(inSuite, params, im_protocol_invoke_request_change_channel,
+                   "invoke_request\n"
+                   "  suppress_response: false\n"
+                   "  timed_request: false\n"
+                   "  invoke_requests\n"
+                   "    []\n"
+                   "      path\n"
+                   "        endpoint_id: 1\n"
+                   "        cluster_id: 1284 == 'Channel'\n"
+                   "        command_id: 0 == 'ChangeChannel'\n"
+                   "      Channel::ChangeChannel\n"
+                   "        match: \"channel name\"\n"
+                   "  interaction_model_revison: 1\n");
+}
+
+void TestMetaDataOnlyDecoding(nlTestSuite * inSuite, void * inContext)
+{
+    PayloadDecoderInitParams params;
+
+    // NO CLUSTER DECODE TREE
+    params.SetProtocolDecodeTree(chip::TLVMeta::protocols_meta);
+
+    TestSampleData(inSuite, params, secure_channel_mrp_ack, "mrp_ack: EMPTY\n");
+    TestSampleData(inSuite, params, secure_channel_pkbdf_param_request,
+                   "pbkdf_param_request\n"
+                   "  initiator_random: hex:7C8698755B8E9866BB4FFDC27B733F3B6EF7F83D43FBE0CA6AD2B8C52C8F4236\n"
+                   "  initiator_session_id: 37677\n"
+                   "  passcode_id: 0\n"
+                   "  has_pbkdf_parameters: false\n");
+
+    TestSampleData(inSuite, params, im_protocol_read_request,
+                   "read_request\n"
+                   "  attribute_requests\n"
+                   "    []\n"
+                   "      cluster_id: 49\n"
+                   "      attribute_id: 65532\n"
+                   "    []\n"
+                   "      endpoint_id: 0\n"
+                   "      cluster_id: 48\n"
+                   "      attribute_id: 0\n"
+                   "    []\n"
+                   "      endpoint_id: 0\n"
+                   "      cluster_id: 48\n"
+                   "      attribute_id: 1\n"
+                   "    []\n"
+                   "      endpoint_id: 0\n"
+                   "      cluster_id: 48\n"
+                   "      attribute_id: 2\n"
+                   "    []\n"
+                   "      endpoint_id: 0\n"
+                   "      cluster_id: 48\n"
+                   "      attribute_id: 3\n"
+                   "    []\n"
+                   "      endpoint_id: 0\n"
+                   "      cluster_id: 40\n"
+                   "      attribute_id: 2\n"
+                   "    []\n"
+                   "      endpoint_id: 0\n"
+                   "      cluster_id: 40\n"
+                   "      attribute_id: 4\n"
+                   "    []\n"
+                   "      cluster_id: 49\n"
+                   "      attribute_id: 3\n"
+                   "  fabric_filtered: false\n"
+                   "  interaction_model_revison: 1\n");
+    TestSampleData(inSuite, params, im_protocol_report_data,
+                   "report_data\n"
+                   "  attribute_reports\n"
+                   "    []\n"
+                   "      attribute_data\n"
+                   "        data_version: 28559721\n"
+                   "        path\n"
+                   "          endpoint_id: 0\n"
+                   "          cluster_id: 49\n"
+                   "          attribute_id: 3\n"
+                   "        ATTRIBUTE: 49/3\n"
+                   "    []\n"
+                   "      attribute_data\n"
+                   "        data_version: 664978787\n"
+                   "        path\n"
+                   "          endpoint_id: 0\n"
+                   "          cluster_id: 40\n"
+                   "          attribute_id: 4\n"
+                   "        ATTRIBUTE: 40/4\n"
+                   "    []\n"
+                   "      attribute_data\n"
+                   "        data_version: 664978787\n"
+                   "        path\n"
+                   "          endpoint_id: 0\n"
+                   "          cluster_id: 40\n"
+                   "          attribute_id: 2\n"
+                   "        ATTRIBUTE: 40/2\n"
+                   "    []\n"
+                   "      attribute_data\n"
+                   "        data_version: 1414030794\n"
+                   "        path\n"
+                   "          endpoint_id: 0\n"
+                   "          cluster_id: 48\n"
+                   "          attribute_id: 3\n"
+                   "        ATTRIBUTE: 48/3\n"
+                   "    []\n"
+                   "      attribute_data\n"
+                   "        data_version: 1414030794\n"
+                   "        path\n"
+                   "          endpoint_id: 0\n"
+                   "          cluster_id: 48\n"
+                   "          attribute_id: 2\n"
+                   "        ATTRIBUTE: 48/2\n"
+                   "    []\n"
+                   "      attribute_data\n"
+                   "        data_version: 1414030794\n"
+                   "        path\n"
+                   "          endpoint_id: 0\n"
+                   "          cluster_id: 48\n"
+                   "          attribute_id: 1\n"
+                   "        ATTRIBUTE: 48/1\n"
+                   "    []\n"
+                   "      attribute_data\n"
+                   "        data_version: 1414030794\n"
+                   "        path\n"
+                   "          endpoint_id: 0\n"
+                   "          cluster_id: 48\n"
+                   "          attribute_id: 0\n"
+                   "        ATTRIBUTE: 48/0\n"
+                   "    []\n"
+                   "      attribute_data\n"
+                   "        data_version: 28559721\n"
+                   "        path\n"
+                   "          endpoint_id: 0\n"
+                   "          cluster_id: 49\n"
+                   "          attribute_id: 65532\n"
+                   "        ATTRIBUTE: 49/65532\n"
+                   "  suppress_response: true\n"
+                   "  interaction_model_revison: 1\n");
+
+    // Different content
+    TestSampleData(inSuite, params, im_protocol_report_data_acl,
+                   "report_data\n"
+                   "  attribute_reports\n"
+                   "    []\n"
+                   "      attribute_data\n"
+                   "        data_version: 3420147058\n"
+                   "        path\n"
+                   "          endpoint_id: 0\n"
+                   "          cluster_id: 31\n"
+                   "          attribute_id: 0\n"
+                   "        ATTRIBUTE: 31/0\n"
+                   "  suppress_response: true\n"
+                   "  interaction_model_revison: 1\n");
+}
+
+void TestEmptyClusterMetaDataDecode(nlTestSuite * inSuite, void * inContext)
+{
+    PayloadDecoderInitParams params;
+
+    params.SetProtocolDecodeTree(chip::TLVMeta::protocols_meta).SetClusterDecodeTree(empty_meta);
+
+    TestSampleData(inSuite, params, secure_channel_mrp_ack, "mrp_ack: EMPTY\n");
+    TestSampleData(inSuite, params, im_protocol_report_data_acl,
+                   "report_data\n"
+                   "  attribute_reports\n"
+                   "    []\n"
+                   "      attribute_data\n"
+                   "        data_version: 3420147058\n"
+                   "        path\n"
+                   "          endpoint_id: 0\n"
+                   "          cluster_id: 31\n"
+                   "          attribute_id: 0\n"
+                   "        0x1f::ATTR(0x0)\n"                 // Cluster 31, attribute 0
+                   "          UnknownTag(0x100)\n"             // List entry (acl is a list)
+                   "            ContextSpecific(0x1): 5\n"     // privilege
+                   "            ContextSpecific(0x2): 2\n"     // authMode
+                   "            ContextSpecific(0x3)\n"        // subjects
+                   "              UnknownTag(0x100): 112233\n" // List entry (subjects is a list)
+                   "            ContextSpecific(0x4): NULL\n"  // targets
+                   "            ContextSpecific(0xFE): 1\n"    // fabricIndex
+                   "  suppress_response: true\n"
+                   "  interaction_model_revison: 1\n");
+}
+
+void TestMissingDecodeData(nlTestSuite * inSuite, void * inContext)
+{
+    PayloadDecoderInitParams params;
+
+    params.SetProtocolDecodeTree(empty_meta).SetClusterDecodeTree(empty_meta);
+
+    TestSampleData(inSuite, params, secure_channel_mrp_ack, "PROTO(0x0, 0x10): UNKNOWN\n");
+    TestSampleData(inSuite, params, im_protocol_report_data_acl, "PROTO(0x1, 0x5): UNKNOWN\n");
+}
+
+void TestWrongDecodeData(nlTestSuite * inSuite, void * inContext)
+{
+    PayloadDecoderInitParams params;
+
+    params.SetProtocolDecodeTree(fake_protocols_meta).SetClusterDecodeTree(empty_meta);
+
+    TestSampleData(inSuite, params, secure_channel_mrp_ack, "proto16: EMPTY\n");
+    TestSampleData(inSuite, params, im_protocol_report_data_acl,
+                   "proto5\n"
+                   "  ContextSpecific(0x1)\n"
+                   "    UnknownTag(0x100)\n"
+                   "      ContextSpecific(0x1)\n"
+                   "        ContextSpecific(0x0): 3420147058\n"
+                   "        ContextSpecific(0x1)\n"
+                   "          ContextSpecific(0x2): 0\n"
+                   "          ContextSpecific(0x3): 31\n"
+                   "          ContextSpecific(0x4): 0\n"
+                   "        ContextSpecific(0x2)\n"
+                   "          UnknownTag(0x100)\n"
+                   "            ContextSpecific(0x1): 5\n"
+                   "            ContextSpecific(0x2): 2\n"
+                   "            ContextSpecific(0x3)\n"
+                   "              UnknownTag(0x100): 112233\n"
+                   "            ContextSpecific(0x4): NULL\n"
+                   "            ContextSpecific(0xFE): 1\n"
+                   "  ContextSpecific(0x4): true\n"
+                   "  ContextSpecific(0xFF): 1\n");
+}
+
+void TestNestingOverflow(nlTestSuite * inSuite, void * inContext)
+{
+    PayloadDecoderInitParams params;
+    params.SetProtocolDecodeTree(fake_protocols_meta).SetClusterDecodeTree(empty_meta);
+
+    uint8_t data_buffer[1024];
+    chip::TLV::TLVWriter writer;
+
+    writer.Init(data_buffer, sizeof(data_buffer));
+
+    chip::TLV::TLVType unusedType;
+
+    // Protocols start with an anonymous tagged structure, after which lists can be of any tags
+    NL_TEST_ASSERT(inSuite, writer.StartContainer(AnonymousTag(), kTLVType_Structure, unusedType) == CHIP_NO_ERROR);
+
+    // nesting overflow here
+    for (uint8_t i = 0; i < 32; i++)
+    {
+        NL_TEST_ASSERT(inSuite, writer.StartContainer(ContextTag(i), kTLVType_List, unusedType) == CHIP_NO_ERROR);
+    }
+    // Go back to 24 (still too much nesting)
+    for (uint8_t i = 0; i < 8; i++)
+    {
+        NL_TEST_ASSERT(inSuite, writer.EndContainer(kTLVType_List) == CHIP_NO_ERROR);
+    }
+    for (uint8_t i = 0; i < 4; i++)
+    {
+        NL_TEST_ASSERT(
+            inSuite, writer.StartContainer(ContextTag(static_cast<uint8_t>(i + 0x10)), kTLVType_List, unusedType) == CHIP_NO_ERROR);
+    }
+    for (uint8_t i = 0; i < 4; i++)
+    {
+        NL_TEST_ASSERT(inSuite, writer.EndContainer(kTLVType_List) == CHIP_NO_ERROR);
+    }
+    // Go back to 8
+    for (uint8_t i = 0; i < 16; i++)
+    {
+        NL_TEST_ASSERT(inSuite, writer.EndContainer(kTLVType_List) == CHIP_NO_ERROR);
+    }
+    for (uint8_t i = 0; i < 4; i++)
+    {
+        NL_TEST_ASSERT(
+            inSuite, writer.StartContainer(ContextTag(static_cast<uint8_t>(i + 0x20)), kTLVType_List, unusedType) == CHIP_NO_ERROR);
+    }
+    for (uint8_t i = 0; i < 4; i++)
+    {
+        NL_TEST_ASSERT(inSuite, writer.EndContainer(kTLVType_List) == CHIP_NO_ERROR);
+    }
+    // Go back to 4
+    for (uint8_t i = 0; i < 4; i++)
+    {
+        NL_TEST_ASSERT(inSuite, writer.EndContainer(kTLVType_List) == CHIP_NO_ERROR);
+    }
+    for (uint8_t i = 0; i < 4; i++)
+    {
+        NL_TEST_ASSERT(
+            inSuite, writer.StartContainer(ContextTag(static_cast<uint8_t>(i + 0x30)), kTLVType_List, unusedType) == CHIP_NO_ERROR);
+    }
+    for (uint8_t i = 0; i < 4; i++)
+    {
+        NL_TEST_ASSERT(inSuite, writer.EndContainer(kTLVType_List) == CHIP_NO_ERROR);
+    }
+    // close everything
+    for (uint8_t i = 0; i < 4; i++)
+    {
+        NL_TEST_ASSERT(inSuite, writer.EndContainer(kTLVType_List) == CHIP_NO_ERROR);
+    }
+    NL_TEST_ASSERT(inSuite, writer.EndContainer(kTLVType_Structure) == CHIP_NO_ERROR);
+
+    SamplePayload fake_payload{ chip::Protocols::InteractionModel::Id, 5, chip::ByteSpan(data_buffer, writer.GetLengthWritten()) };
+
+    TestSampleData(inSuite, params, fake_payload,
+                   "proto5\n"
+                   "  ContextSpecific(0x0)\n"
+                   "    ContextSpecific(0x1)\n"
+                   "      ContextSpecific(0x2)\n"
+                   "        ContextSpecific(0x3)\n"
+                   "          ContextSpecific(0x4)\n"
+                   "            ContextSpecific(0x5)\n"
+                   "              ContextSpecific(0x6)\n"
+                   "                ContextSpecific(0x7)\n"
+                   "                  ContextSpecific(0x8)\n"
+                   "                    ContextSpecific(0x9)\n"
+                   "                      ContextSpecific(0xA)\n"
+                   "                        ContextSpecific(0xB)\n"
+                   "                          ContextSpecific(0xC)\n"
+                   "                            ContextSpecific(0xD)\n"
+                   "                              ContextSpecific(0xE)\n"
+                   "                                ContextSpecific(0xF): NESTING DEPTH REACHED\n"
+                   "                  ContextSpecific(0x20)\n"
+                   "                    ContextSpecific(0x21)\n"
+                   "                      ContextSpecific(0x22)\n"
+                   "                        ContextSpecific(0x23)\n"
+                   "          ContextSpecific(0x30)\n"
+                   "            ContextSpecific(0x31)\n"
+                   "              ContextSpecific(0x32)\n"
+                   "                ContextSpecific(0x33)\n");
+}
+
+const nlTest sTests[] = {
+    NL_TEST_DEF("TestFullDataDecoding", TestFullDataDecoding),                     //
+    NL_TEST_DEF("TestMetaDataOnlyDecoding", TestMetaDataOnlyDecoding),             //
+    NL_TEST_DEF("TestEmptyClusterMetaDataDecode", TestEmptyClusterMetaDataDecode), //
+    NL_TEST_DEF("TestMissingDecodeData", TestMissingDecodeData),                   //
+    NL_TEST_DEF("TestWrongDecodeData", TestWrongDecodeData),                       //
+    NL_TEST_DEF("TestNestingOverflow", TestNestingOverflow),                       //
+    NL_TEST_SENTINEL()                                                             //
+};
+
+} // namespace
+
+int TestDecode()
+{
+    nlTestSuite theSuite = { "TestDecode", sTests, nullptr, nullptr };
+    nlTestRunner(&theSuite, nullptr);
+    return nlTestRunnerStats(&theSuite);
+}
+
+CHIP_REGISTER_TEST_SUITE(TestDecode)
diff --git a/src/lib/format/tests/TestFlatTree.cpp b/src/lib/format/tests/TestFlatTree.cpp
new file mode 100644
index 0000000..8eebb98
--- /dev/null
+++ b/src/lib/format/tests/TestFlatTree.cpp
@@ -0,0 +1,124 @@
+/*
+ *
+ *    Copyright (c) 2023 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.
+ */
+#include <lib/format/FlatTree.h>
+
+#include <lib/core/TLVTags.h>
+#include <lib/support/UnitTestRegistration.h>
+
+#include <array>
+
+#include <string.h>
+
+#include <nlunit-test.h>
+
+namespace {
+
+using namespace chip::FlatTree;
+using namespace chip::TLV;
+
+struct NamedTag
+{
+    Tag tag;
+    const char * name;
+};
+
+Entry<NamedTag> node1[] = {
+    { { ContextTag(1), "hello" } },
+    { { ContextTag(2), "world" } },
+};
+
+Entry<NamedTag> node2[] = {
+    { { ProfileTag(123, 1), "a" } },
+    { { ProfileTag(234, 2), "b" } },
+    { { ProfileTag(345, 3), "c" } },
+};
+
+Entry<NamedTag> node3[] = {
+    { { AnonymousTag(), "foo" } },
+};
+
+#define _ENTRY(n)                                                                                                                  \
+    {                                                                                                                              \
+        sizeof(n) / sizeof(n[0]), n                                                                                                \
+    }
+
+std::array<Node<NamedTag>, 3> tree = { {
+    _ENTRY(node1),
+    _ENTRY(node2),
+    _ENTRY(node3),
+} };
+
+class ByTag
+{
+public:
+    constexpr ByTag(Tag tag) : mTag(tag) {}
+    bool operator()(const NamedTag & item) { return item.tag == mTag; }
+
+private:
+    const Tag mTag;
+};
+
+class ByName
+{
+public:
+    constexpr ByName(const char * name) : mName(name) {}
+    bool operator()(const NamedTag & item) { return strcmp(item.name, mName) == 0; }
+
+private:
+    const char * mName;
+};
+
+void TestFlatTreeFind(nlTestSuite * inSuite, void * inContext)
+{
+    NL_TEST_ASSERT(inSuite, strcmp(FindEntry(tree, 0, ByTag(ContextTag(1)))->data.name, "hello") == 0);
+    NL_TEST_ASSERT(inSuite, strcmp(FindEntry(tree, 0, ByTag(ContextTag(2)))->data.name, "world") == 0);
+    NL_TEST_ASSERT(inSuite, FindEntry(tree, 0, ByTag(ContextTag(3))) == nullptr);
+
+    NL_TEST_ASSERT(inSuite, FindEntry(tree, 0, ByName("hello"))->data.tag == ContextTag(1));
+    NL_TEST_ASSERT(inSuite, FindEntry(tree, 0, ByName("world"))->data.tag == ContextTag(2));
+    NL_TEST_ASSERT(inSuite, FindEntry(tree, 0, ByName("foo")) == nullptr);
+
+    NL_TEST_ASSERT(inSuite, FindEntry(tree, 1, ByTag(ContextTag(1))) == nullptr);
+    NL_TEST_ASSERT(inSuite, strcmp(FindEntry(tree, 1, ByTag(ProfileTag(234, 2)))->data.name, "b") == 0);
+    NL_TEST_ASSERT(inSuite, strcmp(FindEntry(tree, 1, ByTag(ProfileTag(345, 3)))->data.name, "c") == 0);
+    NL_TEST_ASSERT(inSuite, FindEntry(tree, 1, ByTag(AnonymousTag())) == nullptr);
+
+    NL_TEST_ASSERT(inSuite, FindEntry(tree, 2, ByTag(ContextTag(1))) == nullptr);
+    NL_TEST_ASSERT(inSuite, strcmp(FindEntry(tree, 2, ByTag(AnonymousTag()))->data.name, "foo") == 0);
+
+    // out of array
+    NL_TEST_ASSERT(inSuite, FindEntry(tree, 3, ByTag(AnonymousTag())) == nullptr);
+    NL_TEST_ASSERT(inSuite, FindEntry(tree, 100, ByTag(AnonymousTag())) == nullptr);
+    NL_TEST_ASSERT(inSuite, FindEntry(tree, 1000, ByTag(AnonymousTag())) == nullptr);
+    NL_TEST_ASSERT(inSuite, FindEntry(tree, 9999999, ByTag(AnonymousTag())) == nullptr);
+}
+
+const nlTest sTests[] = {
+    NL_TEST_DEF("TestFlatTreeFind", TestFlatTreeFind), //
+    NL_TEST_SENTINEL()                                 //
+};
+
+} // namespace
+
+int TestFlatTree()
+{
+    nlTestSuite theSuite = { "FlatTree", sTests, nullptr, nullptr };
+    nlTestRunner(&theSuite, nullptr);
+    return nlTestRunnerStats(&theSuite);
+}
+
+CHIP_REGISTER_TEST_SUITE(TestFlatTree)
diff --git a/src/lib/format/tests/TestFlatTreePosition.cpp b/src/lib/format/tests/TestFlatTreePosition.cpp
new file mode 100644
index 0000000..ee5f6b3
--- /dev/null
+++ b/src/lib/format/tests/TestFlatTreePosition.cpp
@@ -0,0 +1,274 @@
+/*
+ *
+ *    Copyright (c) 2023 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.
+ */
+#include <lib/format/FlatTree.h>
+#include <lib/format/FlatTreePosition.h>
+
+#include <lib/core/TLVTags.h>
+#include <lib/support/UnitTestRegistration.h>
+
+#include <array>
+#include <vector>
+
+#include <string.h>
+
+#include <nlunit-test.h>
+
+namespace {
+
+using namespace chip::FlatTree;
+using namespace chip::TLV;
+
+struct NamedTag
+{
+    Tag tag;
+    const char * name;
+};
+
+/// Tree definition for
+///
+/// |- hello     (1)
+/// \- world     (2)
+///    |- a      (123, 1)
+///    |- b      (234, 2)
+///    |  \- foo (A)
+///    \- c      (345, 3)
+///
+Entry<NamedTag> node1[] = {
+    { { ContextTag(1), "hello" }, kInvalidNodeIndex },
+    { { ContextTag(2), "world" }, 1 },
+};
+
+Entry<NamedTag> node2[] = {
+    { { ProfileTag(123, 1), "a" }, kInvalidNodeIndex },
+    { { ProfileTag(234, 2), "b" }, 2 },
+    { { ProfileTag(345, 3), "c" }, kInvalidNodeIndex },
+};
+
+Entry<NamedTag> node3[] = {
+    { { AnonymousTag(), "foo" }, kInvalidNodeIndex },
+};
+
+#define _ENTRY(n)                                                                                                                  \
+    {                                                                                                                              \
+        sizeof(n) / sizeof(n[0]), n                                                                                                \
+    }
+
+std::array<Node<NamedTag>, 3> tree = { {
+    _ENTRY(node1),
+    _ENTRY(node2),
+    _ENTRY(node3),
+} };
+
+class ByTag
+{
+public:
+    constexpr ByTag(Tag tag) : mTag(tag) {}
+    bool operator()(const NamedTag & item) { return item.tag == mTag; }
+
+private:
+    const Tag mTag;
+};
+
+class ByName
+{
+public:
+    constexpr ByName(const char * name) : mName(name) {}
+    bool operator()(const NamedTag & item) { return strcmp(item.name, mName) == 0; }
+
+private:
+    const char * mName;
+};
+
+#define ASSERT_HAS_NAME(p, n)                                                                                                      \
+    NL_TEST_ASSERT(inSuite, p.Get() != nullptr);                                                                                   \
+    NL_TEST_ASSERT(inSuite, strcmp(p.Get()->name, n) == 0)
+
+#define ASSERT_HAS_CONTEXT_TAG(p, t)                                                                                               \
+    NL_TEST_ASSERT(inSuite, p.Get() != nullptr);                                                                                   \
+    NL_TEST_ASSERT(inSuite, p.Get()->tag == ContextTag(t))
+
+#define ASSERT_HAS_PROFILE_TAG(p, a, b)                                                                                            \
+    NL_TEST_ASSERT(inSuite, p.Get() != nullptr);                                                                                   \
+    NL_TEST_ASSERT(inSuite, p.Get()->tag == ProfileTag(a, b))
+
+template <size_t N>
+std::vector<Tag> GetPath(Position<NamedTag, N> & position)
+{
+    std::vector<Tag> result;
+
+    for (const auto & item : position.CurrentPath())
+    {
+        result.push_back(item->data.tag);
+    }
+
+    return result;
+}
+
+bool HasPath(const std::vector<Tag> & v, Tag a)
+{
+    return (v.size() == 1) && (v[0] == a);
+}
+
+bool HasPath(const std::vector<Tag> & v, Tag a, Tag b)
+{
+    return (v.size() == 2) && (v[0] == a) && (v[1] == b);
+}
+
+bool HasPath(const std::vector<Tag> & v, Tag a, Tag b, Tag c)
+{
+    return (v.size() == 3) && (v[0] == a) && (v[1] == b) && (v[2] == c);
+}
+
+void TestSimpleEnterExit(nlTestSuite * inSuite, void * inContext)
+{
+    Position<NamedTag, 10> position(tree.data(), tree.size());
+
+    // at start, top of tree has no value
+    NL_TEST_ASSERT(inSuite, position.Get() == nullptr);
+    NL_TEST_ASSERT(inSuite, GetPath(position).empty());
+
+    // Go to hello, try going to invalid 2x, then go back
+    position.Enter(ByTag(ContextTag(1)));
+    ASSERT_HAS_NAME(position, "hello");
+    NL_TEST_ASSERT(inSuite, HasPath(GetPath(position), ContextTag(1)));
+
+    position.Enter(ByTag(ContextTag(1)));
+    NL_TEST_ASSERT(inSuite, position.Get() == nullptr);
+    NL_TEST_ASSERT(inSuite, GetPath(position).empty());
+    position.Enter(ByTag(ContextTag(1)));
+    NL_TEST_ASSERT(inSuite, position.Get() == nullptr);
+    position.Exit();
+    NL_TEST_ASSERT(inSuite, position.Get() == nullptr);
+    position.Exit();
+    NL_TEST_ASSERT(inSuite, position.Get() != nullptr);
+    ASSERT_HAS_NAME(position, "hello");
+    NL_TEST_ASSERT(inSuite, HasPath(GetPath(position), ContextTag(1)));
+    position.Exit();
+    NL_TEST_ASSERT(inSuite, position.Get() == nullptr);
+}
+
+void TestDeeperEnter(nlTestSuite * inSuite, void * inContext)
+{
+    Position<NamedTag, 10> position(tree.data(), tree.size());
+
+    NL_TEST_ASSERT(inSuite, position.Get() == nullptr);
+
+    position.Enter(ByName("world"));
+    ASSERT_HAS_CONTEXT_TAG(position, 2);
+    NL_TEST_ASSERT(inSuite, HasPath(GetPath(position), ContextTag(2)));
+
+    position.Enter(ByTag(ProfileTag(123, 1)));
+    ASSERT_HAS_NAME(position, "a");
+    NL_TEST_ASSERT(inSuite, HasPath(GetPath(position), ContextTag(2), ProfileTag(123, 1)));
+
+    position.Enter(ByTag(AnonymousTag()));
+    NL_TEST_ASSERT(inSuite, position.Get() == nullptr);
+    NL_TEST_ASSERT(inSuite, GetPath(position).empty());
+    position.Exit();
+    ASSERT_HAS_NAME(position, "a");
+    NL_TEST_ASSERT(inSuite, HasPath(GetPath(position), ContextTag(2), ProfileTag(123, 1)));
+
+    position.Exit();
+    ASSERT_HAS_NAME(position, "world");
+
+    position.Enter(ByName("b"));
+    ASSERT_HAS_PROFILE_TAG(position, 234, 2);
+
+    position.Enter(ByTag(AnonymousTag()));
+    ASSERT_HAS_NAME(position, "foo");
+    NL_TEST_ASSERT(inSuite, HasPath(GetPath(position), ContextTag(2), ProfileTag(234, 2), AnonymousTag()));
+
+    // test some unknown
+    for (int i = 0; i < 100; i++)
+    {
+        position.Enter(ByTag(AnonymousTag()));
+        NL_TEST_ASSERT(inSuite, position.Get() == nullptr);
+        NL_TEST_ASSERT(inSuite, GetPath(position).empty());
+    }
+    for (int i = 0; i < 100; i++)
+    {
+        NL_TEST_ASSERT(inSuite, position.Get() == nullptr);
+        NL_TEST_ASSERT(inSuite, GetPath(position).empty());
+        position.Exit();
+    }
+    NL_TEST_ASSERT(inSuite, HasPath(GetPath(position), ContextTag(2), ProfileTag(234, 2), AnonymousTag()));
+    ASSERT_HAS_NAME(position, "foo");
+    position.Exit();
+
+    NL_TEST_ASSERT(inSuite, HasPath(GetPath(position), ContextTag(2), ProfileTag(234, 2)));
+    ASSERT_HAS_NAME(position, "b");
+    position.Exit();
+
+    NL_TEST_ASSERT(inSuite, HasPath(GetPath(position), ContextTag(2)));
+    ASSERT_HAS_NAME(position, "world");
+
+    // root and stay there
+    position.Exit();
+    position.Exit();
+    position.Exit();
+    position.Exit();
+    NL_TEST_ASSERT(inSuite, GetPath(position).empty());
+
+    // can still navigate from the root
+    position.Enter(ByName("world"));
+    NL_TEST_ASSERT(inSuite, HasPath(GetPath(position), ContextTag(2)));
+    ASSERT_HAS_CONTEXT_TAG(position, 2);
+}
+
+void TestDescendLimit(nlTestSuite * inSuite, void * inContext)
+{
+    Position<NamedTag, 2> position(tree.data(), tree.size());
+
+    position.Enter(ByName("world"));
+    ASSERT_HAS_CONTEXT_TAG(position, 2);
+
+    position.Enter(ByName("b"));
+    ASSERT_HAS_PROFILE_TAG(position, 234, 2);
+
+    // only 2 positions can be remembered. Running out of space
+    position.Enter(ByName("foo"));
+    NL_TEST_ASSERT(inSuite, position.Get() == nullptr);
+    NL_TEST_ASSERT(inSuite, GetPath(position).empty());
+
+    position.Exit();
+    NL_TEST_ASSERT(inSuite, HasPath(GetPath(position), ContextTag(2), ProfileTag(234, 2)));
+    ASSERT_HAS_NAME(position, "b");
+    ASSERT_HAS_PROFILE_TAG(position, 234, 2);
+
+    position.Exit();
+    NL_TEST_ASSERT(inSuite, HasPath(GetPath(position), ContextTag(2)));
+    ASSERT_HAS_NAME(position, "world");
+    ASSERT_HAS_CONTEXT_TAG(position, 2);
+}
+
+const nlTest sTests[] = {
+    NL_TEST_DEF("TestSimpleEnterExit", TestSimpleEnterExit), //
+    NL_TEST_DEF("TestDeeperEnter", TestDeeperEnter),         //
+    NL_TEST_DEF("TestDescendLimit", TestDescendLimit),       //
+    NL_TEST_SENTINEL()                                       //
+};
+
+} // namespace
+
+int TestFlatTreePosition()
+{
+    nlTestSuite theSuite = { "FlatTree", sTests, nullptr, nullptr };
+    nlTestRunner(&theSuite, nullptr);
+    return nlTestRunnerStats(&theSuite);
+}
+
+CHIP_REGISTER_TEST_SUITE(TestFlatTreePosition)
diff --git a/src/lib/format/tests/sample_data.cpp b/src/lib/format/tests/sample_data.cpp
new file mode 100644
index 0000000..b530419
--- /dev/null
+++ b/src/lib/format/tests/sample_data.cpp
@@ -0,0 +1,131 @@
+/*
+ *
+ *    Copyright (c) 2023 Project CHIP Authors
+ *    All rights reserved.
+ *
+ *    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.
+ */
+#include "sample_data.h"
+
+namespace chip {
+namespace TestData {
+namespace {
+
+const uint8_t payload_0_16[] = {};
+
+const uint8_t payload_0_32[] = { 0x15, 0x30, 0x01, 0x20, 0X7C, 0x86, 0x98, 0x75, 0X5B, 0X8E, 0x98, 0x66, 0XBB, 0X4F, 0XFD, 0XC2,
+                                 0X7B, 0x73, 0X3F, 0X3B, 0X6E, 0XF7, 0XF8, 0X3D, 0x43, 0XFB, 0XE0, 0XCA, 0X6A, 0XD2, 0XB8, 0XC5,
+                                 0X2C, 0X8F, 0x42, 0x36, 0x25, 0x02, 0X2D, 0x93, 0x24, 0x03, 0x00, 0x28, 0x04, 0x18 };
+
+const uint8_t payload_0_33[] = { 0x15, 0x30, 0x01, 0x20, 0X7C, 0x86, 0x98, 0x75, 0X5B, 0X8E, 0x98, 0x66, 0XBB, 0X4F, 0XFD,
+                                 0XC2, 0X7B, 0x73, 0X3F, 0X3B, 0X6E, 0XF7, 0XF8, 0X3D, 0x43, 0XFB, 0XE0, 0XCA, 0X6A, 0XD2,
+                                 0XB8, 0XC5, 0X2C, 0X8F, 0x42, 0x36, 0x30, 0x02, 0x20, 0XA4, 0X4E, 0XB3, 0XE1, 0XA7, 0x51,
+                                 0XA8, 0X8A, 0x32, 0XBA, 0XB5, 0X9E, 0XF1, 0X6E, 0XB9, 0x76, 0X4C, 0x20, 0XE1, 0XA9, 0XDD,
+                                 0XBE, 0XF6, 0XEF, 0XE3, 0XF5, 0x88, 0XC9, 0x43, 0XC5, 0x84, 0x24, 0x25, 0x03, 0XE8, 0X9C,
+                                 0x35, 0x04, 0x25, 0x01, 0XE8, 0x03, 0x30, 0x02, 0x20, 0XE8, 0XFC, 0X1E, 0X6F, 0XD0, 0x02,
+                                 0x34, 0x22, 0XB3, 0XCA, 0X7E, 0XCE, 0XDD, 0x34, 0x44, 0x44, 0x55, 0X1C, 0x81, 0X4D, 0X3D,
+                                 0X0B, 0X0E, 0XB9, 0XC0, 0x96, 0XF0, 0X0E, 0X8A, 0x80, 0x51, 0XB2, 0x18, 0x18 };
+
+const uint8_t payload_0_34[] = { 0x15, 0x30, 0x01, 0x41, 0x04, 0x22, 0XAB, 0XC7, 0XA8, 0x43, 0x52, 0x85, 0x04, 0x56,
+                                 0XBD, 0X4A, 0x51, 0x09, 0x05, 0XFE, 0X6B, 0XB7, 0x82, 0XA0, 0x86, 0X3A, 0x93, 0x82,
+                                 0x55, 0X0E, 0x12, 0x28, 0x02, 0x08, 0x01, 0XB2, 0X2E, 0XEC, 0x41, 0x02, 0XC6, 0X0F,
+                                 0x80, 0x08, 0x28, 0x42, 0XB9, 0x73, 0x97, 0x05, 0XFC, 0XD3, 0X7F, 0x13, 0x46, 0x51,
+                                 0x44, 0X2A, 0x41, 0XE3, 0x72, 0X3D, 0XFF, 0XE0, 0x27, 0x80, 0x77, 0x92, 0X0D, 0x18 };
+
+const uint8_t payload_0_35[] = { 0x15, 0x30, 0x01, 0x41, 0x04, 0XB6, 0XA4, 0X4A, 0x33, 0x47, 0XC6, 0XB7, 0x79, 0x00, 0XA3,
+                                 0x67, 0X4C, 0XA1, 0X9F, 0x40, 0XF2, 0X5F, 0x05, 0X6F, 0X8C, 0XB3, 0x44, 0XEC, 0X1B, 0X4F,
+                                 0XA7, 0x88, 0X8B, 0X9E, 0X6B, 0x57, 0X0B, 0x70, 0x10, 0x43, 0X1C, 0X5D, 0X0B, 0XE4, 0x02,
+                                 0X1F, 0XE7, 0X4A, 0x96, 0XC4, 0x07, 0x21, 0x76, 0X5F, 0XDA, 0x68, 0x02, 0XBE, 0X8D, 0XFD,
+                                 0XF5, 0x62, 0x43, 0x32, 0x27, 0x53, 0x13, 0X4D, 0XC2, 0x30, 0x02, 0x20, 0x40, 0XE7, 0x45,
+                                 0x22, 0x75, 0XE3, 0X8A, 0XEB, 0XAF, 0X0E, 0X0F, 0X6F, 0XAB, 0x33, 0XA1, 0XB0, 0XCB, 0X5A,
+                                 0XEB, 0X5E, 0x82, 0x42, 0x30, 0XDD, 0x40, 0XD0, 0x07, 0X1D, 0XC7, 0XE5, 0X5C, 0x87, 0x18 };
+
+const uint8_t payload_0_36[] = { 0x15, 0x30, 0x01, 0x20, 0x60, 0x08, 0XC7, 0X2E, 0XDE, 0XC9, 0XD2, 0X5D, 0X4A,
+                                 0x36, 0x52, 0X2F, 0X0B, 0XF2, 0x30, 0x58, 0XF9, 0x37, 0X8E, 0XFE, 0x38, 0XCB,
+                                 0XBC, 0XCE, 0X8C, 0x68, 0x53, 0x90, 0x01, 0x69, 0XBC, 0x38, 0x18 };
+
+const uint8_t payload_0_64[] = { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 };
+
+const uint8_t payload_1_2[] = { 0x15, 0x36, 0x00, 0x17, 0x24, 0x03, 0x31, 0x25, 0x04, 0XFC, 0XFF, 0x18, 0x17, 0x24, 0x02, 0x00,
+                                0x24, 0x03, 0x30, 0x24, 0x04, 0x00, 0x18, 0x17, 0x24, 0x02, 0x00, 0x24, 0x03, 0x30, 0x24, 0x04,
+                                0x01, 0x18, 0x17, 0x24, 0x02, 0x00, 0x24, 0x03, 0x30, 0x24, 0x04, 0x02, 0x18, 0x17, 0x24, 0x02,
+                                0x00, 0x24, 0x03, 0x30, 0x24, 0x04, 0x03, 0x18, 0x17, 0x24, 0x02, 0x00, 0x24, 0x03, 0x28, 0x24,
+                                0x04, 0x02, 0x18, 0x17, 0x24, 0x02, 0x00, 0x24, 0x03, 0x28, 0x24, 0x04, 0x04, 0x18, 0x17, 0x24,
+                                0x03, 0x31, 0x24, 0x04, 0x03, 0x18, 0x18, 0x28, 0x03, 0x24, 0XFF, 0x01, 0x18 };
+
+const uint8_t payload_1_5[] = {
+    0x15, 0x36, 0x01, 0x15, 0x35, 0x01, 0x26, 0x00, 0x69, 0XC9, 0XB3, 0x01, 0x37, 0x01, 0x24, 0x02, 0x00, 0x24, 0x03, 0x31, 0x24,
+    0x04, 0x03, 0x18, 0x24, 0x02, 0x00, 0x18, 0x18, 0x15, 0x35, 0x01, 0x26, 0x00, 0x63, 0XC5, 0XA2, 0x27, 0x37, 0x01, 0x24, 0x02,
+    0x00, 0x24, 0x03, 0x28, 0x24, 0x04, 0x04, 0x18, 0x25, 0x02, 0x01, 0x80, 0x18, 0x18, 0x15, 0x35, 0x01, 0x26, 0x00, 0x63, 0XC5,
+    0XA2, 0x27, 0x37, 0x01, 0x24, 0x02, 0x00, 0x24, 0x03, 0x28, 0x24, 0x04, 0x02, 0x18, 0x25, 0x02, 0XF1, 0XFF, 0x18, 0x18, 0x15,
+    0x35, 0x01, 0x26, 0x00, 0XCA, 0x65, 0x48, 0x54, 0x37, 0x01, 0x24, 0x02, 0x00, 0x24, 0x03, 0x30, 0x24, 0x04, 0x03, 0x18, 0x24,
+    0x02, 0x02, 0x18, 0x18, 0x15, 0x35, 0x01, 0x26, 0x00, 0XCA, 0x65, 0x48, 0x54, 0x37, 0x01, 0x24, 0x02, 0x00, 0x24, 0x03, 0x30,
+    0x24, 0x04, 0x02, 0x18, 0x24, 0x02, 0x00, 0x18, 0x18, 0x15, 0x35, 0x01, 0x26, 0x00, 0XCA, 0x65, 0x48, 0x54, 0x37, 0x01, 0x24,
+    0x02, 0x00, 0x24, 0x03, 0x30, 0x24, 0x04, 0x01, 0x18, 0x35, 0x02, 0x24, 0x00, 0X3C, 0x25, 0x01, 0x84, 0x03, 0x18, 0x18, 0x18,
+    0x15, 0x35, 0x01, 0x26, 0x00, 0XCA, 0x65, 0x48, 0x54, 0x37, 0x01, 0x24, 0x02, 0x00, 0x24, 0x03, 0x30, 0x24, 0x04, 0x00, 0x18,
+    0x24, 0x02, 0x00, 0x18, 0x18, 0x15, 0x35, 0x01, 0x26, 0x00, 0x69, 0XC9, 0XB3, 0x01, 0x37, 0x01, 0x24, 0x02, 0x00, 0x24, 0x03,
+    0x31, 0x25, 0x04, 0XFC, 0XFF, 0x18, 0x24, 0x02, 0x04, 0x18, 0x18, 0x18, 0x29, 0x04, 0x24, 0XFF, 0x01, 0x18
+};
+
+const uint8_t payload_1_5_acl[] = { 0x15, 0x36, 0x01, 0x15, 0x35, 0x01, 0x26, 0x00, 0x72, 0x4D, 0xDB, 0xCB, 0x37, 0x01, 0x24,
+                                    0x02, 0x00, 0x24, 0x03, 0x1F, 0x24, 0x04, 0x00, 0x18, 0x36, 0x02, 0x15, 0x24, 0x01, 0x05,
+                                    0x24, 0x02, 0x02, 0x36, 0x03, 0x06, 0x69, 0xB6, 0x01, 0x00, 0x18, 0x34, 0x04, 0x24, 0xFE,
+                                    0x01, 0x18, 0x18, 0x18, 0x18, 0x18, 0x29, 0x04, 0x24, 0xFF, 0x01, 0x18 };
+
+const uint8_t payload_1_8[] = {
+    0x15, 0x28, 0x00, 0x28, 0x01, 0x36, 0x02, 0x15, 0x37, 0x00, 0x24, 0x00, 0x01, 0x24, 0x01,
+    0x06, 0x24, 0x02, 0x02, 0x18, 0x35, 0x01, 0x18, 0x18, 0x18, 0x24, 0xFF, 0x01, 0x18,
+};
+
+const uint8_t payload_1_9[] = {
+    0x15, 0x28, 0x00, 0x36, 0x01, 0x15, 0x35, 0x01, 0x37, 0x00, 0x24, 0x00, 0x01, 0x24, 0x01, 0x06, 0x24,
+    0x02, 0x02, 0x18, 0x35, 0x01, 0x24, 0x00, 0x00, 0x18, 0x18, 0x18, 0x18, 0x24, 0xFF, 0x01, 0x18,
+};
+
+// Response for window_covering::config_status
+// Response contains a bitmap value;
+const uint8_t payload_1_5_window_covering[] = { 0x15, 0x36, 0x01, 0x15, 0x35, 0x01, 0x26, 0x00, 0xEA, 0x99, 0x7C, 0x7A, 0x37,
+                                                0x01, 0x24, 0x02, 0x01, 0x25, 0x03, 0x02, 0x01, 0x24, 0x04, 0x07, 0x18, 0x24,
+                                                0x02, 0x1B, 0x18, 0x18, 0x18, 0x29, 0x04, 0x24, 0xFF, 0x01, 0x18 };
+
+// Change channel invoke. This has a command input (channel name)
+const uint8_t payload_1_8_change_channel[] = { 0x15, 0x28, 0x00, 0x28, 0x01, 0x36, 0x02, 0x15, 0x37, 0x00, 0x24, 0x00,
+                                               0x01, 0x25, 0x01, 0x04, 0x05, 0x24, 0x02, 0x00, 0x18, 0x35, 0x01, 0x2C,
+                                               0x00, 0x0C, 0x63, 0x68, 0x61, 0x6E, 0x6E, 0x65, 0x6C, 0x20, 0x6E, 0x61,
+                                               0x6D, 0x65, 0x18, 0x18, 0x18, 0x24, 0xFF, 0x01, 0x18 };
+
+} // namespace
+
+const SamplePayload secure_channel_mrp_ack              = { chip::Protocols::Id(VendorId::Common, 0), 16,
+                                               ByteSpan(payload_0_16, sizeof(payload_0_16)) };
+const SamplePayload secure_channel_pkbdf_param_request  = { chip::Protocols::Id(VendorId::Common, 0), 32, ByteSpan(payload_0_32) };
+const SamplePayload secure_channel_pkbdf_param_response = { chip::Protocols::Id(VendorId::Common, 0), 33, ByteSpan(payload_0_33) };
+const SamplePayload secure_channel_pase_pake1           = { chip::Protocols::Id(VendorId::Common, 0), 34, ByteSpan(payload_0_34) };
+const SamplePayload secure_channel_pase_pake2           = { chip::Protocols::Id(VendorId::Common, 0), 35, ByteSpan(payload_0_35) };
+const SamplePayload secure_channel_pase_pake3           = { chip::Protocols::Id(VendorId::Common, 0), 36, ByteSpan(payload_0_36) };
+const SamplePayload secure_channel_status_report        = { chip::Protocols::Id(VendorId::Common, 0), 64, ByteSpan(payload_0_64) };
+
+const SamplePayload im_protocol_read_request    = { chip::Protocols::Id(VendorId::Common, 1), 2, ByteSpan(payload_1_2) };
+const SamplePayload im_protocol_report_data     = { chip::Protocols::Id(VendorId::Common, 1), 5, ByteSpan(payload_1_5) };
+const SamplePayload im_protocol_invoke_request  = { chip::Protocols::Id(VendorId::Common, 1), 8, ByteSpan(payload_1_8) };
+const SamplePayload im_protocol_invoke_response = { chip::Protocols::Id(VendorId::Common, 1), 9, ByteSpan(payload_1_9) };
+
+const SamplePayload im_protocol_invoke_request_change_channel = { chip::Protocols::Id(VendorId::Common, 1), 8,
+                                                                  ByteSpan(payload_1_8_change_channel) };
+
+const SamplePayload im_protocol_report_data_acl = { chip::Protocols::Id(VendorId::Common, 1), 5, ByteSpan(payload_1_5_acl) };
+const SamplePayload im_protocol_report_data_window_covering = { chip::Protocols::Id(VendorId::Common, 1), 5,
+                                                                ByteSpan(payload_1_5_window_covering) };
+
+} // namespace TestData
+} // namespace chip
diff --git a/src/lib/format/tests/sample_data.h b/src/lib/format/tests/sample_data.h
new file mode 100644
index 0000000..d8bf586
--- /dev/null
+++ b/src/lib/format/tests/sample_data.h
@@ -0,0 +1,50 @@
+/*
+ *
+ *    Copyright (c) 2023 Project CHIP Authors
+ *    All rights reserved.
+ *
+ *    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.
+ */
+#include <lib/support/Span.h>
+#include <protocols/Protocols.h>
+
+namespace chip {
+namespace TestData {
+
+struct SamplePayload
+{
+    Protocols::Id protocolId;
+    uint8_t messageType;
+    ByteSpan payload;
+};
+
+extern const SamplePayload secure_channel_mrp_ack;
+extern const SamplePayload secure_channel_pkbdf_param_request;
+extern const SamplePayload secure_channel_pkbdf_param_response;
+extern const SamplePayload secure_channel_pase_pake1;
+extern const SamplePayload secure_channel_pase_pake2;
+extern const SamplePayload secure_channel_pase_pake3;
+extern const SamplePayload secure_channel_status_report;
+
+extern const SamplePayload im_protocol_read_request;
+extern const SamplePayload im_protocol_report_data;
+extern const SamplePayload im_protocol_invoke_request;
+extern const SamplePayload im_protocol_invoke_response;
+
+// different data reports for content tests
+extern const SamplePayload im_protocol_report_data_acl;
+extern const SamplePayload im_protocol_report_data_window_covering;
+extern const SamplePayload im_protocol_invoke_request_change_channel;
+
+} // namespace TestData
+} // namespace chip
diff --git a/src/lib/format/tlv_meta.h b/src/lib/format/tlv_meta.h
new file mode 100644
index 0000000..483f211
--- /dev/null
+++ b/src/lib/format/tlv_meta.h
@@ -0,0 +1,84 @@
+/*
+ *
+ *    Copyright (c) 2023 Project CHIP Authors
+ *    All rights reserved.
+ *
+ *    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.
+ */
+#pragma once
+
+#include <lib/core/TLVTags.h>
+
+namespace chip {
+namespace TLVMeta {
+
+static constexpr uint32_t kAttributeProfile = 1;
+static constexpr uint32_t kCommandProfile   = 2;
+static constexpr uint32_t kEventProfile     = 3;
+
+constexpr TLV::Tag ClusterTag(uint32_t cluster_id)
+{
+    return TLV::CommonTag(cluster_id);
+}
+
+constexpr TLV::Tag AttributeTag(uint32_t attribute_id)
+{
+    return TLV::ProfileTag(kAttributeProfile, attribute_id);
+}
+
+constexpr TLV::Tag CommandTag(uint32_t command_id)
+{
+    return TLV::ProfileTag(kCommandProfile, command_id);
+}
+
+constexpr TLV::Tag EventTag(uint32_t event_id)
+{
+    return TLV::ProfileTag(kEventProfile, event_id);
+}
+
+constexpr TLV::Tag ConstantValueTag(uint64_t value)
+{
+    // Re-use common tag for a constant value
+    // Will make "RawValue be equal to value"
+    return TLV::ProfileTag(static_cast<uint32_t>(value >> 32), static_cast<uint32_t>(value & 0xFFFFFFFF));
+}
+
+enum class ItemType : uint8_t
+{
+    kDefault,
+    kList,
+    kEnum,
+    kBitmap,
+
+    // Special protocol types
+    kProtocolClusterId,
+    kProtocolAttributeId,
+    kProtocolCommandId,
+    kProtocolEventId,
+
+    kProtocolPayloadAttribute,
+    kProtocolPayloadCommand,
+    kProtocolPayloadEvent,
+
+    kProtocolBinaryData,
+};
+
+struct ItemInfo
+{
+    TLV::Tag tag;
+    const char * name;
+    ItemType type;
+};
+
+} // namespace TLVMeta
+} // namespace chip
diff --git a/src/lib/support/BufferWriter.h b/src/lib/support/BufferWriter.h
index 72f50b3..351860e 100644
--- a/src/lib/support/BufferWriter.h
+++ b/src/lib/support/BufferWriter.h
@@ -87,6 +87,28 @@
     uint8_t * Buffer() { return mBuf; }
     const uint8_t * Buffer() const { return mBuf; }
 
+    BufferWriter & Format(const char * format, ...) ENFORCE_FORMAT(2, 3)
+    {
+        va_list args;
+        va_start(args, format);
+        VFormat(format, args);
+        va_end(args);
+        return *this;
+    }
+
+    void Reset() { mNeeded = 0; }
+
+    /// Since this uses vsnprintf internally, on overflow
+    /// this will write one less byte that strictly can be
+    /// written (since null terminator will be in the binary data)
+    BufferWriter & VFormat(const char * format, va_list args) ENFORCE_FORMAT(2, 0);
+
+    /// Assume a specific size for the buffer instead of mSize
+    ///
+    /// This is to allow avoiding off-by-one overflow truncation
+    /// when we know the underlying buffer size is larger.
+    BufferWriter & VFormatWithSize(size_t size, const char * format, va_list args) ENFORCE_FORMAT(3, 0);
+
 protected:
     uint8_t * mBuf;
     size_t mSize;
diff --git a/src/lib/support/StringBuilder.h b/src/lib/support/StringBuilder.h
index 9de1538..16bd88d 100644
--- a/src/lib/support/StringBuilder.h
+++ b/src/lib/support/StringBuilder.h
@@ -63,6 +63,13 @@
     /// not fit, this replaces the last 3 characters with "."
     StringBuilderBase & AddMarkerIfOverflow();
 
+    StringBuilderBase & Reset()
+    {
+        mWriter.Reset();
+        NullTerminate();
+        return *this;
+    }
+
     /// access the underlying value
     const char * c_str() const { return reinterpret_cast<const char *>(mWriter.Buffer()); }
 
diff --git a/src/tracing/json/BUILD.gn b/src/tracing/json/BUILD.gn
index c303db0..9f5c8f8 100644
--- a/src/tracing/json/BUILD.gn
+++ b/src/tracing/json/BUILD.gn
@@ -15,6 +15,26 @@
 import("//build_overrides/build.gni")
 import("//build_overrides/chip.gni")
 
+import("${chip_root}/build/chip/buildconfig_header.gni")
+
+declare_args() {
+  # Include the hex content of the payload in the json output
+  matter_log_json_payload_hex = false
+
+  # Include the decoded payload in the json outut
+  matter_log_json_payload_decode_full = false
+}
+
+buildconfig_header("log-json-buildconfig") {
+  header = "log_json_build_config.h"
+  header_dir = "log_json"
+
+  defines = [
+    "MATTER_LOG_JSON_DECODE_HEX=${matter_log_json_payload_hex}",
+    "MATTER_LOG_JSON_DECODE_FULL=${matter_log_json_payload_decode_full}",
+  ]
+}
+
 # As this uses std::string, this library is NOT for use
 # for embedded devices.
 static_library("json") {
@@ -24,9 +44,18 @@
   ]
 
   public_deps = [
+    ":log-json-buildconfig",
     "${chip_root}/src/lib/address_resolve",
     "${chip_root}/src/tracing",
     "${chip_root}/src/transport",
     "${chip_root}/third_party/jsoncpp",
   ]
+
+  if (matter_log_json_payload_decode_full) {
+    public_deps += [
+      "${chip_root}/src/controller/data_model:cluster-tlv-metadata",
+      "${chip_root}/src/lib/format:protocol-decoder",
+      "${chip_root}/src/lib/format:protocol-tlv-metadata",
+    ]
+  }
 }
diff --git a/src/tracing/json/json_tracing.cpp b/src/tracing/json/json_tracing.cpp
index d3c3e1c..b0e3959 100644
--- a/src/tracing/json/json_tracing.cpp
+++ b/src/tracing/json/json_tracing.cpp
@@ -23,6 +23,8 @@
 #include <lib/support/StringBuilder.h>
 #include <transport/TracingStructs.h>
 
+#include <log_json/log_json_build_config.h>
+
 #include <json/json.h>
 
 #include <errno.h>
@@ -30,6 +32,16 @@
 #include <sstream>
 #include <string>
 
+#if MATTER_LOG_JSON_DECODE_HEX
+#include <lib/support/BytesToHex.h> // nogncheck
+#endif
+
+#if MATTER_LOG_JSON_DECODE_FULL
+#include <lib/format/protocol_decoder.h> // nogncheck
+#include <tlv/meta/clusters_meta.h>      // nogncheck
+#include <tlv/meta/protocols_meta.h>     // nogncheck
+#endif
+
 namespace chip {
 namespace Tracing {
 namespace Json {
@@ -38,6 +50,49 @@
 
 using chip::StringBuilder;
 
+#if MATTER_LOG_JSON_DECODE_FULL
+
+using namespace chip::Decoders;
+
+using PayloadDecoderType = chip::Decoders::PayloadDecoder<64, 256>;
+
+// Gets the current value of the decoder until a NEST exit is returned
+::Json::Value GetPayload(PayloadDecoderType & decoder)
+{
+    ::Json::Value value;
+    PayloadEntry entry;
+    StringBuilder<128> formatter;
+
+    while (decoder.Next(entry))
+    {
+        switch (entry.GetType())
+        {
+        case PayloadEntry::IMPayloadType::kNestingEnter:
+            formatter.Reset().Add(entry.GetName()); // name gets destroyed by decoding
+            value[formatter.c_str()] = GetPayload(decoder);
+            break;
+        case PayloadEntry::IMPayloadType::kNestingExit:
+            return value;
+        case PayloadEntry::IMPayloadType::kAttribute:
+            value[formatter.Reset().AddFormat("ATTR(%u/%u)", entry.GetClusterId(), entry.GetAttributeId()).c_str()] =
+                "<NOT_DECODED>";
+            break;
+        case PayloadEntry::IMPayloadType::kCommand:
+            value[formatter.Reset().AddFormat("CMD(%u/%u)", entry.GetClusterId(), entry.GetCommandId()).c_str()] = "<NOT_DECODED>";
+            continue;
+        case PayloadEntry::IMPayloadType::kEvent:
+            value[formatter.Reset().AddFormat("EVNT(%u/%u)", entry.GetClusterId(), entry.GetEventId()).c_str()] = "<NOT_DECODED>";
+            continue;
+        default:
+            value[entry.GetName()] = entry.GetValueText();
+            break;
+        }
+    }
+    return value;
+}
+
+#endif
+
 void DecodePayloadHeader(::Json::Value & value, const PayloadHeader * payloadHeader)
 {
 
@@ -87,12 +142,30 @@
     }
 }
 
-void DecodePayloadData(::Json::Value & value, chip::ByteSpan payload)
+void DecodePayloadData(::Json::Value & value, chip::ByteSpan payload, Protocols::Id protocolId, uint8_t messageType)
 {
-    value["payloadSize"] = static_cast<::Json::Value::UInt>(payload.size());
+    value["size"] = static_cast<::Json::Value::UInt>(payload.size());
 
-    // TODO: a decode would be useful however it likely requires more decode
-    //       metadata
+#if MATTER_LOG_JSON_DECODE_HEX
+    char hex_buffer[1024];
+    if (chip::Encoding::BytesToUppercaseHexString(payload.data(), payload.size(), hex_buffer, sizeof(hex_buffer)) == CHIP_NO_ERROR)
+    {
+        value["hex"] = hex_buffer;
+    }
+#endif // MATTER_LOG_JSON_DECODE_HEX
+
+#if MATTER_LOG_JSON_DECODE_FULL
+
+    PayloadDecoderType decoder(PayloadDecoderInitParams()
+                                   .SetProtocolDecodeTree(chip::TLVMeta::protocols_meta)
+                                   .SetClusterDecodeTree(chip::TLVMeta::clusters_meta)
+                                   .SetProtocol(protocolId)
+                                   .SetMessageType(messageType));
+
+    decoder.StartDecoding(payload);
+
+    value["decoded"] = GetPayload(decoder);
+#endif // MATTER_LOG_JSON_DECODE_FULL
 }
 
 } // namespace
@@ -133,6 +206,7 @@
 void JsonBackend::LogMessageSend(MessageSendInfo & info)
 {
     ::Json::Value value;
+
     value["event"] = "MessageSend";
 
     switch (info.messageType)
@@ -150,7 +224,7 @@
 
     DecodePayloadHeader(value["payloadHeader"], info.payloadHeader);
     DecodePacketHeader(value["packetHeader"], info.packetHeader);
-    DecodePayloadData(value["payload"], info.payload);
+    DecodePayloadData(value["payload"], info.payload, info.payloadHeader->GetProtocolID(), info.payloadHeader->GetMessageType());
 
     OutputValue(value);
 }
@@ -176,7 +250,7 @@
 
     DecodePayloadHeader(value["payloadHeader"], info.payloadHeader);
     DecodePacketHeader(value["packetHeader"], info.packetHeader);
-    DecodePayloadData(value["payload"], info.payload);
+    DecodePayloadData(value["payload"], info.payload, info.payloadHeader->GetProtocolID(), info.payloadHeader->GetMessageType());
 
     OutputValue(value);
 }