Mit MQTT einen Roboter steuern - [Teil 3]

Dans la première et la deuxième partie de cette série de blogs, nous vous avons expliqué les connaissances de base sur le MQTT et la manière dont il peut être utilisé avec différents microcontrôleurs. Dans cet article de blog, le robot (roulant) mentionné verra le jour et sera contrôlé par une simple télécommande.

Pour que ce projet réussisse, un courtier MQTT est nécessaire. La façon dont vous pouvez facilement mettre en œuvre cela avec un Raspberry Pi est expliquée en détail dans la première partie de cette série de blogs.

Exigences en matière de matériel

Pour cet article de blog, vous aurez besoin des éléments suivants, voir le tableau 1.

Pos Numéro Composante
1 1 Raspberry Pi (obligatoire)
2 1 Alimentation adaptée
3 2

Carte de développement WLAN WiFi du module ESP32 NodeMCU (obligatoire)

4 2

Potentiomètre

(obligatoire)
5 1 Un pont en H L298N
6 1 Breadboard
7 1 Un kit de robot roulant
8 1 Certains Câble Jumper F2F
9 1 Powerbank pour votre téléphone portable

Tableau 1 : Matériel nécessaire

Pour le Raspberry Pi, gardez à l'esprit que vous avez également besoin d'une carte MicroSD et d'une alimentation électrique en plus du matériel mentionné ci-dessus. Pour cela, vous devez copier le Raspberry Pi OS (anciennement Raspbian) comme image sur la carte et installer un courtier MQTT en conséquence selon les instructions de la partie 1 de cette série.

Exigences en matière de logiciels

Vous aurez besoin du logiciel suivant pour l'article de blog :

Comment installer des bibliothèques via la gestion des bibliothèques est sous https://www.az-delivery.de/blogs/azdelivery-blog-fur-arduino-und-raspberry-pi/arduino-ide-programmieren-fuer-einsteiger-teil-1 Section Gestion de la bibliothèque, décrite plus en détail.

Prérequis

Tout d'abord, veuillez vérifier si le courtier MQTT a été lancé au moment du démarrage du Raspberry Pi. Pour ce faire, entrez la commande du code 1 dans le terminal.

sudo service mosquitto status

Code 1 : Demande dans Terminal si le moustique a été lancé

Si la sortie indique un actif (en cours), voir figure 1, aucune autre commande n'est nécessaire.

Figure 1 : Statut du Mosquitto-Broker dans le terminal

Si le résultat diffère, veuillez vérifier à nouveau si mosquitto a été installé correctement et si le service a été inclus correctement dans le démarrage automatique. Les instructions exactes se trouvent dans la partie 1.

La télécommande

Afin de pouvoir contrôler notre robot roulant plus tard, il a besoin d'une télécommande. Celle-ci doit remplir deux tâches :

  1. Enregistrez la vitesse pour l'avant, l'arrière, la gauche et la droite
  2. Envoi des données au courtier MQTT

Dans ce cas, la direction du mouvement est mise en œuvre par le biais de deux potentiomètres, voir figure 2.

Illustration 2: Câblage de la télécommande

Comme microcontrôleur, un module ESP32 NodeMCU WLAN WiFi Development Board est utilisé. Parce que ce microcontrôleur a plus d'une entrée analogique et que le réseau local sans fil (WLAN) est construit directement sur le contrôleur. Cela permet d'économiser le câblage supplémentaire d'une carte WiFi par rapport à d'autres microcontrôleurs.

 

Le code 2 indique le code source complet de la télécommande. Pour que le code fonctionne, vous devez ajouter vos "identifiants WiFi" et les noms des courtiers MQTT entre les crochets "" dans les lignes avec les commentaires "Entrez le nom Wifi", "Entrez le mot de passe" et "Nom du courtier MQTT".


//-----------------------------------------------------
// Télécommande ESP-NodeMCU
// courtier mqtt et mappage de l'entrée analogique
// Auteur: Joern Weise
// Licence: GNU GPl 3.0
// Créé: 20 janv.2021
// Mise à jour: 20 janvier 2021
//-----------------------------------------------------
#include
#include // Lib pour MQTT Pub and Sub

// Définir les paramètres WiFi
#ifndef STASSID
#define STASSID "" // Entrez le nom Wfi
#define STAPSK "" // Entrez la clé d'accès
#endif

#define ADVANCEDIAG 1

#define ADC_STRAIGHT 36
#define ADC_CROSS 39

const char * MQTT_BROKER = ""; // Nom du courtier mqtt
const char * PubTopicStraight = "/ RemoteControl / Straight"; // Topic first temp
const char * PubTopicCross = "/ RemoteControl / Cross"; // Sujet deuxième temp
String clientID = "RemoteController"; // Nom du client pour MQTT-Broker

int iLastStraight, iLastCross;
// Créer des objets pour mqtt
WiFiClient espClient;
PubSubClient mqttClient (espClient);

#define MSG_BUFFER_SIZE (50)
char msg [MSG_BUFFER_SIZE];


void setup()
{
Serial.begin (115200);
Serial.println ("Télécommande démarrée");
writeAdvanceDiag ("Init WiFi", vrai);
setupWifi ();
writeAdvanceDiag ("Init Wifi - DONE", true);
writeAdvanceDiag ("Définir MQTT-Server", true);
mqttClient.setServer (MQTT_BROKER, 1883);
iLastStraight = -7;
iLastCross = -7;
writeAdvanceDiag ("Terminer la configuration () - Fonction", true);
}

boucle void () {
if (! mqttClient.connected ())
reconnectMQTT ();

mqttClient.loop ();
// Lire la valeur de l'entrée analogique et la valeur de la carte
int iMappedStraight = carte (analogRead (ADC_STRAIGHT), 4095,0, -2,2);
if (iMappedStraight! = iLastStraight)
  {
snprintf (msg, MSG_BUFFER_SIZE, "% 1d", iMappedStraight); // Convertit le message en caractère
mqttClient.publish (PubTopicStraight, msg, true); // Envoyer au courtier
writeAdvanceDiag ("Envoyer directement:" + String (iMappedStraight), true); // gehört zur vorherigen Zeile
iLastStraight = iMappedStraight;
  }
// Lire la valeur de l'entrée analogique et la valeur de la carte
int iMappedCross = carte (analogRead (ADC_CROSS), 4095,0, -2,2);
if (iMappedCross! = iLastCross)
  {
snprintf (msg, MSG_BUFFER_SIZE, "% 1d", iMappedCross); // Convertit le message en caractère
mqttClient.publish (PubTopicCross, msg, true); // Envoyer au courtier
writeAdvanceDiag ("Send Cross:" + String (iMappedCross), true);
iLastCross = iMappedCross;
  }
}

/*
* =================================================================
* Fonction: setupWifi
* Retours: nul
* Description: Configurez le wifi pour vous connecter au réseau
* =================================================================
*/
void setupWifi ()
{
Serial.println ("Connexion à:" + String (STASSID));
WiFi.mode (WIFI_STA);
WiFi.begin (STASSID, STAPSK);
while (WiFi.status ()! = WL_CONNECTED)
  {
retard (500);
Serial.print (".");
  }
Serial.println ("");
Serial.println ("WiFi connecté");
Serial.println ("Adresse IP:");
Serial.println (WiFi.localIP ());
}

/*
* =================================================================
* Fonction: reconnectMQTT
* Retours: nul
* Description: s'il n'y a pas de connexion à MQTT, cette fonction est
* appelé. De plus, le sujet souhaité est enregistré.
* =================================================================
*/
void reconnectMQTT ()
{
while (! mqttClient.connected ())
  {
writeAdvanceDiag ("Connexion à MQTT-Broker", true);
if (mqttClient.connect (clientID.c_str ()))
    {
Serial.println ("Connecté à MQTT-Broker" + String (MQTT_BROKER)); // gehört zur vorherigen Zeile
    }
autre
    {
writeAdvanceDiag ("Échec avec rc =" + String (mqttClient.state ()), true);// gehört zur vorherigen Zeile
Serial.println ("Prochaine connexion MQTT en 3 secondes");
retard (3000);
    }
  }
}

/*
* =================================================================
* Fonction: writeAdvanceDiag
* Retours: nul
* Description: écrit un msg avancé sur le moniteur série, si
* ADVANCEDIAG> = 1
* msg: Message pour le moniteur série
* newLine: Message avec saut de ligne (vrai)
* =================================================================
*/
void writeAdvanceDiag (String msg, booléen newLine)
{
if (bool (ADVANCEDIAG)) // Vérifie si le diagnostic avancé est activé
  {
if (newLine)
Serial.println (msg);
autre
Serial.print (msg);
  }
}

Code 2 : Contrôle à distance du code source

La fonction de base est cachée dans la fonction loop(). Ici, la valeur actuelle des potentiomètres sur les deux entrées analogiques est lue de manière cyclique et comparée à la dernière valeur connue. En cas de changement, cette valeur est cartographiée et envoyée au courtier MQTT. Le principe de base est le même comportement que le potentiomètre de la partie 2, sauf que la valeur analogique est d'abord cartographiée et ensuite seulement transmise au courtier.

Pour que la télécommande puisse être utilisée partout par la suite, elle est alimentée par un powerbank. Celui-ci fournit 5V à une sortie USB et alimente ainsi le microcontrôleur en énergie.

Le robot roulant

La mise en œuvre du robot roulant doit être aussi simple que la télécommande. En principe, deux tâches doivent être effectuées par le microcontrôleur du robot roulant :

  1. Réception des valeurs de la télécommande via le courtier MQTT
  2. Convertir les valeurs reçues afin que les moteurs tournent correctement

Afin de remplir le point 2, un pont en H L298N est utilisé ici. Ce qu'est exactement un pont en H sera expliqué à nouveau en détail dans un prochain blog ; pour l'instant, il suffit de savoir que ce composant électrique est responsable de la commande des moteurs. En même temps, le pont en H élimine la nécessité d'une deuxième source de tension, car pour des tensions allant jusqu'à 12V, le pont en H L298N fournit 5V à une sortie séparée. Mais théoriquement, vous pouvez utiliser deux sources de tension distinctes pour le MicroController et le H-Bridge. Il est important que les deux soient portés au même potentiel, c'est pourquoi les deux connexions GND doivent être reliées ensemble.

La figure 3 montre le câblage du robot roulant avec une seule source de tension.

Figure 3 : Câblage du robot roulant

Veuillez vous assurer que la tension de 5V est connectée aux broches spécifiées du module ESP32 NodeMCU WLAN WiFi Development Board. N'utilisez pas la broche GND directement à côté de V5, sinon le microcontrôleur ne démarrera pas. Le pont en H L298N doit également avoir le jumper 5V branché, sinon vous n'obtiendrez pas une tension de 5V du module, voir figure 4 bordure rouge. Pour les tensions supérieures à 5V, il est obligatoire de retirer le jumper, sinon les composants du module seront endommagés.

Figure 4 : Jumper 5V sur L298N

 

Vous pouvez voir le code source du robot roulant dans le code 3. Encore une fois, entrez les paramètres de votre réseau dans les lignes avec les commentaires "Enter Wifi name", "Enter Passkey" et "Name of the mqtt broker".

 //-----------------------------------------------------
// Robot ESP-NodeMCU
// courtier mqtt et mappage de l'entrée analogique
// Auteur: Joern Weise
// Licence: GNU GPl 3.0
// Créé: 20 janv.2021
// Mise à jour: 29 janvier 2021
//-----------------------------------------------------
#include
#include // Lib pour MQTT Pub and Sub

// Définir les paramètres WiFi
#ifndef STASSID
#define STASSID "" // Entrez le nom du Wifi
#define STAPSK "" // Entrez la clé d'accès
#endif

#define ADVANCEDIAG 1

#define MAXSPEED 255
#define MINSPEED 155
// Trucs MQTT
const char * MQTT_BROKER = ""; // Nom du courtier mqtt
Chaîne clientID = "AZBot"; // Nom du client pour le courtier MQTT
const char * SubTopicStraight = "/ RemoteControl / Straight"; // Sujet premier temp
const char * SubTopicCross = "/ RemoteControl / Cross"; // Sujet deuxième temp

int iMQTTStraight, iMQTTCross, iMQTTStraightNew, iMQTTCrossNew, iMQTTStraightLast, iMQTTCrossLast;

// Créer des objets pour mqtt
WiFiClient espClient;
PubSubClient mqttClient (espClient);

// timer vars pour debounce
ulDebounce long non signé = 10; // temps anti-rebond
unsigned long ulLastDebounceTimeStraight, ulLastDebounceTimeCross; // Bouton de minuterie de débouce

// Configuration PWM et moteur
// Moteur A
const int moteur1Pin1 = 27;
const int moteur1Pin2 = 26;
const int enable1Pin = 14;
const int motor1channel = 0;
// moteur B
const int moteur2Pin1 = 17;
const int moteur2Pin2 = 5;
const int enable2Pin = 16;
const int moteur2channel = 1;

// Définition des propriétés PWM
const int freq = 30000;
résolution int constante = 8;

booléen bUpdateMovement = false; // Sera défini, si de nouveaux mouvements de mqtt sont disponibles
/*
=================================================================
Fonction: setup
Renvoie: void
Description: fonction de configuration nécessaire
=================================================================
*/
void setup ()
{
// définit les broches comme sorties:
pinMode (motor1Pin1, OUTPUT);
pinMode (motor1Pin2, OUTPUT);
pinMode (enable1Pin, OUTPUT);
pinMode (motor2Pin1, OUTPUT);
pinMode (motor2Pin2, OUTPUT);
pinMode (enable2Pin, OUTPUT);
Serial.begin (115200);
Serial.println ("Télécommande démarrée");
iMQTTStraightNew = 0;
iMQTTCrossNew = 0;
writeAdvanceDiag ("Init WiFi", vrai);
setupWifi ();
writeAdvanceDiag ("Init Wifi - DONE", vrai);
writeAdvanceDiag ("Définir MQTT-Server", true);
mqttClient.setServer (MQTT_BROKER, 1883);
writeAdvanceDiag ("Définir la fonction de rappel", true);
mqttClient.setCallback (rappel);
writeAdvanceDiag ("Set PWM-Channels", true);
ledcSetup (motor1channel, fréq, résolution); // Configurer PWM pour le moteur 1 // appartient à la ligne précédente
ledcSetup (motor2channel, fréq, résolution); // Configurer PWM pour le moteur 2 // appartient à la ligne précédente
ledcAttachPin (enable1Pin, moteur1channel); // Attache le canal 1 au moteur 1 // appartient à la ligne précédente
ledcAttachPin (enable2Pin, moteur2channel); // Attache le canal 2 au moteur 2 // appartient à la ligne précédente

writeAdvanceDiag ("Terminer la configuration () - Fonction", true);
}

/*
=================================================================
Fonction: boucle
Renvoie: void
Description: fonction de boucle nécessaire
=================================================================
*/
boucle vide ()
{
if (! mqttClient.connected ())
reconnectMQTT ();

mqttClient.loop ();
DebounceStraight ();
DebounceCross ();
int iSpeedMotor1, iSpeedMotor2;
if (bUpdateMovement) // Vérifier s'il y a un nouveau mouvement disponible depuis mqtt // appartient à la ligne précédente
  {
Serial.println ("Valeur actuelle directement:" + String (iMQTTStraight));
Serial.println ("Croix de valeur actuelle:" + String (iMQTTCross));
si (iMQTTStraight! = 0)
    {
si (iMQTTStraight <0)
      {
digitalWrite (moteur1Pin1, LOW);
digitalWrite (motor1Pin2, HIGH);
digitalWrite (motor2Pin1, LOW);
digitalWrite (motor2Pin2, HIGH);
      }
autre
      {
digitalWrite (motor1Pin1, HIGH);
digitalWrite (moteur1Pin2, LOW);
digitalWrite (motor2Pin1, HIGH);
digitalWrite (motor2Pin2, LOW);
      }
si (abs (iMQTTStraight) == 1)
      {
iSpeedMotor1 = MAXSPEED - (MAXSPEED - MINSPEED) / 2;
iSpeedMotor2 = MAXSPEED - (MAXSPEED - MINSPEED) / 2;
      }
autre
      {
iSpeedMotor1 = MAXSPEED;
iSpeedMotor2 = MAXSPEED;
      }
    }
autre
    {
iSpeedMotor1 = 0;
iSpeedMotor2 = 0;
    }

si (iMQTTCross! = 0)
    {
si (iMQTTCross <0)
      {
si (iSpeedMotor1 == MAXSPEED)
        {
si (abs (iMQTTCross) == 1)
iSpeedMotor1 = MAXSPEED - (MAXSPEED - MINSPEED) / 2; // appartient à la ligne précédente
autre
iSpeedMotor1 = MINSPEED;
          }
autre
        {
si (abs (iMQTTCross) == 1)
iSpeedMotor1 = MINSPEED;
autre
iSpeedMotor1 = 0;
        }
Serial.println ("New Speed ​​motor 1:" + String (iSpeedMotor1));
      }
autre
      {
si (iSpeedMotor2 == MAXSPEED)
        {
si (abs (iMQTTCross) == 1)
iSpeedMotor2 = MAXSPEED - (MAXSPEED - MINSPEED) / 2; // appartient à la ligne précédente
autre
iSpeedMotor2 = MINSPEED;
        }
autre
        {
si (abs (iMQTTCross) == 1)
iSpeedMotor2 = MINSPEED;
autre
iSpeedMotor2 = 0;
        }
Serial.println ("New Speed ​​motor 2:" + String (iSpeedMotor2));
      }
    }
// Ecrire la vitesse sur le moteur pwm
ledcWrite (motor1channel, iSpeedMotor1);
ledcWrite (motor2channel, iSpeedMotor2);
bUpdateMovement = faux; // Nouvel ensemble de mouvements
  }
}

/*
=================================================================
Fonction: setupWifi
Renvoie: void
Description: Configurer le wifi pour se connecter au réseau
=================================================================
*/
void setupWifi ()
{
Serial.println ("Connexion à:" + String (STASSID));
WiFi.mode (WIFI_STA);
WiFi.begin (STASSID, STAPSK);
while (WiFi.status ()! = WL_CONNECTED)
  {
retard (500);
Serial.print (".");
  }
Serial.println ("");
Serial.println ("WiFi connecté");
Serial.println ("Adresse IP:");
Serial.println (WiFi.localIP ());
}

/*
=================================================================
Fonction: callback
Renvoie: void
Description: Sera automatiquement appelé, si un sujet souscrit
a un nouveau message
topic: renvoie le sujet, d'où provient un nouveau msg
payload: le message du sujet
length: longueur du msg, important pour obtenir le conntent
=================================================================
*/
rappel void (char * topic, byte * payload, unsigned int length)
{
String strMessage = "";
writeAdvanceDiag ("Message arrivé du sujet:" + String (topic), true); // gehört zur vorherigen Zeile
writeAdvanceDiag ("Longueur du message:" + Chaîne (longueur), vrai);
pour (int i = 0; i <longueur; i ++)
strMessage + = String ((char) payload [i]);
writeAdvanceDiag ("Le message est:" + strMessage, true);
if (String (topic) == String (SubTopicStraight))
  {
iMQTTStraightNew = strMessage.toInt ();
  }
else if (Chaîne (sujet) == Chaîne (SubTopicCross))
  {
iMQTTCrossNew = strMessage.toInt ();
  }
}

/*
=================================================================
Fonction: reconnectMQTT
Renvoie: void
Description: s'il n'y a pas de connexion à MQTT, cette fonction est
appelé. De plus, le sujet souhaité est enregistré.
=================================================================
*/
void reconnectMQTT ()
{
while (! mqttClient.connected ())
  {
writeAdvanceDiag ("Connexion à MQTT-Broker", true);
if (mqttClient.connect (clientID.c_str ()))
    {
Serial.println ("Connecté à MQTT-Broker" + String (MQTT_BROKER)); // gehört zur vorherigen Zeile
writeAdvanceDiag ("Souscrire au sujet '" + String (SubTopicStraight) + "'", true); // gehört zur vorherigen Zeile
mqttClient.subscribe (SubTopicStraight, 1); // Sujet de sous-scibe "SubTopicStraight" // gehört zur vorherigen Zeile
writeAdvanceDiag ("S'abonner au sujet '" + String (SubTopicCross) + "'", true); // gehört zur vorherigen Zeile
mqttClient.subscribe (SubTopicCross, 1); // Sujet de sous-scibe "SubTopicCross" // gehört zur vorherigen Zeile
    }
autre
    {
writeAdvanceDiag ("Échec avec rc =" + String (mqttClient.state ()), true); // gehört zur vorherigen Zeile
Serial.println ("Prochaine connexion MQTT en 3 secondes");
retard (3000);
    }
  }
}


/*
=================================================================
Fonction: writeAdvanceDiag
Renvoie: void
Description: écrit un msg avancé sur le moniteur série, si
ADVANCEDIAG> = 1
msg: Message pour le moniteur série
newLine: Message avec saut de ligne (vrai)
=================================================================
*/
void writeAdvanceDiag (String msg, booléen newLine)
{
if (bool (ADVANCEDIAG)) // Vérifie si le diagnostic avancé est activé
  {
if (newLine)
Serial.println (msg);
autre
Serial.print (msg);
  }
}

/*
=================================================================
Fonction: DebounceStraight
Renvoie: void
Description: définir une nouvelle valeur, si la débouce est terminée
S'il y a un nouveau bUpdateMovement valide
deviendra vrai
=================================================================
*/
void DebounceStraight ()
{
if (iMQTTStraightNew! = iMQTTStraightLast)
ulLastDebounceTimeStraight = millis ();

if ((millis () - ulLastDebounceTimeStraight)> ulDebounce)
  {

if (iMQTTStraightNew! = iMQTTStraight)
    {
iMQTTStraight = iMQTTStraightNew;
writeAdvanceDiag ("Nouvelle valeur directe" + String (iMQTTStraight), true); // gehört zur vorherigen Zeile
bUpdateMovement = vrai;
    }
  }
iMQTTStraightLast = iMQTTStraightNew;

}

/*
=================================================================
Fonction: DebounceCross
Renvoie: void
Description: définir une nouvelle valeur, si la débouce est terminée
S'il y a un nouveau bUpdateMovement valide
deviendra vrai
=================================================================
*/
void DebounceCross ()
{
si (iMQTTCrossNew! = iMQTTCrossLast)
ulLastDebounceTimeCross = millis ();

if ((millis () - ulLastDebounceTimeCross)> ulDebounce)
  {
si (iMQTTCrossNew! = iMQTTCross)
    {
iMQTTCross = iMQTTCrossNew;
writeAdvanceDiag ("Nouvelle valeur croisée" + String (iMQTTCross), true); // gehört zur vorherigen Zeile
bUpdateMovement = vrai;
    }
  }
iMQTTCrossLast = iMQTTCrossNew;
}

Code 3 : Code source du robot roulant

Le principe de base du robot roulant est presque analogue à celui de l'Arduino avec affichage LCD de la partie 2. Le robot roulant s'initialise et se connecte au WLAN. Dans l'étape suivante, le robot roulant se connecte au courtier MQTT et s'abonne aux sujets qui le concernent. Si une valeur des sujets change, la fonction de rappel est appelée et la nouvelle valeur est adoptée.

Dans le cas du robot roulant, qui s'enregistre auprès du courtier MQTT sous le nom d'AZbot, la valeur du mouvement vers l'avant et latéral est toujours débloquée grâce aux fonctions "DebounceStraight" et "DebounceCross". Cependant, si nous recevons des sauts de signal rapides de la télécommande, le code ne répond pas au changement à chaque cycle.

Enfin, la valeur correspondante de la télécommande est reprise dans la fonction de boucle et les signaux PWM correspondants sont réglés.

 

Figure 5 : La petite AzBot est autorisée à conduire

Le robot roulant montré, y compris la télécommande, est simple. Vous pouvez modifier ce cadre de base à plusieurs endroits. Un module joystick serait idéal, surtout avec la télécommande, par exemple. Le code source peut également être optimisé, mais il reste simple pour les débutants.

Une grande variété de modifications est également envisageable pour le robot roulant. Dans cet exemple, le changement de direction vers la gauche ou la droite est réalisé en réduisant la vitesse d'un moteur. Dans ce cas, on pourrait envisager de relier la roue avant à un servomoteur afin de pouvoir mieux diriger vers la gauche ou la droite. Le robot devient plus convivial pour les enfants si deux écrans OLED avec des yeux sont montés et si nécessaire un buzzer comme klaxon.

Vous voyez, le début d'un projet beaucoup plus important est fait, maintenant vous décidez comment et ce que votre robot roulant doit être capable de faire.

 

Ce projet et d'autres peuvent être trouvés sur GitHub à https://github.com/M3taKn1ght/Blog-Repo

Esp-32Tarte aux framboises

Laisser un commentaire

Tous les commentaires sont modérés avant d'être publiés

Messages de blogs recommandés

  1. Installez maintenant ESP32 via l'administrateur de la carte
  2. Lüftersteuerung Raspberry Pi
  3. Arduino IDE - Programmieren für Einsteiger - Teil 1
  4. ESP32 - das Multitalent
  5. OTA-Over the Air-ESP Programmation par WiFi