ESP32-H2 Zigbee Node (Z2M) mit "Visual Studio Code" und "ESP-IDF Espressif Framework" entwickeln

Hallo, ich versuche mich gerade an der Espressif-IDF in Kombination mit VS Code, alternativ zum Arduino IDE und könnte dabei etwas Starthilfe gebrauchen…

Ich habe “Visual Studio Code” unter Windows 11 installiert, es kommt den von mir verwendeten JetBrains IDEs recht nahe und stellt ansich kein Problem dar.
Das Espressif-IDF SDK ist da schon ne andere Hausnummer. Das habe ich laut mit dem VSCode-Plugin runtergeladen und installiert und es auch irgendwie mal geschafft das Blink-Beispiel zu flashen.

Nun möchte ich einen Zigbee-Node (ZED) bauen welcher per PWM eine einfarbige LED an einem GPIO an/aus und dimmen kann. Farbe benötige ich nicht dazu.

Grundlegend muss ich einen Node bauen der sich dann später im HA mittels Z2M einbettet. Soweit ich das verstanden habe muss man dafür bestimmte “Cluster” bereitstellen. Für die LED AN/AUS kommt wohl das “On/Off Cluster”, für die Helligkeitssteuerung das “Level Cluster” in Frage. Darüber hinaus braucht es wohl auch noch ein paar grundlegende Parameter die im “Basic Cluster” enthalten sind (Wie heißt mein Node, welcher Typ ist er, welche Stromversorgung hat er, etc.). In der ersten Kommunikation mit HA, dem “Interview” übermittelt der Zigbee-Node seine Eigenschaften ans HA und dieses stellt dann entsprechende Bedienelemente/Anzeigen zur Verfügung.

Wie fange ich da jetzt an die Dinge zu verstehen? Ich finde viel Information im Netz und über KI Bots aber das bringt mich alles nicht weiter, weil die teils falsch oder mit anderen Versionen arbeiten.

Also versuche ich zunächst das Light-Beispiel zu verstehen, was schonmal daran scheitert das die Beispiele Fehler enthalten, z.B. wird in der main/CMakeLists.txt auf die ZCL-Utility verwiesen, die es in dem angegebenen Pfad garnicht gibt:

idf_component_register(
    SRC_DIRS  "." "../../../common/zcl_utility/src"
    INCLUDE_DIRS "." "../../../common/zcl_utility/include"
    PRIV_REQUIRES nvs_flash esp_driver_uart ieee802154
)

Die befindet sich im Examples Verzeichnis (bei mir unter %USERPROFILE%\esp\v5.5.1\esp-idf\examples\zigbee). Korrigiert man den Pfad, bzw. importiert man dieses Unterverzeichnis in sein Projekt, klappt es auch mit dem Compile.

Dann aber erkenne ich das das Beispiel die LED-Strip Library verwendet um die On-Board WS8211 RGB-LED anzusteuern, für ein plumpes on/off… ich aber möchte nur einen GPIO ansteuern, den dann aber mit PWM.

Aber weiter zum Verständnis des Code und der Zigbee Cluster. In “main/esp_zb_light.c” findet man die Initialisierungs-Routine die from Zigbee-Framework aufgerufen wird:

static void esp_zb_task(void *pvParameters)
{
    /* initialize Zigbee stack */
    esp_zb_cfg_t zb_nwk_cfg = ESP_ZB_ZED_CONFIG();
    esp_zb_init(&zb_nwk_cfg);
    esp_zb_on_off_light_cfg_t light_cfg = ESP_ZB_DEFAULT_ON_OFF_LIGHT_CONFIG();
    esp_zb_ep_list_t *esp_zb_on_off_light_ep = esp_zb_on_off_light_ep_create(HA_ESP_LIGHT_ENDPOINT, &light_cfg);
    zcl_basic_manufacturer_info_t info = {
        .manufacturer_name = ESP_MANUFACTURER_NAME,
        .model_identifier = ESP_MODEL_IDENTIFIER,
    };

    esp_zcl_utility_add_ep_basic_manufacturer_info(esp_zb_on_off_light_ep, HA_ESP_LIGHT_ENDPOINT, &info);
    esp_zb_device_register(esp_zb_on_off_light_ep);
    esp_zb_core_action_handler_register(zb_action_handler);
    esp_zb_set_primary_network_channel_set(ESP_ZB_PRIMARY_CHANNEL_MASK);
    ESP_ERROR_CHECK(esp_zb_start(false));
    esp_zb_stack_main_loop();
}

Zunächst wohl die grundlegende Initialisierung eines Zigbee Endgerätes (ZED):

esp_zb_cfg_t zb_nwk_cfg = ESP_ZB_ZED_CONFIG();
esp_zb_init(&zb_nwk_cfg);

Hier wird es interessant. Es wird eine Variable “light_cfg” vom Typ “esp_zb_on_off_light_cfg_t” erzeugt und über die Funktion “ESP_ZB_DEFAULT_ON_OFF_LIGHT_CONFIG()” mit Inhalt befüllt:

    esp_zb_on_off_light_cfg_t light_cfg = ESP_ZB_DEFAULT_ON_OFF_LIGHT_CONFIG();

Die Datei “esp_zigbee_type.h” definiert diesen Typ

typedef struct esp_zb_on_off_light_cfg_s {
    esp_zb_basic_cluster_cfg_t basic_cfg;       /*!<  Basic cluster configuration, @ref esp_zb_basic_cluster_cfg_s */
    esp_zb_identify_cluster_cfg_t identify_cfg; /*!<  Identify cluster configuration, @ref esp_zb_identify_cluster_cfg_s */
    esp_zb_groups_cluster_cfg_t groups_cfg;     /*!<  Groups cluster configuration, @ref esp_zb_groups_cluster_cfg_s */
    esp_zb_scenes_cluster_cfg_t scenes_cfg;     /*!<  Scenes cluster configuration, @ref esp_zb_scenes_cluster_cfg_s */
    esp_zb_on_off_cluster_cfg_t on_off_cfg;     /*!<  On off cluster configuration, @ref esp_zb_on_off_cluster_cfg_s */
} esp_zb_on_off_light_cfg_t;

und die Funktion (eher ein Makro, kommt aus “esp_zigbee_ha_standard.h”) befüllt die Member des Structs mit:

/**
 * @brief Zigbee HA standard on-off light device default config value.
 *
 */
#define ESP_ZB_DEFAULT_ON_OFF_LIGHT_CONFIG()                                                        \
    {                                                                                               \
        .basic_cfg =                                                                                \
            {                                                                                       \
                .zcl_version = ESP_ZB_ZCL_BASIC_ZCL_VERSION_DEFAULT_VALUE,                          \
                .power_source = ESP_ZB_ZCL_BASIC_POWER_SOURCE_DEFAULT_VALUE,                        \
            },                                                                                      \
        .identify_cfg =                                                                             \
            {                                                                                       \
                .identify_time = ESP_ZB_ZCL_IDENTIFY_IDENTIFY_TIME_DEFAULT_VALUE,                   \
            },                                                                                      \
        .groups_cfg =                                                                               \
            {                                                                                       \
                .groups_name_support_id = ESP_ZB_ZCL_GROUPS_NAME_SUPPORT_DEFAULT_VALUE,             \
            },                                                                                      \
        .scenes_cfg =                                                                               \
            {                                                                                       \
                .scenes_count = ESP_ZB_ZCL_SCENES_SCENE_COUNT_DEFAULT_VALUE,                        \
                .current_scene = ESP_ZB_ZCL_SCENES_CURRENT_SCENE_DEFAULT_VALUE,                     \
                .current_group = ESP_ZB_ZCL_SCENES_CURRENT_GROUP_DEFAULT_VALUE,                     \
                .scene_valid = ESP_ZB_ZCL_SCENES_SCENE_VALID_DEFAULT_VALUE,                         \
                .name_support = ESP_ZB_ZCL_SCENES_NAME_SUPPORT_DEFAULT_VALUE,                       \
            },                                                                                      \
        .on_off_cfg =                                                                               \
            {                                                                                       \
                .on_off = ESP_ZB_ZCL_ON_OFF_ON_OFF_DEFAULT_VALUE,                                   \
            },                                                                                      \
    }

Anstelle also zu wissen was ein “Light” Zigbee-Node so alles an Cluster bereitstellen muss um in HA ordentlich zu arbeiten, ist das hier in eine Funktion gegossen. Vermutlich könnte man diesen Einzeiler auf in Einzelne Kommandos auflösen die jeden Cluster separat definiert. Hier enthalten ist:

  • Basic Cluster
  • Identify Cluster
  • Groups Cluster
  • Screnes Cluster
  • On/Off Cluster

Du hast zumindest in dem was du geschrieben hast einen Denkfehler, ich weiß nicht ob der auch in deinem Kopf ist: :winking_face_with_tongue:

Home Assistant hat nix mit Zigbee zu tun. Auch wenn du dir jetzt ein Device bastelst, das Zigbee spricht, muss dein Zigbee-Broker (also Z2M, ZHA, Conbee…) wissen, was man mit dem Moped anstellen kann. Die Broker bedienen dabei nicht blind alles Zigbee-ige was sich bei ihnen meldet, sondern sie müssen das Gerät kennen. Ich muss jetzt als Beispiel ZHA nehmen, weil ich mich damit beschäftigt habe: wenn du ein Zigbee-Gerät gebaut hast, musst du danach auch einen Custom-Quirk ins ZHA bauen, damit der weiß was damit zu tun ist. (Z2M vermutlich das selbe mit anderer Terminologie) Und dann muss der Broker im Letzten Schritt in Home Assistant Entitäten erzeugen, aber da bist du noch nicht.

Danke, das verstehe ich soweit (zumindest glaube ich es). Genau das sind ja die Definitionen von Zigbee 3 und den Clustern und Attributen, etc. Wenn ich also in HA ein “Licht” haben und ansteuern will, dann muss mein Device entsprechende Attribute und Endpunkte dafür bereitstellen. Da gibt es ganz sicher eine Definition dafür.

Immerhin habe ich es mit dem On/Off Example und ein wenig hacken geschafft einen GPIO13 (blaue LED auf dem ESP32-H2 Super-Mini Board) ins Z2M von HA aufzunehmen und anzusteuern:

Der Art der Stromversorgung (“power_source”) wird im Basic-Cluster definiert. Im ESP-IDF ist dafür dieser Struct verfügbar:

typedef struct esp_zb_basic_cluster_cfg_s {
    uint8_t  zcl_version;                       /*!<  ZCL version */
    uint8_t  power_source;                      /*!<  The sources of power available to the device */
} esp_zb_basic_cluster_cfg_t;

Er ist Teil der Typendefinition von “esp_zb_on_off_light_cfg_s”. Die Instanz-Variable “light_cfg” im Code wird durch die Funktion “ESP_ZB_DEFAULT_ON_OFF_LIGHT_CONFIG()” erzeugt. Für den Member .basic_cfg.power_source wird die Defintion ESP_ZB_ZCL_BASIC_POWER_SOURCE_DEFAULT_VALUE verwendet, welche in esp_zigbee_zcl_basic.h mit 0x00 (=Netzbetrieb) vorbelegt wird. Batteriebetrieb hätte den Wert 0x03. Eine global verfügbare Definition für diesen Wert habe ich nicht direkt gefunden, also habe ich sie einfach der “esp_zb_light.h” hinzugefügt:

#define ESP_ZB_ZCL_BASIC_POWER_SOURCE_BATTERY 0x03

Dann ändere ich die Energiequelle nach der Initialisierung von “light_cfg”:

    // set power-source to battery
    light_cfg.basic_cfg.power_source = ESP_ZB_ZCL_BASIC_POWER_SOURCE_BATTERY;

und Tataaa:

Jetzt habe ich mir mal den Identify-Cluster vorgenommen. Damit kann man einzelne Endgeräte erkennen, indem diese sich auf Befehl “bemerkbar” machen (z.B. eine LED blinken lassen oder ein Tonsignal von sich geben) was meiner Meinung nach viel zu selten unterstützt wird und doch so hilfreich sein kann.

Im HA Z2M Entwickler-Konsole kann man diesen dann z.B. so bedienen:


Die “identifytime” legt fest wie lange die Signalgebung läuft (Sekunden). Stellt man sie auf “0” wird der Identify gestoppt. Der Zigbee Standard besagt das die Identifikations-Blink-Periode 0,5s sein soll, sieht aber auch noch mehr Spielereien vor. Ich denke das reicht für meinen Anwendungsfall, bei dem dann die Lichterkette blinkt.

Umgesetzt habe ich das in der Funktion “zb_attribute_handler()” die vom Zigbee-Stack beim eintreffen einer Cluster-Nachricht aufgerufen wird:

static esp_err_t zb_attribute_handler(const esp_zb_zcl_set_attr_value_message_t *message)
{
    esp_err_t ret = ESP_OK;
    bool light_state = 0;

    ESP_RETURN_ON_FALSE(message, ESP_FAIL, TAG, "Empty message");
    ESP_RETURN_ON_FALSE(message->info.status == ESP_ZB_ZCL_STATUS_SUCCESS, ESP_ERR_INVALID_ARG, TAG, "Received message: error status(%d)",
                        message->info.status);

    ESP_LOGI(TAG, "Received message: endpoint(%d), cluster(0x%x), attribute(0x%x), data size(%d)", message->info.dst_endpoint, message->info.cluster,
             message->attribute.id, message->attribute.data.size);

    if (message->info.dst_endpoint == ESP_ZED_ENDPOINT) {
        if (message->info.cluster == ESP_ZB_ZCL_CLUSTER_ID_ON_OFF) {
            ...
        }
        else if (message->info.cluster == ESP_ZB_ZCL_CLUSTER_ID_IDENTIFY) {
            if (message->attribute.id == ESP_ZB_ZCL_ATTR_IDENTIFY_IDENTIFY_TIME_ID) {
                uint16_t identify_time = *(uint16_t *)message->attribute.data.value;
                ESP_LOGI(TAG, "Identify time set to: %d seconds", identify_time);
                if (identify_time > 0) {
                    start_identify_blink(ESP_ZB_ZCL_IDENTIFY_EFFECT_ID_BLINK, identify_time);
                } else {
                    start_identify_blink(ESP_ZB_ZCL_IDENTIFY_EFFECT_ID_STOP, 0);
                }
            }
        }
    }
    return ret;
}

Realisiert habe ich den Identifier mittels zweier Timer. Einer ist für das Blinken zuständig und einer für die Gesamtdauer der Idenfitikation:

static TimerHandle_t identify_timer = NULL;
static TimerHandle_t identify_timeout_timer = NULL;

...

static void identify_timer_timeout_callback(TimerHandle_t timer)
{
    xTimerStop(identify_timer, 0);
    xTimerStop(identify_timeout_timer, 0);
    light_driver_set_power(LIGHT_OFF);
}

static void identify_timer_callback(TimerHandle_t timer)
{
    static bool led_state = false;
    light_driver_set_power(led_state);
    led_state = !led_state;
}

static void start_identify_blink(uint8_t effect_id, uint16_t identify_time)
{
    if (effect_id == ESP_ZB_ZCL_IDENTIFY_EFFECT_ID_BLINK) {
        identify_timeout_timer = xTimerCreate("identityTimeout", pdMS_TO_TICKS(identify_time * 1000), pdTRUE, NULL, identify_timer_timeout_callback);
        if (identify_timer == NULL) {
            identify_timer = xTimerCreate("identify", pdMS_TO_TICKS(500), pdTRUE, NULL, identify_timer_callback);
        }
        xTimerStart(identify_timeout_timer, 0);
        xTimerStart(identify_timer, 0);
    } else if (effect_id == ESP_ZB_ZCL_IDENTIFY_EFFECT_ID_STOP) {
        xTimerStop(identify_timer, 0);
        light_driver_set_power(0);
    }
}

Ich bin übrigens jetzt komplett von den Cluster-Marko-Funktionen auf Einzelfunktionen umgestiegen, einfach weil ich dann mehr Gefühl dafür habe was mein Code kann und was nicht.

Für den interessierten Programmierer hier mal mein Cluster-Code:

static void esp_zb_task(void *pvParameters)
{
    /* initialize Zigbee stack */
    esp_zb_cfg_t zb_nwk_cfg = ESP_ZB_ZED_CONFIG();
    esp_zb_init(&zb_nwk_cfg);

    // Create endpoint list
    esp_zb_ep_list_t *ep_list = esp_zb_ep_list_create();
    
    // Define endpoint configuration
    esp_zb_endpoint_config_t endpoint_config = {
        .endpoint = ESP_ZED_ENDPOINT,
        .app_profile_id = ESP_ZB_AF_HA_PROFILE_ID,
        .app_device_id = ESP_ZB_HA_ON_OFF_LIGHT_DEVICE_ID,
        .app_device_version = 0,
    };
        
    // create cluster list
    esp_zb_cluster_list_t *cluster_list = esp_zb_zcl_cluster_list_create();

    //------------------------ Create Basic Cluster ----------------------------
    esp_zb_basic_cluster_cfg_t basic_cfg = {
        .zcl_version = ESP_ZB_ZCL_BASIC_ZCL_VERSION_DEFAULT_VALUE,
        .power_source = ESP_ZB_ZCL_BASIC_POWER_SOURCE_BATTERY
    };
    esp_zb_attribute_list_t *basic_cluster = esp_zb_basic_cluster_create(&basic_cfg);
    // Add attributes to Basic cluster
    esp_zb_basic_cluster_add_attr(basic_cluster, ESP_ZB_ZCL_ATTR_BASIC_MANUFACTURER_NAME_ID, "\x09Espressif"); // Pascal-String
    esp_zb_basic_cluster_add_attr(basic_cluster, ESP_ZB_ZCL_ATTR_BASIC_MODEL_IDENTIFIER_ID,  "\x08ESP32H2");
    // Add to cluster list
    esp_zb_cluster_list_add_basic_cluster(cluster_list, basic_cluster, ESP_ZB_ZCL_CLUSTER_SERVER_ROLE);

    //------------------------ Create On/Off Cluster ----------------------------
    esp_zb_on_off_cluster_cfg_t on_off_cfg = {
        .on_off = false  // Initial state: off
    };
    esp_zb_attribute_list_t *on_off_cluster = esp_zb_on_off_cluster_create(&on_off_cfg);
    // Add attributes to Basic cluster
    // Add to cluster list
    esp_zb_cluster_list_add_on_off_cluster(cluster_list, on_off_cluster, ESP_ZB_ZCL_CLUSTER_SERVER_ROLE);

    //------------------------ Create Identify Cluster (Server-Role) ----------------------------
    esp_zb_identify_cluster_cfg_t identify_cfg = {
        .identify_time = 10
    };
    esp_zb_attribute_list_t *identitfy_cluster = esp_zb_identify_cluster_create(&identify_cfg);
    // Add attributes to identify cluster
    // Add to cluster list
    esp_zb_cluster_list_add_identify_cluster(cluster_list, identitfy_cluster, ESP_ZB_ZCL_CLUSTER_SERVER_ROLE);

    //------------------------ Create Groups Cluster ----------------------------
    esp_zb_groups_cluster_cfg_t group_cfg = {
            .groups_name_support_id =   true
    };
    esp_zb_attribute_list_t *groups_cluster = esp_zb_groups_cluster_create(&group_cfg);
    // Add attributes to groups cluster
    // Add to cluster list
    esp_zb_cluster_list_add_groups_cluster(cluster_list, groups_cluster, ESP_ZB_ZCL_CLUSTER_SERVER_ROLE);

    //------------------------ Create Scene Cluster ----------------------------
    esp_zb_scenes_cluster_cfg_t scene_cfg = {
        .current_group = 0,
        .current_scene = 0,
        .name_support = true,
        .scene_valid = true,
        .scenes_count = 0
    };
    esp_zb_attribute_list_t *scenes_cluster = esp_zb_scenes_cluster_create(&scene_cfg);
    // Add attributes to scene cluster
    // Add to cluster list
    esp_zb_cluster_list_add_scenes_cluster(cluster_list, scenes_cluster, ESP_ZB_ZCL_CLUSTER_SERVER_ROLE);

    // Add cluster list to endpoint
    esp_zb_ep_list_add_ep(ep_list, cluster_list, endpoint_config);

    // Add cluster list to endpoint
    esp_zb_device_register(ep_list);

    //esp_zb_device_add_set_attr_value_cb(zb_attribute_handler); // act on attribute changes only
    esp_zb_core_action_handler_register(zb_action_handler); // act on everything
    esp_zb_set_primary_network_channel_set(ESP_ZB_PRIMARY_CHANNEL_MASK);
    ESP_ERROR_CHECK(esp_zb_start(false));
    esp_zb_stack_main_loop();
}

Als nächstes nehme ich mir nun die Helligkeitssteuerung vor. Dazu möchte ich den GPIO an dem über eine KSQ meine LED dran hängt per PWM in der Helligkeit einstellen. Das wird also ein Level-Cluster.

:crayon:by HarryP: Zusammenführung Doppelpost (bei Änderungen oder hinzufügen von Inhalten bitte die „Bearbeitungsfunktion“ anstatt „Antworten“ zu nutzen)

@harryp Ich habe mir schon was dabei gedacht das in einzelne Posts zu setzen, so könnte man dem Thema besser und strukturiert folgen. Nun ist alles in einem riesigen Sums den vermutlich keiner liest. Aber da ich hier scheinbar ohnehin einen Monolog führe stelle ich meinen Report ein. Das Thema ist in diesem Forum womöglich falsch platziert da es ja eher um Programmierung geht?

Ich folge deinem Monolog gebannt :smiley:

Du hast jetzt allerdings eine Tiefe erreicht, in dem ich nur noch zum „rubberducking“ taugen würde. Gegenwärtig beschränkt sich mein Wissen auf das einbinden offiziell nicht supporteter Aliexpress-Module. Mein komplett eigenes Zigbee-Gerät zu programmieren, steht auf der Bucket-Liste.

Wegen 2 zusammengefassten Posts - da muss ich einfach nur lachen.

Solche Tutorials gehören eigentlich in den Showroom (wo ich es hin verschoben habe), aber da Du ja der Meinung bist, dass

Steht es es Dir frei, den Monolog zu beenden.

@cHAOS vielleicht willst du dein Projekt zu Github verschieben, da findest du vielleicht auch das interessiertere Publikum

@harryp bei allem Verständnis, dass du hier sehr viel Arbeit machst, die du Mega machst und einen riesen Dienst für die Community leistest, für den ich dir von Herzen danke: die Antwort war ein bisschen harsch formuliert :innocent:

1 „Gefällt mir“

@harryp danke fürs verschieben. Heißt das das ich hier nun nach Bedarf einzelne Posts erstellen und nachpflegen kann ohne Dir unnötige Arbeit zu bereiten?

@falkoz danke für Dein Feedback und Interesse. Sehr gern führe ich fort. Eingangs ging es mir ja mehr um Fragen und Hilfe zur Programmierung, aber natürlich käme unweigerlich die Aufgabe auf mich zu mein Zigbee Device auch für HA (Z2M) verfügbar zu machen. Ich habe mir das Prinzip mittels “external converter” schon angesehen, aber das stand noch im Nachgang. Hier würde ich sehr gern auf Deine Expertise zurückgreifen.

Richtig spannend wird nun der Teil um Energiesparfunktionen, denn mein Akku soll ja möglichst lange halten.

Ja, wenn es im Rahmen bleibt! :wink:

Also, weiter gehts. Die Helligkeitssteuerung habe ich nun implementiert und dabei hat sich gezeigt das mein Ansatz, die Clusterconfig individuell und nicht mit HA-Makros zusammenzustellen genau richtig und hilfreich war, vor allem was den Lerneffekt angeht.

Ich expose nun zusätzlich den Level-Cluster mit dem ich die LED in der Helligkeit steuere. Die LED hängt über eine KSQ (Konstantstromquelle) mit deren Enable an einem PWM-fähigen IO-Pin (sind beim ESP32-H2 fast alle). Dazu initialisiert man einen PWM Timer und steuert über den Duty-Cycle, also das Verhältnis von EIN und AUS Schaltzeit bei einer bestimmten Frequenz (ich habe 5kHz gewählt) die effektive Helligkeit, aber natürlich auch den Stromverbrauch. D.h. bei nachlassender Akkukapazität könnte man die Helligkeit reduzieren um eine längere Laufzeit zu ermöglichen, oder aber man passt die Helligkeit der Umgebungshelligkeit an.
Der Zigbee Standard besagt das die Helligkeit in einem uint8 mit den Werten 0 (0%) bis 254 (100%) gesteuert wird. Meine PWM setzt die Duty-Werte im Bereich 0..1023, da muss man also etwas transponieren.

Als KSQ habe ich diese hier genommen:


Aber grundsätzlich muss die auf die LED abgestimmt sein bzw. angestimmt werden. Meine LED möchte 100 mA bei einer Durchlassspannung von 2,6V, also trimme ich die an einem Labornetzteil mit Amperemeter und fixiere dann den Poti mit Lack bzw ersetze ihn durch einen Festwiderstand.
Optimal für die LED Lebensdauer wäre es natürlich einen Stromsensor im Ausgangspfad zu haben, oder gar eine digital regelbare KSQ, aber für meinen Anwendungsfall etwas Overkill.

In Kombination mit dem On/Off Cluster habe ich nun folgende Login implementiert:

  • Init nach Reboot ohne Flash Parameter => LED ist aus und Helligkeit auf 100% gestellt.
  • LED Off => Die aktuell eingestellte Helligkeit wird gespeichert und die LED ausgeschaltet indem auf 0% gestellt wird
  • LED On => Der zuletzt gespeicherte Helligkeitswert wird wieder eingestellt.
  • Helligkeitsänderung => setzt den aktuellen und den gespeicherten Wert neu
    Den Quellcode dazu liefere ich nach…

Ich habe auf Eure Anregung hin nun doch ein Github-Projekt erstellt: GitHub - igittigitt/zigbee_xmas_battery_lightstrip: Zigbee controlled, rechargeable battery driven LED light strip
Dort findet man immer den letzten Stand meines Codes.

Einen Zielkonflikt galt es noch zu lösen. Im Deep-Sleep (Verbrauch sollte nur ca. 1 µA sein) sind die LED-Controller (LEDCs) ebenfalls ausgeschaltet. Damit die Lichterkette aber weiterhin leuchtet braucht man also einen Puffer, irgendeine Art Elektronik die die zuletzt eingestellte PWM Frequenz weiter sendet bis sie einen neuen Befehl erhält und das möglichst ohne selbst viel Strom zu verbrauchen.

Eine Option wäre ein LED-Controller z.B. vom Typ “PCA9685”. Dieser hat 12 Känäle und wird über I²C programmiert (wozu es fertige Libraries gibt). Er arbeitet mit einer Betriebsspannung von 2,3 bis 5,5 V, also ideal für den Batteriebetrieb. Er kann selbst aber nur 25 mA pro Kanal treiben, man bräuchte also einen nachgeschalteten “Verstärker” wie meine schon erwähnte KSQ oder einen Mosfet/Transistor. Die 12 Kanäle sind absolute Overkill, auf der anderen Seite gibt es fertige Shields von AliExpress schon ab 2€.

Die andere Option wäre auf den Deep-Sleep zu verzichten und stattdessen nur in den Light-Sleep Mode zu gehen, bei diesem blieben die LED-Controller nämlich aktiv. Dafür verbraucht dieser Modus aber auch 800 µA anstelle 1 µA im Deep-Sleep. Andererseits kommt der Stromverbrauch nur währen dem Betrieb der Lichter zustande. Die Lichterketten LEDs schlucken 50-100 mA, da fällt das 1 mA nicht ins Gewicht.

Ich entscheide mich erstmal für die Variante Light-Sleep bevor ich in die Materialschlacht gehe, das scheint mir effinzient genug.

So, nach einigem hin und her habe ich nun raus wie man im Light-Sleep das PWM aufrecht erhält. Das Problem hier ist das im Light-Sleep die meiste Peripherie ausgeschaltet wird, jedoch nicht der RTC-Timer (den braucht der LS u.a. um nach einer gewissen Zeit wieder aufwachen zu können). Die LEDC arbeiten normalerweise nicht mit dieser Clocksource, weil sie nur ca. 8 MHz liefert und somit nur für eine PWM-Auflösung von 8 Bit taugt, was 255 Helligkeitswerten entspricht (ja, -1!). Für Fading-Effekte oder gar RGB-LED Anwendungen eher zuwenig, aber für meine Anwendung zur reinen 1-farbigen Helligkeitssteuerung völlig ausreichend.

Somit reduziert sich der Hardwareaufwand um einen externen PWM-Buffer. Achja, ganz ohne PWM, also einfach nur AN/AUS via GPIO geht ebenfalls mit Light-Sleep. Ich wollte aber eine Helligkeitssteuerung, daher etwas mehr Programmieraufwand.

Hier mal der INIT-Code, falls jemand das gleiche Problem hat:

#define BLUE_LED_GPIO        13 // CONFIG_H2SUPERMINI_BLUE_LED_GPIO
#define LEDC_TIMER LEDC_TIMER_0
#define LEDC_CHANNEL LEDC_CHANNEL_0
#define LEDC_DUTY_RES LEDC_TIMER_8_BIT  // 0..254
#define LEDC_FREQUENCY 5000              // 5 kHz

void pwm_init(void)
{
    // Timer konfigurieren
    ledc_timer_config_t ledc_timer = {
        .speed_mode = LEDC_LOW_SPEED_MODE,
        .timer_num = LEDC_TIMER,
        .duty_resolution = LEDC_DUTY_RES,
        .freq_hz = LEDC_FREQUENCY,
        .clk_cfg = LEDC_USE_RC_FAST_CLK // provide PWM while in light-sleep (was LEDC_AUTO_CLK)
    };
    ESP_ERROR_CHECK(ledc_timer_config(&ledc_timer));

    // Channel konfigurieren
    ledc_channel_config_t ledc_channel = {
        .speed_mode = LEDC_LOW_SPEED_MODE,
        .channel = LEDC_CHANNEL,
        .timer_sel = LEDC_TIMER,
        .intr_type = LEDC_INTR_DISABLE,
        .gpio_num = BLUE_LED_GPIO,
        .duty = 0,
        .hpoint = 0,
        .sleep_mode = LEDC_SLEEP_MODE_KEEP_ALIVE // Keep LEDC output in light-sleep
    };
    ESP_ERROR_CHECK(ledc_channel_config(&ledc_channel));
}

Damit habe ich jetzt langsam alle Puzzleteile zusammen. Jetzt schaue ich mal wie ich ordentliche Ampere-Traces erstellen kann um die verschiedenen Optionen und Regler mal gegeneinander zu testen.

Stromverbrauch

So, nun meine allerersten Messungen.

Der Versuchsaufbau:
[ Li-Ion Batterie, 3,7V 3300mAh ] => [ Buck/Boost TPS63020 3,3V ] => [ ESP32-H2 Super-Mini ]
image


Die Software:
Ich starte nur eine einfache endlosschleife in der ich zu Anfang die blaue on-board LED (GPIO13) mittels LEDC PWM auf 100% Duty, also volle Helligkeit ansteure.
Sobald ich den BOOT-Taster drücke (GPIO9) ändere ich den Duty-Cycle auf 10% (gedimmte LED) und versetze ich den ESP in den “Light-Sleep Mode”. Die LED leuchtet in diesem Modus weiter, da ich den RTC-Clock für die PWM-Erzeugung nutze.

Das Ergebnis:
Im Normalbetrieb => 9,36 mA (ohne LED 8,66 mA)
Im Sleep => 0,73 mA (ohne LED 0,66 mA)

Der nächste Test dann mit Zigbee. Dazu verwende ich dann nicht mehr die “esp_sleep.h” (Generisch) sondern die “zb_esp_sleep.h” aus dem Espressif-IDF, da hier die Koordination mit dem Zigbee-Netzwerk bereits integriert ist. So sollte es grundsätzlich möglich sein den ESP in den Sleep zu legen, die Netzverbindung aber aufrecht zu erhalten, ohne dabei jedoch das Funkmoden dauerhaft eingeschaltet zu haben.

Gleicher Aufbau, nur die Software ist jetzt die mit Zigbee-Stack, aber noch ohne Sleep-Funktionen. Stromaufnahme des ESP => 26,5 mA
Lastspitzen (und die wird es geben) kann ich aktuell mit meinem Digitalmultimeter nicht sehen, auch fehlt mir die Langzeitmessung. Würde er aber so weiterlaufen und gehe ich davon aus das sich mein Li-Ion Akku bis 80% der Nennkapazität entlade (Li Ion Akkus darf man nie ganz entladen, eigentlich sollte man bei einer Restkapazität von 40% schon wieder aufladen) komme ich rein rechnerisch auf eine Betriebszeit von 100 h ~ 4 Tage. Alles noch ohne LED Last, die den Löwenanteil ausmacht, locker mal das doppelte der ESP-Last, also nochmal 50 mA drauf, da wäre dann schon nach 36 h ~ 1,5 Tage schluss…

Schlafenszeit…

Bei einem “Sleepy Device” wie ich es jetzt konstruieren möchte ist zu beachten das sich dieses in regelmäßigen Abständen beim Z2M (Coordinator) meldet um nicht als “Offline” klassifiziert zu werden. Das Intervall ist vom Timeout-Wert im Z2M abhängig, was meiner Meinung nach hier konfiguriert ist:

Dabei sollte ich die 10 Minuten, aber maximal 360 Minuten im Blick haben. Das ist aber auch schon eine ganze Menge, weit mehr als ich vor habe meinen ESP schlafen zu legen. Da ich ja auch Ereignisse im Z2M reagieren muss sollte ich nicht länger als 30-60 Sekunden schlafen, sonst wird die Bedienung irgendwie sperrig. Ich muss einen guten Kompromiss finden, zwischen Energiesparen und Verfügbarkeit

Meine Idee wäre das ich nach dem booten des ESP (=neue Batterie eingelegt) erstmal einige Minuten online bleibe um auf Nachrichten zu reagieren und dann in den Light-Sleep wechsle. Diesen kann man entweder durch drücken des BOOT-Buttons direkt verlassen, oder der Timer weckt den ESP nach 60 Sekunden wieder auf. Bei beidem verbindet sich der Node wieder und überprüft ob Anweisungen für ihn im Z2M vorliegen, bzw. er sendet selbst Statusdaten wie die Batteriespannung. Dann legt er sich nach einem gewissen Timeout wieder hin, sagen wir 10 Sekunden nach der zuletzt verarbeiteten Botschaft. Neue Botschaften retriggern diesen Timeout wieder. Wenn ich also rumspielen will, dann kann ich das auch tun und das Gerät reagiert sofort.

In Software gegossen sieht es ja mit ESP-IDF so aus das unter der Haube ein FreeRTOS werkelt, also ein sog. Echtzeitbetriebssystem (Real-Time-Operating-System). Dabei ist das Programm nicht linear sondern Ereignisgesteuert. Jede auszuführende Funktion wird als Task auf einen Stack gelegt und “beizeiten” (abhängig von Prioritäten, etc.) vom RTOS ausgeführt. Dabei kann auch die Ausführungszeit begrenzt werden um z.B. langlaufende Prozesse zu stoppen, wenn es notwendig sein sollte.

Die main() sieht bei mir dann so aus:

void app_main(void)
{
    // Zigbee init
    esp_zb_platform_config_t config = {
        .radio_config = ESP_ZB_DEFAULT_RADIO_CONFIG(),
        .host_config = ESP_ZB_DEFAULT_HOST_CONFIG(),
    };
    ESP_ERROR_CHECK(nvs_flash_init());
    ESP_ERROR_CHECK(esp_zb_platform_config(&config));

    xTaskCreate(esp_zb_task, "Zigbee_main", 4096, NULL, 5, NULL);
}

Es wird also der ESP und das Zigbee-Modem initialisiert und wenn das geklappt hat, dann wird ein Task erzeugt der die Funktion “esp_zb_task()” aufruft. Dort geschieht dann die eigentliche Zigbee-Stack Initialisierung, die Verbindung zum Mesh, das Pairing, der Kommunikationsaustausch, etc. Am Ende dieser Funktion werden noch Handler registiert, also Funktionen die beim eintreten bestimmter Ereignisse aufgerufen weren, z.B. wenn Z2M Aufgaben für den Node hat, oder umgekehrt. Dann wird das ganze gestartet und der letzte Befehl ist einfach eine Endlosschleife die sich nur um die Zigbee-Themen kümmert:

    esp_zb_core_action_handler_register(zb_action_handler); // act on everything
    esp_zb_set_primary_network_channel_set(ESP_ZB_PRIMARY_CHANNEL_MASK);
    ESP_ERROR_CHECK(esp_zb_start(false));
    esp_zb_stack_main_loop(); // inifinite loop
}
1 „Gefällt mir“

Die Idee ist, parallel zum Zigbee-Task (esp_zb_task) einen Management-Task (sleep_management_task) laufen zu lassen der sich nur um das Power-Management kümmert.

void app_main(void)
{
    ESP_LOGI(TAG, "Starting Zigbee Sleepy End Device");
    
    // Zigbee init
    esp_zb_platform_config_t config = {
        .radio_config = ESP_ZB_DEFAULT_RADIO_CONFIG(),
        .host_config = ESP_ZB_DEFAULT_HOST_CONFIG(),
    };
    ESP_ERROR_CHECK(nvs_flash_init());
    ESP_ERROR_CHECK(esp_zb_platform_config(&config));

    // Starte Zigbee Task
    xTaskCreate(esp_zb_task, "Zigbee_main", 4096, NULL, 5, NULL);
    
    // Starte Sleep Management Task
    xTaskCreate(sleep_management_task, "Sleep_mgmt", 3072, NULL, 4, NULL);
}