Networking & Communication

Connected devices, protocol choices, and practical system integration

Project Timeline

Assignment Introduction

Networking & Communication

This week explores how devices communicate across different layers, from simple wired protocols to wireless links and full smart-home integration. The focus was on comparing communication methods, choosing suitable ESP platforms, and building reliable connections between input, output, and networked systems.

Along the way, the project moves from direct board-to-board exchange toward larger connected workflows with Zigbee and Home Assistant. And somewhere near the end, there is also a small hidden experiment that takes communication in a very different direction.

Material Bill

Components and resources used for this week’s project

  • Input Devices Week

    Result of Input Devices Week.

  • Output Devices Week

    Result of Output Devices Week.

Software & Tools

Popular Communication Methods

There is no single best communication method for every project. The right choice depends on range, speed, power consumption, wiring complexity, and whether communication happens locally, device-to-device, or through a larger network.

Before looking at the individual communication methods, it helps to understand a few core terms first. They appear throughout the comparison and make the differences between the protocols much easier to understand.

Synchronous vs. asynchronous: Imagine two people clapping to the same beat. Synchronous means both sides share a common timing signal, called a clock, so the receiver always knows exactly when a new bit of data is valid. Asynchronous means there is no shared beat. Instead, both sides agree on a speed beforehand, and start and stop markers around each byte tell the receiver when data begins and ends.

Clock signal: A clock signal is a wire that rapidly switches between high and low voltage at a fixed rhythm, like a metronome ticking. Every tick tells the receiver when a bit is ready to be read. Without a shared clock, both sides must independently count time at the same agreed speed. With a clock wire, higher speeds and more reliable transfers are possible because timing is never guessed.

Duplex: Full-Duplex means both directions work at once, like a phone call. Half-Duplex means only one side can send at a time, like a walkie-talkie. Simplex means communication only goes in one direction, like a TV broadcast where the receiver never talks back.

The methods below are the ones I find most useful to compare early in a project, because they cover the most common situations from short wired buses to direct wireless links and network-based communication.

SPI

up to ~80 MHz

SPI is a synchronous, full-duplex bus. One device acts as the master and controls the clock line. It picks which slave it wants to talk to by pulling that slave’s dedicated Chip Select (CS) pin low. Because data flows in both directions at the same time, one wire from master to slave (MOSI) and one from slave to master (MISO), every transfer sends and receives simultaneously, even if only one direction is relevant to the application.

Typical use cases: Fast displays, SD card modules, high-speed motion sensors, SPI flash memory chips, and DACs.

ProsOne of the fastest communication options available, timing is completely predictable because the master controls the clock, it is supported by virtually every microcontroller, and it is easy to inspect with a logic analyzer.

ConsIt needs three shared wires plus one dedicated CS wire per slave, so the pin count grows with each device. There is no built-in addressing, error detection, or delivery confirmation.

SPI Setup: Call SPI.begin(SCK, MISO, MOSI) in setup(). Set the CS pin as an output and pull it HIGH initially. Wrap each transfer in beginTransaction() and endTransaction() so multiple devices can safely share the bus. Match the frequency and mode in SPISettings to the slave’s datasheet. When unsure, start at 1 MHz with MODE0.

Examples

Wire Connection

In this example, the data is transferred over a direct UART connection between devices. This is the most straightforward wired setup, because both boards communicate through matching TX and RX lines without requiring additional network infrastructure.

Nicos and my Dev Board communicating
Nicos and my Dev Board communicating

Wireless Connection

In this example, data is transferred wirelessly over BLE between the Input Devices project and the Output Devices project. This shows how one project can act as the sender while another receives the values and reacts to them without any direct cable connection.

Fast communication using Bluetooth Low Energy
// ============================================================
//  LED DEVICE - BLE Server / Receiver
//  ESP32-H2 | 205x WS2812B on Pin 10
// ============================================================
#include <Arduino.h>
#include <FastLED.h>
#include <BLEDevice.h>
#include <BLEServer.h>
#include <BLEUtils.h>

#define LED_PIN     10
#define NUM_LEDS    205
#define COLOR_ORDER GRB
#define LED_TYPE    WS2812

// Same UUIDs as the client!
#define SERVICE_UUID        "4fafc201-1fb5-459e-8fcc-c5c9c331914b"
#define CHARACTERISTIC_UUID "beb5483e-36e1-4688-b7f5-ea07361b26a8"

CRGB leds[NUM_LEDS];

// ---------- Data structure (same as client) ----------
struct __attribute__((packed)) LightState {
  uint8_t hue;
  uint8_t brightness;
  uint8_t enabled;
};

// Flags and storage for incoming data
volatile bool       newData    = false;
volatile LightState incoming   = {0, 128, 1};

// ---------- BLE Characteristic Callback ----------
class LightCharCallbacks : public BLECharacteristicCallbacks {
  void onWrite(BLECharacteristic* pChar) override {
    // Called when client writes to this characteristic
    if (pChar->getLength() == sizeof(LightState)) {
      memcpy((void*)&incoming, pChar->getData(), sizeof(LightState));
      newData = true;  // Flag that new data has arrived
    }
  }
};

// ---------- BLE Server Callback ----------
class ServerCB : public BLEServerCallbacks {
  void onConnect(BLEServer*)    override { Serial.println("[BLE] Client connected"); }
  void onDisconnect(BLEServer*) override {
    Serial.println("[BLE] Disconnected - re-advertise");
    BLEDevice::startAdvertising();   // Make server immediately discoverable again
  }
};

// ---------- Apply LEDs ----------
void applyLEDs(const LightState& s) {
  if (!s.enabled) {
    fill_solid(leds, NUM_LEDS, CRGB::Black);  // Turn all LEDs off
  } else {
    fill_solid(leds, NUM_LEDS, CHSV(s.hue, 255, s.brightness)); // Set hue & brightness
  }
  FastLED.show();
}

// ---------- Setup ----------
void setup() {
  Serial.begin(115200);
  Serial.println("=== LED Device (BLE Server) ===");

  // Initialize LED strip
  FastLED.addLeds<LED_TYPE, LED_PIN, COLOR_ORDER>(leds, NUM_LEDS);
  FastLED.clear();
  FastLED.show();

  // Initialize BLE stack
  BLEDevice::init("H2-CeilingLamp");

  // Create BLE server
  BLEServer*  pServer  = BLEDevice::createServer();
  pServer->setCallbacks(new ServerCB());

  // Create BLE service
  BLEService* pService = pServer->createService(SERVICE_UUID);

  // Create BLE characteristic for incoming writes
  BLECharacteristic* pChar = pService->createCharacteristic(
    CHARACTERISTIC_UUID,
    BLECharacteristic::PROPERTY_WRITE
  );
  pChar->setCallbacks(new LightCharCallbacks());

  // Start the service
  pService->start();

  // Start advertising so client can find this server
  BLEAdvertising* pAdv = BLEDevice::getAdvertising();
  pAdv->addServiceUUID(SERVICE_UUID);
  pAdv->setScanResponse(true);
  BLEDevice::startAdvertising();

  Serial.println("[BLE] Server started - waiting for controller...");
}

void loop() {
  if (newData) {
    newData = false;

    // Copy volatile data safely
    LightState copy;
    memcpy(&copy, (const void*)&incoming, sizeof(LightState));

    // Apply LEDs according to received state
    applyLEDs(copy);

    Serial.printf("[LED] Hue=%3d  Brightness=%3d  Enabled=%d\n",
                  copy.hue, copy.brightness, copy.enabled);
  }
  delay(10);
}

BLE Server / Receiver

Initialize BLE Stack: The ESP32 is configured as a BLE server with the name H2-CeilingLamp. This makes it discoverable to BLE clients.

Create BLE Service and Characteristic: A BLE service is created using a predefined UUID. Inside that service, a characteristic with write permission is added so clients can send data to the server.

Set Up Callbacks: LightCharCallbacks reacts whenever a client writes to the characteristic, stores the incoming LightState, and raises the newData flag. ServerCB monitors connection events, prints a message when a client connects, and immediately restarts advertising when the client disconnects.

Start Advertising: The server starts advertising its service UUID so BLE clients can discover it and initiate the connection.

Handle Incoming Data: In the main loop, the server checks whether newData is set. If new data has arrived, it copies the received state into a local variable and applies the LED changes for hue, brightness, and on/off state.

BLE Client / Sender

Initialize the BLE Stack: The ESP32 is configured as a BLE client with the device name H2-Controller. This prepares the board to scan for nearby BLE servers.

Start Scanning for Devices: The client performs an active scan for advertising BLE devices, while a callback processes each discovered device as soon as it appears.

Detect the Target Server: Every discovered device is checked by name. If the name matches H2-CeilingLamp, the scan stops and that device is stored as the target for the connection attempt.

Create a BLE Client and Connect: A BLE client object is created to manage the connection, and the controller then tries to connect to the selected server device.

Discover the Service and Characteristic: After connecting, the client searches for the required BLE service using its UUID. Inside that service, it then locates the characteristic that is used for data exchange.

Send Data to the Server: The controller packs hue, brightness, and the LED on/off state into a LightState structure and sends it with writeValue(). The write is sent without response to keep communication faster.

Handle Connection Events: When the connection is established successfully, the controller marks itself as connected. If the connection drops, the flag is cleared and the client automatically starts scanning again so it can reconnect.

// ============================================================
//  CONTROLLER DEVICE - BLE Client / Sender
//  ESP32-H2 | Potentiometer + Button + OLED + local NeoPixel
// ============================================================
#include <Arduino.h>
#include <FastLED.h>
#include <Wire.h>
#include <U8g2lib.h>
#include <BLEDevice.h>
#include <BLEUtils.h>
#include <BLEScan.h>
#include <BLEAdvertisedDevice.h>
#include <BLEClient.h>

// --- PINS ---
#define NUM_LEDS    1
#define DATA_PIN    8
#define POTI_PIN    4
#define BUTTON_PIN  11
#define SDA_PIN     10
#define SCL_PIN     12

// --- BLE UUIDs (same as the server!) ---
#define SERVICE_UUID        "4fafc201-1fb5-459e-8fcc-c5c9c331914b"
#define CHARACTERISTIC_UUID "beb5483e-36e1-4688-b7f5-ea07361b26a8"

// --- DISPLAY ---
U8G2_SSD1306_128X64_NONAME_F_HW_I2C u8g2(U8G2_R0, U8X8_PIN_NONE);

// --- Local LED ---
CRGB leds[NUM_LEDS];

// --- ADC Smoothing ---
#define SMOOTHING_SAMPLES 10
int readings[SMOOTHING_SAMPLES];
int readIndex   = 0;
int smoothTotal = 0;

// --- Modes / State ---
enum Mode { MODE_HUE, MODE_BRIGHTNESS };
Mode    currentMode   = MODE_HUE;
bool    ledEnabled    = true;
bool    displayOn     = true;
uint8_t hue           = 0;
uint8_t brightness    = 128;
int     lastPotiValue = -1;

// --- Display Timeout ---
#define DISPLAY_TIMEOUT_MS 5000
unsigned long lastActivityTime = 0;

// --- Button ---
bool          lastButtonState  = HIGH;
unsigned long buttonPressTime  = 0;
bool          longPressHandled = false;

// --- BLE State ---
BLEClient*               pClient     = nullptr;
BLERemoteCharacteristic* pRemoteChar = nullptr;
bool bleConnected  = false;
bool doConnect     = false;
bool doScan        = false;
BLEAdvertisedDevice* targetDevice = nullptr;

// ---------- Data Structure (same as server) ----------
struct __attribute__((packed)) LightState {
  uint8_t hue;
  uint8_t brightness;
  uint8_t enabled;
};

// ---------- BLE Scan Callback ----------
class ScanCallbacks : public BLEAdvertisedDeviceCallbacks {
  void onResult(BLEAdvertisedDevice dev) override {
    if (dev.haveName() &&
        String(dev.getName().c_str()) == "H2-CeilingLamp") {
      Serial.println("[BLE] H2-CeilingLamp found!");
      BLEDevice::getScan()->stop();
      targetDevice = new BLEAdvertisedDevice(dev);
      doConnect    = true;
    }
  }
};

// ---------- BLE Client Callbacks ----------
class ClientCB : public BLEClientCallbacks {
  void onConnect(BLEClient*)    override { bleConnected = true;  Serial.println("[BLE] Connected!"); }
  void onDisconnect(BLEClient*) override {
    bleConnected = false;
    pRemoteChar  = nullptr;
    doScan       = true;
    Serial.println("[BLE] Disconnected - scanning again...");
  }
};

// ---------- Connect to Server ----------
bool connectToServer() {
  if (pClient) { delete pClient; pClient = nullptr; }

  pClient = BLEDevice::createClient();
  pClient->setClientCallbacks(new ClientCB());

  if (!pClient->connect(targetDevice)) {
    Serial.println("[BLE] Connection attempt failed");
    return false;
  }

  BLERemoteService* pSvc = pClient->getService(SERVICE_UUID);
  if (!pSvc) {
    Serial.println("[BLE] Service not found");
    pClient->disconnect();
    return false;
  }

  pRemoteChar = pSvc->getCharacteristic(CHARACTERISTIC_UUID);
  if (!pRemoteChar) {
    Serial.println("[BLE] Characteristic not found");
    pClient->disconnect();
    return false;
  }

  Serial.println("[BLE] Ready to send");
  return true;
}

// ---------- Send Light State ----------
void sendState() {
  if (!bleConnected || !pRemoteChar) return;
  LightState s = { hue, brightness, (uint8_t)ledEnabled };
  pRemoteChar->writeValue((uint8_t*)&s, sizeof(s), false); // false = no response (faster)
}

// ---------- Helper Functions ----------
void wakeDisplay() {
  lastActivityTime = millis();
  if (!displayOn) {
    displayOn = true;
    u8g2.setPowerSave(0);
  }
}

void updateLocalLED() {
  leds[0] = ledEnabled ? CRGB(CHSV(hue, 255, brightness)) : CRGB::Black;
  FastLED.show();
}

void drawDisplay() {
  if (!displayOn) return;

  int displayValue = (currentMode == MODE_HUE) ? hue : brightness;
  int barWidth     = map(displayValue, 0, 255, 0, 118);
  int percent      = map(displayValue, 0, 255, 0, 100);

  u8g2.clearBuffer();

  // --- Line 1: Mode Label ---
  u8g2.setFont(u8g2_font_6x10_tf);
  if (currentMode == MODE_HUE)
    u8g2.drawStr(0, 10, "[ HUE ]   Brightness");
  else
    u8g2.drawStr(0, 10, "  Hue   [ BRIGHTNESS ]");

  // --- Separator ---
  u8g2.drawHLine(0, 13, 128);

  // --- Large percentage ---
  u8g2.setFont(u8g2_font_logisoso32_tf);
  u8g2.setCursor(0, 50);
  u8g2.print(percent);
  u8g2.print("%");

  // --- Status bottom-right ---
  u8g2.setFont(u8g2_font_6x10_tf);
  // BLE indicator
  u8g2.drawStr(82, 50, bleConnected ? "BT:OK" : "BT:--");
  // LED ON/OFF
  u8g2.drawStr(104, 60, ledEnabled ? "ON" : "OFF");

  // --- Progress bar ---
  u8g2.drawFrame(0, 54, 128, 10);
  if (barWidth > 0) u8g2.drawBox(1, 55, barWidth, 8);

  u8g2.sendBuffer();
}

// ============================================================
void setup() {
  Serial.begin(115200);
  delay(500);
  Serial.println("=== Controller Device (BLE Client) ===");

  analogReadResolution(12);
  analogSetAttenuation(ADC_11db);
  for (int i = 0; i < SMOOTHING_SAMPLES; i++) readings[i] = 0;

  pinMode(BUTTON_PIN, INPUT_PULLUP);

  FastLED.addLeds<NEOPIXEL, DATA_PIN>(leds, NUM_LEDS);

  Wire.begin(SDA_PIN, SCL_PIN);
  u8g2.begin();
  u8g2.setPowerSave(0);

  // Initialize BLE and scan for server
  BLEDevice::init("H2-Controller");
  BLEScan* pScan = BLEDevice::getScan();
  pScan->setAdvertisedDeviceCallbacks(new ScanCallbacks());
  pScan->setActiveScan(true);
  pScan->start(10, false);  // initial 10s scan

  lastActivityTime = millis();
  updateLocalLED();
  drawDisplay();
  Serial.println("Setup done - scanning for H2-CeilingLamp...");
}

// ============================================================
void loop() {

  // == BLE Management ==
  if (doConnect) {
    doConnect = false;
    if (connectToServer()) {
      sendState(); // immediately send current state
      drawDisplay();
    }
  }
  if (doScan) {
    doScan = false;
    BLEDevice::getScan()->start(5, false);
  }

  // == POTENTIOMETER ==
  int raw = analogRead(POTI_PIN);
  smoothTotal -= readings[readIndex];
  readings[readIndex] = raw;
  smoothTotal += readings[readIndex];
  readIndex = (readIndex + 1) % SMOOTHING_SAMPLES;

  int potiValue = constrain(map(smoothTotal / SMOOTHING_SAMPLES, 0, 3504, 0, 255), 0, 255);

  if (abs(potiValue - lastPotiValue) >= 3) {
    wakeDisplay();
    lastPotiValue = potiValue;

    if (currentMode == MODE_HUE)      hue        = potiValue;
    else                              brightness = potiValue;

    updateLocalLED();
    sendState();
    drawDisplay();
  }

  // == BUTTON ==
  bool buttonState = digitalRead(BUTTON_PIN);

  if (buttonState == LOW && lastButtonState == HIGH) {
    buttonPressTime  = millis();
    longPressHandled = false;
  }

  if (buttonState == LOW && !longPressHandled) {
    if (millis() - buttonPressTime >= 3000) {
      ledEnabled       = !ledEnabled;
      longPressHandled = true;
      wakeDisplay();
      updateLocalLED();
      sendState();
      drawDisplay();
      Serial.printf("[Button] Long Press -> LED: %s\n", ledEnabled ? "ON" : "OFF");
    }
  }

  if (buttonState == HIGH && lastButtonState == LOW) {
    if (!longPressHandled) {
      if (!displayOn) {
        wakeDisplay();
        drawDisplay();
        Serial.println("[Button] Short Press -> Display wake");
      } else {
        currentMode = (currentMode == MODE_HUE) ? MODE_BRIGHTNESS : MODE_HUE;
        wakeDisplay();
        drawDisplay();
        Serial.printf("[Button] Short Press -> Mode: %s\n",
                      currentMode == MODE_HUE ? "HUE" : "BRIGHTNESS");
      }
    }
  }

  lastButtonState = buttonState;

  // == Display Timeout ==
  if (displayOn && millis() - lastActivityTime >= DISPLAY_TIMEOUT_MS) {
    displayOn = false;
    u8g2.setPowerSave(1);
    Serial.println("[Display] Standby");
  }

  delay(10);
}

Bring it all together

To complete the ceiling-light system in a more robust and energy-conscious way, the devices should ultimately communicate over Zigbee instead of relying only on the direct links shown above. This makes the setup much better suited for low-power operation, long-term use, and integration into a broader smart-home workflow. At the same time, Zigbee opens the door to a more flexible architecture in which both devices are no longer isolated prototypes, but part of a coordinated system that can be expanded over time.

The final goal is therefore not only device-to-device communication, but full integration into my existing smart-home environment. By moving to Zigbee, both boards can become part of a network that supports local control, automation, and future extensions far beyond the original lamp interaction.

Setup a local Zigbee Network

In this step, a small local Zigbee network is created between a router and a client. One ESP32-H2 acts as the Zigbee router, while the second ESP32-H2 behaves as the client device. The same functional relationship as before is maintained, but now the communication is handled through a dedicated low-power wireless network layer instead of a direct point-to-point connection.

This makes it possible to validate the data flow, pairing behavior, and general responsiveness of the setup in a controlled environment before introducing the complexity of a larger smart-home system.

Communication using power saving Zigbee Protocol
Response Behavior

In my tests, the Zigbee connection felt noticeably slower than BLE. It was still fully sufficient for controlling the system reliably, but it did not translate input changes with quite the same immediacy or fluid real-time feel.

Zigbee Coordinator / Color Dimmer Switch

Zigbee Coordinator Initialization: The ESP32 is configured as a Zigbee coordinator with ZIGBEE_COORDINATOR, meaning it manages the local Zigbee network. Zigbee.begin() starts the Zigbee stack and brings the board up as the central device that accepts bindings from other endpoints.

Adding an Endpoint (Dimmer Switch): zbSwitch is created as a Color Dimmer Switch endpoint and registered with Zigbee.addEndpoint(&zbSwitch). This gives the coordinator an addressable Zigbee endpoint that bound devices can communicate with.

Allow Multiple Bindings: zbSwitch.allowMultipleBinding(true) allows one switch endpoint to control more than one light at the same time if several devices bind to it.

Open Network for Pairing: Zigbee.setRebootOpenNetwork(180) keeps the network open for three minutes after boot. During this window, new Zigbee lights can discover the coordinator and bind to its endpoint.

Sending Commands: sendZigbee() converts HSV values from the potentiometer into RGB values with FastLED. If the switch is bound and enabled, it sends lightOn() or lightOff(), followed by setLightColor(r, g, b) and setLightLevel(brightness) to update the light in real time.

Bound State Check: zbSwitch.bound() returns whether the endpoint is already bound to a light. Until binding is complete, no Zigbee commands are transmitted and the display keeps showing the pairing hint.

Continuous Update Loop: Potentiometer and button changes trigger sendZigbee() to update color, brightness, and on/off state continuously. Conceptually this works like the BLE example above, but here the communication is routed through Zigbee clusters and endpoints instead of BLE characteristics.

// ============================================================
//  POTI DEVICE - Zigbee Coordinator / Color Dimmer Switch
//  Tools -> Zigbee Mode -> Zigbee ZCZR
// ============================================================
#ifndef ZIGBEE_MODE_ZCZR
#error "Set Zigbee Mode to ZCZR: Tools -> Zigbee Mode"
#endif

#include "Zigbee.h"
#include <FastLED.h>
#include <Wire.h>
#include <U8g2lib.h>

// --- PINS ---
#define NUM_LEDS    1
#define DATA_PIN    8
#define POTI_PIN    4
#define BUTTON_PIN  11
#define SDA_PIN     10
#define SCL_PIN     12

// --- Zigbee ---
#define SWITCH_ENDPOINT 5
ZigbeeColorDimmerSwitch zbSwitch = ZigbeeColorDimmerSwitch(SWITCH_ENDPOINT);

// --- Display ---
U8G2_SSD1306_128X64_NONAME_F_HW_I2C u8g2(U8G2_R0, U8X8_PIN_NONE);

// --- Local LED ---
CRGB leds[NUM_LEDS];

// --- ADC Smoothing ---
#define SMOOTHING_SAMPLES 10
int readings[SMOOTHING_SAMPLES];
int readIndex   = 0;
int smoothTotal = 0;

// --- State ---
enum Mode { MODE_HUE, MODE_BRIGHTNESS };
Mode    currentMode   = MODE_HUE;
bool    ledEnabled    = true;
bool    displayOn     = true;
uint8_t hue           = 0;
uint8_t brightness    = 128;
int     lastPotiValue = -1;

#define DISPLAY_TIMEOUT_MS 5000
unsigned long lastActivityTime = 0;

bool          lastButtonState  = HIGH;
unsigned long buttonPressTime  = 0;
bool          longPressHandled = false;

// ============================================================
//  Send Zigbee Command: Convert HSV -> RGB, then send color + level separately
// ============================================================
void sendZigbee() {
  if (!zbSwitch.bound()) return;

  if (!ledEnabled) {
    zbSwitch.lightOff();
    return;
  }

  // Convert HSV -> RGB using FastLED rainbow palette
  CRGB rgb;
  hsv2rgb_rainbow(CHSV(hue, 255, 255), rgb);

  zbSwitch.lightOn();
  zbSwitch.setLightColor(rgb.r, rgb.g, rgb.b);
  zbSwitch.setLightLevel(brightness);
}

// ============================================================
//  Helper Functions
// ============================================================
void wakeDisplay() {
  lastActivityTime = millis();
  if (!displayOn) {
    displayOn = true;
    u8g2.setPowerSave(0);
  }
}

void updateLocalLED() {
  if (ledEnabled) leds[0] = CRGB(CHSV(hue, 255, brightness));
  else            leds[0] = CRGB::Black;
  FastLED.show();
}

void drawDisplay() {
  if (!displayOn) return;

  int displayValue = (currentMode == MODE_HUE) ? hue : brightness;
  int barWidth     = map(displayValue, 0, 255, 0, 118);
  int percent      = map(displayValue, 0, 255, 0, 100);
  bool bound       = zbSwitch.bound();

  u8g2.clearBuffer();

  // --- Mode Label ---
  u8g2.setFont(u8g2_font_6x10_tf);
  if (currentMode == MODE_HUE)
    u8g2.drawStr(0, 10, "[ HUE ]   Brightness");
  else
    u8g2.drawStr(0, 10, "  Hue   [ BRIGHTNESS ]");

  u8g2.drawHLine(0, 13, 128);

  // --- Large Value ---
  if (!bound) {
    // Not yet bound - show pairing hint
    u8g2.setFont(u8g2_font_6x10_tf);
    u8g2.drawStr(10, 36, "Waiting for light...");
    u8g2.drawStr(20, 50, "Zigbee pairing");
  } else {
    u8g2.setFont(u8g2_font_logisoso32_tf);
    u8g2.setCursor(0, 50);
    u8g2.print(percent);
    u8g2.print("%");
  }

  // --- Status ---
  u8g2.setFont(u8g2_font_6x10_tf);
  u8g2.drawStr(80, 40, bound ? "ZB:OK" : "ZB:--");
  u8g2.drawStr(104, 60, ledEnabled ? "ON" : "OFF");

  // --- Progress Bar ---
  u8g2.drawFrame(0, 54, 128, 10);
  if (barWidth > 0) u8g2.drawBox(1, 55, barWidth, 8);

  u8g2.sendBuffer();
}

// ============================================================
//  SETUP
// ============================================================
void setup() {
  Serial.begin(115200);
  delay(300);
  Serial.println("=== Poti Controller (Zigbee Coordinator) ===");

  analogReadResolution(12);
  analogSetAttenuation(ADC_11db);
  for (int i = 0; i < SMOOTHING_SAMPLES; i++) readings[i] = 0;

  pinMode(BUTTON_PIN, INPUT_PULLUP);
  FastLED.addLeds<NEOPIXEL, DATA_PIN>(leds, NUM_LEDS);

  // Start display first
  Wire.begin(SDA_PIN, SCL_PIN);
  u8g2.begin();
  u8g2.setPowerSave(0);

  lastActivityTime = millis();
  updateLocalLED();
  drawDisplay();
  Serial.println("[Display] OK");

  // Zigbee Setup
  zbSwitch.setManufacturerAndModel("ESP H2 Maker", "Poti Controller");
  zbSwitch.allowMultipleBinding(true);
  Zigbee.addEndpoint(&zbSwitch);
  Zigbee.setRebootOpenNetwork(180);  // 3 min pairing window after boot

  if (!Zigbee.begin(ZIGBEE_COORDINATOR)) {
    Serial.println("[Zigbee] Start failed - restarting");
    ESP.restart();
  }
  Serial.println("[Zigbee] Coordinator started - waiting for binding...");
}

// ============================================================
//  LOOP
// ============================================================
void loop() {

  // == POTENTIOMETER ==
  int raw = analogRead(POTI_PIN);
  smoothTotal -= readings[readIndex];
  readings[readIndex] = raw;
  smoothTotal += readings[readIndex];
  readIndex = (readIndex + 1) % SMOOTHING_SAMPLES;

  int potiValue = constrain(
    map(smoothTotal / SMOOTHING_SAMPLES, 0, 3504, 0, 255), 0, 255);

  if (abs(potiValue - lastPotiValue) >= 3) {
    wakeDisplay();
    lastPotiValue = potiValue;

    if (currentMode == MODE_HUE)  hue        = potiValue;
    else                          brightness = potiValue;

    updateLocalLED();
    sendZigbee();
    drawDisplay();

    Serial.printf("[Poti] %s: %d%%\n",
      currentMode == MODE_HUE ? "Hue" : "Brightness",
      map(potiValue, 0, 255, 0, 100));
  }

  // == BUTTON ==
  bool buttonState = digitalRead(BUTTON_PIN);

  if (buttonState == LOW && lastButtonState == HIGH) {
    buttonPressTime  = millis();
    longPressHandled = false;
  }

  if (buttonState == LOW && !longPressHandled) {
    if (millis() - buttonPressTime >= 3000) {
      ledEnabled       = !ledEnabled;
      longPressHandled = true;
      wakeDisplay();
      updateLocalLED();
      sendZigbee();
      drawDisplay();
      Serial.printf("[Button] Long Press -> LED: %s\n",
                    ledEnabled ? "ON" : "OFF");
    }
  }

  if (buttonState == HIGH && lastButtonState == LOW) {
    if (!longPressHandled) {
      if (!displayOn) {
        wakeDisplay();
        drawDisplay();
        Serial.println("[Button] Short Press -> Display wake");
      } else {
        currentMode = (currentMode == MODE_HUE) ? MODE_BRIGHTNESS : MODE_HUE;
        wakeDisplay();
        drawDisplay();
        Serial.printf("[Button] Short Press -> Mode: %s\n",
                      currentMode == MODE_HUE ? "HUE" : "BRIGHTNESS");
      }
    }
  }

  lastButtonState = buttonState;

  // == Display Timeout ==
  if (displayOn && millis() - lastActivityTime >= DISPLAY_TIMEOUT_MS) {
    displayOn = false;
    u8g2.setPowerSave(1);
    Serial.println("[Display] Standby");
  }

  delay(10);
}
// ============================================================
//  LED DEVICE - Zigbee Router / Color Dimmable Light
//  Tools -> Zigbee Mode -> Zigbee ZCZR
// ============================================================
#ifndef ZIGBEE_MODE_ZCZR
#error "Set Zigbee Mode to ZCZR: Tools -> Zigbee Mode"
#endif

#include "Zigbee.h"
#include <FastLED.h>

// --- LED Settings ---
#define LED_PIN     10
#define NUM_LEDS    205
#define COLOR_ORDER GRB
#define LED_TYPE    WS2812

// --- Zigbee ---
#define ZIGBEE_LIGHT_ENDPOINT 10
ZigbeeColorDimmableLight zbLight = ZigbeeColorDimmableLight(ZIGBEE_LIGHT_ENDPOINT);

// --- LED Buffer ---
CRGB leds[NUM_LEDS];

// ============================================================
// Callback when Zigbee controller changes light state or color
// ============================================================
void onColorChange(bool state, uint8_t red, uint8_t green,
                   uint8_t blue, uint8_t level) {
  if (!state) {
    // Turn off all LEDs
    fill_solid(leds, NUM_LEDS, CRGB::Black);
    FastLED.show();
    return;
  }

  float bri = level / 255.0f;
  for (int i = 0; i < NUM_LEDS; i++) {
    leds[i].r = (uint8_t)(red   * bri);
    leds[i].g = (uint8_t)(green * bri);
    leds[i].b = (uint8_t)(blue  * bri);
  }
  FastLED.show();
}

// ============================================================
// SETUP
// ============================================================
void setup() {
  Serial.begin(115200);

  // Initialize LEDs
  FastLED.addLeds<LED_TYPE, LED_PIN, COLOR_ORDER>(leds, NUM_LEDS);
  FastLED.clear();
  FastLED.show();

  // Zigbee light capabilities
  zbLight.setLightColorCapabilities(ZIGBEE_COLOR_CAPABILITY_HUE_SATURATION | ZIGBEE_COLOR_CAPABILITY_X_Y);
  zbLight.onLightChangeRgb(onColorChange);  // Register callback
  zbLight.setManufacturerAndModel("ESP H2 Maker", "Ceiling Lamp");

  // Add endpoint to Zigbee stack
  Zigbee.addEndpoint(&zbLight);

  // Start Zigbee as a Router
  if (!Zigbee.begin(ZIGBEE_ROUTER, false)) {
    Serial.println("Zigbee failed - restarting");
    ESP.restart();
  }
  Serial.println("[Zigbee] Router started");
}

// ============================================================
// LOOP
// ============================================================
void loop() {
  delay(10);  // Just keep the loop alive
}

Zigbee Router / Color Dimmable Light

Zigbee Router Initialization: The ESP32 is configured as a Zigbee router with ZIGBEE_ROUTER. A router joins the Zigbee network created by the coordinator and can relay messages between devices while also exposing its own light endpoint.

Adding the LED Endpoint: zbLight is created as a Color Dimmable Light endpoint and registered with Zigbee.addEndpoint(&zbLight). This makes the device reachable as endpoint 10 for incoming light commands.

Defining Light Capabilities: zbLight.setLightColorCapabilities(...) declares that the light accepts Hue/Saturation and XY color commands. This allows the coordinator to send full color information instead of only simple on/off or dimming values.

Registering the Callback: zbLight.onLightChangeRgb(onColorChange) registers a callback that runs whenever the coordinator updates the light. The function receives the on/off state, RGB values, and brightness level.

Receiving Commands: When the coordinator sends lightOn(), lightOff(), color, or brightness updates, Zigbee passes these values into onColorChange(). The callback then updates the FastLED buffer for all 205 LEDs to match the new state.

Continuous Operation: The loop() stays minimal because Zigbee communication is event-driven here. The router simply remains available on the network and reacts automatically whenever the coordinator sends a new command.

Manufacturer & Model

zbSwitch.setManufacturerAndModel() and zbLight.setManufacturerAndModel() assign identifiable device names to both endpoints. This metadata helps Zigbee devices and controller tools recognize them more reliably during pairing and binding.

Integrate into existing Zigbee Network

Once Zigbee communication has been set up and tested successfully on a smaller local scale, the next step is to integrate both devices into my existing smart-home system so they can communicate as part of the wider network. This changes the setup from an isolated prototype into a practical home-automation component that can interact with other devices, dashboards, and rules.

That broader integration also adds long-term flexibility: the input device can later be reused to control temperature, additional lights, or other smart-home functions, while the output device can be operated comfortably from my phone as part of the same system. At the same time, the original direct relationship between input and output remains conceptually intact, so the system still preserves the immediate interaction that defined the first version.

Controlling Ceiling Light using Home Assistant UI
Version Changes: Local vs Home Assistant

Local version: The Poti is the boss. It creates its own private Zigbee network, and the lamp simply joins that network. No external coordinator or Home Assistant dongle is involved.

HA version: The Home Assistant dongle is the boss. Both devices join that existing Zigbee network like normal smart-home devices. The lamp appears in Home Assistant as a controllable light, while the Poti still keeps its direct bound control over the lamp.

In the Home Assistant version, 3 additional endpoints are added. The two ZigbeeAnalog endpoints report hue and brightness as values that Home Assistant can read and use in automations, dashboards, or other device logic. The ZigbeeBinary endpoint reports the button state, so button presses can also become automation triggers inside Home Assistant.

Show Code Changes
// ============================================================
//  POTI DEVICE - Zigbee Router / Color Dimmer Switch with HA reporting
// ============================================================
#ifndef ZIGBEE_MODE_ZCZR
#error "Set Zigbee Mode to ZCZR: Tools -> Zigbee Mode"
#endif

#include "Zigbee.h"
#include <FastLED.h>
#include <Wire.h>
#include <U8g2lib.h>

// --- PINS ---
#define NUM_LEDS    1
#define DATA_PIN    8
#define POTI_PIN    4
#define BUTTON_PIN  11
#define SDA_PIN     10
#define SCL_PIN     12

// --- Zigbee Endpoints ---
#define SWITCH_ENDPOINT  5   // ColorDimmerSwitch -> binding to the light
#define HUE_ENDPOINT     6   // AnalogInput       -> Hue 0-100% in HA
#define BRIGHT_ENDPOINT  7   // AnalogInput       -> Brightness 0-100% in HA
#define BUTTON_ENDPOINT  8   // BinaryInput       -> Button state in HA

ZigbeeColorDimmerSwitch zbSwitch  = ZigbeeColorDimmerSwitch(SWITCH_ENDPOINT);
ZigbeeAnalog            zbHue     = ZigbeeAnalog(HUE_ENDPOINT);
ZigbeeAnalog            zbBright  = ZigbeeAnalog(BRIGHT_ENDPOINT);
ZigbeeBinary            zbButton  = ZigbeeBinary(BUTTON_ENDPOINT);

U8G2_SSD1306_128X64_NONAME_F_HW_I2C u8g2(U8G2_R0, U8X8_PIN_NONE);
CRGB leds[NUM_LEDS];

// --- ADC Smoothing ---
#define SMOOTHING_SAMPLES 10
int readings[SMOOTHING_SAMPLES];
int readIndex   = 0;
int smoothTotal = 0;

// --- State ---
enum Mode { MODE_HUE, MODE_BRIGHTNESS };
Mode    currentMode   = MODE_HUE;
bool    ledEnabled    = true;
bool    displayOn     = true;
uint8_t hue           = 0;
uint8_t brightness    = 128;
int     lastPotiValue = -1;

#define DISPLAY_TIMEOUT_MS 5000
unsigned long lastActivityTime = 0;

bool          lastButtonState  = HIGH;
unsigned long buttonPressTime  = 0;
bool          longPressHandled = false;

// ============================================================
// Send Zigbee updates to bound light and HA endpoints
// ============================================================
void sendZigbee() {
  // 1. Direct light control via binding (local, fast)
  if (zbSwitch.bound()) {
    if (!ledEnabled) {
      zbSwitch.lightOff();
    } else {
      CRGB rgb;
      hsv2rgb_rainbow(CHSV(hue, 255, 255), rgb);
      zbSwitch.lightOn();
      zbSwitch.setLightColor(rgb.r, rgb.g, rgb.b);
      zbSwitch.setLightLevel(brightness);
    }
  }

  // 2. Report values to HA (Analog + Binary Endpoints)
  if (Zigbee.connected()) {
    float huePercent  = map(hue,        0, 255, 0, 100);
    float briPercent  = map(brightness, 0, 255, 0, 100);

    zbHue.setAnalogInput(huePercent);
    zbHue.reportAnalogInput();

    zbBright.setAnalogInput(briPercent);
    zbBright.reportAnalogInput();
  }
}

// Report button state to HA
void reportButton(bool pressed) {
  if (Zigbee.connected()) {
    zbButton.setBinaryInput(pressed);
    zbButton.reportBinaryInput();
  }
}

// ============================================================
// Display handling
// ============================================================
void wakeDisplay() {
  lastActivityTime = millis();
  if (!displayOn) {
    displayOn = true;
    u8g2.setPowerSave(0);
  }
}

void updateLocalLED() {
  if (ledEnabled) leds[0] = CRGB(CHSV(hue, 255, brightness));
  else            leds[0] = CRGB::Black;
  FastLED.show();
}

void drawDisplay() {
  if (!displayOn) return;

  bool connected = Zigbee.connected();
  bool bound     = zbSwitch.bound();

  int displayValue = (currentMode == MODE_HUE) ? hue : brightness;
  int barWidth     = map(displayValue, 0, 255, 0, 118);
  int percent      = map(displayValue, 0, 255, 0, 100);

  u8g2.clearBuffer();

  // --- Mode Label ---
  u8g2.setFont(u8g2_font_6x10_tf);
  if (currentMode == MODE_HUE)
    u8g2.drawStr(0, 10, "[ HUE ]   Brightness");
  else
    u8g2.drawStr(0, 10, "  Hue   [ BRIGHTNESS ]");
  u8g2.drawHLine(0, 13, 128);

  // --- Connection / Binding Status ---
  if (!connected) {
    u8g2.drawStr(12, 36, "Searching HA network...");
  } else if (!bound) {
    u8g2.drawStr(4, 30, "Connected to HA!");
    u8g2.drawStr(4, 44, "Set binding in ZHA");
  } else {
    u8g2.setFont(u8g2_font_logisoso32_tf);
    u8g2.setCursor(0, 50);
    u8g2.print(percent);
    u8g2.print("%");
  }

  u8g2.setFont(u8g2_font_6x10_tf);
  if (!connected)     u8g2.drawStr(86, 10, "ZB:--");
  else if (!bound)    u8g2.drawStr(74, 10, "ZB:JOIN");
  else                u8g2.drawStr(86, 10, "ZB:OK");
  u8g2.drawStr(104, 60, ledEnabled ? "ON" : "OFF");

  // --- Progress Bar ---
  u8g2.drawFrame(0, 54, 128, 10);
  if (barWidth > 0) u8g2.drawBox(1, 55, barWidth, 8);

  u8g2.sendBuffer();
}

// ============================================================
// SETUP
// ============================================================
void setup() {
  Serial.begin(115200);
  delay(300);

  analogReadResolution(12);
  analogSetAttenuation(ADC_11db);
  for (int i = 0; i < SMOOTHING_SAMPLES; i++) readings[i] = 0;

  pinMode(BUTTON_PIN, INPUT_PULLUP);
  FastLED.addLeds<NEOPIXEL, DATA_PIN>(leds, NUM_LEDS);

  Wire.begin(SDA_PIN, SCL_PIN);
  u8g2.begin();
  u8g2.setPowerSave(0);
  lastActivityTime = millis();
  updateLocalLED();
  drawDisplay();

  // --- Configure Endpoints ---
  zbSwitch.setManufacturerAndModel("ESP H2 Maker", "Poti Controller");

  zbHue.addAnalogInput();
  zbHue.setManufacturerAndModel("ESP H2 Maker", "Poti Hue");
  zbHue.setAnalogInputDescription("Hue (%)");
  zbHue.setAnalogInputApplication(ESP_ZB_ZCL_AI_PERCENTAGE_OTHER);
  zbHue.setAnalogInputMinMax(0.0, 100.0);
  zbHue.setAnalogInputResolution(1.0);

  zbBright.addAnalogInput();
  zbBright.setManufacturerAndModel("ESP H2 Maker", "Poti Brightness");
  zbBright.setAnalogInputDescription("Brightness (%)");
  zbBright.setAnalogInputApplication(ESP_ZB_ZCL_AI_PERCENTAGE_OTHER);
  zbBright.setAnalogInputMinMax(0.0, 100.0);
  zbBright.setAnalogInputResolution(1.0);

  zbButton.addBinaryInput();
  zbButton.setManufacturerAndModel("ESP H2 Maker", "Poti Button");
  zbButton.setBinaryInputDescription("Button");

  // --- Register Endpoints ---
  Zigbee.addEndpoint(&zbSwitch);
  Zigbee.addEndpoint(&zbHue);
  Zigbee.addEndpoint(&zbBright);
  Zigbee.addEndpoint(&zbButton);

  Zigbee.setPrimaryChannelMask(ESP_ZB_TRANSCEIVER_ALL_CHANNELS_MASK);

  // --- Start Zigbee ---
  if (!Zigbee.begin(ZIGBEE_ROUTER, false)) {
    Serial.println("[Zigbee] Start failed - restart");
    ESP.restart();
  }

  // --- Setup Reporting AFTER begin() + connected() ---
  Serial.println("[Zigbee] Waiting for network...");
  while (!Zigbee.connected()) { delay(100); }
  zbHue.setAnalogInputReporting(0, 5, 1.0);
  zbBright.setAnalogInputReporting(0, 5, 1.0);

  Serial.println("[Zigbee] Router + Reporting ready");
}

// ============================================================
// LOOP
// ============================================================
void loop() {
  static unsigned long lastStatusUpdate = 0;
  if (millis() - lastStatusUpdate >= 1000) {
    lastStatusUpdate = millis();
    drawDisplay();
  }

  // --- POTENTIOMETER ---
  int raw = analogRead(POTI_PIN);
  smoothTotal -= readings[readIndex];
  readings[readIndex] = raw;
  smoothTotal += readings[readIndex];
  readIndex = (readIndex + 1) % SMOOTHING_SAMPLES;

  int potiValue = constrain(
    map(smoothTotal / SMOOTHING_SAMPLES, 0, 3504, 0, 255), 0, 255);

  if (abs(potiValue - lastPotiValue) >= 3) {
    wakeDisplay();
    lastPotiValue = potiValue;
    if (currentMode == MODE_HUE)  hue        = potiValue;
    else                          brightness = potiValue;
    updateLocalLED();
    sendZigbee();
    drawDisplay();
  }

  // --- BUTTON ---
  bool buttonState = digitalRead(BUTTON_PIN);

  if (buttonState == LOW && lastButtonState == HIGH) {
    buttonPressTime  = millis();
    longPressHandled = false;
    reportButton(true);   // Button pressed -> HA
  }

  if (buttonState == LOW && !longPressHandled) {
    if (millis() - buttonPressTime >= 3000) {
      ledEnabled       = !ledEnabled;
      longPressHandled = true;
      wakeDisplay();
      updateLocalLED();
      sendZigbee();
      drawDisplay();
      Serial.printf("[Button] Long Press -> LED: %s\n",
                    ledEnabled ? "ON" : "OFF");
    }
  }

  if (buttonState == HIGH && lastButtonState == LOW) {
    reportButton(false);  // Button released -> HA

    if (!longPressHandled) {
      if (!displayOn) {
        wakeDisplay();
        drawDisplay();
      } else {
        currentMode = (currentMode == MODE_HUE) ? MODE_BRIGHTNESS : MODE_HUE;
        wakeDisplay();
        drawDisplay();
      }
    }
  }

  lastButtonState = buttonState;

  if (displayOn && millis() - lastActivityTime >= DISPLAY_TIMEOUT_MS) {
    displayOn = false;
    u8g2.setPowerSave(1);
  }

  delay(10);
}
// ============================================================
//  LED DEVICE - Zigbee Router / Color Dimmable Light
//  Tools -> Zigbee Mode -> Zigbee ZCZR
// ============================================================
#ifndef ZIGBEE_MODE_ZCZR
#error "Set Zigbee Mode to ZCZR: Tools -> Zigbee Mode"
#endif

#include "Zigbee.h"
#include <FastLED.h>

// --- LED Settings ---
#define LED_PIN     10
#define NUM_LEDS    205
#define COLOR_ORDER GRB
#define LED_TYPE    WS2812

// --- Zigbee Endpoint ---
#define ZIGBEE_LIGHT_ENDPOINT 10
ZigbeeColorDimmableLight zbLight = ZigbeeColorDimmableLight(ZIGBEE_LIGHT_ENDPOINT);

// --- LED Buffer ---
CRGB leds[NUM_LEDS];

// ============================================================
// Callback for when the Zigbee controller changes light state or color
// ============================================================
void onColorChange(bool state, uint8_t red, uint8_t green,
                   uint8_t blue, uint8_t level) {
  if (!state) {
    // Turn off all LEDs
    fill_solid(leds, NUM_LEDS, CRGB::Black);
    FastLED.show();
    return;
  }

  // Apply brightness scaling to RGB values
  float bri = level / 255.0f;
  for (int i = 0; i < NUM_LEDS; i++) {
    leds[i].r = (uint8_t)(red   * bri);
    leds[i].g = (uint8_t)(green * bri);
    leds[i].b = (uint8_t)(blue  * bri);
  }
  FastLED.show();
}

// ============================================================
// SETUP
// ============================================================
void setup() {
  Serial.begin(115200);

  // Initialize LED strip
  FastLED.addLeds<LED_TYPE, LED_PIN, COLOR_ORDER>(leds, NUM_LEDS);
  FastLED.clear();
  FastLED.show();

  // Configure Zigbee light capabilities
  zbLight.setLightColorCapabilities(ZIGBEE_COLOR_CAPABILITY_HUE_SATURATION | ZIGBEE_COLOR_CAPABILITY_X_Y);
  zbLight.onLightChangeRgb(onColorChange);  // Register callback
  zbLight.setManufacturerAndModel("ESP H2 Maker", "Ceiling Lamp");

  // Add endpoint to Zigbee stack
  Zigbee.addEndpoint(&zbLight);

  // Use all channels for Zigbee transceiver
  Zigbee.setPrimaryChannelMask(ESP_ZB_TRANSCEIVER_ALL_CHANNELS_MASK);

  // Start Zigbee as a router
  if (!Zigbee.begin(ZIGBEE_ROUTER, false)) {
    Serial.println("[Zigbee] Start failed - restarting");
    ESP.restart();
  }
  Serial.println("[Zigbee] Router started");
}

// ============================================================
// LOOP
// ============================================================
void loop() {
  // Minimal loop, all updates are event-driven via Zigbee callback
  delay(10);
}

Binding

To create a direct binding to the Ceiling Lamp, just like in the local version, open ZHA -> Devices -> "Poti Controller" -> More Options -> Manage Zigbee Device -> Bindings -> "Ceiling Lamp" -> Bind.

Once that binding is created, both devices continue to communicate directly with each other. This keeps the controller and the lamp linked even though both are now part of the larger Home Assistant Zigbee network.

Binding the Poti Controller to the Ceiling Lamp in ZHA
Setup Home Assistant Binding
Additional Home Assistant entities from Zigbee endpoints
Setup Home Assistant Automation using Sensor Data

Using the Endpoints

Because the Home Assistant version adds three extra endpoints, the input device can now be used for more than only direct lamp control. In Home Assistant, three separate entities appear for Hue, Brightness, and the Button.

This means you can, for example, create a Home Assistant automation through Settings -> Automations & Scenes -> + Create Automation and use the input button to toggle another lamp. In the same way, the percentage-based controls can also be reused elsewhere.

And that is not limited to lighting. The controller logic can be forwarded into many other Home Assistant actions, so the same input hardware can become part of a much broader automation system.

Toggling Lamp using Switch Endpoint in Home Assistant
Sensor Data in Home Assistant UI

Easteregg

As a fitting extra for the Easter weekend, there is also a more unusual communication experiment: a Morse-code connection between an ESP32-S3 and an ESP32-C6. Instead of using a typical bus or radio protocol, one board listens for serial input, translates the incoming text into Morse code, and outputs that information as timed light pulses.

The second board observes those light impulses, decodes them back into text, and then presents the recovered message on an I2C display. This creates a playful but fully functional communication chain that turns a simple optical signal into a readable message again on the receiving side.

Project Files

Downloads

Arduino Files

  • File BLE Client & Sender (zipped .ino folder)
    Download Download
  • File Zigbee Input & Output (zipped .ino folder)
    Download Download
  • File Home Assistant Input & Output (zipped .ino folder)
    Download Download
Start 0%