WebControlPanel

network
network/WebControlPanel.h
network beta beta

Tool: WebControlPanel Abstraction haut niveau au-dessus d'un serveur HTTP + WebSocket pour créer très simplement un petit tableau de bord web permettant de piloter un montage (moteur, LED, relais...) depuis un navigateur, sans avoir à écrire une seule ligne de HTML/JS/JSON.

Méthodes publiques

Méthode Description Paramètres Retour
WebControlPanel()
WebControlPanel(const String& title = "Panneau CREPP", uint16_t httpPort = 80, uint16_t wsPort = 81);
connectToWiFi()
connectToWiFi(const String& ssid, const String& password, unsigned long timeoutMs = 15000);
startAP()
startAP(const String& ssid, const String& password = "", int channel = 1);
Démarre un point d'accès WiFi autonome (la carte crée son propre réseau, pratique pour un atelier sans WiFi disponible). / ssid: Nom du réseau créé.
password: Mot de passe (8 caractères minimum), vide = réseau ouvert.
channel: Canal WiFi utilisé. /
getIP()
getIP() const;
Adresse IP à saisir dans le navigateur pour accéder au panneau. /
begin()
begin();
Démarre les serveurs HTTP et WebSocket. A appeler une fois dans setup(), après avoir ajouté les widgets et connecté le WiFi. /
loop()
loop();
A appeler à chaque tour de loop() : traite les nouvelles connexions HTTP et les messages WebSocket entrants. /
addLed()
addLed(const String& id, const String& label, const String& color = "#4CAF50");
addLabel()
addLabel(const String& id, const String& label, const String& initialValue = "");
addChart()
addChart(const String& id, const String& label, float minValue, float maxValue, uint16_t maxPoints = 30);
setLed()
setLed(const String& id, bool state);
setLabel()
setLabel(const String& id, const String& value);
setSliderValue()
setSliderValue(const String& id, int value);
setTextInput()
setTextInput(const String& id, const String& value);
setCheckbox()
setCheckbox(const String& id, bool checked);
setRadioSelection()
setRadioSelection(const String& id, int index);
setColor()
setColor(const String& id, const String& hexColor);
pushChartValue()
pushChartValue(const String& id, float value);
Ajoute un nouveau point à un widget CHART déjà ajouté avec addChart() : mis à jour en temps réel sur tous les navigateurs connectés, et le point le plus ancien est supprimé si le nombre maximal de points (maxPoints) est dépassé. /
hexToRgb()
hexToRgb(const String& hexColor, uint8_t& r, uint8_t& g, uint8_t& b);
Convertit une couleur "#rrggbb" (telle que reçue par un COLOR_PICKER) en composantes rouge/vert/bleu (0-255), pratique pour piloter directement une LED RGB (ex: analogWrite). (r/g/b ne sont alors pas modifiés). / true
true si hexColor est bien au format "#rrggbb", false sinon (r/g/b ne sont alors pas modifiés). /
getStatus()
getStatus() const {

Méthodes privées


            findWidget(const String& id);;
generateCss() const;;
generateJs() const;;
renderButtonHtml(const PanelWidget& w) const;;
renderSliderHtml(const PanelWidget& w) const;;
renderLedHtml(const PanelWidget& w) const;;
renderLabelHtml(const PanelWidget& w) const;;
renderDPadHtml(const PanelWidget& w) const;;
renderTextInputHtml(const PanelWidget& w) const;;
renderCheckboxHtml(const PanelWidget& w) const;;
renderRadioGroupHtml(const PanelWidget& w) const;;
renderColorPickerHtml(const PanelWidget& w) const;;
renderChartHtml(const PanelWidget& w) const;;
handleWsMessage(const String& msg);;
stopAllDPads();;
pushUpdate(const String& type, const String& id, const String& extraJsonFields);;
escapeJson(const String& text);;
escapeHtmlAttr(const String& text);;
panel("Robot CREPP");;
begin(115200);;
begin(MOTOR_IN1, MOTOR_IN2, MOTOR_EN);;
startAP("Robot-CREPP", "robotique56");;
addButton("avance", "Avancer", [](){;
setSpeed(80);;
addButton("recule", "Reculer", [](){;
setSpeed(-80);;
addButton("stop", "Stop", [](){;
stop();;
addSlider("vitesse", "Vitesse", -100, 100, 0,
                  [](int v){;
addDPad("pad",
  //     [](){;
setSpeed(80);;
setSpeed(-80);;
setSpeed(-40);;
setSpeed(40);;
stop();;
addLed("actif", "Moteur en marche");;
addLabel("info", "Vitesse actuelle", "0");;
addTextInput("nom", "Nom du robot", "R2D2",
                     [](const String& v){;
println("Nom : " + v);;
addCheckbox("phares", "Phares allumés", false,
                    [](bool on){;
digitalWrite(LED_BUILTIN, on);;
addRadioGroup("mode", "Mode de conduite",
                      {"Normal", "Sport", "Economie"}, 0,
                      [](int index){;
addColorPicker("couleur_led", "Couleur LED RGB", "#4CAF50",
                       [](const String& hex){;
analogWrite(PIN_R, r);;
analogWrite(PIN_G, g);;
analogWrite(PIN_B, b);;
begin();;
loop();;
readSpeed(speed);;
setLed("actif", speed != 0);;

                    

Variables membres


            String id;
String label;
PanelWidgetType type;
String textValue;
return _status;
String _title;
int8_t speed;

                    

Code source


        // WebControlPanel.h
#pragma once

#include <Arduino.h>
#include <WiFi.h>
#include <WebSocketsServer.h>
#include <vector>
#include <functional>
#include "../StatusDevice.h"
#include "WebSocketAppBase.h"

namespace crepp::network {

/**
 * @brief Type d'un widget affiché sur le panneau web.
 */
enum class PanelWidgetType : uint8_t {
    BUTTON,       ///< Bouton cliquable -> déclenche un callback sans argument
    SLIDER,       ///< Curseur -> déclenche un callback avec la valeur (int)
    LED,          ///< Voyant de statut (allumé / éteint, couleur configurable)
    LABEL,        ///< Simple texte affiché, mis à jour depuis le programme
    DPAD,         ///< Croix directionnelle façon joystick (haut/bas/gauche/droite)
    TEXT_INPUT,   ///< Champ de saisie de texte libre
    CHECKBOX,     ///< Case à cocher (on/off)
    RADIO_GROUP,  ///< Choix unique parmi une liste d'options
    COLOR_PICKER, ///< Sélecteur de couleur (palette RGB native du navigateur)
    CHART         ///< Courbe temps réel (historique de valeurs numériques)
};

/**
 * @brief Représente un élément de l'interface (bouton, slider, LED ou texte).
 *
 * @note Les utilisateurs de la bibliothèque n'ont normalement pas besoin de
 *       manipuler cette structure directement : elle est créée et gérée en
 *       interne par WebControlPanel::addButton() / addSlider() / addLed() /
 *       addLabel().
 */
struct PanelWidget {
    String id;      ///< Identifiant unique (utilisé aussi en HTML/JS)
    String label;   ///< Texte affiché à côté du widget
    PanelWidgetType type;

    std::function<void()> onClick;      ///< Callback appelé pour un BUTTON
    std::function<void(int)> onChange;  ///< Callback appelé pour un SLIDER

    int sliderMin = 0;
    int sliderMax = 100;
    int sliderValue = 0;

    bool ledState = false;
    String ledColor = "#4CAF50";

    String textValue; ///< Valeur affichée pour un LABEL

    // DPAD : un callback par direction, appelé tant que le bouton reste
    // appuyé (voir le protocole "press:"/"release:" dans le .cpp), plus un
    // callback optionnel appelé au relâchement (typiquement motor.stop()).
    std::function<void()> onUp;
    std::function<void()> onDown;
    std::function<void()> onLeft;
    std::function<void()> onRight;
    std::function<void()> onStop;

    // TEXT_INPUT / COLOR_PICKER : callback appelé avec la nouvelle valeur
    // texte (chaîne libre pour TEXT_INPUT, couleur hexadécimale "#rrggbb"
    // pour COLOR_PICKER). textValue (ci-dessus) sert de valeur courante
    // pour TEXT_INPUT ; colorValue ci-dessous pour COLOR_PICKER.
    std::function<void(const String&)> onTextChange;
    String colorValue = "#ffffff";

    // CHECKBOX
    std::function<void(bool)> onToggle;
    bool checkboxState = false;

    // RADIO_GROUP : réutilise onChange(int) (même signature que SLIDER) pour
    // signaler l'index sélectionné.
    std::vector<String> options;
    int selectedIndex = 0;

    // CHART : historique borné de valeurs numériques (voir addChart() /
    // pushChartValue()). Pas de callback : widget d'affichage seul.
    std::vector<float> chartData;
    float chartMin = 0.0f;
    float chartMax = 100.0f;
    uint16_t chartMaxPoints = 30;
};

/*
 * =========================
 * Guide d'extension : ajouter un nouveau type de widget
 * -------------------------
 * Toute la logique d'un type de widget est volontairement regroupée à
 * quelques endroits bien identifiés, pour qu'ajouter ou modifier un type
 * (ex: un champ de texte, un choix multiple...) reste rapide :
 *
 *   1. PanelWidgetType : ajouter la nouvelle valeur d'enum.
 *   2. PanelWidget      : ajouter les champs de données nécessaires
 *                         (comme sliderMin/sliderMax pour SLIDER).
 *   3. addXxx()         : méthode publique de construction du widget
 *                         (voir addButton/addSlider/addLed/addLabel).
 *   4. renderXxxHtml()  : génère le HTML du widget + l'appelle depuis
 *                         generateHtmlPage() (un cas de switch, une ligne).
 *   5. generateJs()     : ajouter une entrée dans la table `handlers` du
 *                         JavaScript pour appliquer les mises à jour
 *                         temps réel reçues du Pico (un objet, une clé).
 *   6. setXxx()         : si le programme doit pouvoir mettre à jour ce
 *                         widget depuis loop(), utiliser pushUpdate() en
 *                         suivant setLed()/setLabel()/setSliderValue().
 *
 * Rien d'autre à toucher : begin(), loop(), le HTTP et le WebSocket restent
 * génériques et n'ont pas besoin d'être modifiés.
 * =========================
 */

/*
 * =========================
 * Tool: WebControlPanel
 * -------------------------
 * Abstraction haut niveau au-dessus d'un serveur HTTP + WebSocket pour créer
 * très simplement un petit tableau de bord web permettant de piloter un
 * montage (moteur, LED, relais...) depuis un navigateur, sans avoir à écrire
 * une seule ligne de HTML/JS/JSON.
 *
 * @badge network
 * @badge beta
 *
 * Fonctionnalités :
 * - Connexion WiFi (station) ou point d'accès autonome
 * - Génération automatique d'une page HTML responsive
 * - Boutons, curseurs (sliders), voyants (LED) et étiquettes de texte
 * - Communication bidirectionnelle en temps réel via WebSocket
 * - Callbacks C++ (lambdas) appelés directement quand l'utilisateur clique
 *   ou déplace un curseur : aucune notion de HTTP/WebSocket/JSON à connaître
 *
 * @note Pensé pour un usage pédagogique/débutant (ateliers CREPP) : un seul
 *       objet, quelques appels à addButton()/addSlider()/addLed()/addLabel()
 *       dans setup(), puis begin() et loop(). Pour un serveur HTTP "brut"
 *       avec vos propres routes, voir PicoWebServer. Pour un terminal de
 *       debug avec historique de logs, voir PicoWebTerminal.
 * =========================
 */
class WebControlPanel : public WebSocketAppBase {
public:
    /**
     * @brief Construit le panneau (ne démarre rien tant que begin() n'est
     *        pas appelé).
     * @param title    Titre affiché en haut de la page web.
     * @param httpPort Port du serveur HTTP (page web), 80 par défaut.
     * @param wsPort   Port du serveur WebSocket (temps réel), 81 par défaut.
     */
    explicit WebControlPanel(const String& title = "Panneau CREPP",
                              uint16_t httpPort = 80,
                              uint16_t wsPort = 81);

    /*
     * =========================
     * Réseau
     * =========================
     */

    /**
     * @brief Connecte la carte à un réseau WiFi existant (mode station).
     *        Appel bloquant : attend la connexion jusqu'à timeoutMs.
     * @param ssid      Nom du réseau WiFi.
     * @param password  Mot de passe du réseau.
     * @param timeoutMs Délai maximum d'attente en millisecondes.
     * @return true si la connexion a réussi.
     */
    bool connectToWiFi(const String& ssid, const String& password,
                        unsigned long timeoutMs = 15000);

    /**
     * @brief Démarre un point d'accès WiFi autonome (la carte crée son
     *        propre réseau, pratique pour un atelier sans WiFi disponible).
     * @param ssid     Nom du réseau créé.
     * @param password Mot de passe (8 caractères minimum), vide = réseau ouvert.
     * @param channel  Canal WiFi utilisé.
     */
    void startAP(const String& ssid, const String& password = "", int channel = 1);

    /**
     * @brief Adresse IP à saisir dans le navigateur pour accéder au panneau.
     */
    String getIP() const;

    /*
     * =========================
     * Cycle de vie
     * =========================
     */

    /**
     * @brief Démarre les serveurs HTTP et WebSocket.
     *        A appeler une fois dans setup(), après avoir ajouté les widgets
     *        et connecté le WiFi.
     */
    void begin();

    /**
     * @brief A appeler à chaque tour de loop() : traite les nouvelles
     *        connexions HTTP et les messages WebSocket entrants.
     */
    void loop();

    /*
     * =========================
     * Construction de l'interface
     * -------------------------
     * A appeler dans setup(), avant begin().
     * =========================
     */

    /**
     * @brief Ajoute un bouton à l'interface.
     * @param id      Identifiant unique du bouton.
     * @param label   Texte affiché sur le bouton.
     * @param onClick Fonction appelée (sans argument) à chaque clic.
     */
    void addButton(const String& id, const String& label,
                   std::function<void()> onClick);

    /**
     * @brief Ajoute un curseur (slider) à l'interface.
     * @param id           Identifiant unique du curseur.
     * @param label        Texte affiché au-dessus du curseur.
     * @param minValue     Valeur minimale.
     * @param maxValue     Valeur maximale.
     * @param initialValue Valeur affichée au chargement de la page.
     * @param onChange     Fonction appelée avec la nouvelle valeur (int)
     *                     dès que l'utilisateur déplace le curseur.
     */
    void addSlider(const String& id, const String& label,
                   int minValue, int maxValue, int initialValue,
                   std::function<void(int)> onChange);

    /**
     * @brief Ajoute un voyant de statut (LED) à l'interface.
     * @param id    Identifiant unique du voyant.
     * @param label Texte affiché à côté du voyant.
     * @param color Couleur CSS utilisée quand le voyant est allumé.
     */
    void addLed(const String& id, const String& label,
                const String& color = "#4CAF50");

    /**
     * @brief Ajoute une étiquette de texte (valeur en lecture seule côté
     *        utilisateur, mise à jour depuis le programme via setLabel()).
     * @param id           Identifiant unique de l'étiquette.
     * @param label        Texte descriptif (ex: "Température").
     * @param initialValue Valeur affichée au départ (ex: "-- °C").
     */
    void addLabel(const String& id, const String& label,
                  const String& initialValue = "");

    /**
     * @brief Ajoute une croix directionnelle façon joystick (4 boutons en
     *        croix : haut/bas/gauche/droite), pratique pour piloter un
     *        moteur ou un robot. Chaque direction appelle son callback tant
     *        que le bouton reste appuyé (utile pour "avancer tant que
     *        j'appuie"), et onStop est appelé au relâchement, quelle que
     *        soit la direction relâchée.
     * @param id      Identifiant unique de la croix.
     * @param onUp    Callback appelé pendant l'appui sur le bouton haut.
     * @param onDown  Callback appelé pendant l'appui sur le bouton bas.
     * @param onLeft  Callback appelé pendant l'appui sur le bouton gauche.
     * @param onRight Callback appelé pendant l'appui sur le bouton droite.
     * @param onStop  Callback appelé au relâchement (ex: motor.stop()).
     *                Optionnel : peut être laissé vide (nullptr).
     */
    void addDPad(const String& id,
                 std::function<void()> onUp,
                 std::function<void()> onDown,
                 std::function<void()> onLeft,
                 std::function<void()> onRight,
                 std::function<void()> onStop = nullptr);

    /**
     * @brief Ajoute un champ de saisie de texte libre.
     * @param id           Identifiant unique du champ.
     * @param label        Texte descriptif affiché au-dessus.
     * @param initialValue Valeur affichée au chargement de la page.
     * @param onChange     Fonction appelée avec le texte saisi, une fois
     *                     la saisie validée (Entrée ou perte de focus).
     */
    void addTextInput(const String& id, const String& label,
                       const String& initialValue,
                       std::function<void(const String&)> onChange);

    /**
     * @brief Ajoute une case à cocher.
     * @param id             Identifiant unique de la case.
     * @param label          Texte affiché à côté de la case.
     * @param initialChecked Etat initial (cochée ou non).
     * @param onChange       Fonction appelée avec le nouvel état à chaque clic.
     */
    void addCheckbox(const String& id, const String& label, bool initialChecked,
                      std::function<void(bool)> onChange);

    /**
     * @brief Ajoute un groupe de boutons radio (un seul choix possible).
     * @param id           Identifiant unique du groupe.
     * @param label        Texte descriptif affiché au-dessus des options.
     * @param options      Liste des libellés des options, dans l'ordre.
     * @param initialIndex Index (0 = première option) sélectionné au départ.
     * @param onChange     Fonction appelée avec l'index choisi à chaque
     *                     changement de sélection.
     */
    void addRadioGroup(const String& id, const String& label,
                        const std::vector<String>& options, int initialIndex,
                        std::function<void(int)> onChange);

    /**
     * @brief Ajoute un sélecteur de couleur (palette RGB native du
     *        navigateur : l'utilisateur choisit une couleur, le programme
     *        reçoit un code hexadécimal "#rrggbb"). Pratique pour piloter
     *        une LED RGB. Voir aussi WebControlPanel::hexToRgb() pour
     *        convertir la valeur reçue en composantes r/g/b.
     * @param id           Identifiant unique du sélecteur.
     * @param label        Texte descriptif affiché au-dessus.
     * @param initialColor Couleur affichée au départ, au format "#rrggbb".
     * @param onChange     Fonction appelée avec la couleur choisie
     *                     ("#rrggbb") à chaque validation du sélecteur.
     */
    void addColorPicker(const String& id, const String& label,
                         const String& initialColor,
                         std::function<void(const String&)> onChange);

    /**
     * @brief Ajoute une courbe temps réel (widget d'affichage seul, sans
     *        interaction utilisateur), pratique pour visualiser l'évolution
     *        d'une température, d'une distance, d'une vitesse...
     * @param id        Identifiant unique de la courbe.
     * @param label     Texte descriptif affiché au-dessus.
     * @param minValue  Valeur minimale de l'échelle verticale.
     * @param maxValue  Valeur maximale de l'échelle verticale.
     * @param maxPoints Nombre de points conservés (les plus anciens sont
     *                  supprimés au-delà), 30 par défaut.
     */
    void addChart(const String& id, const String& label,
                  float minValue, float maxValue, uint16_t maxPoints = 30);

    /*
     * =========================
     * Mise à jour depuis le programme principal
     * -------------------------
     * A appeler dans loop(), pour refléter l'état réel du montage sur
     * l'interface web (tous les clients connectés sont mis à jour en
     * temps réel).
     * =========================
     */

    /// Allume ou éteint un widget LED déjà ajouté avec addLed().
    void setLed(const String& id, bool state);

    /// Modifie le texte affiché pour un widget LABEL déjà ajouté avec addLabel().
    void setLabel(const String& id, const String& value);

    /// Force la valeur affichée d'un widget SLIDER (ex: resynchronisation).
    void setSliderValue(const String& id, int value);

    /// Modifie le texte affiché dans un widget TEXT_INPUT déjà ajouté avec addTextInput().
    void setTextInput(const String& id, const String& value);

    /// Force l'état d'un widget CHECKBOX déjà ajouté avec addCheckbox().
    void setCheckbox(const String& id, bool checked);

    /// Force l'option sélectionnée d'un widget RADIO_GROUP (index, 0 = première option).
    void setRadioSelection(const String& id, int index);

    /// Force la couleur affichée d'un widget COLOR_PICKER ("#rrggbb").
    void setColor(const String& id, const String& hexColor);

    /**
     * @brief Ajoute un nouveau point à un widget CHART déjà ajouté avec
     *        addChart() : mis à jour en temps réel sur tous les navigateurs
     *        connectés, et le point le plus ancien est supprimé si le
     *        nombre maximal de points (maxPoints) est dépassé.
     */
    void pushChartValue(const String& id, float value);

    /**
     * @brief Convertit une couleur "#rrggbb" (telle que reçue par un
     *        COLOR_PICKER) en composantes rouge/vert/bleu (0-255), pratique
     *        pour piloter directement une LED RGB (ex: analogWrite).
     * @return true si hexColor est bien au format "#rrggbb", false sinon
     *         (r/g/b ne sont alors pas modifiés).
     */
    static bool hexToRgb(const String& hexColor, uint8_t& r, uint8_t& g, uint8_t& b);

    /// Etat du périphérique (OK une fois begin() appelé avec succès).
    StatusDevice getStatus() const { return _status; }

private:
    String _title;

    std::vector<PanelWidget> _widgets;
    StatusDevice _status = StatusDevice::DEVICE_ERROR_INIT;

    PanelWidget* findWidget(const String& id);

    // Génération de la page HTML : le service HTTP lui-même (accepter les
    // clients, envoyer la réponse) est géré par WebSocketAppBase::loopServers().
    String generateHtmlPage() override;
    String generateCss() const;
    String generateJs() const;

    // Rendu HTML d'un widget, un point d'entrée par type (voir le guide
    // d'extension ci-dessus). Chacune ne connaît que son propre widget.
    String renderButtonHtml(const PanelWidget& w) const;
    String renderSliderHtml(const PanelWidget& w) const;
    String renderLedHtml(const PanelWidget& w) const;
    String renderLabelHtml(const PanelWidget& w) const;
    String renderDPadHtml(const PanelWidget& w) const;
    String renderTextInputHtml(const PanelWidget& w) const;
    String renderCheckboxHtml(const PanelWidget& w) const;
    String renderRadioGroupHtml(const PanelWidget& w) const;
    String renderColorPickerHtml(const PanelWidget& w) const;
    String renderChartHtml(const PanelWidget& w) const;

    // WebSocket : messages entrants du navigateur ("click:id", "slide:id:val")
    void handleWsMessage(const String& msg);

    // Sécurité : appelle onStop() de tous les widgets DPAD (voir begin(),
    // déclenché sur WStype_DISCONNECTED).
    void stopAllDPads();

    // Construit puis diffuse (broadcast) un message de mise à jour au format
    // {"type":"<type>","id":"<id>" <extraJsonFields>}. extraJsonFields doit
    // commencer par une virgule, ex: ",\"state\":true". Ne fait rien si
    // aucun navigateur n'est connecté. Toutes les méthodes setXxx() passent
    // par ici : ajouter un nouveau setXxx() se résume à un appel à
    // pushUpdate() (voir setLed/setLabel/setSliderValue).
    void pushUpdate(const String& type, const String& id, const String& extraJsonFields);

    // Petite fonction utilitaire d'échappement pour les valeurs textuelles
    // insérées dans les messages JSON envoyés au navigateur.
    static String escapeJson(const String& text);

    // Echappement pour les valeurs textuelles insérées dans un attribut HTML
    // (ex: value='...') : évite qu'une apostrophe tapée dans un TEXT_INPUT
    // ne casse la page générée.
    static String escapeHtmlAttr(const String& text);

    // Types de message JSON envoyés au navigateur (Pico -> navigateur) et
    // préfixes des messages reçus (navigateur -> Pico). Centralisés ici pour
    // qu'il n'y ait qu'un seul endroit à modifier si le protocole évolue :
    // ces constantes sont utilisées à la fois par le C++ (pushUpdate,
    // handleWsMessage) et recopiées dans le JavaScript généré (generateJs).
    static constexpr const char* kMsgLed    = "led";
    static constexpr const char* kMsgLabel  = "label";
    static constexpr const char* kMsgSlider = "slider";
    static constexpr const char* kMsgText   = "text";
    static constexpr const char* kMsgCheck  = "check";
    static constexpr const char* kMsgRadio  = "radio";
    static constexpr const char* kMsgColor  = "color";
    static constexpr const char* kMsgChart  = "chart";
    static constexpr const char* kCmdClick   = "click:";
    static constexpr const char* kCmdSlide   = "slide:";
    static constexpr const char* kCmdPress   = "press:";   ///< "press:<id>:<up|down|left|right>"
    static constexpr const char* kCmdRelease = "release:"; ///< "release:<id>"
    static constexpr const char* kCmdText    = "text:";    ///< "text:<id>:<valeur>"
    static constexpr const char* kCmdCheck   = "check:";   ///< "check:<id>:<0|1>"
    static constexpr const char* kCmdRadio   = "radio:";   ///< "radio:<id>:<index>"
    static constexpr const char* kCmdColor   = "color:";   ///< "color:<id>:<#rrggbb>"
};

} // namespace crepp::network

/*
@example

@include
#include <crepp/network/WebControlPanel.h>
#include <crepp/tools/Motor.h>
@end_include

@macro
#define MOTOR_IN1 6
#define MOTOR_IN2 7
#define MOTOR_EN  8
@end_macro

@instance
crepp::network::WebControlPanel panel("Robot CREPP");
crepp::tools::Motor motor;
@end_instance

@setup
  Serial.begin(115200);
  motor.begin(MOTOR_IN1, MOTOR_IN2, MOTOR_EN);

  // Pas de WiFi à disposition dans l'atelier : on crée un point d'accès.
  panel.startAP("Robot-CREPP", "robotique56");

  panel.addButton("avance", "Avancer", [](){ motor.setSpeed(80); });
  panel.addButton("recule", "Reculer", [](){ motor.setSpeed(-80); });
  panel.addButton("stop", "Stop", [](){ motor.stop(); });

  panel.addSlider("vitesse", "Vitesse", -100, 100, 0,
                  [](int v){ motor.setSpeed((int8_t)v); });

  // Alternative aux 3 boutons ci-dessus : une croix directionnelle.
  // panel.addDPad("pad",
  //     [](){ motor.setSpeed(80); },   // haut  -> avance
  //     [](){ motor.setSpeed(-80); },  // bas   -> recule
  //     [](){ motor.setSpeed(-40); },  // gauche -> pivote (exemple)
  //     [](){ motor.setSpeed(40); },   // droite -> pivote (exemple)
  //     [](){ motor.stop(); });        // relâchement -> stop

  panel.addLed("actif", "Moteur en marche");
  panel.addLabel("info", "Vitesse actuelle", "0");

  // Nouveaux widgets disponibles :
  panel.addTextInput("nom", "Nom du robot", "R2D2",
                     [](const String& v){ Serial.println("Nom : " + v); });

  panel.addCheckbox("phares", "Phares allumés", false,
                    [](bool on){ digitalWrite(LED_BUILTIN, on); });

  panel.addRadioGroup("mode", "Mode de conduite",
                      {"Normal", "Sport", "Economie"}, 0,
                      [](int index){ Serial.println("Mode choisi : " + String(index)); });

  panel.addColorPicker("couleur_led", "Couleur LED RGB", "#4CAF50",
                       [](const String& hex){
                           uint8_t r, g, b;
                           if (crepp::network::WebControlPanel::hexToRgb(hex, r, g, b)) {
                               // analogWrite(PIN_R, r); analogWrite(PIN_G, g); analogWrite(PIN_B, b);
                           }
                       });

  panel.begin();
  Serial.println("Connectez-vous a http://" + panel.getIP());
@end_setup

@loop
  panel.loop();

  int8_t speed;
  motor.readSpeed(speed);
  panel.setLed("actif", speed != 0);
  panel.setLabel("info", String(speed));
@end_loop

@end_example
*/