1025  Matter Protocol Simulation Lab

1025.1 Lab: Simulate Matter Protocol Communication

25 min | Intermediate | P08.C45.U04

NoteLearning Objectives

By completing this lab, you will be able to:

  • Understand Matter device data model: Device types, endpoints, clusters, attributes, and commands
  • Implement cluster-based communication: On/Off, Level Control, and descriptor clusters
  • Simulate device commissioning: PASE (Passcode-Authenticated Session Establishment) process
  • Create device bindings: Establish controller-to-device and device-to-device relationships
  • Experience multi-admin fabric: Same device controlled by multiple simulated controllers
  • Observe device discovery: mDNS-style service advertisement simulation

1025.2 Prerequisites

Before diving into this lab, you should be familiar with:

TipWhy Simulate Matter?

Real Matter development requires the Matter SDK (connectedhomeip), Thread Border Router hardware, and significant setup time. This simulation lets you:

  1. Learn protocol concepts without hardware investment or SDK installation
  2. Visualize cluster interactions that would require packet capture tools with real Matter
  3. Experiment with multi-admin scenarios instantly
  4. Test binding and groupcast without physical devices

The simulation accurately models Matter’s data model, interaction patterns, and state machines - the same concepts you will apply with real Matter implementations.

1025.3 Matter Concepts Demonstrated

This hands-on lab uses the Wokwi ESP32 simulator to demonstrate Matter protocol concepts. Since Wokwi does not have native Matter SDK support, we simulate the Matter application layer using ESP-NOW for the physical transport, while implementing authentic Matter data models, cluster structures, device commissioning, binding, and multi-admin fabric concepts.

Concept Real Matter This Simulation
Transport Layer Thread/Wi-Fi/Ethernet ESP-NOW 2.4 GHz
Data Model Device Types, Endpoints, Clusters Identical structure
Commissioning PASE over BLE, CASE over IP Simulated handshake
Fabric Multi-admin with NOCs Simulated fabric IDs
Binding Node binding table Endpoint-to-endpoint links
Clusters Standard Matter clusters On/Off, Level Control, Descriptor
Interaction Read, Write, Subscribe, Invoke Command/Response pattern

1025.4 Lab Architecture

The simulation creates a Matter network with four ESP32 devices representing a typical smart home setup:

%%{init: {'theme': 'base', 'themeVariables': { 'primaryColor': '#2C3E50', 'primaryTextColor': '#fff', 'primaryBorderColor': '#16A085', 'lineColor': '#E67E22', 'secondaryColor': '#7F8C8D', 'tertiaryColor': '#ECF0F1', 'fontSize': '14px'}}}%%
graph TB
    subgraph Controllers["Matter Controllers (Multi-Admin)"]
        C1["ESP32 #1<br/>Controller A<br/>'Google Home'<br/>Fabric ID: 0x0001"]
        C2["ESP32 #2<br/>Controller B<br/>'Apple Home'<br/>Fabric ID: 0x0002"]
    end

    subgraph Devices["Matter Devices"]
        D1["ESP32 #3<br/>Dimmable Light<br/>Node ID: 0x1234<br/>On/Off + Level"]
        D2["ESP32 #4<br/>Smart Switch<br/>Node ID: 0x5678<br/>On/Off Client"]
    end

    subgraph Interactions["Matter Interactions"]
        I1["Commissioning"]
        I2["Binding"]
        I3["Command/Invoke"]
    end

    C1 -->|"Fabric 0x0001"| D1
    C1 -->|"Fabric 0x0001"| D2
    C2 -->|"Fabric 0x0002"| D1
    D2 -->|"Binding"| D1

    C1 --- I1
    D2 --- I2
    C2 --- I3

    style C1 fill:#E67E22,stroke:#2C3E50,stroke-width:2px,color:#fff
    style C2 fill:#E67E22,stroke:#2C3E50,stroke-width:2px,color:#fff
    style D1 fill:#16A085,stroke:#2C3E50,stroke-width:2px,color:#fff
    style D2 fill:#16A085,stroke:#2C3E50,stroke-width:2px,color:#fff
    style I1 fill:#ECF0F1,stroke:#7F8C8D,stroke-width:1px,color:#2C3E50
    style I2 fill:#ECF0F1,stroke:#7F8C8D,stroke-width:1px,color:#2C3E50
    style I3 fill:#ECF0F1,stroke:#7F8C8D,stroke-width:1px,color:#2C3E50

Figure 1025.1: Matter simulation architecture showing two controllers (multi-admin), a dimmable light, and a smart switch with binding

1025.5 Embedded Wokwi Simulator

TipHow to Use This Simulator
  1. Click the green Play button to start the simulation
  2. Watch the Serial Monitor for detailed Matter protocol logs
  3. Copy the code below into the simulator to run the full demonstration
  4. Press BOOT button on Controller A to commission devices
  5. Press BOOT button on Controller B to join existing fabric (multi-admin)
  6. Press BOOT button on Smart Switch to toggle the bound light
NoteSimulator Setup Instructions

Since Wokwi does not persist embedded projects, copy the complete Matter simulation code below into the simulator. For a multi-node simulation, open multiple browser tabs with separate Wokwi instances.

Quick Start: Copy the code, set NODE_TYPE to match the role (0 = Controller A, 1 = Controller B, 2 = Dimmable Light, 3 = Smart Switch), and run the simulation.

1025.6 Matter Device Architecture

Before running the simulation, understand how Matter organizes device capabilities:

%%{init: {'theme': 'base', 'themeVariables': { 'primaryColor': '#2C3E50', 'primaryTextColor': '#fff', 'primaryBorderColor': '#16A085', 'lineColor': '#16A085', 'secondaryColor': '#E67E22', 'tertiaryColor': '#7F8C8D', 'fontSize': '11px'}}}%%
graph TB
    subgraph Device["Matter Smart Light (Device Type: Dimmable Color Light)"]
        subgraph Endpoint["Endpoint 1"]
            direction TB
            subgraph App["Application Clusters"]
                OO["On/Off Cluster<br/>OnOff attribute<br/>On() command<br/>Off() command<br/>Toggle() command"]
                LC["Level Control Cluster<br/>CurrentLevel attribute<br/>MoveToLevel() cmd<br/>Step() command"]
                CC["Color Control Cluster<br/>Hue, Saturation<br/>Color Temperature<br/>MoveToColor() cmd"]
            end
            subgraph Common["Common Clusters"]
                ID["Identify Cluster<br/>Blink for identification"]
                GR["Groups Cluster<br/>Add to room groups"]
                SC["Scenes Cluster<br/>Save/recall presets"]
            end
        end
    end

    subgraph Controller["Matter Controller (Google Home)"]
        CMD["Send: MoveToLevel(brightness=75%)"]
    end

    Controller -->|"Matter Interaction"| LC

    style Device fill:#2C3E50,stroke:#16A085,color:#fff
    style Endpoint fill:#16A085,stroke:#2C3E50,color:#fff
    style App fill:#E67E22,stroke:#2C3E50,color:#fff
    style Common fill:#7F8C8D,stroke:#2C3E50,color:#fff
    style OO fill:#fff,stroke:#16A085,color:#2C3E50
    style LC fill:#fff,stroke:#16A085,color:#2C3E50
    style CC fill:#fff,stroke:#16A085,color:#2C3E50
    style ID fill:#fff,stroke:#7F8C8D,color:#2C3E50
    style GR fill:#fff,stroke:#7F8C8D,color:#2C3E50
    style SC fill:#fff,stroke:#7F8C8D,color:#2C3E50
    style Controller fill:#7F8C8D,stroke:#2C3E50,color:#fff

Figure 1025.2: Matter device architecture showing how a Smart Light device is composed of clusters (On/Off, Level Control, Color Control) that define its capabilities. Controllers interact with devices by reading/writing attributes and invoking commands on specific clusters.

Key Matter Concepts: - Device Type: Defines what the device is (light, lock, thermostat) - Endpoint: A logical device within a physical device (e.g., dual outlet has 2 endpoints) - Cluster: A collection of related attributes and commands (On/Off, Level Control) - Attribute: A data value that can be read/written (CurrentLevel, OnOff state) - Command: An action that can be invoked (On, Off, MoveToLevel)

1025.6.1 Matter Data Model Structure

%%{init: {'theme': 'base', 'themeVariables': { 'primaryColor': '#2C3E50', 'primaryTextColor': '#fff', 'primaryBorderColor': '#16A085', 'lineColor': '#E67E22', 'secondaryColor': '#7F8C8D', 'fontSize': '12px'}}}%%
graph TB
    subgraph Device["Matter Device"]
        subgraph EP0["Endpoint 0 (Root)"]
            D0["Descriptor Cluster"]
            B0["Basic Information"]
        end
        subgraph EP1["Endpoint 1 (Application)"]
            OO["On/Off Cluster<br/>- OnOff attribute<br/>- On(), Off(), Toggle()"]
            LC["Level Control Cluster<br/>- CurrentLevel attr<br/>- MoveToLevel()"]
            D1["Descriptor Cluster"]
        end
    end

    style EP0 fill:#7F8C8D,stroke:#2C3E50,color:#fff
    style EP1 fill:#16A085,stroke:#2C3E50,color:#fff
    style OO fill:#fff,stroke:#16A085,color:#2C3E50
    style LC fill:#fff,stroke:#16A085,color:#2C3E50

Key concepts: - Device: Physical hardware (ESP32 in our simulation) - Endpoint: Logical device within physical device (Endpoint 0 = root, Endpoint 1+ = application) - Cluster: Collection of related attributes and commands (On/Off, Level Control) - Attribute: Data value (OnOff state, CurrentLevel) - Command: Action that can be invoked (On, Off, Toggle, MoveToLevel)

1025.7 Complete Matter Simulation Code

This comprehensive code implements Matter protocol concepts including device data model, cluster interactions, commissioning, binding, and multi-admin fabric support:

// ============================================================
// Matter Protocol Simulator for ESP32
// Demonstrates: Data Model, Clusters, Commissioning, Binding, Multi-Admin
// Uses ESP-NOW as transport layer to simulate Thread/Wi-Fi
// ============================================================

#include <WiFi.h>
#include <esp_now.h>
#include <esp_wifi.h>

// ============ NODE CONFIGURATION ============
// Change this for each ESP32 in the simulation:
// 0 = Controller A (Google Home simulation)
// 1 = Controller B (Apple Home simulation)
// 2 = Dimmable Light (Matter Light Device Type)
// 3 = Smart Switch (Matter On/Off Light Switch)
#define NODE_TYPE 2

// ============ MATTER CONSTANTS ============
// Cluster IDs (from Matter Specification)
#define CLUSTER_ON_OFF          0x0006
#define CLUSTER_LEVEL_CONTROL   0x0008
#define CLUSTER_DESCRIPTOR      0x001D
#define CLUSTER_BINDING         0x001E
#define CLUSTER_BASIC_INFO      0x0028

// Attribute IDs
#define ATTR_ON_OFF             0x0000  // On/Off cluster
#define ATTR_CURRENT_LEVEL      0x0000  // Level Control cluster
#define ATTR_DEVICE_TYPE_LIST   0x0000  // Descriptor cluster
#define ATTR_SERVER_LIST        0x0001  // Descriptor cluster
#define ATTR_CLIENT_LIST        0x0002  // Descriptor cluster

// Command IDs
#define CMD_OFF                 0x00
#define CMD_ON                  0x01
#define CMD_TOGGLE              0x02
#define CMD_MOVE_TO_LEVEL       0x00
#define CMD_MOVE                0x01
#define CMD_STEP                0x02

// Device Types (from Matter Specification)
#define DEVICE_TYPE_DIMMABLE_LIGHT  0x0101
#define DEVICE_TYPE_ON_OFF_SWITCH   0x0103
#define DEVICE_TYPE_ROOT_NODE       0x0016

// Interaction Types
#define INTERACTION_READ        0x01
#define INTERACTION_WRITE       0x02
#define INTERACTION_SUBSCRIBE   0x03
#define INTERACTION_INVOKE      0x04
#define INTERACTION_REPORT      0x05

// Commissioning States
#define COMM_STATE_IDLE         0x00
#define COMM_STATE_ADVERTISING  0x01
#define COMM_STATE_PASE         0x02
#define COMM_STATE_CASE         0x03
#define COMM_STATE_OPERATIONAL  0x04

// ============ MATTER DATA STRUCTURES ============

// Endpoint Structure
typedef struct {
  uint16_t endpointId;
  uint16_t deviceType;
  uint16_t serverClusters[10];
  uint8_t serverClusterCount;
  uint16_t clientClusters[10];
  uint8_t clientClusterCount;
} MatterEndpoint_t;

// Cluster Attribute
typedef struct {
  uint16_t attributeId;
  uint8_t dataType;
  uint32_t value;
  bool reportable;
  uint32_t lastReportTime;
} ClusterAttribute_t;

// Binding Entry
typedef struct {
  uint64_t targetNodeId;
  uint16_t targetEndpoint;
  uint16_t clusterId;
  bool valid;
} BindingEntry_t;

// Fabric Entry (Multi-Admin)
typedef struct {
  uint16_t fabricId;
  uint64_t rootPublicKey;  // Simulated NOC
  uint8_t fabricLabel[32];
  bool valid;
} FabricEntry_t;

// Matter Frame (Interaction Model)
typedef struct __attribute__((packed)) {
  uint8_t interactionType;
  uint64_t sourceNodeId;
  uint64_t destNodeId;
  uint16_t sourceEndpoint;
  uint16_t destEndpoint;
  uint16_t clusterId;
  uint16_t attributeOrCommandId;
  uint32_t value;
  uint16_t fabricId;
  uint8_t transactionId;
} MatterFrame_t;

// Commissioning Frame
typedef struct __attribute__((packed)) {
  uint8_t messageType;  // 0=Announce, 1=PASE_Req, 2=PASE_Resp, 3=Commission, 4=Ack
  uint64_t nodeId;
  uint16_t vendorId;
  uint16_t productId;
  uint32_t setupPasscode;  // For PASE (in real Matter, this goes through SPAKE2+)
  uint16_t fabricId;
  uint8_t deviceName[32];
} CommissioningFrame_t;

// ============ DEVICE STATE ============

// Light Device State
struct LightState {
  bool onOff;
  uint8_t currentLevel;  // 0-254
  uint8_t targetLevel;
  uint16_t transitionTime;
} lightState;

// Node State
struct NodeState {
  uint64_t nodeId;
  uint16_t vendorId;
  uint16_t productId;
  uint8_t deviceName[32];
  uint8_t commissioningState;
  uint32_t setupPasscode;

  // Fabrics (Multi-Admin support)
  FabricEntry_t fabrics[5];
  uint8_t fabricCount;

  // Endpoints
  MatterEndpoint_t endpoints[5];
  uint8_t endpointCount;

  // Bindings
  BindingEntry_t bindings[10];
  uint8_t bindingCount;

  // Subscription tracking
  uint64_t subscribers[10];
  uint8_t subscriberCount;
} node;

// Controller State
struct ControllerState {
  uint16_t fabricId;
  uint64_t commissionedNodes[10];
  uint8_t commissionedCount;
  uint8_t pendingTransactionId;
} controller;

// ============ ESP-NOW TRANSPORT ============
uint8_t broadcastMAC[] = {0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF};

// ============ HARDWARE ============
#define LED_BUILTIN 2
#define BUTTON_PIN 0  // BOOT button

// ============ HELPER FUNCTIONS ============

void printHex64(uint64_t val) {
  Serial.printf("%08X%08X", (uint32_t)(val >> 32), (uint32_t)val);
}

const char* getInteractionName(uint8_t type) {
  switch(type) {
    case INTERACTION_READ: return "Read";
    case INTERACTION_WRITE: return "Write";
    case INTERACTION_SUBSCRIBE: return "Subscribe";
    case INTERACTION_INVOKE: return "Invoke";
    case INTERACTION_REPORT: return "Report";
    default: return "Unknown";
  }
}

const char* getClusterName(uint16_t clusterId) {
  switch(clusterId) {
    case CLUSTER_ON_OFF: return "On/Off";
    case CLUSTER_LEVEL_CONTROL: return "Level Control";
    case CLUSTER_DESCRIPTOR: return "Descriptor";
    case CLUSTER_BINDING: return "Binding";
    case CLUSTER_BASIC_INFO: return "Basic Information";
    default: return "Unknown";
  }
}

const char* getCommandName(uint16_t clusterId, uint16_t cmdId) {
  if (clusterId == CLUSTER_ON_OFF) {
    switch(cmdId) {
      case CMD_OFF: return "Off";
      case CMD_ON: return "On";
      case CMD_TOGGLE: return "Toggle";
    }
  } else if (clusterId == CLUSTER_LEVEL_CONTROL) {
    switch(cmdId) {
      case CMD_MOVE_TO_LEVEL: return "MoveToLevel";
      case CMD_MOVE: return "Move";
      case CMD_STEP: return "Step";
    }
  }
  return "Unknown";
}

// ============ MATTER DATA MODEL SETUP ============

void setupDimmableLightEndpoints() {
  // Endpoint 0: Root Node (required)
  node.endpoints[0].endpointId = 0;
  node.endpoints[0].deviceType = DEVICE_TYPE_ROOT_NODE;
  node.endpoints[0].serverClusters[0] = CLUSTER_DESCRIPTOR;
  node.endpoints[0].serverClusters[1] = CLUSTER_BASIC_INFO;
  node.endpoints[0].serverClusterCount = 2;
  node.endpoints[0].clientClusterCount = 0;

  // Endpoint 1: Dimmable Light
  node.endpoints[1].endpointId = 1;
  node.endpoints[1].deviceType = DEVICE_TYPE_DIMMABLE_LIGHT;
  node.endpoints[1].serverClusters[0] = CLUSTER_ON_OFF;
  node.endpoints[1].serverClusters[1] = CLUSTER_LEVEL_CONTROL;
  node.endpoints[1].serverClusters[2] = CLUSTER_DESCRIPTOR;
  node.endpoints[1].serverClusterCount = 3;
  node.endpoints[1].clientClusterCount = 0;

  node.endpointCount = 2;

  // Initialize light state
  lightState.onOff = false;
  lightState.currentLevel = 254;
  lightState.targetLevel = 254;

  Serial.println("[MATTER] Dimmable Light data model initialized");
  Serial.println("         Endpoint 0: Root Node (Descriptor, Basic Info)");
  Serial.println("         Endpoint 1: Dimmable Light (On/Off, Level Control)");
}

void setupOnOffSwitchEndpoints() {
  // Endpoint 0: Root Node
  node.endpoints[0].endpointId = 0;
  node.endpoints[0].deviceType = DEVICE_TYPE_ROOT_NODE;
  node.endpoints[0].serverClusters[0] = CLUSTER_DESCRIPTOR;
  node.endpoints[0].serverClusters[1] = CLUSTER_BINDING;
  node.endpoints[0].serverClusterCount = 2;
  node.endpoints[0].clientClusterCount = 0;

  // Endpoint 1: On/Off Light Switch
  node.endpoints[1].endpointId = 1;
  node.endpoints[1].deviceType = DEVICE_TYPE_ON_OFF_SWITCH;
  node.endpoints[1].serverClusters[0] = CLUSTER_DESCRIPTOR;
  node.endpoints[1].serverClusterCount = 1;
  node.endpoints[1].clientClusters[0] = CLUSTER_ON_OFF;  // Client cluster!
  node.endpoints[1].clientClusterCount = 1;

  node.endpointCount = 2;

  Serial.println("[MATTER] On/Off Switch data model initialized");
  Serial.println("         Endpoint 0: Root Node (Descriptor, Binding)");
  Serial.println("         Endpoint 1: On/Off Switch (On/Off Client)");
}

void setupControllerEndpoints() {
  // Controller acts as commissioner and client for all clusters
  node.endpoints[0].endpointId = 0;
  node.endpoints[0].deviceType = DEVICE_TYPE_ROOT_NODE;
  node.endpoints[0].serverClusterCount = 0;
  node.endpoints[0].clientClusters[0] = CLUSTER_ON_OFF;
  node.endpoints[0].clientClusters[1] = CLUSTER_LEVEL_CONTROL;
  node.endpoints[0].clientClusters[2] = CLUSTER_BINDING;
  node.endpoints[0].clientClusterCount = 3;

  node.endpointCount = 1;

  Serial.printf("[MATTER] Controller initialized (Fabric ID: 0x%04X)\n", controller.fabricId);
}

// ============ COMMISSIONING ============

void startAdvertising() {
  node.commissioningState = COMM_STATE_ADVERTISING;

  CommissioningFrame_t frame;
  frame.messageType = 0;  // Announce
  frame.nodeId = node.nodeId;
  frame.vendorId = node.vendorId;
  frame.productId = node.productId;
  frame.setupPasscode = node.setupPasscode;
  frame.fabricId = 0;  // Not yet commissioned
  memcpy(frame.deviceName, node.deviceName, 32);

  Serial.println("\n[COMMISSIONING] Starting device advertisement (simulated mDNS)");
  Serial.printf("                Node ID: 0x%llX\n", node.nodeId);
  Serial.printf("                Setup Code: %08d\n", node.setupPasscode);
  Serial.println("                State: ADVERTISING - Waiting for controller...");

  esp_now_send(broadcastMAC, (uint8_t*)&frame, sizeof(frame));
}

void sendPASERequest(uint64_t targetNodeId, uint32_t passcode) {
  CommissioningFrame_t frame;
  frame.messageType = 1;  // PASE Request
  frame.nodeId = node.nodeId;
  frame.setupPasscode = passcode;
  frame.fabricId = controller.fabricId;

  Serial.println("\n[COMMISSIONING] Sending PASE request (simulated SPAKE2+)");
  Serial.printf("                Target: 0x%llX\n", targetNodeId);
  Serial.printf("                Passcode: %08d\n", passcode);

  esp_now_send(broadcastMAC, (uint8_t*)&frame, sizeof(frame));
}

void handleCommissioningFrame(CommissioningFrame_t* frame, const uint8_t* senderMAC) {
  switch(frame->messageType) {
    case 0:  // Announce (device advertising)
      if (NODE_TYPE <= 1) {  // Controllers
        Serial.println("\n[DISCOVERY] Discovered Matter device:");
        Serial.printf("            Node ID: 0x%llX\n", frame->nodeId);
        Serial.printf("            Device: %s\n", frame->deviceName);
        Serial.printf("            Vendor: 0x%04X, Product: 0x%04X\n",
                      frame->vendorId, frame->productId);

        // Auto-commission for simulation
        if (controller.commissionedCount < 10) {
          Serial.println("\n[COMMISSIONING] Initiating PASE with discovered device...");
          sendPASERequest(frame->nodeId, frame->setupPasscode);
        }
      }
      break;

    case 1:  // PASE Request
      if (NODE_TYPE >= 2) {  // Devices
        if (frame->setupPasscode == node.setupPasscode) {
          Serial.println("\n[COMMISSIONING] PASE request received - passcode verified!");
          node.commissioningState = COMM_STATE_PASE;

          // Send PASE response
          CommissioningFrame_t response;
          response.messageType = 2;  // PASE Response
          response.nodeId = node.nodeId;
          response.fabricId = frame->fabricId;

          esp_now_send(broadcastMAC, (uint8_t*)&response, sizeof(response));

          Serial.printf("            Joining Fabric: 0x%04X\n", frame->fabricId);
        } else {
          Serial.println("\n[COMMISSIONING] PASE failed - invalid passcode!");
        }
      }
      break;

    case 2:  // PASE Response
      if (NODE_TYPE <= 1) {  // Controllers
        Serial.println("\n[COMMISSIONING] PASE successful!");
        Serial.println("                Issuing Node Operational Certificate (NOC)...");

        // Send commission complete
        CommissioningFrame_t complete;
        complete.messageType = 3;  // Commission
        complete.nodeId = frame->nodeId;
        complete.fabricId = controller.fabricId;

        esp_now_send(broadcastMAC, (uint8_t*)&complete, sizeof(complete));

        // Track commissioned device
        controller.commissionedNodes[controller.commissionedCount++] = frame->nodeId;
        Serial.printf("                Device 0x%llX commissioned to Fabric 0x%04X\n",
                      frame->nodeId, controller.fabricId);
      }
      break;

    case 3:  // Commission complete
      if (NODE_TYPE >= 2 && frame->nodeId == node.nodeId) {
        Serial.println("\n[COMMISSIONING] Commission complete!");
        Serial.printf("                Now operational on Fabric 0x%04X\n", frame->fabricId);

        // Add fabric to device
        if (node.fabricCount < 5) {
          node.fabrics[node.fabricCount].fabricId = frame->fabricId;
          node.fabrics[node.fabricCount].valid = true;
          sprintf((char*)node.fabrics[node.fabricCount].fabricLabel,
                  "Fabric_%04X", frame->fabricId);
          node.fabricCount++;

          Serial.printf("                Total fabrics: %d (Multi-Admin enabled)\n",
                        node.fabricCount);
        }

        node.commissioningState = COMM_STATE_OPERATIONAL;

        // Send acknowledgment
        CommissioningFrame_t ack;
        ack.messageType = 4;
        ack.nodeId = node.nodeId;
        ack.fabricId = frame->fabricId;
        esp_now_send(broadcastMAC, (uint8_t*)&ack, sizeof(ack));
      }
      break;

    case 4:  // Acknowledgment
      if (NODE_TYPE <= 1) {
        Serial.printf("\n[COMMISSIONING] Device 0x%llX acknowledged on Fabric 0x%04X\n",
                      frame->nodeId, frame->fabricId);
        Serial.println("                Device is now fully operational!");
      }
      break;
  }
}

// ============ CLUSTER OPERATIONS ============

void handleOnOffCommand(uint8_t command, uint16_t fabricId) {
  Serial.printf("\n[CLUSTER] On/Off command received: %s (Fabric: 0x%04X)\n",
                getCommandName(CLUSTER_ON_OFF, command), fabricId);

  bool previousState = lightState.onOff;

  switch(command) {
    case CMD_OFF:
      lightState.onOff = false;
      break;
    case CMD_ON:
      lightState.onOff = true;
      break;
    case CMD_TOGGLE:
      lightState.onOff = !lightState.onOff;
      break;
  }

  // Update physical LED
  digitalWrite(LED_BUILTIN, lightState.onOff ? HIGH : LOW);

  Serial.printf("          Light state: %s -> %s\n",
                previousState ? "ON" : "OFF",
                lightState.onOff ? "ON" : "OFF");

  // Report to all subscribers (all fabrics)
  reportAttributeChange(CLUSTER_ON_OFF, ATTR_ON_OFF, lightState.onOff);
}

void handleLevelCommand(uint8_t command, uint8_t level, uint16_t transitionTime) {
  Serial.printf("\n[CLUSTER] Level Control command: %s\n",
                getCommandName(CLUSTER_LEVEL_CONTROL, command));

  switch(command) {
    case CMD_MOVE_TO_LEVEL:
      lightState.targetLevel = level;
      lightState.transitionTime = transitionTime;
      Serial.printf("          Target level: %d (transition: %dms)\n",
                    level, transitionTime * 100);
      break;
  }

  // Simulate instant transition for demo
  lightState.currentLevel = lightState.targetLevel;

  // Adjust LED brightness (PWM would be used in real implementation)
  if (lightState.onOff) {
    analogWrite(LED_BUILTIN, lightState.currentLevel);
  }

  reportAttributeChange(CLUSTER_LEVEL_CONTROL, ATTR_CURRENT_LEVEL, lightState.currentLevel);
}

void reportAttributeChange(uint16_t clusterId, uint16_t attrId, uint32_t value) {
  MatterFrame_t report;
  report.interactionType = INTERACTION_REPORT;
  report.sourceNodeId = node.nodeId;
  report.sourceEndpoint = 1;
  report.clusterId = clusterId;
  report.attributeOrCommandId = attrId;
  report.value = value;

  Serial.printf("[REPORT] Attribute change: %s.%s = %lu\n",
                getClusterName(clusterId),
                attrId == 0 ? "Primary" : "Secondary",
                value);
  Serial.printf("         Broadcasting to all fabrics (%d)\n", node.fabricCount);

  // Broadcast to all fabrics
  for (int i = 0; i < node.fabricCount; i++) {
    report.fabricId = node.fabrics[i].fabricId;
    esp_now_send(broadcastMAC, (uint8_t*)&report, sizeof(report));
  }
}

// ============ BINDING ============

void addBinding(uint64_t targetNodeId, uint16_t targetEndpoint, uint16_t clusterId) {
  if (node.bindingCount >= 10) {
    Serial.println("[BINDING] Error: Binding table full!");
    return;
  }

  node.bindings[node.bindingCount].targetNodeId = targetNodeId;
  node.bindings[node.bindingCount].targetEndpoint = targetEndpoint;
  node.bindings[node.bindingCount].clusterId = clusterId;
  node.bindings[node.bindingCount].valid = true;
  node.bindingCount++;

  Serial.printf("\n[BINDING] Added binding:\n");
  Serial.printf("          Target Node: 0x%llX\n", targetNodeId);
  Serial.printf("          Target Endpoint: %d\n", targetEndpoint);
  Serial.printf("          Cluster: %s (0x%04X)\n", getClusterName(clusterId), clusterId);
  Serial.printf("          Total bindings: %d\n", node.bindingCount);
}

void sendBoundCommand(uint8_t command) {
  if (node.bindingCount == 0) {
    Serial.println("\n[BINDING] No bindings configured - command not sent");
    return;
  }

  Serial.printf("\n[BINDING] Sending command to %d bound device(s):\n", node.bindingCount);

  for (int i = 0; i < node.bindingCount; i++) {
    if (!node.bindings[i].valid) continue;

    MatterFrame_t frame;
    frame.interactionType = INTERACTION_INVOKE;
    frame.sourceNodeId = node.nodeId;
    frame.destNodeId = node.bindings[i].targetNodeId;
    frame.sourceEndpoint = 1;
    frame.destEndpoint = node.bindings[i].targetEndpoint;
    frame.clusterId = node.bindings[i].clusterId;
    frame.attributeOrCommandId = command;
    frame.value = 0;
    frame.fabricId = node.fabrics[0].fabricId;  // Use first fabric
    frame.transactionId = random(256);

    Serial.printf("          -> Node 0x%llX, Endpoint %d, Cmd: %s\n",
                  node.bindings[i].targetNodeId,
                  node.bindings[i].targetEndpoint,
                  getCommandName(node.bindings[i].clusterId, command));

    esp_now_send(broadcastMAC, (uint8_t*)&frame, sizeof(frame));
  }
}

// ============ MATTER FRAME HANDLING ============

void handleMatterFrame(MatterFrame_t* frame, const uint8_t* senderMAC) {
  // Check if frame is for this node
  if (frame->destNodeId != 0 && frame->destNodeId != node.nodeId) {
    return;  // Not for us
  }

  Serial.printf("\n[INTERACTION] %s from Node 0x%llX (Fabric: 0x%04X)\n",
                getInteractionName(frame->interactionType),
                frame->sourceNodeId, frame->fabricId);
  Serial.printf("              Cluster: %s, Endpoint: %d\n",
                getClusterName(frame->clusterId), frame->destEndpoint);

  switch(frame->interactionType) {
    case INTERACTION_INVOKE:
      // Handle command invocation
      if (frame->clusterId == CLUSTER_ON_OFF) {
        handleOnOffCommand(frame->attributeOrCommandId, frame->fabricId);
      } else if (frame->clusterId == CLUSTER_LEVEL_CONTROL) {
        handleLevelCommand(frame->attributeOrCommandId, frame->value & 0xFF,
                          (frame->value >> 8) & 0xFFFF);
      }
      break;

    case INTERACTION_READ:
      // Handle attribute read
      {
        MatterFrame_t response;
        response.interactionType = INTERACTION_REPORT;
        response.sourceNodeId = node.nodeId;
        response.destNodeId = frame->sourceNodeId;
        response.clusterId = frame->clusterId;
        response.attributeOrCommandId = frame->attributeOrCommandId;
        response.fabricId = frame->fabricId;

        if (frame->clusterId == CLUSTER_ON_OFF && frame->attributeOrCommandId == ATTR_ON_OFF) {
          response.value = lightState.onOff;
        } else if (frame->clusterId == CLUSTER_LEVEL_CONTROL) {
          response.value = lightState.currentLevel;
        }

        esp_now_send(broadcastMAC, (uint8_t*)&response, sizeof(response));
        Serial.printf("              Responded with value: %lu\n", response.value);
      }
      break;

    case INTERACTION_SUBSCRIBE:
      // Add to subscriber list
      if (node.subscriberCount < 10) {
        node.subscribers[node.subscriberCount++] = frame->sourceNodeId;
        Serial.printf("              Added subscriber: 0x%llX (total: %d)\n",
                      frame->sourceNodeId, node.subscriberCount);
      }
      break;

    case INTERACTION_REPORT:
      // Handle incoming report (for controllers)
      Serial.printf("              Received report: %s = %lu\n",
                    getClusterName(frame->clusterId), frame->value);
      break;
  }
}

// ============ ESP-NOW CALLBACKS ============

void OnDataRecv(const uint8_t* mac, const uint8_t* data, int len) {
  if (len == sizeof(CommissioningFrame_t)) {
    handleCommissioningFrame((CommissioningFrame_t*)data, mac);
  } else if (len == sizeof(MatterFrame_t)) {
    handleMatterFrame((MatterFrame_t*)data, mac);
  }
}

void OnDataSent(const uint8_t* mac, esp_now_send_status_t status) {
  // Silent callback - reduce serial noise
}

// ============ SETUP ============

void setup() {
  Serial.begin(115200);
  delay(1000);

  Serial.println("\n============================================");
  Serial.println("       MATTER PROTOCOL SIMULATOR");
  Serial.println("============================================\n");

  // Initialize hardware
  pinMode(LED_BUILTIN, OUTPUT);
  pinMode(BUTTON_PIN, INPUT_PULLUP);

  // Initialize WiFi in station mode for ESP-NOW
  WiFi.mode(WIFI_STA);
  WiFi.disconnect();

  // Initialize ESP-NOW
  if (esp_now_init() != ESP_OK) {
    Serial.println("[ERROR] ESP-NOW initialization failed!");
    return;
  }

  esp_now_register_recv_cb(OnDataRecv);
  esp_now_register_send_cb(OnDataSent);

  // Add broadcast peer
  esp_now_peer_info_t peerInfo = {};
  memcpy(peerInfo.peer_addr, broadcastMAC, 6);
  peerInfo.channel = 0;
  peerInfo.encrypt = false;
  esp_now_add_peer(&peerInfo);

  // Configure based on node type
  switch(NODE_TYPE) {
    case 0:  // Controller A (Google Home)
      node.nodeId = 0x0000000000000001;
      controller.fabricId = 0x0001;
      strcpy((char*)node.deviceName, "Controller_A");
      setupControllerEndpoints();
      Serial.println("[CONFIG] Running as: Controller A (Google Home simulation)");
      Serial.printf("         Fabric ID: 0x%04X\n", controller.fabricId);
      Serial.println("\n[HELP] Press BOOT button to start commissioning...\n");
      break;

    case 1:  // Controller B (Apple Home)
      node.nodeId = 0x0000000000000002;
      controller.fabricId = 0x0002;
      strcpy((char*)node.deviceName, "Controller_B");
      setupControllerEndpoints();
      Serial.println("[CONFIG] Running as: Controller B (Apple Home simulation)");
      Serial.printf("         Fabric ID: 0x%04X\n", controller.fabricId);
      Serial.println("\n[HELP] Press BOOT button to join device as second admin...\n");
      break;

    case 2:  // Dimmable Light
      node.nodeId = 0x0000000000001234;
      node.vendorId = 0xFFF1;
      node.productId = 0x8001;
      node.setupPasscode = 20202021;  // Default Matter test passcode
      strcpy((char*)node.deviceName, "Dimmable_Light");
      setupDimmableLightEndpoints();
      Serial.println("[CONFIG] Running as: Dimmable Light");
      Serial.printf("         Node ID: 0x%llX\n", node.nodeId);
      Serial.printf("         Setup Passcode: %08d\n", node.setupPasscode);
      Serial.println("\n[HELP] Press BOOT button to start advertising...\n");
      break;

    case 3:  // Smart Switch
      node.nodeId = 0x0000000000005678;
      node.vendorId = 0xFFF1;
      node.productId = 0x8002;
      node.setupPasscode = 20202022;
      strcpy((char*)node.deviceName, "Smart_Switch");
      setupOnOffSwitchEndpoints();

      // Pre-configure binding to light for demo
      addBinding(0x0000000000001234, 1, CLUSTER_ON_OFF);

      Serial.println("[CONFIG] Running as: Smart Switch");
      Serial.printf("         Node ID: 0x%llX\n", node.nodeId);
      Serial.println("         Pre-bound to Dimmable Light (0x1234)");
      Serial.println("\n[HELP] Press BOOT button to toggle bound light...\n");
      break;
  }

  Serial.println("============================================\n");
}

// ============ MAIN LOOP ============

bool lastButtonState = HIGH;
unsigned long lastButtonPress = 0;
unsigned long lastStatusPrint = 0;

void loop() {
  // Button handling with debounce
  bool buttonState = digitalRead(BUTTON_PIN);

  if (buttonState == LOW && lastButtonState == HIGH && millis() - lastButtonPress > 200) {
    lastButtonPress = millis();

    switch(NODE_TYPE) {
      case 0:  // Controller A - listen for devices
      case 1:  // Controller B - join existing fabric
        Serial.println("\n[ACTION] Scanning for Matter devices...");
        // Controllers just listen for announcements
        break;

      case 2:  // Dimmable Light
        if (node.commissioningState == COMM_STATE_IDLE) {
          startAdvertising();
        } else {
          // Toggle light locally
          handleOnOffCommand(CMD_TOGGLE, node.fabrics[0].fabricId);
        }
        break;

      case 3:  // Smart Switch
        if (node.commissioningState == COMM_STATE_OPERATIONAL || node.bindingCount > 0) {
          // Send toggle to bound light
          static uint8_t toggleCmd = CMD_ON;
          sendBoundCommand(toggleCmd);
          toggleCmd = (toggleCmd == CMD_ON) ? CMD_OFF : CMD_ON;
        } else {
          startAdvertising();
        }
        break;
    }
  }
  lastButtonState = buttonState;

  // Periodic status for devices
  if (NODE_TYPE == 2 && millis() - lastStatusPrint > 10000) {
    lastStatusPrint = millis();
    Serial.println("\n[STATUS] Light Device Status:");
    Serial.printf("         State: %s, Level: %d%%\n",
                  lightState.onOff ? "ON" : "OFF",
                  (lightState.currentLevel * 100) / 254);
    Serial.printf("         Fabrics: %d, Subscribers: %d\n",
                  node.fabricCount, node.subscriberCount);
    for (int i = 0; i < node.fabricCount; i++) {
      Serial.printf("         - Fabric 0x%04X: %s\n",
                    node.fabrics[i].fabricId,
                    node.fabrics[i].fabricLabel);
    }
  }

  // Re-advertise if not commissioned
  static unsigned long lastAdvert = 0;
  if (NODE_TYPE >= 2 && node.commissioningState == COMM_STATE_ADVERTISING) {
    if (millis() - lastAdvert > 5000) {
      lastAdvert = millis();
      startAdvertising();
    }
  }

  delay(10);
}

1025.8 Step-by-Step Instructions

1025.8.1 Step 1: Understand the Matter Data Model

Before running the simulation, understand how Matter organizes device capabilities. The simulation implements the same data model structure as real Matter devices.

1025.8.2 Step 2: Run the Dimmable Light

  1. Open Wokwi simulator
  2. Copy the simulation code above
  3. Ensure NODE_TYPE is set to 2 (Dimmable Light)
  4. Click Play and observe Serial Monitor output
  5. Press BOOT button to start advertising
  6. You should see commissioning messages in the Serial Monitor

1025.8.3 Step 3: Run Controller A (in a second tab)

  1. Open a new browser tab with Wokwi
  2. Copy the same code
  3. Change NODE_TYPE to 0 (Controller A)
  4. Click Play to start
  5. The controller will discover and commission the light automatically
  6. Watch the PASE handshake and NOC issuance messages

1025.8.4 Step 4: Experience Multi-Admin (third tab)

  1. Open another Wokwi tab
  2. Set NODE_TYPE to 1 (Controller B)
  3. Start the simulation
  4. Controller B will commission the same light to its fabric (0x0002)
  5. The light now belongs to TWO fabrics simultaneously!

1025.8.5 Step 5: Test Device Binding (fourth tab)

  1. Open a fourth Wokwi tab
  2. Set NODE_TYPE to 3 (Smart Switch)
  3. Start the simulation
  4. The switch is pre-bound to the light
  5. Press BOOT on the switch to send Toggle commands to the light
  6. Watch the bound command flow in all Serial Monitors

1025.9 What You’re Observing

NoteMatter Protocol Flow Explained

Commissioning (PASE):

Light: [COMMISSIONING] Starting device advertisement (simulated mDNS)
       Node ID: 0x1234, Setup Code: 20202021

Controller A: [DISCOVERY] Discovered Matter device: Dimmable_Light
              [COMMISSIONING] Initiating PASE with discovered device...

Light: [COMMISSIONING] PASE request received - passcode verified!
       Joining Fabric: 0x0001

Controller A: [COMMISSIONING] PASE successful! Issuing NOC...
              Device 0x1234 commissioned to Fabric 0x0001

Multi-Admin (Second Fabric):

Controller B: [COMMISSIONING] Initiating PASE with device 0x1234
Light: [COMMISSIONING] Commission complete!
       Now operational on Fabric 0x0002
       Total fabrics: 2 (Multi-Admin enabled)

Cluster Command (Binding):

Switch: [BINDING] Sending command to 1 bound device(s):
        -> Node 0x1234, Endpoint 1, Cmd: Toggle

Light: [CLUSTER] On/Off command received: Toggle (Fabric: 0x0001)
       Light state: OFF -> ON
[REPORT] Attribute change: On/Off.Primary = 1
         Broadcasting to all fabrics (2)

1025.10 Challenge Exercises

WarningChallenge 1: Add Level Control

Modify the Smart Switch to also control brightness:

  1. Add a binding to CLUSTER_LEVEL_CONTROL
  2. Implement a sendLevelCommand() function
  3. Use the BOOT button to cycle through brightness levels (25%, 50%, 75%, 100%)

Hint: The handleLevelCommand() function already exists - you just need to send the appropriate frame.

WarningChallenge 2: Implement Groups

Matter supports groupcast commands to control multiple devices simultaneously:

  1. Add a group ID field to the Matter frame
  2. Create a group binding that targets group ID 0x0001
  3. Modify the light to check group membership
  4. Send commands to the group instead of individual nodes

Hint: In real Matter, groups use a separate Group cluster (0x0004) and group multicast addresses.

WarningChallenge 3: Add Scenes Support

Scenes let you save and recall device states:

  1. Add a SceneEntry structure with level and on/off state
  2. Implement storeScene() when button is long-pressed
  3. Implement recallScene() when button is short-pressed
  4. Store scenes per-fabric (each controller has its own scenes)

Hint: Scene cluster ID is 0x0005. Scenes are stored per endpoint and per group.

1025.11 Key Matter Concepts Summary

Concept Description Simulation Behavior
Endpoint Logical device within physical device Endpoint 0 = root, Endpoint 1 = application
Cluster Feature grouping (On/Off, Level) Defines available attributes and commands
Attribute Data value that can be read/subscribed OnOff, CurrentLevel values stored
Command Action that can be invoked On, Off, Toggle, MoveToLevel
Fabric Administrative domain (ecosystem) Separate fabric IDs for each controller
Binding Device-to-device relationship Switch bound to light for direct control
Commissioning Device onboarding process PASE simulation with passcode exchange
Multi-Admin Same device in multiple fabrics Light controlled by both controllers

1025.12 Knowledge Check

After completing this lab, you should be able to answer:

  1. What is the difference between an endpoint and a cluster?
    • An endpoint is a logical device within a physical device (e.g., a dual-outlet has 2 endpoints). A cluster is a collection of related attributes and commands (e.g., On/Off cluster).
  2. Why does Matter require Endpoint 0?
    • Endpoint 0 is the “root node” that contains network-wide information like supported device types, basic information, and access control. It’s mandatory for all Matter devices.
  3. How does multi-admin work in Matter?
    • Each controller issues its own fabric credentials (NOC). A device can store multiple fabric entries, allowing it to be controlled by different ecosystems (Apple, Google, Amazon) simultaneously.
  4. What is binding and why is it useful?
    • Binding creates a direct relationship between devices. A switch bound to a light can control it directly without going through a controller, enabling faster response and local operation.
  5. What happens if two controllers send conflicting commands?
    • In Matter, the last command wins. There’s no conflict resolution - if Apple says “on” and Google says “off” within milliseconds, the final state depends on processing order.

1025.13 Summary

TipKey Takeaways
  1. Matter’s data model is hierarchical: Device -> Endpoints -> Clusters -> Attributes/Commands

  2. Commissioning establishes trust: PASE verifies the setup passcode, then NOCs establish fabric membership

  3. Multi-admin is real and works: A single device can belong to multiple fabrics and respond to multiple controllers

  4. Binding enables device-to-device control: Switches can control lights directly without controller involvement

  5. All interactions follow the same pattern: Read, Write, Subscribe, Invoke, and Report are the five interaction types

  6. Reports go to all fabrics: When state changes, all subscribed controllers receive updates regardless of which fabric triggered the change

1025.14 What’s Next

Now that you’ve experienced Matter protocol concepts hands-on, continue to: