/*********************************************************************************************************************
 *
 *   ██████╗ ███╗   ███╗██████╗ ██╗     ██╗███╗   ██╗███████╗
 *  ██╔════╝ ████╗ ████║██╔══██╗██║     ██║████╗  ██║██╔════╝
 *  ██║      ██╔████╔██║██║  ██║██║     ██║██╔██╗ ██║█████╗
 *  ██║      ██║╚██╔╝██║██║  ██║██║     ██║██║╚██╗██║██╔══╝
 *  ╚██████╗ ██║ ╚═╝ ██║██████╔╝███████╗██║██║ ╚████║███████╗
 *   ╚═════╝ ╚═╝     ╚═╝╚═════╝ ╚══════╝╚═╝╚═╝  ╚═══╝╚══════╝
 *
 *  CmdLine : Interpreteur de commandes interactif
 *            Serial + Telnet / WiFi / Bluetooth
 *  Version  : v24 — addCommands() : second tableau de commandes utilisateur optionnel
 *  CORRIGE  : retour Bluetooth absent (3 passes v22), Serial hardcode → _stream
 *             surcharges print(const char*)/println(const char*) (v23)
 *
 *  Licence  : Libre d'utilisation — base sur Terminal.cpp (rweather, MIT)
 *  Auteur   : Biechy Jean-Marc — Ecole d'Informatique Institution Saint-Jean Colmar France 2026
 *
 *********************************************************************************************************************
 *
 *  DESCRIPTION GENERALE
 *  ────────────────────
 *  Cette librairie fournit un interpreteur de commandes (CLI) interactif
 *  accessible simultanement par TROIS canaux independants :
 *
 *    Canal 1 : _stream   → Serial USB/UART (PuTTY Serial, moniteur Arduino…)
 *    Canal 2 : _c        → Reseau TCP Telnet port 23 (PuTTY Telnet, MobaXterm…)
 *                          EthernetClient (UNO R4) ou WiFiClient (ESP8266/ESP32)
 *    Canal 3 : _btStream → Bluetooth Serial (application Android, ESP32 BT)
 *                          Actif seulement apres setBluetoothOutput(&SerialBT)
 *
 *  Chaque canal d'ENTREE maintient son propre etat (buffer, curseur, historique,
 *  completion TAB) via une instance CanalState → les canaux sont independants.
 *  La SORTIE est simultanee sur les 3 canaux actifs :
 *    cmdLine.print("texte")
 *         │
 *         ├──► _stream.print()    → Canal 1 : Serial / SerialBT
 *         ├──► _c->print()        → Canal 2 : Telnet TCP (si client connecte)
 *         └──► _btStream->print() → Canal 3 : Bluetooth SerialBT (si active v22)
 *
 *********************************************************************************************************************
 *
 *  PLATEFORMES SUPPORTEES
 *  ──────────────────────
 *  Plateforme              Reseau          Librairie requise     Type client
 *  ──────────────────────  ──────────────  ──────────────────    ─────────────────
 *  Arduino UNO R4 Minima   Ethernet W5500  Ethernet@2.0.2        EthernetClient
 *  ESP8266                 WiFi TCP        ESP8266WiFi (core)    WiFiClient
 *  ESP32                   WiFi TCP        WiFi (core)           WiFiClient
 *  ESP32                   Bluetooth       BluetoothSerial       Stream* generique
 *
 *  La selection de plateforme est automatique via CmdLineKey.h.
 *  Override manuel possible : #define CMDLINE_PLATFORM_ESP32 avant #include <CmdLine.h>
 *
 *********************************************************************************************************************
 *
 *  FONCTIONNALITES
 *  ───────────────
 *  ┌─────────────────────────────────────────────────────────────────────────┐
 *  │  Saisie de ligne interactive                                            │
 *  │    • Echo des caracteres (configurable : setEcho)                       │
 *  │    • Backspace / DEL : supprime le caractere a gauche du curseur        │
 *  │    • ← → : deplace le curseur (insertion au milieu de la ligne)         │
 *  ├─────────────────────────────────────────────────────────────────────────┤
 *  │  Historique des commandes                                               │
 *  │    • ↑ ↓ : navigation dans l'historique                                │
 *  │    • Taille configurable : CMDLINE_HISTORY_SIZE (defaut 10)             │
 *  │    • Sauvegarde de la ligne en cours avant le 1er ↑                    │
 *  │    • Doublons consecutifs ignores                                       │
 *  ├─────────────────────────────────────────────────────────────────────────┤
 *  │  Completion TAB                                                         │
 *  │    • 1er TAB : 0 match → BEL  /  1 match → completion + espace         │
 *  │               N matchs → liste toutes les commandes + charge la 1ere   │
 *  │    • TABs suivants : defile les correspondances (cycle Re-TAB)          │
 *  │    • Prefixe memorise → cycle toujours sur le meme prefixe d'origine   │
 *  ├─────────────────────────────────────────────────────────────────────────┤
 *  │  Insensibilite a la casse (CMDLINE_CASE_INSENSITIVE=1, defaut)          │
 *  │    • PING = Ping = ping  /  SaveConf, LEDon, serveurWEB... preserves   │
 *  │    • cmdStrcmpLower() : compare sans modifier la table _commands[]      │
 *  │    • Exemple : "SAVECONF 1" → trouve "SaveConf" ✓  arg="1" intact      │
 *  ├─────────────────────────────────────────────────────────────────────────┤
 *  │  Sortie simultanee _stream + reseau + Bluetooth  (3 canaux v22)         │
 *  │    • cmdLine.print(...)         → remplace Serial.print(...)            │
 *  │    • cmdLine.println(...)       → remplace Serial.println(...)          │
 *  │    • cmdLine.print(val, base)   → remplace Serial.print(val, DEC/HEX)  │
 *  │    • cmdLine.write(buf, len)    → remplace Serial.write(buf, len)       │
 *  │    • Supporte : int, float, long, String, char[], bool, F("..."), ...   │
 *  └─────────────────────────────────────────────────────────────────────────┘
 *
 *********************************************************************************************************************
 *
 *  COMPATIBILITE TERMINAUX (testee)
 *  ─────────────────────────────────
 *  Terminal           ENTER            Backspace   Echo local    Action requise
 *  ─────────────────  ───────────────  ──────────  ────────────  ──────────────────────────────
 *  PuTTY Serial       CR (0x0D)        0x08        NON           Flow control → None
 *  PuTTY Telnet       CR+LF ou CR+NUL  0x08/0x7F   NON (gere     Decocher "Return key sends
 *                     (selon config)               par la lib)   Telnet New Line" si besoin
 *  MobaXterm          CR+LF            0x7F        OUI           cmdLine.setEcho(false)
 *  Tera Term          CR+LF            0x7F        NON           Rien
 *  telnet Linux       CR+LF            0x7F        NON           Rien
 *  Serial BT Android  CR ou CR+LF      0x7F/0x08   selon app     cmdLineBT.setEcho(false)
 *  (app v1.49)                                                   si doublement des caracteres
 *
 *********************************************************************************************************************
 *
 *  MACHINE D'ETAT TELNET — readNetworkKey()
 *  ─────────────────────────────────────────
 *  Chaque octet recu du client reseau passe par une machine d'etat a 9 etats.
 *  L'etat est stocke dans CanalState.tstate → persiste entre les appels loop()
 *  car les sequences multi-octets (IAC WILL xx, ESC [ A) peuvent arriver
 *  en plusieurs paquets TCP successifs.
 *
 *  ┌──────────┐  0xFF (IAC)    ┌──────────┐  WILL    ┌──────────┐
 *  │ TS_INIT  │──────────────► │ TS_IAC   │────────► │ TS_WILL  │──► repond IAC WILL ECHO (1 fois)
 *  │          │                │          │           └──────────┘    + IAC WILL SGA
 *  │          │                │          │  WONT    ┌──────────┐
 *  │          │                │          │────────► │ TS_WONT  │──► ignore
 *  │          │                │          │           └──────────┘
 *  │          │                │          │  DO      ┌──────────┐
 *  │          │                │          │────────► │  TS_DO   │──► repond WILL SGA si SGA
 *  │          │                │          │           └──────────┘    sinon WONT
 *  │          │                │          │  DONT    ┌──────────┐
 *  │          │                │          │────────► │ TS_DONT  │──► ignore
 *  │          │                └──────────┘           └──────────┘
 *  │          │
 *  │          │  0x1B (ESC)    ┌──────────┐  '[' recu + armement timer 40ms
 *  │          │──────────────► │  TS_ESC  │──────────────────────────────────► ┌───────────┐
 *  │          │                │          │  timeout 40ms (rien recu)           │ TS_MATCH  │──A──► KEY_UP
 *  │          │                │          │──────────────────────► TS_INIT      │           │──B──► KEY_DOWN
 *  │          │                └──────────┘                                     │           │──C──► KEY_RIGHT
 *  │          │                                                   timeout 200ms │           │──D──► KEY_LEFT
 *  │          │                                         (sequence incomplete)──►TS_INIT     └───────────┘
 *  │          │
 *  │          │  0x0D (CR)     ┌──────────┐
 *  │          │──────────────► │  TS_CR   │──► KEY_RETURN  puis absorbe 0x0A (LF) ou 0x00 (NUL)
 *  │          │                └──────────┘    → retour TS_INIT
 *  │          │
 *  │          │  0x09 TAB     ──► KEY_TAB
 *  │          │  0x08/0x7F BS ──► KEY_BACKSPACE
 *  │          │  0x20..0x7E   ──► caractere ASCII imprimable (retourne tel quel comme int)
 *  │          │  autres       ──► KEY_NONE (ignore silencieusement)
 *  └──────────┘
 *
 *  Note Bluetooth : CMDLINE_NO_IAC defini → branche TS_IAC desactivee
 *  a la compilation. Les sequences ESC (fleches) et CR/LF restent actives.
 *  Tous les etats retournent a TS_INIT apres traitement d'un seul octet option.
 *
 *********************************************************************************************************************
 *
 *  MACHINE D'ETAT ESC SERIAL — update()
 *  ──────────────────────────────────────
 *  Version simplifiee sans timeout (Serial est synchrone : les 3 octets
 *  ESC [ A arrivent dans la meme lecture sans delai entre eux).
 *
 *  Variable : _serialEscState (uint8_t membre prive de CmdLine)
 *
 *  _serialEscState :
 *    0  ──► 0x1B (ESC) recu ──► etat 1
 *    1  ──► '[' recu         ──► etat 2   /   tout autre octet → etat 0 (abandonne)
 *    2  ──► 0x41 (A) → KEY_UP       → etat 0
 *           0x42 (B) → KEY_DOWN     → etat 0
 *           0x43 (C) → KEY_RIGHT    → etat 0
 *           0x44 (D) → KEY_LEFT     → etat 0
 *           autre    → ignore       → etat 0
 *
 *********************************************************************************************************************
 *
 *  NEGOCIATION TELNET — desactivation de l'echo local PuTTY (RFC 857)
 *  ──────────────────────────────────────────────────────────────────
 *  PuTTY Telnet affiche localement chaque octet qu'il envoie (echo local).
 *  Sans desactivation : TAB → "^I" affiche, fleche ↑ → "^[[A" affiche.
 *
 *  Solution — negociation reactive (une seule fois par connexion) :
 *    1. Le client envoie son bloc IAC de negociation a la connexion.
 *    2. A la reception du 1er WILL du client, l'Arduino repond :
 *         IAC WILL ECHO  (FF FB 01) → "c'est moi qui gere l'echo"
 *         IAC WILL SGA   (FF FB 03) → mode full-duplex (pas de Go-Ahead)
 *    3. PuTTY repond IAC DO ECHO et desactive son echo local.
 *
 *  La reponse n'est envoyee QU'UNE FOIS (flag willEchoSent dans CanalState).
 *  redrawLine() sert de filet de securite si la negociation arrive en retard.
 *  Non disponible en mode Bluetooth (CMDLINE_NO_IAC) — BT n'est pas TCP.
 *
 *********************************************************************************************************************
 *
 *  CORRECTION BUG v22 — RETOUR BLUETOOTH ABSENT
 *  ──────────────────────────────────────────────
 *  Symptome : commandes BT bien recues, aucune reponse sur l'app Android.
 *
 *  Cause 1 (v22 passe 1) : print/println/write ecrivaient hardcode sur `Serial`
 *    au lieu de `_stream`. Correction : Serial.xxx() → _stream.xxx()
 *
 *  Cause 2 (v22 passe 2) : les fonctions de commande appellent cmdLine.print()
 *    (objet lie a Serial), jamais cmdLineBT.print(). BT ne recevait rien.
 *    Correction : ajout 3eme canal _btStream dans cmdLine.
 *    setBluetoothOutput(&SerialBT) → cmdLine.print() ecrit sur Serial+Telnet+BT
 *
 *  Cause 3 (v22 passe 3) : #include <BluetoothSerial.h> dans CmdLine.h causait
 *    des conflits de symboles avec la lib WiFi → Telnet corrompu.
 *    Correction : BluetoothSerial* → Stream* (BluetoothSerial herite de Stream).
 *    Aucun include supplementaire requis dans la lib.
 *
 *  Architecture finale v22 :
 *    CmdLine cmdLine(Serial)
 *      _stream   = Serial       → Canal 1 : Serial (USB / PuTTY Serial)
 *      _c        = clientTelnet → Canal 2 : Telnet TCP port 23
 *      _btStream = &SerialBT    → Canal 3 : Bluetooth Android     (v22 ✓)
 *      _btActive = true         → active par setBluetoothOutput()
 *
 *********************************************************************************************************************
 *
 *  HISTORIQUE DES VERSIONS
 *  ────────────────────────
 *  v23 : Ajout surcharges explicites print(const char*) / println(const char*).
 *        Cas d'usage : sprintf(hexByte, "%02X", val); cmdLine.print(hexByte);
 *        Sans ces surcharges le template T=char* fonctionnait mais le choix
 *        du compilateur n'etait pas garanti (ambiguite avec String/FlashString).
 *  v22 : CORRECTION BUG retour Bluetooth absent — 3 passes successives.
 *        Serial hardcode → _stream. 3eme canal BT (Stream*). Conflit WiFi/BT.
 *  v21 : Ajout print(long, base) / println(long, base) / write(buf, len).
 *        Compatible Serial.print(val, DEC/HEX/OCT/BIN).
 *  v20 : Multi-plateforme (UNO R4, ESP8266, ESP32 WiFi, ESP32 Bluetooth).
 *        Fichier CmdLineKey.h separe. Renommage CmdLineTelnet_UNOR4 → CmdLine.
 *        updateTelnet() → updateNetwork(). _clientConnected() abstrait.
 *  v19 : Correction bug casse — cmdStrcmpLower() / cmdStrncmpLower().
 *        Les noms cmd_t peuvent avoir des majuscules (SaveConf, LEDon...).
 *  v18 : setEcho(bool) / getEcho(). CMDLINE_ECHO_DEFAULT.
 *        HISTORY_SIZE → CMDLINE_HISTORY_SIZE.
 *  v17 : Documentation complete. CMDLINE_CASE_INSENSITIVE.
 *  v16 : Reecriture basee sur Terminal.cpp (rweather, MIT).
 *        Machine d'etat Telnet 9 etats. Timeout ESC 40ms.
 *        Backspace = \b \b. WILL ECHO reactif (RFC 857).
 *
 *********************************************************************************************************************
 *
 *  USAGE MINIMAL — sketch Arduino
 *  ────────────────────────────────
 *
 *  // ── Optionnel : surcharger les constantes avant l'include ─────────────────
 *  // #define CMDLINE_BUFFER_SIZE     256
 *  // #define CMDLINE_HISTORY_SIZE     20
 *  // #define CMDLINE_ECHO_DEFAULT      0   // desactive par defaut (MobaXterm)
 *  // #define CMDLINE_PLATFORM_ESP32        // forcer ESP32 WiFi
 *
 *  #include <CmdLine.h>                     // Inclut CmdLineKey.h automatiquement
 *
 *  CmdLine       cmdLine(Serial);           // Canal 1 : Serial
 *  CmdLineServer serverTelnet(23);          // Serveur TCP port 23 (EthernetServer ou WiFiServer)
 *  CmdLineClient clientTelnet;              // Client courant (EthernetClient ou WiFiClient)
 *  #define countof(a) (sizeof(a)/sizeof(a[0]))
 *
 *  // ── Fonctions de commande : signature obligatoire void f(const char *arg) ──
 *  void cmdHello(const char *arg) { cmdLine.println(F("Hello !")); }
 *  void cmdPing (const char *arg) { cmdLine.print(F("pong : ")); cmdLine.println(arg); }
 *
 *  const cmd_t commands[] = {
 *      { "hello",    cmdHello },
 *      { "ping",     cmdPing  },
 *      { "SaveConf", cmdSave  },   // Noms avec majuscules supportes (v19+)
 *  };
 *
 *  void setup() {
 *      Serial.begin(115200);
 *      // Ethernet.begin(mac, ip);  // UNO R4
 *      // WiFi.begin(ssid, pass);   // ESP8266 / ESP32
 *      serverTelnet.begin();
 *      cmdLine.begin(commands, countof(commands));
 *  }
 *
 *  void loop() {
 *      cmdLine.update();                            // Canal Serial
 *      clientTelnet = serverTelnet.available();     // UNO R4 / ESP8266
 *      // if (serverTelnet.hasClient()) ...         // ESP32 (gestion deconnexion)
 *      cmdLine.updateNetwork(clientTelnet);         // Canal reseau Telnet/WiFi
 *      // cmdLine.setEcho(false);   // MobaXterm — evite le doublement
 *      // cmdLine.setEcho(true);    // PuTTY Telnet — echo normal
 *  }
 *
 *********************************************************************************************************************/

#ifndef __CMDLINE_H__
#define __CMDLINE_H__

#include "CmdLineKey.h"    // Selection plateforme + codes touches + enum TelnetState + constantes IAC


// ════════════════════════════════════════════════════════════════════════════
//  CANAL BLUETOOTH OPTIONNEL — 3eme sortie simultanee  (v22)
//
//  BluetoothSerial herite de Stream → pas besoin d'inclure BluetoothSerial.h
//  ici dans la lib. L'inclure causait des conflits de symboles avec la lib
//  WiFi (IAC Telnet corrompu) quand CMDLINE_PLATFORM_ESP32 etait actif.
//
//  Solution (v22 passe 3) :
//    _btStream est un simple pointeur Stream* — le .ino qui inclut deja
//    BluetoothSerial.h appelle setBluetoothOutput(&SerialBT), et &SerialBT
//    est implicitement compatible Stream*.
//
//  CMDLINE_HAS_BLUETOOTH est defini inconditionnellement car le code BT dans
//  print/println/write est INERTE si _btStream == nullptr (valeur par defaut).
//  Il ne genere aucune consommation RAM ou CPU supplementaire quand BT est inactif.
// ════════════════════════════════════════════════════════════════════════════
#define CMDLINE_HAS_BLUETOOTH   // Canal BT disponible — actif seulement si setBluetoothOutput() appele


// ════════════════════════════════════════════════════════════════════════════
//  CONSTANTES PARAMETRABLES
//
//  Ces valeurs peuvent etre redefinies dans le sketch .ino AVANT l'include :
//    #define CMDLINE_BUFFER_SIZE 256
//    #include <CmdLine.h>
//
//  Toutes sont protegees par #ifndef → elles ne s'appliquent que si
//  l'utilisateur n'a pas encore defini la constante.
// ════════════════════════════════════════════════════════════════════════════

#ifndef CMDLINE_BUFFER_SIZE
#define CMDLINE_BUFFER_SIZE     128     // Taille max d'une ligne de commande (octets)
                                        // Inclut commande + espace + argument + '\0'
                                        // RAM consommee : BUFFER × HISTORY × 2 canaux
                                        //   ex : 128 × 10 × 2 = 2560 octets
#endif

#ifndef CMDLINE_HISTORY_SIZE
#define CMDLINE_HISTORY_SIZE     10     // Nombre de commandes memorisees dans l'historique
                                        // Navigation ↑ ↓ dans cette fenetre circulaire
                                        // Doublons consecutifs non enregistres
#endif

#ifndef ESC_TIMEOUT_MS
#define ESC_TIMEOUT_MS           40     // Delai max (ms) apres ESC isole avant abandon
                                        // Si rien n'arrive en 40ms → ESC seul (ignore)
                                        // Valeur issue de Terminal.cpp (rweather)
#endif

#ifndef SEQ_TIMEOUT_MS
#define SEQ_TIMEOUT_MS          200     // Delai max (ms) pour completer une sequence ESC[x
                                        // ESC[ recu mais A/B/C/D trop long → abandon
#endif

#ifndef CMDLINE_CASE_INSENSITIVE
#define CMDLINE_CASE_INSENSITIVE  1     // 1 = insensible a la casse (defaut)
                                        //   PING = Ping = ping → toujours reconnu
                                        //   Noms cmd_t avec majuscules preserves :
                                        //   SaveConf, LEDon, serveurWEB...
                                        //   L'argument n'est JAMAIS converti
                                        // 0 = strcmp strict (casse obligatoire)
#endif

#ifndef CMDLINE_ECHO_DEFAULT
#define CMDLINE_ECHO_DEFAULT      1     // 1 = echo active au demarrage (PuTTY Telnet)
                                        // 0 = echo desactive (MobaXterm — double sinon)
                                        // Modifiable a l'execution : setEcho(true/false)
#endif


// ════════════════════════════════════════════════════════════════════════════
//  STRUCTURES PUBLIQUES
// ════════════════════════════════════════════════════════════════════════════

// ── cmd_t ─────────────────────────────────────────────────────────────────────
//  Entree de la table des commandes.
//  Declarer globalement dans le sketch :
//    const cmd_t commands[] = {
//        { "ping",     cmdPing     },
//        { "SaveConf", cmdSaveConf },   // majuscules preservees et reconnues
//    };
//  Signature obligatoire de la fonction : void nomFonction(const char *arg)
//    IN  arg : partie argument de la ligne (chaine vide "" si aucun argument)
//    OUT —   : utiliser cmdLine.print() / println() pour repondre
// ─────────────────────────────────────────────────────────────────────────────
struct cmd_t {
    const char *command;              // Nom de la commande (ex : "ping", "SaveConf")
    void (*action)(const char *arg);  // Pointeur vers la fonction a appeler
};


// ── CanalState ────────────────────────────────────────────────────────────────
//  Etat complet d'un canal d'entree (Serial OU reseau).
//  Deux instances dans CmdLine : _stS (Serial) et _stN (Network).
//  Garantit l'independance totale des deux canaux d'entree.
//
//  Initialisation : initSt() remet tout a 0 / false / valeurs par defaut.
//  Consommation RAM par instance : ≈ 1560 octets (BUFFER=128, HISTORY=10).
// ─────────────────────────────────────────────────────────────────────────────
struct CanalState {

    // ── Buffer de saisie ──────────────────────────────────────────────────────
    //  Ligne en cours de frappe, non terminee par '\0' pendant la saisie.
    //  Terminee par '\0' uniquement dans onEnter() avant processCommand().
    char   buf[CMDLINE_BUFFER_SIZE];  // Tableau des caracteres saisis
    size_t len;   // Nombre de caracteres valides dans buf  (0 .. BUFFER_SIZE-1)
                  // → Modifie par : insertChar() +1, delLeft() -1
    size_t cur;   // Position du curseur dans buf           (0=debut .. len=fin)
                  // → Modifie par : insertChar() +1, delLeft() -1, moveCursor() ±1
                  // → Remis a 0 par prompt(), remis a len par historyNav()

    // ── Historique circulaire FIFO ────────────────────────────────────────────
    //  histHead pointe sur la prochaine case libre a ecrire (avance apres chaque push).
    //  Les entrees valides sont a index : (histHead - 1) mod N  = plus recente
    //                                    (histHead - histCount) mod N = plus ancienne
    char   history[CMDLINE_HISTORY_SIZE][CMDLINE_BUFFER_SIZE]; // Tableau circulaire
    int    histCount; // Nombre d'entrees valides        (0 .. CMDLINE_HISTORY_SIZE)
                      // → Incremente par historyPush() (plafonne a HISTORY_SIZE)
    int    histHead;  // Index de la prochaine ecriture  (0 .. HISTORY_SIZE-1)
                      // → Avance mod HISTORY_SIZE apres chaque historyPush()
    int    histPos;   // Position de navigation :
                      //   -1 = on est sur la ligne courante (pas en navigation)
                      //    0 = entree la plus recente (1er ↑)
                      //    histCount-1 = entree la plus ancienne
                      // → Modifie par historyNav() : ↑ +1, ↓ -1, retour -1

    // ── Sauvegarde ligne courante (navigation ↑↓) ────────────────────────────
    //  Avant le 1er ↑, la ligne en cours est copiee dans savedLine.
    //  Quand on revient en bas avec ↓ jusqu'a histPos=-1, savedLine est restauree.
    char   savedLine[CMDLINE_BUFFER_SIZE]; // Copie de buf avant le 1er ↑
    bool   lineSaved;  // true = savedLine contient une copie valide
                       // → Mis a true par historyNav() au 1er ↑
                       // → Mis a false par prompt() (reinitialisation)

    // ── Completion TAB ────────────────────────────────────────────────────────
    //  1er TAB : tabMatchIdx passe de -1 a la valeur initiale du cycle.
    //  Re-TAB  : tabMatchIdx s'incremente (cycle modulo tabMatchCount).
    //  Tout autre touche remet tabMatchIdx a -1 (dans handleKey).
    int    tabMatchCount; // Nombre total de correspondances pour le prefixe actuel
                          // → Calcule dans tabComplete() au 1er TAB
    int    tabMatchIdx;   // -1 = pas de cycle actif (attente 1er TAB)
                          // ≥0 = index de la prochaine correspondance a afficher
                          // → Incremente apres chaque Re-TAB
    size_t tabPrefixLen;  // Longueur du prefixe lors du 1er TAB
                          // → Conserve pendant tout le cycle Re-TAB
                          // → Permet de comparer toujours sur le meme prefixe d'origine

    // ── Machine d'etat reseau ─────────────────────────────────────────────────
    //  Utilisee UNIQUEMENT par readNetworkKey() / updateNetwork().
    //  Ignoree pour le canal Serial (qui a sa propre machine _serialEscState).
    //  Persiste entre les appels de loop() → sequences multi-octets sur plusieurs paquets TCP.
    TelnetState   tstate;        // Etat courant (TS_INIT .. TS_DONT)
                                 // → Modifie a chaque octet reseau recu
    unsigned long timer;         // Timestamp millis() pour timeouts ESC (40ms) / SEQ (200ms)
                                 // → Arme quand tstate passe a TS_ESC ou TS_MATCH
    bool          willEchoSent;  // true = IAC WILL ECHO + SGA deja envoye pour ce client
                                 // → Empeche de renvoyer la negociation plusieurs fois
                                 // → Remis a false par initSt() (nouvelle connexion)
};


// ════════════════════════════════════════════════════════════════════════════
//  CLASSE PRINCIPALE — CmdLine
// ════════════════════════════════════════════════════════════════════════════

class CmdLine {
public:

    // ── Constructeur ─────────────────────────────────────────────────────────
    //  Initialise les etats internes des deux canaux (Serial et reseau).
    //  Ne demarre pas la communication — appeler Serial.begin() avant begin().
    //
    //  IN  stream : reference au stream Serial de l'Arduino
    //              (ex : Serial, Serial1, SerialBT pour ESP32 Bluetooth)
    //  VAR modifiees : _stS et _stN (initSt : tout a 0, histPos=-1, tabMatchIdx=-1)
    //                  _stream = stream (reference conservee pour toute la vie de l'objet)
    explicit CmdLine(Stream &stream);


    // ── Initialisation ───────────────────────────────────────────────────────
    //  Enregistre la table des commandes et affiche le premier prompt "> ".
    //  A appeler dans setup() apres Serial.begin() et l'init reseau.
    //
    //  IN  commands : tableau global cmd_t declare dans le sketch
    //  IN  num      : nombre d'entrees — utiliser countof(commands)
    //  OUT —        : affiche "> " sur _stream (prompt initial)
    //  VAR modifiees : _commands = commands
    //                  _num      = num
    //                  _stS.len=0, _stS.cur=0, _stS.histPos=-1
    void begin(const cmd_t *commands, size_t num);

    // ── Table de commandes utilisateur (optionnelle) ────────────────────────
    //  Enregistre un second tableau de commandes parcouru apres commands[].
    //  A appeler dans setup() apres begin(), uniquement si un tableau
    //  utilisateur est defini. Si non appele, _user_commands reste nullptr
    //  et processCommand() / tabComplete() fonctionnent comme avant (v23).
    //
    //  IN  cmds : tableau global cmd_t utilisateur (ex : user_commands[])
    //  IN  n    : nombre d entrees — utiliser countof(user_commands)
    //  VAR modifiees : _user_commands = cmds
    //                  _user_num      = n
    void addCommands(const cmd_t *cmds, size_t n);


    // ── Controle de l'echo ───────────────────────────────────────────────────
    //  Active ou desactive le renvoi en echo des caracteres imprimables.
    //
    //  Echo ACTIVE (true — defaut CMDLINE_ECHO_DEFAULT=1) :
    //    L'Arduino renvoie chaque caractere tape vers le terminal.
    //    → Mode standard pour PuTTY Telnet (pas d'echo local).
    //    → Necessaire pour PuTTY Serial.
    //
    //  Echo DESACTIVE (false) :
    //    L'Arduino n'envoie aucun echo des caracteres imprimables.
    //    → Necessaire pour MobaXterm (echo local active → doublement sinon).
    //    → Utile pour tout terminal configure en "local echo ON".
    //    → Pour ESP32 Bluetooth si l'app Android double les caracteres.
    //
    //  Note : redrawLine(), prompt(), et les sequences ANSI (curseur, ESC[K)
    //         sont TOUJOURS envoyes quel que soit l'etat de l'echo.
    //         Seul l'echo brut des caracteres imprimables dans insertChar()
    //         est conditionne par ce flag.
    //
    //  IN  enable : true = echo active  /  false = echo desactive
    //  VAR modifiee : _echoEnabled
    void setEcho(bool enable) { _echoEnabled = enable; }

    //  Retourne l'etat courant de l'echo.
    //  OUT bool : true = active  /  false = desactive
    bool getEcho() const { return _echoEnabled; }


    // ── Canal Bluetooth — 3eme sortie simultanee  (v22) ─────────────────────
    //  Connecte un objet BluetoothSerial comme 3eme canal de sortie.
    //  Apres cet appel, chaque cmdLine.print() / println() / write() ecrira
    //  simultanement sur : _stream (Serial) + Telnet (si connecte) + SerialBT.
    //
    //  Architecture de sortie apres setBluetoothOutput(&SerialBT) :
    //    cmdLine.print("texte")
    //         │
    //         ├──► _stream.print()    → Serial   (USB / PuTTY Serial)
    //         ├──► _c->print()        → Telnet   (WiFiClient port 23)  [si connecte]
    //         └──► _btStream->print() → SerialBT (app Android)         [si active]
    //
    //  Pourquoi Stream* et non BluetoothSerial* ?
    //    Inclure BluetoothSerial.h dans CmdLine.h causait des conflits de symboles
    //    avec la lib WiFi (IAC Telnet corrompu). BluetoothSerial herite de Stream
    //    → &SerialBT est compatible Stream* sans include supplementaire dans la lib.
    //
    //  Appeler dans setup() APRES SerialBT.begin() :
    //    SerialBT.begin("ESP32-CmdLine");
    //    cmdLine.setBluetoothOutput(&SerialBT);   // &SerialBT compatible Stream*
    //
    //  IN  bt : pointeur Stream sur l'objet BluetoothSerial global (&SerialBT)
    //           nullptr pour desactiver le canal BT
    //  VAR modifiees : _btStream = bt
    //                  _btActive = (bt != nullptr)
#ifdef CMDLINE_HAS_BLUETOOTH
    void setBluetoothOutput(Stream *bt) { _btStream = bt; _btActive = (bt != nullptr); }
#endif


    // ── Traitement Serial ─────────────────────────────────────────────────────
    //  Lit les octets disponibles sur _stream et les traite via handleKey().
    //  Retourne immediatement si aucun octet n'est disponible (non bloquant).
    //  A appeler dans loop() a chaque cycle.
    //
    //  Machine d'etat ESC Serial integree (3 etats, sans timeout) :
    //    _serialEscState 0 → 1 (ESC recu) → 2 ([ recu) → 0 (lettre finale)
    //
    //  IN  —          : lit _stream.read() tant que .available() > 0
    //  OUT —          : peut appeler handleKey() pour chaque touche decodee
    //  VAR modifiees  : _serialEscState (machine d'etat ESC Serial)
    //                   via handleKey() → _stS.buf, _stS.len, _stS.cur
    //                   via onEnter() → _stS.histHead, histCount, histPos
    void update();


    // ── Traitement reseau (Telnet / WiFi / Bluetooth) ─────────────────────────
    //  Lit les octets du client reseau, les decode via readNetworkKey()
    //  (machine d'etat 9 etats persistants) et les traite via handleKey().
    //  Memorise le pointeur client pour que print/println ecrivent sur ce canal.
    //  A appeler dans loop() a chaque cycle.
    //
    //  Obtention du client :
    //    UNO R4      : clientTelnet = serverTelnet.available()
    //    ESP8266     : clientTelnet = serverTelnet.available()
    //    ESP32 WiFi  : if (serverTelnet.hasClient()) clientTelnet = serverTelnet.available()
    //    ESP32 BT    : updateNetwork(SerialBT) directement
    //
    //  IN  client : client reseau courant
    //              UNO R4      → EthernetClient (serverTelnet.available())
    //              ESP8266/32  → WiFiClient (serverTelnet.available())
    //              ESP32 BT    → BluetoothSerial (objet global, pas de serverTelnet)
    //  OUT —       : peut appeler handleKey() pour chaque touche decodee
    //  VAR modifiees : _c = &client (memorise pour les sorties print/println)
    //                  via readNetworkKey() → _stN.tstate, timer, willEchoSent
    //                  via handleKey() → _stN.buf, _stN.len, _stN.cur
    //                  via onEnter() → _stN.histHead, histCount, histPos
    void updateNetwork(CmdLineClient &client);


    // ── Sortie simultanee _stream + reseau + Bluetooth ────────────────────────
    //  Ces fonctions remplacent Serial.print(...) dans les fonctions de commande.
    //  Elles envoient simultanement sur les 3 canaux actifs :
    //    Canal 1 : _stream     → Serial (ou SerialBT si _stream=SerialBT)
    //    Canal 2 : _c          → Telnet / WiFi (si _c != nullptr et connecte)
    //    Canal 3 : _btStream   → Bluetooth SerialBT (si _btActive == true)  (v22)
    //
    //  Surcharges explicites (.cpp) :
    //    println()              — saut de ligne seul
    //    print/println(char*)   — chaine C RAM, resultat de sprintf (v23)
    //    print/println(F(...))  — chaine PROGMEM
    //    print/println(n,base)  — entier avec base DEC/HEX/OCT/BIN
    //    write(buf,len)         — octets bruts
    //
    //  Note v22 : avant v22, ces fonctions ecrivaient hardcode sur `Serial`
    //  au lieu de `_stream` → pour cmdLineBT(SerialBT) la sortie allait sur Serial.
    //  Corrige : Serial.xxx() → _stream.xxx() dans toutes les fonctions de sortie.

    void println();                               // Saut de ligne \r\n sur les 3 canaux
    void print  (const char *str);               // Chaine C RAM (char[], sprintf...) → 3 canaux  (v23)
    void println(const char *str);               // Chaine C RAM + \r\n → 3 canaux  (v23)
    void print  (const __FlashStringHelper *t);   // Chaine F("...") PROGMEM → 3 canaux
    void println(const __FlashStringHelper *t);   // Chaine F("...") + \r\n  → 3 canaux

    // Template generique : int, long, float, String, char[], bool...
    // Compile une version pour chaque type utilise dans le sketch.
    //
    // IN  a : valeur a afficher (type quelconque supporte par Print)
    // OUT — : ecrit sur _stream + _c (si connecte) + _btStream (si active)
    template<typename T>
    void print(T a) {
        _stream.print(a);                                       // Canal 1 : Serial (ou SerialBT si _stream=SerialBT)
        if (_c && _clientConnected()) _c->print(a);            // Canal 2 : Telnet (WiFiClient) si connecte
#ifdef CMDLINE_HAS_BLUETOOTH
        if (_btActive && _btStream) _btStream->print(a);       // Canal 3 : Bluetooth (Stream*) si active  (v22)
#endif
    }
    template<typename T>
    void println(T a) {
        _stream.println(a);                                     // Canal 1 : Serial (ou SerialBT si _stream=SerialBT)
        if (_c && _clientConnected()) _c->println(a);          // Canal 2 : Telnet (WiFiClient) si connecte
#ifdef CMDLINE_HAS_BLUETOOTH
        if (_btActive && _btStream) _btStream->println(a);     // Canal 3 : Bluetooth (Stream*) si active  (v22)
#endif
    }

    // ── print(val, base) / println(val, base) ────────────────────────────────
    //  Affichage d'un entier avec base numerique, identique a Serial.print(n, base).
    //  Deux surcharges : long et unsigned long.
    //  Les autres types entiers (int, byte, uint8_t...) se convertissent
    //  implicitement vers l'un de ces deux types.
    //
    //  IN  n    : valeur entiere a afficher
    //  IN  base : DEC (10), HEX (16), OCT (8), BIN (2)
    //  OUT —    : affiche sur _stream + reseau (si connecte) + BT (si active)
    //
    //  Exemples :
    //    cmdLine.print(42, DEC);          → "42"
    //    cmdLine.print(255, HEX);         → "FF"
    //    cmdLine.print(255, BIN);         → "11111111"
    //    cmdLine.println(0x1A2B, HEX);    → "1A2B\r\n"
    void print  (long           n, int base);
    void println(long           n, int base);
    void print  (unsigned long  n, int base);
    void println(unsigned long  n, int base);

    // ── write(buf, len) ──────────────────────────────────────────────────────
    //  Ecriture binaire brute simultanee sur les 3 canaux.
    //  Utile pour envoyer des sequences ANSI, buffers binaires, ou tout
    //  contenu prepare dans un tableau d'octets.
    //
    //  IN  buf  : pointeur sur le tableau d'octets a envoyer
    //  IN  len  : nombre d'octets a envoyer
    //  OUT size_t : nombre d'octets ecrits sur _stream (valeur de reference)
    //
    //  Exemple — texte en rouge ANSI :
    //    uint8_t rouge[] = { 0x1B, '[', '3', '1', 'm' };
    //    cmdLine.write(rouge, sizeof(rouge));
    //    cmdLine.println(F("ERREUR"));
    //    uint8_t reset[] = { 0x1B, '[', '0', 'm' };
    //    cmdLine.write(reset, sizeof(reset));
    size_t write(const uint8_t *buf, size_t len);


private:

    // ════════════════════════════════════════════════════════════════════════
    //  METHODES PRIVEES
    // ════════════════════════════════════════════════════════════════════════

    // ── Decodage reseau ───────────────────────────────────────────────────────

    //  Lit UN octet du client reseau et le decode via la machine d'etat _stN.tstate.
    //  La machine d'etat persiste entre les appels (sequences multi-octets TCP).
    //  Gere : IAC Telnet, sequences ESC ANSI, CR/LF/NUL, TAB, BS/DEL, ASCII.
    //
    //  En mode Bluetooth (CMDLINE_NO_IAC) :
    //    La gestion IAC est desactivee par compilation conditionnelle.
    //    Les sequences ESC (fleches) et CR/LF restent actives.
    //
    //  IN   client : client reseau courant
    //  OUT  int    : KEY_xxx (KEY_UP/DOWN/RIGHT/LEFT/RETURN/BACKSPACE/TAB)
    //               ou char ASCII 0x20..0x7E
    //               ou KEY_NONE si sequence incomplete ou octet ignore
    //  VAR modifiees : _stN.tstate (etat suivant de la machine d'etat)
    //                  _stN.timer  (arme lors de TS_ESC et TS_MATCH)
    //                  _stN.willEchoSent (mis a true au 1er WILL recu)
    int readNetworkKey(CmdLineClient &client);

#ifndef CMDLINE_NO_IAC
    //  Envoie une sequence de negociation Telnet : IAC cmd opt (3 octets).
    //  Exemple : IAC WILL ECHO = { 0xFF, 0xFB, 0x01 }
    //  Non disponible en mode Bluetooth (CMDLINE_NO_IAC defini).
    //
    //  IN   client : client reseau destinataire
    //  IN   cmd    : CMDLINE_WILL / CMDLINE_WONT / CMDLINE_DO / CMDLINE_DONT
    //  IN   opt    : CMDLINE_OPT_ECHO (0x01) / CMDLINE_OPT_SGA (0x03) / autre
    //  OUT  —      : ecrit 3 octets sur le socket TCP
    void telnetCmd(CmdLineClient &client, uint8_t cmd, uint8_t opt);
#endif

    //  Teste si le client reseau est connecte (abstraction multi-plateforme).
    //  L'API differe selon la plateforme :
    //    EthernetClient / WiFiClient  → .connected()
    //    BluetoothSerial              → .hasClient()
    //
    //  OUT bool : true si client connecte et pret a recevoir
    bool _clientConnected() const;


    // ── Traitement commun Serial + reseau ─────────────────────────────────────

    //  Dispatch d'une touche decodee vers l'action correspondante.
    //  Commun aux deux canaux — seule la source (st, out) differe.
    //
    //  IN  st  : etat du canal (_stS pour Serial, _stN pour reseau)
    //  IN  out : stream de sortie (Serial ou client reseau)
    //  IN  key : KEY_RETURN, KEY_BACKSPACE, KEY_TAB, KEY_UP/DOWN/RIGHT/LEFT,
    //            ou char imprimable 0x20..0x7E
    //  OUT —   : appelle la methode correspondante selon la touche
    //  VAR modifiees : st.tabMatchIdx = -1 (reinitialisation cycle TAB
    //                  pour toute touche sauf TAB)
    //                  puis selon la touche :
    //    KEY_RETURN    → onEnter() → processCommand() → prompt()
    //    KEY_BACKSPACE → delLeft()  : st.buf, st.len, st.cur
    //    KEY_TAB       → tabComplete() : st.buf, st.len, st.cur, tabMatchIdx
    //    KEY_UP/DOWN   → historyNav() : st.buf, st.len, st.cur, histPos
    //    KEY_LEFT/RIGHT→ moveCursor() : st.cur
    //    char 0x20..7E → insertChar() : st.buf, st.len, st.cur
    void handleKey(CanalState &st, Print &out, int key);

    //  Appele quand KEY_RETURN est recu.
    //  1. Termine st.buf par '\0'
    //  2. Si buffer non vide : enregistre dans l'historique + execute la commande
    //  3. Affiche le prompt suivant
    //
    //  IN  st, out
    //  OUT — : appelle historyPush(), processCommand(), prompt()
    //  VAR modifiees : st.buf[st.len] = '\0'
    //                  st.histHead, st.histCount (via historyPush)
    //                  st.len=0, st.cur=0, st.histPos=-1 (via prompt)
    void onEnter(CanalState &st, Print &out);

    //  Parse st.buf et execute la commande reconnue.
    //  1. [si CMDLINE_CASE_INSENSITIVE=1] Convertit la commande en minuscules
    //     (uniquement la partie avant le 1er espace — l'argument est intact)
    //  2. Separe commande et argument au 1er espace
    //  3. Recherche dans _commands[] via cmdStrcmpLower()
    //     Match → appelle _commands[i].action(arg)
    //     Pas de match → affiche "Invalid command" sur tous les canaux
    //
    //  IN  st, out
    //  OUT — : appelle action(arg) ou affiche "Invalid command"
    //  VAR modifiees : st.buf (partie commande convertie en minuscules si CASE_INSENSITIVE)
    void processCommand(CanalState &st, Print &out);

    //  Remet le buffer a zero et affiche "> " sur le canal.
    //
    //  IN  st, out
    //  OUT — : affiche "> " sur out
    //  VAR modifiees : st.len=0, st.cur=0, st.histPos=-1, st.lineSaved=false
    void prompt(CanalState &st, Print &out);

    //  Reaffiche la ligne complete depuis le debut de la ligne terminal.
    //  Sequence : \r + "> " + buf[0..len] + ESC[K + ESC[D × (len-cur)
    //
    //  \r (retour colonne 0 sans descendre) :
    //    → Ecrase l'echo parasite de PuTTY sur la meme ligne
    //      (ex: "^I" pour TAB, "^[[A" pour fleche ↑ avant que la negociation IAC
    //       WILL ECHO ne soit effective)
    //    → Apres redrawLine(), la ligne correcte remplace ces artefacts
    //
    //  ESC[K : efface du curseur jusqu'a la fin de la ligne (residus eventuels)
    //  ESC[D × N : replace le curseur a la position st.cur (si < st.len)
    //
    //  IN  st, out
    //  OUT — : ecrit sur out (aucun champ de st modifie)
    void redrawLine(CanalState &st, Print &out);


    // ── Historique ────────────────────────────────────────────────────────────

    //  Ajoute une commande dans l'historique circulaire FIFO.
    //  Ignore les lignes vides (cmd[0]=='\0') et les doublons consecutifs.
    //
    //  Algorithme circulaire :
    //    Ecriture a history[histHead], puis histHead = (histHead+1) mod N
    //    histCount plafonne a CMDLINE_HISTORY_SIZE
    //
    //  IN  st
    //  IN  cmd : chaine '\0'-terminee a enregistrer
    //  OUT —   : rien si vide ou doublon
    //  VAR modifiees : st.history[st.histHead] (copie de cmd)
    //                  st.histHead = (histHead+1) mod HISTORY_SIZE
    //                  st.histCount = min(histCount+1, HISTORY_SIZE)
    void historyPush(CanalState &st, const char *cmd);

    //  Navigation ↑ / ↓ dans l'historique.
    //
    //  ↑ (dir=-1) : remonte vers le passe
    //    1er ↑ → sauvegarde la ligne courante dans st.savedLine (lineSaved=true)
    //    histPos avance : -1 → 0 → 1 → ... → histCount-1 (butee)
    //
    //  ↓ (dir=+1) : redescend vers le present
    //    histPos recule : histCount-1 → ... → 0 → -1
    //    histPos==-1 → restaure st.savedLine dans st.buf
    //
    //  Indexation circulaire :
    //    idx = (histHead - 1 - histPos + HISTORY_SIZE) mod HISTORY_SIZE
    //
    //  IN  st, out
    //  IN  dir : -1 = ↑ (vers le passe)  /  +1 = ↓ (vers le present)
    //  OUT —   : appelle redrawLine() pour afficher la nouvelle ligne
    //  VAR modifiees : st.histPos (±1)
    //                  st.savedLine, st.lineSaved (1er ↑ uniquement)
    //                  st.buf, st.len, st.cur (chargement entree historique)
    void historyNav(CanalState &st, Print &out, int dir);


    // ── Completion TAB ────────────────────────────────────────────────────────

    //  Completion TAB sur le prefixe st.buf[0..cur-1].
    //
    //  1er TAB (tabMatchIdx == -1) :
    //    Compte les commandes dont le debut correspond au prefixe.
    //    0 match  → BEL (0x07) — bip sonore
    //    1 match  → completion immediate : copie le nom + ajoute espace + redrawLine
    //    N matchs → affiche la liste + charge la 1ere + active le cycle Re-TAB
    //               (tabMatchIdx = 1, tabMatchCount = N)
    //
    //  Re-TAB (tabMatchIdx >= 0) :
    //    Charge la Neme correspondance (cycle modulo tabMatchCount)
    //    tabPrefixLen conserve → compare toujours sur le prefixe d'origine
    //    tabMatchIdx s'incremente apres chaque Re-TAB
    //
    //  Si CMDLINE_CASE_INSENSITIVE=1 :
    //    cmdStrncmpLower() → "save" trouve "SaveConf", "saveAll"...
    //    Les noms affiches sont ceux de la table (casse originale preservee)
    //
    //  IN  st, out
    //  OUT — : peut afficher BEL, liste, ou completion via redrawLine()
    //  VAR modifiees : st.tabPrefixLen (memorise au 1er TAB)
    //                  st.tabMatchCount, st.tabMatchIdx (gestion cycle)
    //                  st.buf, st.len, st.cur (completion effective)
    void tabComplete(CanalState &st, Print &out);


    // ── Edition de ligne ──────────────────────────────────────────────────────

    //  Deplace le curseur logique d'une position et envoie ESC[C ou ESC[D.
    //  Ne fait rien aux bornes (cur==0 pour ←, cur==len pour →).
    //
    //  IN  st, out
    //  IN  dir : -1 = ← gauche  /  +1 = → droite
    //  OUT — : envoie ESC[D (←) ou ESC[C (→) sur out
    //  VAR modifiees : st.cur (-1 ou +1 selon dir, si hors borne → inchange)
    void moveCursor(CanalState &st, Print &out, int dir);

    //  Insere le caractere c a la position st.cur dans st.buf.
    //  Decale les caracteres suivants vers la droite (memmove).
    //
    //  Echo conditionne par _echoEnabled :
    //    true  → renvoie c en echo + reaffiche les caracteres decales (PuTTY, Serial)
    //    false → pas d'echo brut (MobaXterm) + redrawLine() si insertion au milieu
    //
    //  C'est le SEUL endroit ou un octet brut ASCII est renvoye en echo.
    //
    //  IN  st, out
    //  IN  c : caractere imprimable 0x20..0x7E (garanti par handleKey())
    //  OUT — : ecrit sur out si _echoEnabled
    //  VAR modifiees : st.buf[st.cur] = c (insertion + decalage droite)
    //                  st.len +1
    //                  st.cur +1 (apres insertion)
    void insertChar(CanalState &st, Print &out, unsigned char c);

    //  Supprime le caractere a gauche du curseur (touche Backspace).
    //  Decale les caracteres suivants vers la gauche (memmove).
    //  Ne fait rien si le curseur est au debut (cur == 0).
    //
    //  Methode d'affichage "\b \b" (issue de Terminal.cpp — rweather) :
    //    1. BS (0x08)  : recule le curseur terminal d'une position
    //    2. Reecriture : reecrit les caracteres decales (si milieu de ligne)
    //    3. ESPACE     : efface le dernier caractere residuel en fin de ligne
    //    4. BS × N     : replace le curseur a la bonne position
    //
    //  Universelle PuTTY + MobaXterm + Serial — evite les sequences ESC[D
    //  dont le comportement varie selon la configuration du terminal.
    //
    //  IN  st, out
    //  OUT — : ecrit sequence \b \b sur out
    //  VAR modifiees : st.buf (decalage gauche d'un caractere)
    //                  st.len -1
    //                  st.cur -1
    void delLeft(CanalState &st, Print &out);


    // ════════════════════════════════════════════════════════════════════════
    //  MEMBRES PRIVES
    // ════════════════════════════════════════════════════════════════════════

    Stream         &_stream;              // Canal 1 : Stream Serial (reference constructeur)
                                          // Serial pour cmdLine(Serial)
                                          // SerialBT pour cmdLineBT(SerialBT) — ESP32 BT
    CmdLineClient  *_c = nullptr;         // Canal 2 : Pointeur client Telnet courant
                                          // Mis a jour a chaque appel updateNetwork()
                                          // nullptr = pas de client → print() n'ecrit pas sur reseau

#ifdef CMDLINE_HAS_BLUETOOTH
    // Canal 3 : Bluetooth — type Stream* pour eviter d'inclure BluetoothSerial.h
    // dans la lib (conflit symboles WiFi/IAC). BluetoothSerial herite de Stream.
    // Active par setBluetoothOutput(&SerialBT) dans setup() du .ino.
    Stream *_btStream = nullptr;          // Pointeur sur SerialBT (Stream* generique)
                                          // nullptr = canal BT inactif (defaut)
    bool    _btActive = false;            // true = canal BT active (setBluetoothOutput appele)
                                          // false = print() n'ecrit pas sur BT (defaut)
#endif

    CanalState _stS;   // Etat complet du canal Serial (buffer, historique, TAB, machine ESC)
    CanalState _stN;   // Etat complet du canal reseau (idem + machine d'etat Telnet 9 etats)

    const cmd_t *_commands      = nullptr; // Pointeur sur la table des commandes systeme
    size_t       _num           = 0;      // Nombre d entrees dans _commands

    const cmd_t *_user_commands = nullptr; // Pointeur sur la table des commandes utilisateur (optionnel)
    size_t       _user_num      = 0;      // Nombre d entrees dans _user_commands (0 si absent)

    bool _echoEnabled = (CMDLINE_ECHO_DEFAULT != 0);  // Etat de l'echo (initialise par constante)
                                                       // Modifiable via setEcho(true/false)

    // Machine d'etat ESC Serial (3 etats simples, sans timeout)
    // Separee de _stN.tstate car Serial est synchrone → pas besoin de timeout
    // 0 = init (attente) / 1 = ESC recu / 2 = ESC+[ recus
    uint8_t _serialEscState = 0;
};

#endif // __CMDLINE_H__
