Die ESP32 Firmware ist modular aufgebaut und nutzt PlatformIO. Dieses Tutorial zeigt wie die ESP32 Firmware um ein eigenes Modul erweitert werden kann. Exemplarisch wird ein Modul zur Interaktion mit einem RGB LED Button Bricklet entwickelt. Als Editor wird Visual Studio Code verwendet.
Als erster Schritt muss das Bauen der ESP32 Standard-Firmware vorbereitet werden. Dazu sollte die Schritte aus der ESP32 Firmware Dokumentation durchgeführt werden.
Alle Pfade und Dateinamen in diesem Tutorial beziehen sich auf das software/
Verzeichnis der ESP32 Firmware. Achte bitte auch darauf das software/
Verzeichnis in Visual Studio Code als PlatformIO Projekt-Verzeichnis zu öffnen.
Dieses Tutorial ist in fünf Phasen unterteilt. Die jeweiligen Ausbaustufen des neuen Moduls liegen unbenutzt der ESP32 Firmware bereits bei und werden hier Schritt für Schritt erklärt.
Es können der ESP32 Brick und der
ESP32 Ethernet Brick für dieses Tutorial verwendet
werden. Der einzige Unterschied zwischen den beiden Bricks ist welche .ini
Datei im Laufe des Tutorial abgeändert werden muss:
esp32.ini
esp32_ethernet.ini
Um das jeweilige Tutorial-Modul zu aktivieren muss dessen Name am Ende der
Optionen custom_backend_modules
, custom_frontend_modules
und
custom_frontend_components
der entsprechenden .ini
Datei hinzufügt
werden und die Firmware mittels "Upload and Monitor"
Ausgabe in Visual Studio Code neu gebaut und auf den Brick geflasht werden.
Am Ende der esp32.ini
bzw. esp32_ethernet.ini
Datei wird wie oben
beschrieben Tutorial Phase 1
den Backend- und Frontend-Modulen hinzugefügt.
Nachdem das Projekt neu compiliert und geflasht wurde taucht im Webinterface das neue Modul als eine leere Unterseite namens "Tutorial (Phase 1)" auf:
Auf der seriellen Konsole wird die Meldung Tutorial (Phase 1) module initialized
ausgegeben:
Module teilen sich in zwei Gruppen auf:
src/modules/
web/src/modules/
Typischerweise treten Module in Backend/Frontend-Paaren auf, dies ist aber nicht zwingend. Es kann Backend-Module ohne entsprechendes Frontend-Modul geben und anders herum.
Aus dem Modulname in der esp32.ini
bzw. esp32_ethernet.ini
Datei leitet
sich der Verzeichnisname für das Modul ab. Aus Tutorial Phase 1
wird
tutorial_phase_1
(alle Zeichen zu Kleinbuchstaben umwandeln und Leerzeichen
durch Unterstriche ersetzen).
Jedes Backend-Modul wird durch eine C++ Klasse repräsentiert. Der Name dieser
Klasse leitet sich auch wieder aus dem Modulnamen ab. Aus Tutorial Phase 1
wird TutorialPhase1
(alle Leerzeichen entfernen).
Die Backend-Modul-Klasse muss in einer Header-Datei deklariert werden, deren
Name dem Verzeichnisnamen des Moduls entspricht und .h
angehängt hat. In
diesem Fall also tutorial_phase_1.h
.
Alle Dateien im Modulverzeichnis, die auf .cpp
, .c
oder .h
enden,
werden unabhängig von ihrem Namen mit in die Firmware kompiliert.
Jedes Frontend-Modul kann optional folgende Dateien beinhalten:
Modulname für die esp32.ini
bzw. esp32_ethernet.ini
Datei: Tutorial Phase 2
(Änderung des Eintrags von Phase 1
zu Phase 2
sowohl bei den Backend- als auch
bei den Frontend-Modulen).
Mit diesem Modul aktiviert taucht im Webinterface eine Unterseite mit Farbanzeige namens "Tutorial (Phase 2)" auf:
Die Farbe wird dabei durch das Backend-Modul festgelegt und an das Frontend-Modul kommuniziert. Dies funktioniert wie folgt:
Das Backend-Modul repräsentiert die Daten, die zum Frontend-Modul kommuniziert
werden sollen, strukturiert als ConfigRoot
Objekt. In diesem Fall ist nur
ein Element namens color
vorhanden, das als Wert einen String mit exakt 7 Byte
Länge hat, um eine Farbe in HTML Notation #RRGGBB
zu speichern. Der Wert
#FF0000
stellt die Farbe Rot dar. Auszug aus tutorial_phase_2.cpp
dazu:
void TutorialPhase2::pre_setup()
{
config = Config::Object({
{"color", Config::Str("#FF0000", 7, 7)}
});
}
Damit die Farbe an das Frontend-Modul kommuniziert wird, muss das ConfigRoot
Objekt dem API Manager als Zustand bekannt gemacht werden. Dafür wird der Name
tutorial_phase_2/config
verwendet. Der API Manager überprüft dann alle 1000
Millisekunden das ConfigRoot
Objekt auf Änderungen und schickt diese
automatisch an das Frontend-Modul. Auszug aus tutorial_phase_2.cpp
dazu:
void TutorialPhase2::register_urls()
{
api.addState("tutorial_phase_2/config", &config);
}
Das Frontend-Modul legt in seiner api.ts
Datei die Struktur der Daten fest,
die es vom Backend-Modul empfangen will:
export interface config
{
color: string
}
In der main.tsx
Datei wird ein Event-Listener für den Zustand
tutorial_phase_2/config
erzeugt, damit die Preact Component reagieren kann,
wenn vom API Manager Änderungen mitgeteilt werden.
In der lambda-Funktion wird der aktuelle Wert des tutorial_phase_2/config
Zustands abgefragt und der enthaltene Farbwert als color
zur Anzeige an den
Zustand der Preact Component zugewiesen:
util.addApiEventListener('tutorial_phase_2/config', () => {
let config = API.get("tutorial_phase_2/config");
this.setState({color: config.color});
});
Als Test kann der Farbwert in tutorial_phase_2.cpp
von #FF0000
(Rot) zu
#0000FF
(Blau) geändert werden:
void TutorialPhase2::pre_setup()
{
config = Config::Object({
{"color", Config::Str("#0000FF", 7, 7)}
});
}
Jetzt wird im Webinterface Blau angezeigt:
Modulname für die esp32.ini
bzw. esp32_ethernet.ini
Datei: Tutorial Phase 3
Mit diesem Modul aktiviert taucht im Webinterface eine Unterseite mit Farbanzeige namens "Tutorial (Phase 3)" auf:
Die Farbe kann jetzt über den Auswahldialog geändert werden.
In der main.tsx
Datei wird auf den change
Events des HTML Elements reagiert.
Bei Änderung der Farbe wird der aktuelle Farbwert des HTML Elements
abgefragt, damit ein neuer Wert für den tutorial_phase_3/config
Zustand
erstellt und dieser an das Backend-Modul übertragen wird:
<input class="form-control" type="color" value={this.state.color} onChange={(event) => {
let config = {color: (event.target as HTMLInputElement).value.toString()};
API.save("tutorial_phase_3/config", config, __("tutorial_phase_3.script.save_config_failed"));
}} />
Das Backend-Modul repräsentiert die Daten, die vom Frontend-Modul empfangen
werden können, strukturiert als ConfigRoot
Objekt. Dies wird einfach
als Kopie config_update
des ersten ConfigRoot
Objekts angelegt,
da es die gleiche Struktur hat. Auszug aus tutorial_phase_3.cpp
dazu:
void TutorialPhase3::pre_setup()
{
config = Config::Object({
{"color", Config::Str("#FF0000", 7, 7)}
});
config_update = config;
}
Damit die Farbe vom Frontend-Modul empfangen werden kann, muss das zweite
ConfigRoot
Objekt dem API Manager als Kommando bekannt gemacht werden.
Dafür wird der Name tutorial_phase_3/config_update
verwendet. Der API Manager
empfängt die Daten vom Frontend-Modul und ruft die Lambda-Funktion auf, um die
Daten zu behandeln. Es wird eine Meldung auf die serielle Konsole ausgegeben und
die neue Farbe gespeichert. Auszug aus tutorial_phase_3.cpp
dazu:
void TutorialPhase3::register_urls()
{
api.addState("tutorial_phase_3/config", &config);
api.addCommand("tutorial_phase_3/config_update", &config_update, {}, [this]() {
String color = config_update.get("color")->asString();
logger.printfln("Tutorial (Phase 3) module received color update: %s", color.c_str());
config.get("color")->updateString(color);
}, false);
}
Als Test kann der Farbwert im Webinterface von #FF0000
(Rot) zu
#00FF00
(Grün) geändert werden:
Auf der seriellen Konsole wird die Meldung Tutorial (Phase 3) module received
color update: #00ff00
ausgegeben:
Modulname für die esp32.ini
bzw. esp32_ethernet.ini
Datei: Tutorial Phase 4
Ab dieser Phase wird vorausgesetzt, dass am Brick ein RGB LED Button Bricklet angeschlossen ist. Der Bricklet-Anschluss kann dabei frei gewählt werden.
Mit diesem Modul aktiviert taucht im Webinterface eine Unterseite mit Farbanzeige namens "Tutorial (Phase 4)" auf:
Die Farbe kann jetzt über den Auswahldialog geändert und an das Backend-Modul und dadurch an das RGB LED Button Bricklet kommuniziert werden.
Die Kommunikation von Frontend zu Backend ist gleichgeblieben. Es wird jetzt
zusätzlich im Backend mit dem RGB LED Button Bricklet über die
C/C++ Bindings für Mikrocontroller kommuniziert. Dazu
wird ein RGB LED Button Bricklet Objekt angelegt. Das zweite Parameter der
tf_rgb_led_button_create
Funktion kann
verwendet werden, um per UID oder
Port-Namen anzugeben welches RGB LED Button Bricklet gemeint ist. Wird dieser
Parameter auf nullptr
gesetzt, dann wird das erste verfügbare RGB LED Button
Bricklet verwendet. Falls das RGB LED Button Bricklet Objekt nicht erzeugt
werden kann, dann wird der Aufruf der setup
Funktion vorzeitig beendet,
bevor initialized
auf true gesetzt wird. Dadurch blendet sich das
Frontend-Modul auf dem Webinterface aus, da das benötige Backend-Modul nicht
zur Verfügung steht. Auszug aus tutorial_phase_4.cpp
dazu:
void TutorialPhase4::setup()
{
if (tf_rgb_led_button_create(&rgb_led_button, nullptr, &hal) != TF_E_OK) {
logger.printfln("No RGB LED Button Bricklet found, disabling Tutorial (Phase 4) module");
return;
}
set_bricklet_color(config.get("color")->asString());
logger.printfln("Tutorial (Phase 4) module initialized");
initialized = true;
}
Initial und bei Änderung der Farbe durch das Frontend-Modul wird die
set_bricklet_color
Funktion aufgerufen, um die LED Farbe des Bricklets zu
ändern. Auszug aus tutorial_phase_4.cpp
dazu:
void TutorialPhase4::register_urls()
{
api.addState("tutorial_phase_4/config", &config);
api.addCommand("tutorial_phase_4/config_update", &config_update, {}, [this]() {
String color = config_update.get("color")->asString();
logger.printfln("Tutorial (Phase 4) module received color update: %s", color.c_str());
config.get("color")->updateString(color);
set_bricklet_color(color);
}, false);
}
Die set_bricklet_color
Funktion nimmt die Farbe in HTML Notation
#RRGGBB
entgegen und zerlegt diese in die Rot-, Grün- und Blau-Anteile, um
diese dann per tf_rgb_led_button_set_color
Funktion an das Bricklet zu senden. Auszug aus tutorial_phase_4.cpp
dazu:
void TutorialPhase4::set_bricklet_color(String color)
{
uint8_t red = hex2num(color.substring(1, 3));
uint8_t green = hex2num(color.substring(3, 5));
uint8_t blue = hex2num(color.substring(5, 7));
if (tf_rgb_led_button_set_color(&rgb_led_button, red, green, blue) != TF_E_OK) {
logger.printfln("Tutorial (Phase 4) module could not set RGB LED Button Bricklet color");
}
}
Als Test kann der Farbwert im Webinterface von #FF0000
(Rot) zu
#00FF00
(Grün) geändert werden.
Vor der Änderung zu Grün:
Nach der Änderung zu Grün:
Modulname für die esp32.ini
bzw. esp32_ethernet.ini
Datei: Tutorial Phase 5
Mit diesem Modul aktiviert taucht im Webinterface eine Unterseite mit Farb- und Tasteranzeige namens "Tutorial (Phase 5)" auf:
Neben der Farbe wird auch der Zustand des Tasters angezeigt.
Die api.ts
Datei des Frontend-Moduls wird erweitert, um den Zustand des
Tasters vom Backend-Modul abfragen zu können. Die neue button
Variable kann
nicht dem existierenden config
Zustand hinzugefügt werden, da der config
Zustand vom Frontend-Modul geändert werden kann, die button
Variable im
Frontend-Modul aber nur lesend zugegriffen werden können soll:
export interface config
{
color: string
}
export interface state
{
button: boolean
}
Entsprechend muss auch ein neues ConfigRoot
Objekt angelegt werden. Auszug
aus tutorial_phase_5.cpp
dazu:
void TutorialPhase5::pre_setup()
{
config = Config::Object({
{"color", Config::Str("#FF0000", 7, 7)}
});
config_update = config;
state = Config::Object({
{"button", Config::Bool(false)}
});
}
Dieses neue ConfigRoot
Objekt muss dann auch dem API Manager als weiterer
Zustand bekannt gemacht werden. Dafür wird der Name tutorial_phase_5/state
verwendet, entsprechend der Änderung der api.ts
im Frontend-Modul. Auszug
aus tutorial_phase_5.cpp
dazu:
void TutorialPhase5::register_urls()
{
api.addState("tutorial_phase_5/config", &config);
api.addCommand("tutorial_phase_5/config_update", &config_update, {}, [this]() {
String color = config_update.get("color")->asString();
logger.printfln("Tutorial (Phase 5) module received color update: %s", color.c_str());
config.get("color")->updateString(color);
set_bricklet_color(color);
}, false);
api.addState("tutorial_phase_5/state", &state, {}, true);
}
Um auf einen Tasterdruck reagieren zu können wird die Funktion
button_state_changed_handler
als Handler für den Button-State-Changed-Callback
des RGB LED Button Bricklets registriert. Dadurch wird diese Funktion beim Drücken
und Loslassen des Tasters automatisch aufgerufen und die Zustandsänderung kann
entsprechend behandelt werden. Auszug aus tutorial_phase_5.cpp
dazu:
static void button_state_changed_handler(TF_RGBLEDButton *rgb_led_button, uint8_t state, void *user_data)
{
TutorialPhase5 *tutorial = (TutorialPhase5 *)user_data;
tutorial->state.get("button")->updateBool(state == TF_RGB_LED_BUTTON_BUTTON_STATE_PRESSED);
}
void TutorialPhase5::setup()
{
if (tf_rgb_led_button_create(&rgb_led_button, nullptr, &hal) != TF_E_OK) {
logger.printfln("No RGB LED Button Bricklet found, disabling Tutorial (Phase 5) module");
return;
}
set_bricklet_color(config.get("color")->asString());
tf_rgb_led_button_register_button_state_changed_callback(&rgb_led_button, button_state_changed_handler, this);
uint8_t state;
if (tf_rgb_led_button_get_button_state(&rgb_led_button, &state) != TF_E_OK) {
logger.printfln("Could not get RGB LED Button Bricklet button state");
} else {
state.get("button")->updateBool(state == TF_RGB_LED_BUTTON_BUTTON_STATE_PRESSED);
}
logger.printfln("Tutorial (Phase 5) module initialized");
initialized = true;
}
In der main.tsx
Datei des Frontend-Moduls muss dann auf die Änderung des
neuen Zustands tutorial_phase_5/state
für den Tasterzustand genau so
reagiert werden, wie auf die Änderung des bisherigen tutorial_phase_5/config
Zustand für die Farbe:
util.addApiEventListener('tutorial_phase_5/state', () => {
let state = API.get("tutorial_phase_5/state");
this.setState({button: state.button});
});
Ein Druck auf den Taster wird im Webinterface angezeigt:
Die Standard-Firmware macht die angeschlossenen Bricklets durch das
Proxy
-Modul extern über die API Bindings und damit
auch Brick Viewer zugänglich. Farbänderungen des RGB LED Button
Bricklets über diesen Weg werden vom Tutorial-Modul bisher nicht wahrgenommen
und daher nicht auf dem Webinterface angezeigt.
Damit externe Farbänderungen vom Tutorial-Modul auch wahrgenommen werden können
wird die Farbe alle 1000 Millisekunden vom RGB LED Button Bricklet abgefragt und
bei Änderung automatisch über den API Manager an das Webinterface übertragen.
Auszug aus tutorial_phase_5.cpp
dazu:
void TutorialPhase5::setup()
{
// ...
uint8_t button_state;
if (tf_rgb_led_button_get_button_state(&rgb_led_button, &button_state) != TF_E_OK) {
logger.printfln("Could not get RGB LED Button Bricklet button state");
} else {
state.get("button")->updateBool(button_state == TF_RGB_LED_BUTTON_BUTTON_STATE_PRESSED);
}
task_scheduler.scheduleWithFixedDelay([this]() {
poll_bricklet_color();
}, 0, 1000);
logger.printfln("Tutorial (Phase 5) module initialized");
initialized = true;
}
void TutorialPhase5::poll_bricklet_color()
{
uint8_t red, green, blue;
if (tf_rgb_led_button_get_color(&rgb_led_button, &red, &green, &blue) != TF_E_OK) {
logger.printfln("Could not get RGB LED Button Bricklet color");
return;
}
String color = "#" + num2hex(red) + num2hex(green) + num2hex(blue);
config.get("color")->updateString(color);
}
Änderung der Farbe von Rot auf Gelb in Brick Viewer:
Jetzt wird im Webinterface Gelb angezeigt:
Damit ist der gesamte Kommunikationsweg von Hardware durch Firmware zum Webinterface und zurück durchlaufen und dieses Tutorial abgeschlossen.