Aller au contenu principal

ESP32 — configuration JSON des pièces

Connectivité et appairage du runtime sécurisé​

Le runtime générique initialise Wi-Fi STA et Bluetooth Classic SPP, ainsi que le PHY LAN8720 sur WT32-ETH01. Ethernet applique l'adresse statique 192.168.0.240/24 et la passerelle 192.168.0.1 utilisées par la configuration du contrôleur. Un ESP32 sans PHY Ethernet peut utiliser le Wi-Fi ; l'échec du PHY ne désactive pas les autres transports.

Mode Ethernet temporaire​

Le runtime est actuellement compilé avec LEUKOS_WIFI_ENABLED = false pour continuer les essais par Ethernet après un brownout reproduit au démarrage de la radio Wi-Fi. Le code Wi-Fi et ses identifiants NVS sont conservés, mais le démarrage STA et les reconnexions sont ignorés tant que ce paramètre reste désactivé. Le provisionnement Wi-Fi conserve les paramètres pour une future réactivation ; il n'allume pas la radio dans ce mode. Le PHY LAN8720 reste initialisé explicitement, et le canal TCP continue d'exiger WireGuard et HMAC. Cette désactivation ne constitue pas une réparation de l'alimentation.

L'appairage radio Bluetooth et l'activation applicative sont distincts. Le nom annoncé est LeukOS-<device_id> (suffixe matériel avant provisionnement). Le node doit disposer d'un contrôleur Bluetooth opérationnel, puis appairer le MCU et ouvrir RFCOMM. Chaque commande et réponse reste signée avec HMAC, nonce et timestamp, y compris sur Bluetooth. Les clés sont provisionnées par le mécanisme série PROVISION1 existant ; SECURITY expose uniquement leur état de validation. Un MCU non provisionné refuse les commandes applicatives.

Les identifiants Wi-Fi sont configurés sur la liaison série physique à 115200 bauds, avec une ligne NETWORKJSON suivie d'un objet JSON contenant ssid et password. Ne pas placer un vrai mot de passe dans un journal, une commande historisée ou un fichier partagé. Le SSID accepte au plus 32 octets et le mot de passe 8 à 63 octets (vide pour un réseau ouvert explicitement choisi). Un SSID vide désactive les tentatives de connexion. Sur une liaison déjà appairée, le même contenu peut être transmis dans une enveloppe HMAC AUTH1. Le MCU répond OK NETWORK après persistance en NVS, puis applique les nouveaux paramètres. La NVS Wi-Fi est séparée de la transaction de sécurité HMAC/WireGuard. Un flash sans effacement conserve les deux ; aucun AP ouvert n'est créé.

La commande série NETWORK, également disponible dans une requête signée, indique les adresses, l'état Wi-Fi, l'activation Bluetooth, la mémoire libre et l'initialisation du tunnel, sans révéler de mot de passe ni de clé. tunnel_initialized=1 signifie que l'interface WireGuard est créée, pas qu'un handshake ou qu'une commande authentifiée a réussi.

Ethernet est prioritaire ; le Wi-Fi prend le relais lorsqu'il a une adresse et qu'Ethernet est indisponible. Un changement d'interface ou d'adresse ferme les anciennes sockets et recrée le tunnel sur la nouvelle interface. Les tentatives Wi-Fi sont espacées de 15 secondes, celles d'initialisation Bluetooth de 30 secondes et celles de WireGuard de 5 secondes. Les callbacks réseau signalent les changements ; la boucle principale gère les sockets et WireGuard.

La validation complète après flash doit couvrir SECURITY READY, NETWORK, la découverte/appairage Bluetooth, une commande HMAC avec réponse vérifiée, un handshake WireGuard et la perte/reprise des liens. Les ports en clair OTA 3232 et CONFIG 3233 restent fermés. Les exemples historiques de transfert direct ci-dessous décrivent le protocole initial, pas le chemin sécurisé actif : la configuration de pièce passe désormais par CONFIGJSON signé via le node.

Le firmware ESP32 de leukOS est générique. Il connaît les mécanismes possibles — relais, mesure analogique, température et présence — mais ne connaît aucune pièce ni aucun module à la compilation. Une configuration de pièce est un document JSON canonique partagé par l'IHM Qt, SQLite et l'ESP32.

La compilation est un contrôle d'intégrité du runtime. Dans le parcours réseau, le bouton Envoyer JSON ne reflashe pas le firmware : seul le document de la pièce est transféré.

Document canonique attendu​

Voici une pièce Bureau contenant une prise et un capteur de présence :

{
"schema": "leukos.domotics.room",
"version": 1,
"id": "room_f43df77cf8b5",
"name": "Bureau",
"icon": "sofa",
"controller": {
"board": "esp32-eth01-v1.4",
"port": "tcp://192.168.0.240:23"
},
"modules": {
"m_595ad682c46d": {
"name": "Prise-1",
"type": "socket",
"kind": "relay",
"pin": "IO2",
"activeLow": false,
"referenceVoltage": 5,
"target": "m_595ad682c46d",
"icon": "socket",
"unit": "",
"min": 0,
"max": 1
},
"m_a90c7d4e82f1": {
"name": "Présence bureau",
"type": "presence",
"kind": "sensor",
"pin": "IO35",
"activeLow": false,
"referenceVoltage": 3.3,
"target": "m_a90c7d4e82f1",
"icon": "motion-sensor",
"unit": "",
"min": 0,
"max": 1
}
}
}

Les clés de modules sont les identifiants stables. Le firmware les reprend dans STATE et SET ; le nom visible peut donc changer sans casser les automatisations.

Champs de la pièce​

CheminObligatoireRègle
schemaouivaleur exacte leukos.domotics.room
versionouientier 1
idoui1 à 80 caractères parmi lettres, chiffres, _ et -
nameouinom affiché, 1 à 80 caractères
iconnonidentifiant d'icône de l'IHM
controller.boardouiesp32-eth01-v1.4 pour cette carte
controller.portouiURL tcp://adresse:port, généralement port 23
modulesouiobjet contenant de 1 à 16 modules

Chaque identifiant de module suit m_ puis 12 caractères hexadécimaux, par exemple m_595ad682c46d. Deux modules d'une même pièce ne peuvent pas utiliser le même GPIO.

Le schéma JSON Schema v1 peut être téléchargé pour valider un fichier dans un éditeur, une application mobile ou une chaîne d'intégration avant son envoi au node.

Champs d'un module​

ChampObligatoireDescription
nameouilibellé destiné à l'utilisateur
typeouichoisit le comportement du firmware
kindouirelay ou sensor, information sémantique pour leukOS
pinouiGPIO au format IO2, IO35, etc.
activeLownoninverse la logique électrique ; défaut false
referenceVoltagenonréférence analogique entre 1,0 et 5,5 V ; défaut 3,3 V
targetnonidentifiant fonctionnel ciblé par l'IHM
icon, unit, min, maxnonmétadonnées d'affichage et de plage

Types et mécanismes​

typeMode interneFonction
lamp, ceiling, fan, socket, washer, valverelaissortie numérique commandable par SET
temperaturetempératureconversion analogique de type LM35 en °C
analoganalogiquevaleur ADC brute de 0 à 4095
presenceprésenceentrée numérique filtrée, avec inversion optionnelle

Sur WT32-ETH01, les broches numériques proposées par l'IHM sont IO0, IO39, IO36, IO15, IO14, IO12, IO35, IO4 et IO2. Les entrées analogiques proposées sont IO39, IO36 et IO35. Certaines broches ont aussi un rôle au démarrage ou pour Ethernet : le câblage réel doit respecter le schéma de la carte.

Comment le firmware interprète le JSON​

1. Choisir le comportement à partir de type​

Le code ne contient pas d'identifiant de prise ou de pièce. Il associe seulement un type fonctionnel à un mode générique :

enum ModuleMode : uint8_t {
MODE_RELAY,
MODE_TEMPERATURE,
MODE_ANALOG,
MODE_PRESENCE
};

bool moduleModeForType(const char* type, ModuleMode& mode) {
if (!strcmp(type, "lamp") || !strcmp(type, "ceiling") ||
!strcmp(type, "fan") || !strcmp(type, "socket") ||
!strcmp(type, "washer") || !strcmp(type, "valve"))
mode = MODE_RELAY;
else if (!strcmp(type, "temperature")) mode = MODE_TEMPERATURE;
else if (!strcmp(type, "analog")) mode = MODE_ANALOG;
else if (!strcmp(type, "presence")) mode = MODE_PRESENCE;
else return false;
return true;
}

Ajouter un nouveau type implique donc deux décisions : son schéma JSON et son mécanisme matériel dans ce répartiteur.

2. Construire les modules en mémoire​

Chaque propriété de l'objet modules devient une structure interne :

struct Module {
char id[32];
char type[24];
uint8_t pin;
ModuleMode mode;
bool activeLow;
float referenceVoltage;
bool on;
bool stableDigital;
uint8_t candidateCount;
};

Module modules[16];
uint8_t moduleCount = 0;

Le parseur cJSON vérifie la version, l'identifiant de pièce, le nombre de modules, le type, le GPIO et l'absence de broche dupliquée avant de toucher aux sorties. La configuration candidate est préparée à part ; elle ne remplace l'ancienne que si tout le document est valide.

cJSON* root = cJSON_ParseWithLength(json.c_str(), json.length());
cJSON* version = cJSON_GetObjectItemCaseSensitive(root, "version");
cJSON* id = cJSON_GetObjectItemCaseSensitive(root, "id");
cJSON* configured = cJSON_GetObjectItemCaseSensitive(root, "modules");

if (!cJSON_IsNumber(version) || version->valueint != 1 ||
!cJSON_IsString(id) || !cJSON_IsObject(configured)) {
error = "invalid_schema";
return false;
}

3. Configurer les GPIO sans état dangereux​

Avant de remplacer une configuration, les anciennes sorties sont arrêtées. Les nouvelles sorties relais démarrent à l'état inactif en tenant compte de activeLow :

if (module.mode == MODE_RELAY) {
digitalWrite(module.pin, module.activeLow ? HIGH : LOW);
pinMode(module.pin, OUTPUT);
} else {
pinMode(module.pin,
module.mode == MODE_PRESENCE && module.activeLow
? INPUT_PULLUP
: INPUT);
}

Écrire le niveau inactif avant pinMode(..., OUTPUT) limite l'impulsion parasite au démarrage d'un relais.

4. Produire les états​

STATE renvoie une paire id=valeur par module :

if (module.mode == MODE_RELAY)
out.print(module.on ? 1 : 0);
else if (module.mode == MODE_TEMPERATURE)
out.print(analogRead(module.pin) *
module.referenceVoltage * 100.0f / 4095.0f, 1);
else if (module.mode == MODE_ANALOG)
out.print(analogRead(module.pin));
else
out.print(readDigitalStable(module) ? 1 : 0);

Exemple de réponse :

m_595ad682c46d=1 m_a90c7d4e82f1=0

La formule de température correspond à un LM35 (10 mV/°C). Un autre capteur analogique nécessite un nouveau type ou une règle de conversion explicite.

5. Appliquer une commande​

Seuls les modules en mode relais acceptent SET :

SET m_595ad682c46d 1
SET m_595ad682c46d 0

Le runtime recherche l'identifiant, applique l'inversion activeLow, puis retourne l'état complet. Un identifiant inconnu ou un capteur produit ERR MODULE.

Protocole de transfert sur TCP 3233​

Le flux contient une ligne d'en-tête puis exactement le nombre d'octets UTF-8 annoncé :

CONFIG 989\n
{...989 octets de JSON...}

Le firmware impose une taille maximale de 8192 octets et répond :

OK CONFIG 3

Le nombre final est la révision conservée. En cas d'échec, la réponse indique la famille de l'erreur, par exemple ERR CONFIG invalid_json, invalid_schema, invalid_module, duplicate_pin ou storage.

Envoyer un fichier depuis un ordinateur​

Le calcul doit utiliser le nombre d'octets, pas le nombre de caractères :

#!/usr/bin/env python3
import json
import socket
from pathlib import Path

document = json.loads(Path("bureau.json").read_text(encoding="utf-8"))
payload = json.dumps(document, ensure_ascii=False, separators=(",", ":")).encode()

if len(payload) > 8192:
raise SystemExit("JSON supérieur à 8192 octets")

with socket.create_connection(("192.168.0.240", 3233), timeout=5) as sock:
sock.sendall(f"CONFIG {len(payload)}\n".encode("ascii") + payload)
print(sock.recv(256).decode().strip())

Le JSON peut venir d'un formulaire Qt, d'un ordinateur ou d'une application mobile. Le producteur importe ou construit le même document canonique ; il ne génère pas une variante spécifique au transport.

Cohérence avec SQLite​

L'API locale conserve le document dans la table domotics_room_config :

curl -X POST \
-H 'Content-Type: application/json' \
--data-binary @bureau.json \
http://127.0.0.1:8080/domotics/config/room_f43df77cf8b5

Relire la valeur persistée :

curl http://127.0.0.1:8080/domotics/config/room_f43df77cf8b5

Un POST valide et stocke le JSON mais ne contacte pas, à lui seul, l'ESP32. L'IHM Qt exécute la séquence complète : enregistrement SQLite, connexion au port 3233, envoi du même payload, puis attente de OK CONFIG.

La configuration sauvegardée dans /etc/leukos/domotics.json, le document SQLite et le payload envoyé à la carte doivent rester sémantiquement identiques. Il ne doit exister qu'une seule représentation métier de la pièce.

Persistance dans l'ESP32​

Après validation, le runtime écrit le JSON et sa révision dans la NVS :

Preferences preferences;

preferences.begin("leukos", false);
size_t written = preferences.putString("room_json", json);
preferences.putUInt("cfg_rev", configRevision + 1);
preferences.end();

Au redémarrage, room_json est relu, revalidé et les GPIO sont reconstruits. Une mise à jour OTA normale préserve cette NVS. Un effacement total de la flash la supprime et la pièce redevient unconfigured=1.

Contrôles avant mise en service​

  1. valider le JSON et vérifier que sa taille ne dépasse pas 8192 octets ;
  2. vérifier que chaque identifiant de module est stable et unique ;
  3. vérifier qu'aucun GPIO n'est utilisé deux fois ;
  4. contrôler activeLow relais débranché de la charge ;
  5. envoyer le JSON et exiger OK CONFIG <révision> ;
  6. lire INFO sur TCP 23 et vérifier room et config_version ;
  7. lire STATE, puis tester chaque relais avec SET ;
  8. redémarrer l'ESP32 et confirmer que la configuration est restaurée.

Pour installer le runtime avant ce premier transfert, voir ESP32 Ethernet — initialisation, premier flash et OTA.