Firmware pour un nœud capteur/actionneur compatible MeshCore :
mesure de température, pilotage de 2 relais (extensible), 2 entrées bouton
poussoir pour un contrôle local des relais. État consultable et relais
pilotables à distance via le mesh LoRa — depuis l'app officielle MeshCore,
ou en ligne de commande avec meshcore-cfg.
Board actuellement supportée : Heltec WiFi LoRa 32 V3 (ESP32-S3). Voir « Adapter à un autre câblage ou une autre board Heltec » pour porter vers un autre modèle.
- Température : lue localement (capteur BME280), consultable à distance.
- 2 relais minimum : état lisible et modifiable à distance, pilotables aussi localement via 2 boutons poussoir dédiés (1 bouton = 1 relais).
- Compatible MeshCore : s'appuie sur le firmware
simple_sensorofficiel (MeshCore, submodule git), rôleADV_TYPE_SENSOR— aucune commande custom inventée là où MeshCore a déjà un mécanisme standard (voir « Conformité MeshCore » plus bas).
git clone --recurse-submodules https://github.com/jmpuch/meshcore-iot.git
cd meshcore-iot
python3 -m venv .venv-platformio && source .venv-platformio/bin/activate
pip install platformio
./scripts/build.sh # compile + génère le binaire fusionné
meshcore-cfg --port /dev/ttyUSBx flash .pio/build/Heltec_v3_iot/firmware-merged.bin(ou directement pio run -e Heltec_v3_iot -t upload --upload-port /dev/ttyUSBx
si vous préférez PlatformIO plutôt que meshcore-cfg)
Avant tout usage réel : changez ADMIN_PASSWORD dans platformio.ini
(voir plus bas) — le firmware est livré avec le mot de passe par défaut
"password".
Pas de commande custom inventée là où MeshCore a déjà un mécanisme standard :
- Relais (lecture/écriture à distance) : mécanisme natif
sensor get <clé>/sensor set <clé> <valeur>/sensor list(SensorManager::getSettingByKey/setSettingValue,CommonCLI.cpp:276-311) — un magasin de paramètres nommés générique, déjà utilisé dans le firmware officiel pour des toggles commegps(EnvironmentSensorManager). Les relais sont exposés ainsi :sensor set relay1 1/sensor get relay1, plutôt qu'une syntaxeset relay1 onqui entrerait en conflit avec le systèmeset <var> <valeur>des variables de configuration classiques — deux magasins distincts dans le firmware. - Température (lecture à distance) : mécanisme de télémétrie natif
(
CayenneLPP,SensorManager::querySensors()) —addTemperature(canal, valeur), interrogeable à distance via le protocole companion standard, déjà géré automatiquement par le firmware pour un capteur BME280, aucun code à écrire. Relais aussi inclus dans cette même trame (addDigitalOutput, canaux 3/4) — visible à la fois viasensor get relay1/relay2(CLI/mesh) et dans la télémétrie standard (app officielle), plus sur l'écran OLED du device. A nécessité de forkerSensorMesh/UITaskdanssrc/(ce ne sont pas des fichiers librairie stables, contrairement àvendor/MeshCore/src/— voir « Architecture » plus bas).
vendor/MeshCore/examples/simple_sensor/ (firmware officiel) est un
template de départ à forker, pas une librairie stable — contrairement
à vendor/MeshCore/src/, jamais modifié. SensorMesh.{h,cpp}/
UITask.{h,cpp}/TimeSeriesData.{h,cpp} sont donc copiés dans notre
propre src/ et personnalisés :
SensorMesh.h: un accesseur protégételemetryBuffer()ajouté (seul changement réel à cette classe), pour quemain.cpppuisse injecter l'état des relais dans la trame de télémétrie.main.cpp: interceptesensor get/set relay1|relay2viahandleCustomCommand()(vérifié en tout premier parSensorMesh, avant mêmesensor get/setstandard) — un seul chemin de code pilote un relais, que ce soit depuis une commande CLI/mesh ou l'appui d'un bouton local (setRelay(idx, bool), jamais de logique dupliquée).UITask.{h,cpp}: affiche l'état des relais sur l'écran OLED, sous les infos radio (RELAY1/RELAY2,ON/OFF).- Lecture directe du BME280 (
beginOwnBme280()) plutôt que via le buffer interneCayenneLPPdeSensorMesh::getTelemValue()— ce dernier contient un bug firmware réel (voir « Bugs trouvés » plus bas), contourné plutôt que corrigé dansvendor/.
- Alimentation : secteur permanent (USB/adaptateur) — pas de gestion batterie à prévoir.
- Capteur température : BME280 (I2C) — supporté nativement
(
ENV_INCLUDE_BME280,EnvironmentSensorManager.cpp, adresse I2C par défaut0x76). Bonus : humidité + pression atmosphérique disponibles aussi sans coût supplémentaire (addRelativeHumidity/addBarometricPressure/addAltitude), le driver les lit de toute façon. - Boutons poussoir : appui = bascule l'état du relais correspondant
(toggle), pas de logique maintenu/relâché. Anti-rebond logiciel
(
DEBOUNCE_MS, paramétrable). - Sécurité accès distant : modèle natif MeshCore — un seul niveau
admin, pas de palier lecture-seule séparé (
simple_sensorréserve tout accès CLI aux clients admin, contrairement aux répéteurs qui ont un mode invité). - Module relais : logique de déclenchement active-haut par défaut
(GPIO HIGH = relais ON), paramétrable (
RELAY_ACTIVE_HIGH, voir plus bas) — dépend du module relais utilisé, pas du firmware.- Point de vigilance matériel : un GPIO flottant/indéterminé
pendant le boot de l'ESP32-S3 (avant l'initialisation du firmware)
pourrait activer un relais brièvement s'il flotte à l'état actif —
prévoir une résistance de pull externe adaptée à la polarité choisie
sur chaque ligne de commande, ou s'assurer que l'état OFF est forcé
le plus tôt possible dans
setup()(déjà le cas dansmain.cpp) avant toute alimentation effective des modules relais.
- Point de vigilance matériel : un GPIO flottant/indéterminé
pendant le boot de l'ESP32-S3 (avant l'initialisation du firmware)
pourrait activer un relais brièvement s'il flotte à l'état actif —
prévoir une résistance de pull externe adaptée à la polarité choisie
sur chaque ligne de commande, ou s'assurer que l'état OFF est forcé
le plus tôt possible dans
Déjà occupées par simple_sensor/Heltec V3 (radio, OLED, GPS réservé même
sans module physique) : 0, 1, 8, 9, 10, 11, 13, 14, 17, 18, 26, 35, 36, 37, 47, 48. I2C capteur (BME280) : 41 (SDA) / 42 (SCL) — pins JTAG
réutilisées (défaut MeshCore 33/34 non utilisé ici).
Affectation retenue (parmi les 5 GPIO libres identifiées, 1 broche de marge conservée) :
| Fonction | GPIO | Num. broche HV3 | Sens | Note |
|---|---|---|---|---|
| RELAY1 | 4 | J3-15 | Sortie | actif-haut par défaut |
| RELAY2 | 5 | J3-16 | Sortie | actif-haut par défaut |
| BUTTON1 (pilote RELAY1) | 6 | J3-17 | Entrée | pull-up interne, bouton vers GND |
| BUTTON2 (pilote RELAY2) | 7 | J3-18 | Entrée | pull-up interne, bouton vers GND |
| (réserve, extension future) | 2 | J3-13 | — | libre |
Numérotation des broches selon la doc officielle Heltec (HTIT-WB32LA_V3.2.pdf,
§2.2 « Pin description », table 2.2-2) — toutes ces GPIO sont sur le
header J3 (côté droit de la carte, écran face à soi, USB-C en haut).
python3 -m venv .venv-platformio && source .venv-platformio/bin/activate # une fois
pip install platformio # une fois
git submodule update --init # une fois
pio run -e Heltec_v3_iot # compile
pio run -e Heltec_v3_iot -t upload # flash (device branché en USB)
pio device monitor -b 115200 # console sérieRégénérer le binaire fusionné (bootloader + partitions + app, celui
que meshcore-cfg flash ou tout flasher externe attend) — pio run seul
ne le fait pas, il faut la cible mergebin séparément :
./scripts/build.sh # environnement par défaut (Heltec_v3_iot)
./scripts/build.sh MonEnvironnement # un autre environnement de platformio.ini
# -> .pio/build/<env>/firmware-merged.bin, prêt pour `meshcore-cfg ... flash ...`Tout ce qui est spécifique à ce montage (pas au board lui-même) est un
simple flag -D dans [env:Heltec_v3_iot] de platformio.ini — pas
besoin de toucher au code C++ pour changer ces valeurs :
| Define | Rôle | Défaut actuel |
|---|---|---|
RELAY1_PIN / RELAY2_PIN |
GPIO de sortie relais | 4 / 5 |
BUTTON1_PIN / BUTTON2_PIN |
GPIO d'entrée bouton | 6 / 7 |
RELAY_ACTIVE_HIGH |
1 = relais ON à l'état haut, 0 = actif-bas |
1 |
DEBOUNCE_MS |
Anti-rebond bouton (ms) | 40 |
ENV_PIN_SDA / ENV_PIN_SCL |
I2C du capteur BME280 | 41 / 42 |
ADVERT_NAME |
Nom affiché sur le mesh | "meshcore-IOT" |
ADVERT_LAT / ADVERT_LON |
Coordonnées GPS statiques | 0.0 / 0.0 |
ADMIN_PASSWORD |
Mot de passe admin CLI/app | "password" — à changer |
Il suffit de changer les valeurs ci-dessus. Deux façons :
- Éditer directement les
-D ...dans[env:Heltec_v3_iot]. - Ou dupliquer la section sous un autre nom (
[env:Heltec_v3_iot_maison2]) pour garder plusieurs variantes de câblage compilables séparément —./scripts/build.sh Heltec_v3_iot_maison2régénère celle-ci précisément.
Plus de travail : chaque board a un jeu de GPIO occupées différent
(radio, écran, USB natif...), donc les 5 GPIO libres retenues pour la V3
(2, 4, 5, 6, 7) ne le sont pas forcément ailleurs. Exemple réel trouvé en
préparant cette section : sur la Heltec V4, les GPIO 2, 5 et 7
sont réservées au contrôle de l'amplificateur radio
(P_LORA_GC1109_PA_EN/P_LORA_KCT8103L_PA_CSD/_CTX dans
vendor/MeshCore/variants/heltec_v4/platformio.ini) — exactement les
broches utilisées ici pour RELAY2/BUTTON2/la réserve sur la V3. Jamais
supposer qu'une affectation GPIO se transpose d'une board à l'autre.
Marche à suivre :
- Lister les boards Heltec disponibles côté firmware officiel :
ls vendor/MeshCore/variants/ | grep heltec - Ouvrir
vendor/MeshCore/variants/<board>/platformio.ini, repérer la section[Heltec_loraXX_vY]en tête de fichier (pas un[env:...]— c'est la base commune, réutilisée par tous les rôles de cette board : repeater/sensor/companion/etc.). Elle liste tout ce que cette board réserve déjà (radio, USB, écran, bouton utilisateur...). - Croiser cette liste avec la doc GPIO officielle Heltec du board visé — puis vérifier physiquement au voltmètre/fils volants avant tout câblage définitif, jamais une simple lecture de doc en confiance aveugle.
- Dans
platformio.ini, dupliquer[env:Heltec_v3_iot]sous un nouveau nom (ex.[env:Heltec_v4_iot]), et remplacer :board = ...(valeur du champboard =de la section[Heltec_loraXX_vY]trouvée à l'étape 2)- le bloc de
-Dspécifiques board (radio, GPS, écran, bouton — tout ce qui n'est pas dans le tableau ci-dessus) par celui de cette même section -I vendor/MeshCore/variants/<board>et la ligne équivalente dansbuild_src_filter- les GPIO relais/boutons du tableau ci-dessus, avec les nouvelles valeurs choisies à l'étape 3
./scripts/build.sh Heltec_v4_iotpour compiler, puis test matériel complet avant tout usage réel — rien de tout ça n'est vérifié tant qu'un vrai device n'a pas confirmé chaque broche physiquement.
vendor/MeshCore est un submodule git épinglé à un commit précis — le
firmware officiel bouge, cette copie non :
cd vendor/MeshCore
git fetch --tags
git log --oneline HEAD..origin/main | head -20 # voir ce qui a changé
git checkout <nouveau-tag-ou-commit> # ex. companion-v1.18.0
cd ../..
git add vendor/MeshCore
git commit -m "Met à jour vendor/MeshCore vers <tag>"Point d'attention : les -D spécifiques à la board dans
[env:Heltec_v3_iot] sont une copie manuelle de la section
[Heltec_lora32_v3] de vendor/MeshCore/variants/heltec_v3/platformio.ini
— une mise à jour du submodule peut faire diverger les deux
silencieusement (nouveau champ ajouté en amont, valeur changée...). Après
toute mise à jour du submodule, comparer les deux à la main :
sed -n '/^\[Heltec_lora32_v3\]/,/^\[env:/p' vendor/MeshCore/variants/heltec_v3/platformio.ini... et reporter les écarts pertinents dans platformio.ini, en gardant
les ajouts propres à ce projet (RELAY*_PIN, ADVERT_*, etc.) intacts.
Recompiler et retester sur matériel réel avant tout déploiement après une
telle mise à jour.
Une fois flashé, ce firmware parle le même CommonCLI/série directe que
n'importe quel autre device MeshCore —
meshcore-cfg
fonctionne donc dessus sans aucune modification (get/set/raw/dump,
CLI ou interface graphique) :
meshcore-cfg --port /dev/ttyUSBx raw "sensor get relay1"
meshcore-cfg --port /dev/ttyUSBx raw "sensor set relay1 1"
meshcore-cfg --port /dev/ttyUSBx raw "sensor list"
# Diagnostic BME280 (lecture directe, indépendante de la télémétrie CayenneLPP)
meshcore-cfg --port /dev/ttyUSBx raw "sensor get temp"
meshcore-cfg --port /dev/ttyUSBx raw "sensor get humidity"
meshcore-cfg --port /dev/ttyUSBx raw "sensor get pressure"
meshcore-cfg --port /dev/ttyUSBx raw "sensor get altitude"
# Diagnostic anti-rebond (simule un train de rebonds électriques réaliste
# sur l'entrée bouton correspondante, sans câblage physique -- doit
# toujours répondre "1 toggle(s)", jamais plus)
meshcore-cfg --port /dev/ttyUSBx raw "sensor get simulate-button1"
meshcore-cfg --port /dev/ttyUSBx raw "sensor get simulate-button2"Device injoignable en USB direct ? meshcore-cfg sait aussi le piloter à
distance via un companion local relayant sur le mesh LoRa (--transport companion --target <clé-publique> --password <mot-de-passe> en CLI, ou
le sélecteur de cible de l'interface graphique) — voir sa propre
documentation.
Même protocole CLI (CommonCLI) qu'en série directe/meshcore-cfg — les
commandes envoyées depuis la console/CLI admin de l'app (après connexion
avec le mot de passe admin du device) sont identiques, textuellement :
sensor set relay1 1 # active RELAY1
sensor set relay1 0 # désactive RELAY1
sensor get relay1 # lit l'état actuel de RELAY1
sensor set relay2 1 # active RELAY2
sensor set relay2 0 # désactive RELAY2
sensor get relay2 # lit l'état actuel de RELAY2
sensor list # état des deux relais en un seul appel
État des relais aussi visible passivement, sans commande, dans l'écran de
télémétrie standard de l'app (canaux DigitalOutput) et sur l'écran OLED
du device.
Trois bugs réels trouvés et corrigés en testant contre du matériel réel — notés ici pour qui reprendrait un projet similaire :
- Un
memcmpcomparait la mauvaise longueur dans notre propre interception de commande ("sensor set relay1 ": le code utilisaitsizeof(...)— qui inclut le terminateur nul — au lieu destrlen(...), donc le préfixe ne matchait jamais dès qu'une vraie valeur suivait.sensor set relay1 1retombait silencieusement sur le handler standard ("can't find custom var") au lieu d'être intercepté. - Un bug firmware réel dans
SensorMesh.cpp(upstream, jamais modifié) : la table internegetDataSize()/getMultiplier()traiteLPP_RELATIVE_HUMIDITYcomme 2 octets/×10, alors que la vraie spec CayenneLPP (CayenneLPP.h, qui écrit réellement la trame) dit 1 octet/×2 — désaligne la lecture interne de tout ce qui suit l'humidité dans le buffer. N'affecte que la relecture interne du firmware, pas la télémétrie réellement émise sur le mesh (le code d'écriture,CayenneLPP::addRelativeHumidity, respecte bien la vraie spec) — contourné en relisant le BME280 directement plutôt que via ce mécanisme interne. - La télémétrie à la demande (depuis l'app) ignorait nos canaux
relais :
handleRequest()(le vrai chemin de réponse à une requête de télémétrie en direct) fait son proprereset()+querySensors()et n'appelle jamaisonSensorDataRead()(qui ne tourne que sur le rafraîchissement périodique interne) — nos canaux relais étaient bien ajoutés en interne puis systématiquement écrasés juste avant l'envoi réel. Corrigé en ajoutant un point d'accrocheonTelemetryRequest()dans notre copie deSensorMesh, appelé juste avant l'envoi.
Usage libre — voir vendor/MeshCore
pour la licence du firmware officiel sur lequel ce projet s'appuie.