Zigbee remote failure

OK - I'm not sure what's actually wrong here, but I'm very tired of going around in circles.

The project - use an ESP32 C6 as a zigbee remote.

The method - use the Zigbee.h library to send a code via the temperature broadcast so that a keypress can be determined. A converter at the Home Assistant/Z2M end turns it into a slightly different entity.

It did work yesterday, but doesn't now.

The partition is Zigbee 4MS with SPIFFS, Zigbee mode is end device.

What works ... the program pairs with HA and the conversion is loaded. Keypresses on the device itself work.

What doesn't work... packets don't seem to be transmitted to HA. The Z2M log doesn't show anything coming in. Most likely that the C6 is sending packets, but HA is likely ignoring them, or some form of formatting error.

I'm lost now.

Converter...

import * as exposes from 'zigbee-herdsman-converters/lib/exposes';
import * as fz from 'zigbee-herdsman-converters/converters/fromZigbee';

const e = exposes.presets;
const ea = exposes.access;

const keypadState = {
    cluster: 'msTemperatureMeasurement',
    type: ['attributeReport', 'readResponse'],
    convert: (model, msg) => {
        const raw = msg.data?.measuredValue;
        if (raw === undefined) return {};
        return { keypad_state: raw / 100 };
    },
};

export default {
    zigbeeModel: ['RS10_Remote'],
    model: 'RS10_Remote',
    vendor: 'SeeedC6-RS10',
    description: 'RS10 keypad remote',
    fromZigbee: [keypadState],
    toZigbee: [],
    exposes: [
        e.numeric('keypad_state', ea.STATE)
            .withDescription('Last pressed key index'),
    ],
};

Current Arduino code...

#include "Arduino.h"
#include "Zigbee.h"

const char* DEVICE_NAME = "RS10_Remote";

const int ROWS = 5;
const int COLS = 4;

int rowPins[ROWS] = {16, 17, 19, 20, 18};
int colPins[COLS] = {2, 21, 22, 23};

char keys[ROWS][COLS] = {
  {'1','2','3','A'},
  {'4','5','6','B'},
  {'7','8','9','C'},
  {'*','0','#','D'},
  {'E','F','G','H'}
};

const int LED_PIN = 1;
const uint8_t ENDPOINT = 10;

ZigbeeTempSensor zbTempSensor(ENDPOINT);

const char keyList[20] = {
  '1','2','3','A',
  '4','5','6','B',
  '7','8','9','C',
  '*','0','#','D',
  'E','F','G','H'
};

uint8_t keyToIndex(char key) {
  for (uint8_t i = 0; i < 20; i++) {
    if (keyList[i] == key) return i + 1;
  }
  return 0;
}
void sendKey(char key)
{
  uint8_t idx = keyToIndex(key);
  if (idx == 0) return;

  Serial.print("Key: ");
  Serial.print(key);
  Serial.print(" idx: ");
  Serial.println(idx);

  zbTempSensor.setTemperature((float)idx);
  zbTempSensor.reportTemperature();

  delay(100);
}

void factoryResetAndReboot() {
  Serial.println("Factory reset requested");
  digitalWrite(LED_PIN, LOW);
  Zigbee.factoryReset();
  delay(1000);
  ESP.restart();
}

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

  pinMode(LED_PIN, OUTPUT);
  digitalWrite(LED_PIN, LOW);

  for (int r = 0; r < ROWS; r++) {
    pinMode(rowPins[r], OUTPUT);
    digitalWrite(rowPins[r], HIGH);
  }

  for (int c = 0; c < COLS; c++) {
    pinMode(colPins[c], INPUT_PULLUP);
  }

  zbTempSensor.setManufacturerAndModel("SeeedC6-RS10", "RS10_Remote");
  zbTempSensor.setMinMaxValue(1, 20);
  zbTempSensor.setDefaultValue(1.0);
  zbTempSensor.setTolerance(1.0);
  zbTempSensor.setPowerSource(ZB_POWER_SOURCE_BATTERY, 100, 35);
  zbTempSensor.addTimeCluster();

  Zigbee.addEndpoint(&zbTempSensor);

  Serial.println("Starting Zigbee...");

  if (!Zigbee.begin()) {
    Serial.println("Zigbee failed to start!");
    ESP.restart();
  }

  Serial.println("Connecting to network");
  while (!Zigbee.connected()) {
    Serial.print(".");
    delay(100);
  }
  Serial.println();
  Serial.println("Successfully connected to Zigbee network");

}

void loop() {

  for (int r = 0; r < ROWS; r++) {
    digitalWrite(rowPins[r], LOW);

    for (int c = 0; c < COLS; c++) {
      if (digitalRead(colPins[c]) == LOW) {
        delay(20);
        if (digitalRead(colPins[c]) == LOW) {
          char key = keys[r][c];
          bool isOneKey = (key == '1');
          digitalWrite(LED_PIN, HIGH);
          sendKey(key);
          unsigned long pressedAt = millis();
          while (digitalRead(colPins[c]) == LOW) {
            if (isOneKey && (millis() - pressedAt >= 5000)) {
              factoryResetAndReboot();
            }
            delay(10);
          }

          digitalWrite(LED_PIN, LOW);
        }
      }      
    }

    digitalWrite(rowPins[r], HIGH);
  }

  delay(10);
}

Your post gives no clue.

Please read and use How to get the best out of this forum - Development Tools / IDE 1.x - Arduino Forum

You mean this wasn't enough?

[quote="msknight, post:1, topic:1443208"] What works ... the program pairs with HA and the conversion is loaded. Keypresses on the device itself work.

What doesn't work... packets don't seem to be transmitted to HA. The Z2M log doesn't show anything coming in. Most likely that the C6 is sending packets, but HA is likely ignoring them, or some form of formatting error. [/quote]

Exactly.

What more do you think I can give, other than this? ie.the code, where it is succeeding, and where it is failing.

Your project uses not very common both hardware and software. The number of helpers having experience of this is likely limited. Adding links to datasheets, technical manuals etc would surely make some helpers taking on the matter.

You can of course sit and wait.

OK - so we've gone from my post giving no clue, to my post giving what's needed, but because I'm a niche case, no one is likely to be interested in helping.

In my opinion, You have not.

That makes it harder but there are helpers very often spending time, digging down when there's documentation to dig down in. Your information is too thin compared to the complexity, a rather niche project.

Personally, I would have thought that using a C6 for zigbee remote, just as C3 have been used to create IR remotes, would have been higher up than being a niche case. But hey ho.

As it looks like, You're quite high up as You are. The lack of precise replies indicates that.

Hi, @msknight

Can you please post a copy of your circuit, a picture of a hand drawn circuit in jpg, png?
Hand drawn and photographed is perfectly acceptable.
Please include ALL hardware, power supplies, component names and pin labels.

How are you powering the Zigbee.
Can you post link to specs/data of the Zigbee?

Do you have a DMM? (Digital MultiMeter)

Thanks.. Tom.... :smiley: :+1: :coffee: :australia:

I managed to get working code. There's still a way to go, and it turns up in HA as "temperature" as opposed to something sensible like keypress... but I'll take it :-)

#include <Arduino.h>

#ifndef ZIGBEE_MODE_ED
#error "Zigbee end device mode is not selected in Tools->Zigbee mode"
#endif

#include "Zigbee.h"

const char* MANUFACTURER_NAME = "SeeedC6-RS10";
const char* MODEL_NAME = "RS10_Remote";

const int ROWS = 5;
const int COLS = 4;

int rowPins[ROWS] = {16, 17, 19, 20, 18};
int colPins[COLS] = {2, 21, 22, 23};

char keys[ROWS][COLS] = {
  {'1','2','3','A'},
  {'4','5','6','B'},
  {'7','8','9','C'},
  {'*','0','#','D'},
  {'E','F','G','H'}
};

const int LED_PIN = 1;
const uint8_t ENDPOINT = 10;

ZigbeeTempSensor zbTempSensor(ENDPOINT);

const char keyList[20] = {
  '1','2','3','A',
  '4','5','6','B',
  '7','8','9','C',
  '*','0','#','D',
  'E','F','G','H'
};

uint8_t keyToIndex(char key) {
  for (uint8_t i = 0; i < 20; i++) {
    if (keyList[i] == key) return i + 1;
  }
  return 0;
}

void sendKey(char key) {
  uint8_t idx = keyToIndex(key);
  if (idx == 0) return;

  // Use multiples of 5 so consecutive keys are always > 0.5 tolerance apart
  float value = (float)(idx);
  Serial.printf("Key: %c -> index %d -> temp %.1f\n", key, idx, value);

  zbTempSensor.setTemperature(value);
  delay(10);  // give the stack a tick before reporting
  bool ok = zbTempSensor.reportTemperature();
  Serial.printf("Report sent: %s\n", ok ? "YES" : "NO");

  digitalWrite(LED_PIN, HIGH);
  delay(300);
  digitalWrite(LED_PIN, LOW);
}

void blinkLedDuringStartup() {
  digitalWrite(LED_PIN, HIGH);
  delay(150);
  digitalWrite(LED_PIN, LOW);
  delay(150);
}

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

  pinMode(LED_PIN, OUTPUT);
  digitalWrite(LED_PIN, LOW);

  for (int r = 0; r < ROWS; r++) {
    pinMode(rowPins[r], OUTPUT);
    digitalWrite(rowPins[r], HIGH);
  }

  for (int c = 0; c < COLS; c++) {
    pinMode(colPins[c], INPUT_PULLUP);
  }

  zbTempSensor.setManufacturerAndModel(MANUFACTURER_NAME, MODEL_NAME);
  zbTempSensor.setMinMaxValue(0, 100);
  zbTempSensor.setDefaultValue(0.0);
  zbTempSensor.setTolerance(0.5);
  zbTempSensor.setPowerSource(ZB_POWER_SOURCE_BATTERY, 100, 35);
  // addTimeCluster() removed - caused null pointer on C6 ED builds

  Zigbee.addEndpoint(&zbTempSensor);

  Serial.print("Starting Zigbee for ");
  Serial.println(MODEL_NAME);

  if (!Zigbee.begin()) {
    Serial.println("Zigbee failed to start!");
    ESP.restart();
  }

  Serial.println("Connecting to network");
  while (!Zigbee.connected()) {
    blinkLedDuringStartup();
    Serial.print(".");
    delay(100);
  }
  Serial.println("Successfully connected to Zigbee network");

  // Stack is fully up and joined - safe to configure reporting now
  zbTempSensor.setReporting(1, 3600, 1);
}

void loop() {
  for (int r = 0; r < ROWS; r++) {
    digitalWrite(rowPins[r], LOW);

    for (int c = 0; c < COLS; c++) {
      if (digitalRead(colPins[c]) == LOW) {
        delay(20);
        if (digitalRead(colPins[c]) == LOW) {
          sendKey(keys[r][c]);
          while (digitalRead(colPins[c]) == LOW) {
            delay(10);
          }
        }
      }
    }

    digitalWrite(rowPins[r], HIGH);
  }

  delay(10);
}

This is what I believe is the final code.

On the C6 Zero, pin 0 is the wake, where the diodes converge.
Pin 1 is the power for the LED.
Pin 2 to 5 are the column pins. They provide signal to power the keyboard matrix when it is in deep sleep.
Pins 6 to 10 are the receiving rows of the matrix. They also have diodes which feed to pin 0 for wake.
That should be it.

Hold 1 down for 5 seconds to pair.

#include <Arduino.h>
#ifndef ZIGBEE_MODE_ED
#error "Zigbee end device mode is not selected in Tools->Zigbee mode"
#endif
#include "Zigbee.h"
#include <Preferences.h>

// ===================== CONFIG =====================
const char* MANUFACTURER_NAME = "SeeedC6-RS10";
const char* MODEL_NAME        = "RS10_Remote";

#define WAKE_PIN       0
const int ROWS         = 5;
const int COLS         = 4;
int rowPins[ROWS]      = {16, 17, 19, 20, 18};
int colPins[COLS]      = {2, 21, 22, 23};
const int LED_PIN      = 1;
const uint8_t ENDPOINT = 10;

ZigbeeTempSensor zbTempSensor(ENDPOINT);
Preferences prefs;

// ===================== HELPERS =====================
bool isKey1Pressed() {
  for (int r = 0; r < ROWS; r++) digitalWrite(rowPins[r], LOW);
  delay(2);
  bool held = (digitalRead(colPins[0]) == LOW);
  for (int r = 0; r < ROWS; r++) digitalWrite(rowPins[r], HIGH);
  return held;
}

// ===================== SEND =====================
void sendKey(int r, int c) {
  uint8_t idx = r * COLS + c + 1;
  Serial.printf("Key at [%d,%d] -> index %d\n", r, c, idx);
  zbTempSensor.setTemperature((float)idx);
  delay(10);
  bool ok1 = zbTempSensor.reportTemperature();
  Serial.printf("Report sent: %s\n", ok1 ? "YES" : "NO");
  delay(700);
  zbTempSensor.setTemperature(0);
  delay(10);
  bool ok2 = zbTempSensor.reportTemperature();
  Serial.printf("Zero sent: %s\n", ok2 ? "YES" : "NO");
  digitalWrite(LED_PIN, HIGH);
}

// ===================== SLEEP =====================
void prepForSleep() {
  for (int c = 0; c < COLS; c++) {
    pinMode(colPins[c], OUTPUT);
    digitalWrite(colPins[c], HIGH);
  }
  delay(5);
}

// ===================== REPAIR MODE =====================
void startRepairMode() {
  Serial.println("=== REPAIR MODE: flagging rejoin and rebooting ===");
  // Rapid flashes to confirm repair triggered
  for (int i = 0; i < 6; i++) {
    digitalWrite(LED_PIN, HIGH); delay(150);
    digitalWrite(LED_PIN, LOW);  delay(150);
  }
  // Write flag to NVS — survives software reset
  prefs.begin("app", false);
  prefs.putBool("rejoin", true);
  prefs.end();
  esp_restart();
}

// ===================== SCAN =====================
bool scanAnyKey(int &row, int &col) {
  for (int r = 0; r < ROWS; r++) {
    digitalWrite(rowPins[r], LOW);
    for (int c = 0; c < COLS; c++) {
      if (digitalRead(colPins[c]) == LOW) {
        delay(20);  // debounce
        if (digitalRead(colPins[c]) == LOW) {
          digitalWrite(rowPins[r], HIGH);

          if (r == 0 && c == 0) {
            Serial.println("Key 1 down — timing hold...");
            digitalWrite(LED_PIN, HIGH);
            unsigned long holdStart = millis();

            while (isKey1Pressed()) {
              delay(50);
              if (millis() - holdStart >= 5000) {
                digitalWrite(LED_PIN, LOW);
                startRepairMode();  // does not return
              }
            }

            // Released before 5 s — normal short press
            digitalWrite(LED_PIN, LOW);
            row = r; col = c;
            return true;

          } else {
            row = r; col = c;
            return true;
          }
        }
      }
    }
    digitalWrite(rowPins[r], HIGH);
  }
  return false;
}

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

  pinMode(LED_PIN, OUTPUT);
  digitalWrite(LED_PIN, LOW);

  pinMode(WAKE_PIN, INPUT_PULLDOWN);
  esp_sleep_enable_ext1_wakeup(
    (1ULL << WAKE_PIN),
    ESP_EXT1_WAKEUP_ANY_HIGH
  );

  for (int r = 0; r < ROWS; r++) {
    pinMode(rowPins[r], OUTPUT);
    digitalWrite(rowPins[r], HIGH);
  }
  for (int c = 0; c < COLS; c++) {
    pinMode(colPins[c], INPUT_PULLUP);
  }

  zbTempSensor.setManufacturerAndModel(MANUFACTURER_NAME, MODEL_NAME);
  zbTempSensor.setMinMaxValue(0, 100);
  zbTempSensor.setDefaultValue(0.0);
  zbTempSensor.setTolerance(0.5);
  zbTempSensor.setPowerSource(ZB_POWER_SOURCE_BATTERY, 100, 35);
  Zigbee.addEndpoint(&zbTempSensor);

  // Check NVS for rejoin flag
  prefs.begin("app", false);
  bool forceRejoin = prefs.getBool("rejoin", false);
  if (forceRejoin) {
    prefs.putBool("rejoin", false);  // clear immediately so next boot is normal
  }
  prefs.end();

  if (forceRejoin) {
    Serial.println("Rejoin flag set — erasing NVS and re-pairing...");
    Zigbee.begin(ZIGBEE_END_DEVICE, true);  // erase_nvs = true

    const unsigned long PAIR_TIMEOUT_MS = 60000UL;
    unsigned long pairStart = millis();

    while (!Zigbee.connected()) {
      if (millis() - pairStart > PAIR_TIMEOUT_MS) {
        Serial.println("Pairing timeout — sleeping.");
        break;
      }
      // 1 Hz flash while searching for coordinator
      digitalWrite(LED_PIN, HIGH); delay(500);
      digitalWrite(LED_PIN, LOW);  delay(500);
    }

    if (Zigbee.connected()) {
      Serial.println("Re-paired successfully.");
      for (int i = 0; i < 3; i++) {
        digitalWrite(LED_PIN, HIGH); delay(100);
        digitalWrite(LED_PIN, LOW);  delay(100);
      }
    }

    prepForSleep();
    esp_deep_sleep_start();
  }

  // Normal boot
  Zigbee.begin();
  Serial.println("Boot complete");
}

// ===================== LOOP =====================
void loop() {
  int r, c;

  if (scanAnyKey(r, c)) {
    digitalWrite(LED_PIN, HIGH);
    sendKey(r, c);

    // Hold LED on until key is physically released
    while (true) {
      bool anyHeld = false;
      for (int row = 0; row < ROWS; row++) {
        digitalWrite(rowPins[row], LOW);
        for (int col = 0; col < COLS; col++) {
          if (digitalRead(colPins[col]) == LOW) anyHeld = true;
        }
        digitalWrite(rowPins[row], HIGH);
      }
      if (!anyHeld) break;
      delay(10);
    }
    digitalWrite(LED_PIN, LOW);
  }

  // Always connect-check then sleep, whether a key fired or not
  if (!Zigbee.connected()) {
    unsigned long start = millis();
    while (!Zigbee.connected()) {
      if (millis() - start > 2000) break;
      delay(10);
    }
  }
  
  Serial.println("Going to sleep");
  prepForSleep();
  esp_deep_sleep_start();
}

There is an optional file, RS10_Remote.js which is loaded to HA at /homeassistant/zigbee2mqtt/external_converters which will rename the temperature entity to keypress.

const fz = require('zigbee-herdsman-converters/converters/fromZigbee');
const exposes = require('zigbee-herdsman-converters/lib/exposes');
const e = exposes.presets;
module.exports = {
    fingerprint: [
        {
            modelID: 'RS10_Remote',
            manufacturerName: 'SeeedC6-RS10',
        },
    ],
    model: 'RS10_Remote',
    vendor: 'Seeed',
    description: 'RS10 Remote (custom mapping: temperature -> keypress)',
    fromZigbee: [
        {
            cluster: 'msTemperatureMeasurement',
            type: ['attributeReport', 'readResponse'],
            convert: (model, msg) => {
                if (msg.data && msg.data.measuredValue !== undefined) {
                    const raw = msg.data.measuredValue;
                    return {
                        keypress: raw > 50 ? raw / 100 : raw,
                    };
                }
            },
        },
    ],
    toZigbee: [],
    exposes: [
        exposes.numeric('keypress', exposes.access.STATE)
            .withDescription('Key press index reported via temperature cluster'),
    ],
};

This is the rather messy dev board implementation.

And lastly, a rough schematic...

Um... this has gone slightly wrong. AI took out the reset code, so I'm battling to get it back in again, and now it isn't working. Sorry

Code updated. That should hopefully be fixed. I think one of the things that drove me nuts here was updating and checking was leaving bits around and I got confused at some points why things weren't working. I should have been more patient.

This project appears to have a problem. The unit is never actually making it into deep sleep. It seems to be crashing immediately after the temperature report. Not sure what I'm dealing with, but it's taking more investigation.

Ooookaaayyyy..... code and circuit diagram updated.

  1. There was a problem with the float of GPIO0, and it took a 5 Meg Ohm resistance to pin it to ground but still allow the weak voltage from the other pins, to wake it.

  2. There was a problem with the converter and the numbers coming through. I had to put a condition in so that it only divised the temperature by 100, if it was actually greater than 50. So many packets were actually sunk by the converter and I couldn't see where the problem was.

  3. There were other processing issues, but as well as straightening LED behaviour, the number should return to 0 after 700ms, so it handles the same button being pressed twice.

  4. After flashing, the pins are in the wrong state so it needs to be rebooted after flashing to be in a working state.

(Phew.)

@Railroader comments are right.
I saw your post and as a Zigbee user, took a look at it.
Although it was Zigbee, the only implementation I'm familiar with is with using Digi International Series 1, 2 and 3 Xbees.
I don't use Home Assistant but run everything on an Atmega 2560.
As such, it's a system I wouldn't expect to get much help if it went wrong.
The only comment I would make is that Xbee's wouldn't be my first choice now.
It's far from 100% reliable and despite a physically small area to cover with coordinator, quite a few routers and several sleeping end devices, dropped transmissions are commonplace.
The only good thing about it is the Digi configuration tool, XCTU.
And it's expensive too.