Meshcore pour la domotique

Bonjour à tous,

Avec @PanoLyon, nous avons lancé Gaulix (https://gaulix.fr), une communauté autour des communications off-grid basées sur LoRa / Meshtastic, avec pour objectif la création d’un réseau LoRa libre à l’échelle nationale.
Une première matérialisation de ce travail est visible ici : https://map.gaulix.fr/

Au-delà de la messagerie texte, le réseau est déjà exploité pour des usages domotiques et télémétriques :

  • serveur MQTT avec règles de routage/diffusion,
  • remontées de télémétries (température, humidité, etc.),
  • pilotage d’équipements à distance,
  • intégration côté Jeedom via Node-RED pour disposer d’une interface exploitable.

Avec l’arrivée de MeshCore, je souhaite capitaliser sur l’existant et surtout structurer proprement l’intégration des télémétries dès maintenant, afin d’éviter les limites rencontrées précédemment lorsque le réseau grandit.

À ce jour :

  • une passerelle IP est opérationnelle,
  • les trames MeshCore sont décodées/déchiffrées et exploitables dans Node-RED, et inversement
  • un dépôt Git est en cours (qui sera évidemment partagé).

Je cherche aujourd’hui des personnes intéressées pour :

  • travailler sur une intégration native ou semi-native dans Jeedom (plugin, démon, ou via MQTT),
  • définir un modèle de données propre pour les télémétries MeshCore,
  • explorer des cas d’usage de domotique longue distance, par exemple le pilotage d’équipements agricoles à plusieurs kilomètres via Jeedom et un smartphone.

Le projet en est encore à un stade expérimental, mais le réseau MeshCore est déjà déployé et visible ici :
https://meshmap.serveurperso.in/ pour notre communauté
MeshCore - Map générale meshcore

Si certains d’entre vous sont intéressés par le LoRa, le off-grid, MQTT, Node-RED ou le développement de plugins Jeedom, je serais ravi d’échanger et de construire quelque chose de propre et pérenne. Qu’il n’y ai pas que HA qui soit capable de mutualiser des compétences

8 « J'aime »

Voila mon git permettant de rajouter un acces mqtt à une heltec V3

bon je continue, donc on a là un repeter meshcore qui est pilotable en mqtt, on a les trame tx et rx brut et quelques infos supplémentaires maintenant
Voici une vu dans mqtt explorer
image
ce qui permet donc de faire notament des lien ip entre 2 passerelles facilement avec nodered
Oui, désolé je ne suis pas dev
image
d’ou quelques grosses interrogations dans le groupe car certain n’ont pas compris comment une liaison radio lyon angoulême en respectant les normes radio ont été possible

Passons à la suite on a de l’hexa dans les topic RAW/ TX étant ce que la radio emet et RX reçoit
Hear permet d’envoyer de hexa directement par l’emetteur radio
Sender permet d’injecter de hexa comme si la passerelle l’avait reçu en radio
et le topic decoded et généré par nodered, c’est juste un topic d’essais de dechiffrage light

maintenant il faut comprendre les trame les déchiffrer correctement et pour cela j’ai installer ce git sur ma vps

et est utilisable dans nodered via un exec


et grace à cela on obtient un topic channel test qui permet de connaitre et dechiffrer toutes les trame advert et message diffusé sur le réseau

voici le flow de dechiffrage que l’on utilise

[
    {
        "id": "aede7aacab40a132",
        "type": "debug",
        "z": "b4b9bf4d7980865a",
        "name": "debug 32",
        "active": true,
        "tosidebar": true,
        "console": false,
        "tostatus": false,
        "complete": "false",
        "statusVal": "",
        "statusType": "auto",
        "x": 1480,
        "y": 620,
        "wires": []
    },
    {
        "id": "3642e14930be2b31",
        "type": "exec",
        "z": "b4b9bf4d7980865a",
        "command": "meshcore-decoder decode",
        "addpay": "payload",
        "append": "",
        "useSpawn": "false",
        "timer": "",
        "winHide": false,
        "oldrc": false,
        "name": "MC decodeur générique",
        "x": 670,
        "y": 600,
        "wires": [
            [
                "e9c6f1c2501d7fe8",
                "1434385a06c3f835"
            ],
            [],
            []
        ]
    },
    {
        "id": "48da578bfd37c3bf",
        "type": "function",
        "z": "b4b9bf4d7980865a",
        "name": "inclusion clef par canal",
        "func": "// 1. Validation du payload\nif (!msg.payload || typeof msg.payload !== 'string') {\n    return null;\n}\n\nconst hexData = msg.payload.trim().toUpperCase();\n\n// Trame hex valide ?\nif (!/^[0-9A-F]+$/.test(hexData) || hexData.length < 6) {\n  //  msg.payload = \"Erreur: Trame hex invalide\";\n  //  return msg;\n}\n\n// 2. Découpage en octets\nconst bytes = hexData.match(/.{2}/g);\n\n// 3. Vérifier le type de message (0x15 = GroupText)\nconst msgType = bytes[0];\nif (msgType !== \"15\") {\n    return null; // pas un GroupText → on ignore silencieusement\n}\n\n// 4. Extraire le RouteType (bits 0–1 de l’octet 1)\nconst routeType = parseInt(bytes[1], 16) & 0x03;\n\n// 5. Extraire le Channel Hash selon le RouteType\nlet channelHash;\n\nswitch (routeType) {\n    case 0x00: // ROUTE_TYPE_FLOOD\n        // 15 00 D9 ...\n        channelHash = bytes[2];\n        break;\n\n    case 0x01: // ROUTE_TYPE_ROUTED\n        // 15 01 FD D9 ...\n        channelHash = bytes[3];\n        break;\n\n    case 0x02: // ROUTE_TYPE_ROUTED\n        // 15 01 FD D9 ...\n        channelHash = bytes[4];\n        break;\n\n    case 0x03: // ROUTE_TYPE_ROUTED\n        // 15 01 FD D9 ...\n        channelHash = bytes[5];\n        break;\n\n    case 0x04: // ROUTE_TYPE_ROUTED\n        // 15 01 FD D9 ...\n        channelHash = bytes[6];\n        break;\n\n    case 0x05: // ROUTE_TYPE_ROUTED\n        // 15 01 FD D9 ...\n        channelHash = bytes[7];\n        break;\n\n    case 0x06: // ROUTE_TYPE_ROUTED\n        // 15 01 FD D9 ...\n        channelHash = bytes[8];\n        break;\n\n    case 0x07: // ROUTE_TYPE_ROUTED\n        // 15 01 FD D9 ...\n        channelHash = bytes[9];\n        break;\n\n    //default:\n       // msg.payload = `Erreur: RouteType non supporté (${routeType})`;\n       // return msg;\n}\n    \n    // Définir la clé en fonction du Channel Hash\n    let key;\n    switch (channelHash) {\n        case 'D9':\n            key = \"9cd8fcf22a47333b591d96a2b848b73f\"; // #test\n            break;\n        case '11':\n            key = \"8b3387e9c5cdea6ac9e5edbaa115cd72\"; // @Public\n            break;\n        case 'CA':\n            key = \"eb50a1bcb3e4e5d7bf69a57c9dada211\"; // #bot\n            break;\n        case '68':\n            key = \"e1ad578d25108e344808f30dfdaaf926\"; // #emergency\n            break;\n        case '3B':\n            key = \"ce51a275a0a0507c43d1651d78292320\"; // #info\n            break;\n        case '78':\n            key = \"7ca4f9aad450fd2a87f308d7c4dd0b4d\"; // #meteo\n            break;\n        case '57':\n            key = \"33dfec1c6022b717d941c1b7606915f3\"; // #frblabla\n            break;\n        //default:\n           // msg.payload = `Erreur: Channel Hash inconnu (${channelHash}).`;\n           // return msg;\n    }\n\n    // Construire la commande\n    //msg.command = `meshcore-decoder decode ${hexData} --key ${key}`;\n    msg.command = `${hexData} --key ${key}`;\n    return msg;\n \n",
        "outputs": 1,
        "timeout": "",
        "noerr": 0,
        "initialize": "",
        "finalize": "",
        "libs": [],
        "x": 400,
        "y": 480,
        "wires": [
            [
                "82f4b0a5121130c0",
                "9667da5b1886ba31"
            ]
        ]
    },
    {
        "id": "9667da5b1886ba31",
        "type": "exec",
        "z": "b4b9bf4d7980865a",
        "command": "meshcore-decoder decode",
        "addpay": "command",
        "append": "",
        "useSpawn": "false",
        "timer": "",
        "winHide": false,
        "oldrc": false,
        "name": "MC décodeur canaux avec clef",
        "x": 690,
        "y": 540,
        "wires": [
            [
                "98fe3a4bc69a3fc9",
                "67a92ef95aad51ad",
                "e9c6f1c2501d7fe8"
            ],
            [
                "e0a0324952f77381"
            ],
            [
                "3d46f017f0ec83c7"
            ]
        ]
    },
    {
        "id": "98fe3a4bc69a3fc9",
        "type": "debug",
        "z": "b4b9bf4d7980865a",
        "name": "debug 35",
        "active": false,
        "tosidebar": true,
        "console": false,
        "tostatus": false,
        "complete": "true",
        "targetType": "full",
        "statusVal": "",
        "statusType": "auto",
        "x": 920,
        "y": 500,
        "wires": []
    },
    {
        "id": "82f4b0a5121130c0",
        "type": "debug",
        "z": "b4b9bf4d7980865a",
        "name": "debug 38",
        "active": false,
        "tosidebar": true,
        "console": true,
        "tostatus": true,
        "complete": "true",
        "targetType": "full",
        "statusVal": "payload",
        "statusType": "auto",
        "x": 630,
        "y": 480,
        "wires": []
    },
    {
        "id": "67a92ef95aad51ad",
        "type": "function",
        "z": "b4b9bf4d7980865a",
        "name": "mise en forme message ",
        "func": "// Récupérer la sortie de la commande\nconst output = msg.payload;\n\n// Vérifier si c’est un GroupText\nif (!output.includes(\"Payload Type: GroupText\")) {\n    msg.payload = \"pas un groupe\";\n    return; // msg;\n}\n\n// Extraction des champs\nconst channelMatch = output.match(/Channel Hash:\\s*([0-9A-F]{2})/);\nconst senderMatch  = output.match(/Sender:\\s*([^\\n]+)/);\nconst messageMatch = output.match(/Message: ([^\\n]+)/);\n\nif (!senderMatch || !messageMatch || !channelMatch) {\n    msg.payload = \"Erreur: champs MeshCore manquants\";\n    return; // msg;\n}\n\nconst channelHash = channelMatch[1].toUpperCase();\nconst sender      = senderMatch[1].trim();\nconst message     = messageMatch[1].trim();\n\nconst channelMap = {\n    \"D9\": \"#test\",\n    \"11\": \"@Public\",\n    \"CA\": \"#bot\",\n    \"68\": \"#emergency\",\n    \"3B\": \"#info\",\n    \"78\": \"#meteo\",\n    \"57\": \"#frblabla\"\n};\n\n// Résolution du nom du channel\nconst channelName = channelMap[channelHash] || `#unknown(${channelHash})`;\n\n// Reformater le message final\nmsg.payload = `MC${channelName} ${sender} ${message}`;\n\nreturn msg;\n/*\n// Récupérer la sortie de la commande\nconst output = msg.payload;\n\n// Vérifier si la sortie contient \"GroupText\"\nif (output.includes(\"Payload Type: GroupText\")) {\n    // Extraire les informations nécessaires\n    const senderMatch = output.match(/Sender: ([^\\n]+)/);\n    const messageMatch = output.match(/Message: ([^\\n]+)/);\n\n    if (senderMatch && messageMatch) {\n        const sender = senderMatch[1].trim();\n        const message = messageMatch[1].trim();\n\n        // Reformater le message\n        msg.payload = `MC#test ${sender} ${message}`;\n    }\n} else {\n    msg.payload = null; // Garder la sortie originale si ce n'est pas un GroupText\n}\n\nreturn msg;\n*/",
        "outputs": 1,
        "timeout": 0,
        "noerr": 0,
        "initialize": "",
        "finalize": "",
        "libs": [],
        "x": 970,
        "y": 540,
        "wires": [
            [
                "924e5320ccd92416",
                "77a7e33e0b30cecf",
                "2de0cfa8fb4b0666"
            ]
        ]
    },
    {
        "id": "924e5320ccd92416",
        "type": "debug",
        "z": "b4b9bf4d7980865a",
        "name": "debug 39",
        "active": false,
        "tosidebar": true,
        "console": false,
        "tostatus": false,
        "complete": "false",
        "statusVal": "",
        "statusType": "auto",
        "x": 1180,
        "y": 500,
        "wires": []
    },
    {
        "id": "f5e54c21bbee9e2f",
        "type": "mqtt out",
        "z": "b4b9bf4d7980865a",
        "name": "",
        "topic": "meshcore/channelTest",
        "qos": "1",
        "retain": "true",
        "respTopic": "",
        "contentType": "",
        "userProps": "",
        "correl": "",
        "expiry": "",
        "broker": "d944b96a0dea644f",
        "x": 1520,
        "y": 580,
        "wires": []
    },
    {
        "id": "77a7e33e0b30cecf",
        "type": "function",
        "z": "b4b9bf4d7980865a",
        "name": "uniquement frblabla vers telegram",
        "func": "// Vérifier que le payload est une chaîne\nif (typeof msg.payload !== \"string\") {\n    return null;\n}\n\n// Laisser passer uniquement MC#frblabla\nif (msg.payload.startsWith(\"MC#frblabla\")) {\n    return msg;\n}\n\n// Sinon, on bloque\nreturn null;\n",
        "outputs": 1,
        "timeout": 0,
        "noerr": 0,
        "initialize": "",
        "finalize": "",
        "libs": [],
        "x": 1260,
        "y": 540,
        "wires": [
            [
                "13b605c806f028ad"
            ]
        ]
    },
    {
        "id": "13b605c806f028ad",
        "type": "mqtt out",
        "z": "b4b9bf4d7980865a",
        "name": "",
        "topic": "meshcore/versMeshtastic",
        "qos": "1",
        "retain": "true",
        "respTopic": "",
        "contentType": "",
        "userProps": "",
        "correl": "",
        "expiry": "",
        "broker": "d944b96a0dea644f",
        "x": 1550,
        "y": 540,
        "wires": []
    },
    {
        "id": "3124975ae77634c4",
        "type": "function",
        "z": "b4b9bf4d7980865a",
        "name": "filtre meshcore/gateway/XXXXXXXXX/raw/rx",
        "func": "if (msg.topic && /^meshcore\\/gateway\\/[^/]+\\/raw\\/rx$/.test(msg.topic)) {\n    return msg;   // Laisser passer\n} else {\n    return null;  // Bloquer\n}\n",
        "outputs": 1,
        "timeout": 0,
        "noerr": 0,
        "initialize": "",
        "finalize": "",
        "libs": [],
        "x": 230,
        "y": 540,
        "wires": [
            [
                "c71a0873e719940a",
                "48da578bfd37c3bf",
                "3642e14930be2b31",
                "27d3dd79af3af1c1"
            ]
        ]
    },
    {
        "id": "bfc53675d8d148d1",
        "type": "mqtt in",
        "z": "b4b9bf4d7980865a",
        "name": "",
        "topic": "meshcore/gateway/#",
        "qos": "2",
        "datatype": "auto-detect",
        "broker": "57c4cff541230e44",
        "nl": false,
        "rap": true,
        "rh": 0,
        "inputs": 0,
        "x": 110,
        "y": 480,
        "wires": [
            [
                "3124975ae77634c4"
            ]
        ]
    },
    {
        "id": "c71a0873e719940a",
        "type": "debug",
        "z": "b4b9bf4d7980865a",
        "name": "debug 34",
        "active": false,
        "tosidebar": true,
        "console": false,
        "tostatus": false,
        "complete": "false",
        "statusVal": "",
        "statusType": "auto",
        "x": 380,
        "y": 580,
        "wires": []
    },
    {
        "id": "e9c6f1c2501d7fe8",
        "type": "function",
        "z": "b4b9bf4d7980865a",
        "name": "mise en json",
        "func": "// Récupérer le payload (string)\nconst input = msg.payload;\n\n// Initialiser l'objet JSON de sortie\nconst result = {};\n\n// Analyser le string pour extraire les informations\nconst lines = input.split('\\n');\n\nfor (const line of lines) {\n    // Ignorer les lignes vides ou les séparateurs\n    if (line.trim() === '' || line.includes('===') || line.includes('✅')) {\n        continue;\n    }\n\n    // Extraire les paires clé/valeur\n    const match = line.match(/^(.*?):\\s*(.*)$/);\n    if (match) {\n        const key = match[1].trim();\n        const value = match[2].trim();\n\n        // Nettoyer les valeurs (ex: \"31 → 26 → EC → 86\" devient [\"31\", \"26\", \"EC\", \"86\"])\n        if (key === \"Path\") {\n            result[key] = value.split('→').map(item => item.trim());\n        }\n        // Gérer les cas spéciaux (ex: \"🔒 Encrypted\")\n        else if (value.includes(\"🔒\")) {\n            result[\"Encrypted\"] = true;\n            result[\"Ciphertext\"] = value.replace(\"🔒 Encrypted (no key available)\", \"\").trim();\n        }\n        // Cas standard\n        else {\n            result[key] = value;\n        }\n    }\n}\n\n// Mettre à jour le payload avec le JSON\nmsg.payload = result;\nreturn msg;\n",
        "outputs": 1,
        "timeout": 0,
        "noerr": 0,
        "initialize": "",
        "finalize": "",
        "libs": [],
        "x": 930,
        "y": 580,
        "wires": [
            [
                "8934d738d8b07a1f",
                "0d7fd806499c9792"
            ]
        ]
    },
    {
        "id": "8934d738d8b07a1f",
        "type": "function",
        "z": "b4b9bf4d7980865a",
        "name": "filtrage canaux connu dechiffré",
        "func": "// Récupérer le payload (JSON)\nconst payload = msg.payload;\n\n// Liste des Channel Hash autorisés\nconst allowedChannelHashes = [\"D9\", \"11\", \"CA\", \"68\", \"3B\", \"78\", \"57\"];\n\n// Vérifier si le payload est un objet et contient les champs nécessaires\nif (typeof payload === 'object' && payload !== null) {\n    const { \"Channel Hash\": channelHash, Ciphertext: ciphertext } = payload;\n\n    // Si le Channel Hash est dans la liste ET qu'il y a un Ciphertext, on ignore le message\n    if (allowedChannelHashes.includes(channelHash) && ciphertext !== undefined) {\n        return null; // ou return; pour arrêter le traitement\n    }\n}\n\n// Si le message ne correspond pas aux critères, on le laisse passer\nreturn msg;\n",
        "outputs": 1,
        "timeout": 0,
        "noerr": 0,
        "initialize": "",
        "finalize": "",
        "libs": [],
        "x": 1250,
        "y": 580,
        "wires": [
            [
                "aede7aacab40a132",
                "f5e54c21bbee9e2f"
            ]
        ]
    },
    {
        "id": "0d7fd806499c9792",
        "type": "debug",
        "z": "b4b9bf4d7980865a",
        "name": "debug 30",
        "active": false,
        "tosidebar": true,
        "console": false,
        "tostatus": false,
        "complete": "false",
        "statusVal": "",
        "statusType": "auto",
        "x": 1040,
        "y": 640,
        "wires": []
    },
    {
        "id": "e0a0324952f77381",
        "type": "debug",
        "z": "b4b9bf4d7980865a",
        "name": "debug 31",
        "active": false,
        "tosidebar": true,
        "console": false,
        "tostatus": false,
        "complete": "false",
        "statusVal": "",
        "statusType": "auto",
        "x": 1040,
        "y": 680,
        "wires": []
    },
    {
        "id": "3d46f017f0ec83c7",
        "type": "debug",
        "z": "b4b9bf4d7980865a",
        "name": "debug 33",
        "active": false,
        "tosidebar": true,
        "console": false,
        "tostatus": false,
        "complete": "false",
        "statusVal": "",
        "statusType": "auto",
        "x": 1040,
        "y": 720,
        "wires": []
    },
    {
        "id": "1434385a06c3f835",
        "type": "debug",
        "z": "b4b9bf4d7980865a",
        "name": "debug 44",
        "active": false,
        "tosidebar": true,
        "console": false,
        "tostatus": false,
        "complete": "false",
        "statusVal": "",
        "statusType": "auto",
        "x": 700,
        "y": 640,
        "wires": []
    },
    {
        "id": "27d3dd79af3af1c1",
        "type": "function",
        "z": "b4b9bf4d7980865a",
        "name": "extract info terminaux",
        "func": "if (typeof msg.payload !== \"string\") return [null, msg];\n\nconst crypto = global.get(\"crypto\");\nconst frame = Buffer.from(msg.payload, \"hex\");\n\n// ---- Header\nconst header = frame[0];\nlet routeType;\nif (header === 0x11) routeType = \"Flood\";\nelse if (header === 0x12) routeType = \"Direct\";\nelse return [null, msg];\n\n// ---- Message Hash (SHA256 first 4 bytes)\nconst messageHash = crypto\n    .createHash(\"sha256\")\n    .update(frame)\n    .digest()\n    .subarray(0, 4)\n    .toString(\"hex\")\n    .toUpperCase();\n\n// ---- Path\nconst pathLen = frame[1];\nconst buf = frame.slice(2 + pathLen);\n\nlet p = 0;\n\n// ---- Public key (32 bytes)\nconst publicKey = buf.slice(p, p += 32).toString(\"hex\");\n\n// ---- Timestamp (4 bytes, juste après la clé publique)\nconst ts = buf.readUInt32LE(p);\np += 4;\nconst timestamp = new Date(ts * 1000).toISOString();\n//node.warn(`Timestamp décimal: ${ts} -> ${timestamp}`);\n\n// ---- Signature (64 bytes)\nconst signature = buf.slice(p, p += 64).toString(\"hex\");\n\n// ---- AppData Flags / Role (1 byte)\nconst flagsByte = buf[p++];\nconst roleMap = {\n    0x01: \"Chat Node\",\n    0x02: \"Repeater\",\n    0x03: \"Room Server\",\n    0x04: \"Sensor\"\n};\nconst deviceRole = roleMap[flagsByte & 0x0F] ?? \"Unknown\";\n\n// ---- Latitude / Longitude si présents (flags 0x10)\nlet latitude = null, longitude = null;\nif (flagsByte & 0x10) {\n    latitude = buf.readUInt32LE(p) / 1e6;\n    p += 4;\n    longitude = buf.readUInt32LE(p) / 1e6;\n    p += 4;\n}\n\n// ---- Device Name si présent (flags 0x80)\nlet name = null;\nif (flagsByte & 0x80) {\n    name = buf.slice(p).toString(\"utf8\").replace(/\\0/g, \"\");\n}\n\n// ---- Output\nmsg.payload = {\n    \"Message Hash\": messageHash,\n    \"Route Type\": routeType,\n    \"Payload Type\": \"Advert\",\n    \"Total Bytes\": frame.length.toString(),\n    \"Device Role\": deviceRole,\n    \"public_key\": publicKey,\n    \"signature\": signature,\n    \"Device Name\": name,\n    \"Location\": latitude !== null\n        ? { \"latitude\": latitude, \"longitude\": longitude }\n        : null,\n    \"Timestamp\": timestamp\n};\n\nmsg.topic = \"meshcore/compagnon/\" + deviceRole + \"/\" + name;\nreturn [msg, null];\n",
        "outputs": 1,
        "timeout": 0,
        "noerr": 0,
        "initialize": "",
        "finalize": "",
        "libs": [],
        "x": 160,
        "y": 620,
        "wires": [
            [
                "57bdcd67202ab88b",
                "97574b7118956dfe"
            ]
        ]
    },
    {
        "id": "57bdcd67202ab88b",
        "type": "debug",
        "z": "b4b9bf4d7980865a",
        "name": "debug 50",
        "active": true,
        "tosidebar": true,
        "console": false,
        "tostatus": false,
        "complete": "payload",
        "targetType": "msg",
        "statusVal": "",
        "statusType": "auto",
        "x": 380,
        "y": 620,
        "wires": []
    },
    {
        "id": "97574b7118956dfe",
        "type": "mqtt out",
        "z": "b4b9bf4d7980865a",
        "name": "",
        "topic": "",
        "qos": "1",
        "retain": "true",
        "respTopic": "",
        "contentType": "",
        "userProps": "",
        "correl": "",
        "expiry": "",
        "broker": "d944b96a0dea644f",
        "x": 110,
        "y": 660,
        "wires": []
    },
    {
        "id": "d944b96a0dea644f",
        "type": "mqtt-broker",
        "name": "MQTT Gaulix Officiel",
        "broker": "mqtt.gaulix.fr",
        "port": 1883,
        "clientid": "",
        "autoConnect": true,
        "usetls": false,
        "protocolVersion": 4,
        "keepalive": 60,
        "cleansession": true,
        "autoUnsubscribe": true,
        "birthTopic": "",
        "birthQos": "0",
        "birthRetain": "false",
        "birthPayload": "",
        "birthMsg": {},
        "closeTopic": "",
        "closeQos": "0",
        "closeRetain": "false",
        "closePayload": "",
        "closeMsg": {},
        "willTopic": "",
        "willQos": "0",
        "willRetain": "false",
        "willPayload": "",
        "willMsg": {},
        "userProps": "",
        "sessionExpiry": ""
    },
    {
        "id": "57c4cff541230e44",
        "type": "mqtt-broker",
        "name": "local",
        "broker": "127.0.0.1",
        "port": "1883",
        "clientid": "NodeRedOVH",
        "autoConnect": true,
        "usetls": false,
        "protocolVersion": "4",
        "keepalive": "60",
        "cleansession": true,
        "autoUnsubscribe": true,
        "birthTopic": "",
        "birthQos": "0",
        "birthPayload": "",
        "birthMsg": {},
        "closeTopic": "",
        "closeQos": "0",
        "closePayload": "",
        "closeMsg": {},
        "willTopic": "",
        "willQos": "0",
        "willPayload": "",
        "willMsg": {},
        "userProps": "",
        "sessionExpiry": ""
    }
]


donc maintenant tous ces canaux sont accessible et traitable dans nodered
il faut maintenant faire l’inverse, et là pas de github
j’y ai passé de longue heure de solitude, et surtout le chiffrement de la date unix,
image
le bout de la fonction point vers le topic sender dans le mqtt
image

[
    {
        "id": "ffd4b146ceb39d2f",
        "type": "debug",
        "z": "b4b9bf4d7980865a",
        "name": "Trame Chiffrée",
        "active": true,
        "tosidebar": true,
        "console": false,
        "tostatus": false,
        "complete": "false",
        "targetType": "msg",
        "statusVal": "",
        "statusType": "auto",
        "x": 740,
        "y": 880,
        "wires": []
    },
    {
        "id": "1ff66e8e768b561a",
        "type": "link out",
        "z": "b4b9bf4d7980865a",
        "name": "link out 4",
        "mode": "link",
        "links": [
            "db2a66394f752bd3"
        ],
        "x": 675,
        "y": 820,
        "wires": []
    },
    {
        "id": "eb6cf496b562cbdf",
        "type": "inject",
        "z": "b4b9bf4d7980865a",
        "name": "",
        "props": [
            {
                "p": "payload"
            }
        ],
        "repeat": "",
        "crontab": "",
        "once": false,
        "onceDelay": 0.1,
        "topic": "",
        "payload": "patacouhette",
        "payloadType": "str",
        "x": 170,
        "y": 880,
        "wires": [
            [
                "00141cb57152e828"
            ]
        ]
    },
    {
        "id": "662d96435f468fc7",
        "type": "mqtt in",
        "z": "b4b9bf4d7980865a",
        "name": "",
        "topic": "meshcore/bot",
        "qos": "2",
        "datatype": "auto-detect",
        "broker": "57c4cff541230e44",
        "nl": false,
        "rap": true,
        "rh": 0,
        "inputs": 0,
        "x": 150,
        "y": 820,
        "wires": [
            [
                "00141cb57152e828"
            ]
        ]
    },
    {
        "id": "ee56d747bd319819",
        "type": "rbe",
        "z": "b4b9bf4d7980865a",
        "name": "anti doublons",
        "func": "rbe",
        "gap": "",
        "start": "",
        "inout": "out",
        "septopics": false,
        "property": "payload",
        "topi": "topic",
        "x": 430,
        "y": 940,
        "wires": [
            [
                "dcaf8bca071c55af",
                "af68ec336e6d85ac"
            ]
        ]
    },
    {
        "id": "ded3818b2b924035",
        "type": "mqtt in",
        "z": "b4b9bf4d7980865a",
        "name": "",
        "topic": "meshcore/depuisMeshtastic",
        "qos": "2",
        "datatype": "auto-detect",
        "broker": "57c4cff541230e44",
        "nl": false,
        "rap": true,
        "rh": 0,
        "inputs": 0,
        "x": 200,
        "y": 940,
        "wires": [
            [
                "ee56d747bd319819"
            ]
        ]
    },
    {
        "id": "dcaf8bca071c55af",
        "type": "function",
        "z": "b4b9bf4d7980865a",
        "name": "function 5",
        "func": "if (typeof msg.payload === \"string\" && msg.payload.includes(\"MC\")) {\n    // On ne diffuse pas le message\n    return null;\n}\n\n// Sinon, on laisse passer\nreturn msg;\n",
        "outputs": 1,
        "timeout": 0,
        "noerr": 0,
        "initialize": "",
        "finalize": "",
        "libs": [],
        "x": 600,
        "y": 940,
        "wires": [
            [
                "00141cb57152e828"
            ]
        ]
    },
    {
        "id": "af68ec336e6d85ac",
        "type": "debug",
        "z": "b4b9bf4d7980865a",
        "name": "debug 43",
        "active": false,
        "tosidebar": true,
        "console": false,
        "tostatus": false,
        "complete": "false",
        "statusVal": "",
        "statusType": "auto",
        "x": 600,
        "y": 980,
        "wires": []
    },
    {
        "id": "00141cb57152e828",
        "type": "function",
        "z": "b4b9bf4d7980865a",
        "name": "chiffrage sur #frblabla avec timeStamps",
        "func": "// ===== MeshCore GroupText encoder (Node-RED final) =====\n\n// Node-RED crypto\nconst crypto = global.get('crypto');\n\n// ---------- CONSTANTS ----------\nconst HEADER = 0x15; // Flood + GroupText\nconst PATH_LEN = 0x00;\nconst CHANNEL_HASH = 0x57;\n\nconst ID_HASH = 0x00;\n//const ID_HASH = 0xFD;\n//const SENDER = \"16cham_kazy\";\nconst SENDER = \"Bot_Gaulix\";\n\n// Channel AES key (16 bytes)\nconst AES_KEY = Buffer.from(\"33dfec1c6022b717d941c1b7606915f3\", \"hex\");\n\n// HMAC key = 32 bytes (AES key padded with zeros)\nconst HMAC_KEY = Buffer.alloc(32);\nAES_KEY.copy(HMAC_KEY);\n\n// ---------- INPUT ----------\nif (typeof msg.payload !== \"string\") {\n    node.error(\"msg.payload must be a string\");\n    return null;\n}\n\nconst MESSAGE = msg.payload;\n\n// ---------- CLEAR DATA ----------\n// Timestamp réseau dynamique depuis variable globale (ISO string)\n/*\nlet ts = global.get('unixtime'); \nnode.warn(ts);\nconst tsBuf = Buffer.alloc(4);\ntsBuf.writeUInt32LE(ts);\nnode.warn(tsBuf);\n*/\n\n// Récupérer la date ISO de la variable globale\nlet iso = global.get('unixtime'); // ex: \"2026-01-09T11:10:53.000Z\"\n\n// Convertir en Unix timestamp en secondes\nlet ts = Math.floor(new Date(iso).getTime() / 1000);\n\n// Écrire dans un buffer little-endian\nconst tsBuf = Buffer.alloc(4);\ntsBuf.writeUInt32LE(ts); // little-endian, exactement comme MeshCore\n\n\n\n\nconst clearData = Buffer.concat([\n    //Buffer.from([ID_HASH]),        // 1 octet\n    tsBuf,                         // 4 octets\n    Buffer.from([ID_HASH]),        // 1 octet\n    Buffer.from(SENDER + \": \", \"utf8\"), // sender\n    Buffer.from(MESSAGE, \"utf8\")        // message\n]);\n\n// ---------- AES-128 ECB (MeshCore style) ----------\nfunction aesEncrypt(key, data) {\n    const cipher = crypto.createCipheriv(\"aes-128-ecb\", key, null);\n    cipher.setAutoPadding(false);\n\n    const paddedLen = Math.ceil(data.length / 16) * 16;\n    const padded = Buffer.alloc(paddedLen);\n    data.copy(padded);\n\n    return Buffer.concat([cipher.update(padded), cipher.final()]);\n}\n\nconst ciphertext = aesEncrypt(AES_KEY, clearData);\n\n// ---------- HMAC-SHA256 (truncate 2 bytes) ----------\nconst mac = crypto.createHmac(\"sha256\", HMAC_KEY)\n    .update(ciphertext)\n    .digest()\n    .subarray(0, 2);\n\n// ---------- PAYLOAD ----------\nconst payload = Buffer.concat([\n    mac,\n    ciphertext\n]);\n\n// ---------- FRAME ----------\nconst frame = Buffer.concat([\n    Buffer.from([HEADER]),    // type + route\n    Buffer.from([PATH_LEN]),  // 1\n    //Buffer.from([ID_HASH]),   // path\n    Buffer.from([CHANNEL_HASH]),\n    payload\n]);\n\n// ---------- OUTPUT ----------\nmsg.payload = frame.toString(\"hex\").toUpperCase();\nreturn msg;\n",
        "outputs": 1,
        "timeout": 0,
        "noerr": 0,
        "initialize": "",
        "finalize": "",
        "libs": [],
        "x": 460,
        "y": 880,
        "wires": [
            [
                "1ff66e8e768b561a",
                "ffd4b146ceb39d2f"
            ]
        ]
    },
    {
        "id": "af2b5e7cb31ab1ca",
        "type": "function",
        "z": "b4b9bf4d7980865a",
        "name": "chiffrage sur @public avec timeStamps",
        "func": "// ===== MeshCore GroupText encoder (Node-RED final) =====\n\n// Node-RED crypto\nconst crypto = global.get('crypto');\n\n// ---------- CONSTANTS ----------\nconst HEADER = 0x15; // Flood + GroupText\nconst PATH_LEN = 0x00;\nconst CHANNEL_HASH = 0x11;\n\nconst ID_HASH = 0x00;\n//const ID_HASH = 0xFD;\n//const SENDER = \"16cham_kazy\";\nconst SENDER = \"Bot_Gaulix\";\n\n// Channel AES key (16 bytes)\nconst AES_KEY = Buffer.from(\"8b3387e9c5cdea6ac9e5edbaa115cd72\", \"hex\"); \n\n// HMAC key = 32 bytes (AES key padded with zeros)\nconst HMAC_KEY = Buffer.alloc(32);\nAES_KEY.copy(HMAC_KEY);\n\n// ---------- INPUT ----------\nif (typeof msg.payload !== \"string\") {\n    node.error(\"msg.payload must be a string\");\n    return null;\n}\n\nconst MESSAGE = msg.payload;\n\n// ---------- CLEAR DATA ----------\n// Timestamp réseau dynamique depuis variable globale (ISO string)\n/*\nlet ts = global.get('unixtime'); \nnode.warn(ts);\nconst tsBuf = Buffer.alloc(4);\ntsBuf.writeUInt32LE(ts);\nnode.warn(tsBuf);\n*/\n\n// Récupérer la date ISO de la variable globale\nlet iso = global.get('unixtime'); // ex: \"2026-01-09T11:10:53.000Z\"\n\n// Convertir en Unix timestamp en secondes\nlet ts = Math.floor(new Date(iso).getTime() / 1000);\n\n// Écrire dans un buffer little-endian\nconst tsBuf = Buffer.alloc(4);\ntsBuf.writeUInt32LE(ts); // little-endian, exactement comme MeshCore\n\n\n\n\nconst clearData = Buffer.concat([\n    //Buffer.from([ID_HASH]),        // 1 octet\n    tsBuf,                         // 4 octets\n    Buffer.from([ID_HASH]),        // 1 octet\n    Buffer.from(SENDER + \": \", \"utf8\"), // sender\n    Buffer.from(MESSAGE, \"utf8\")        // message\n]);\n\n// ---------- AES-128 ECB (MeshCore style) ----------\nfunction aesEncrypt(key, data) {\n    const cipher = crypto.createCipheriv(\"aes-128-ecb\", key, null);\n    cipher.setAutoPadding(false);\n\n    const paddedLen = Math.ceil(data.length / 16) * 16;\n    const padded = Buffer.alloc(paddedLen);\n    data.copy(padded);\n\n    return Buffer.concat([cipher.update(padded), cipher.final()]);\n}\n\nconst ciphertext = aesEncrypt(AES_KEY, clearData);\n\n// ---------- HMAC-SHA256 (truncate 2 bytes) ----------\nconst mac = crypto.createHmac(\"sha256\", HMAC_KEY)\n    .update(ciphertext)\n    .digest()\n    .subarray(0, 2);\n\n// ---------- PAYLOAD ----------\nconst payload = Buffer.concat([\n    mac,\n    ciphertext\n]);\n\n// ---------- FRAME ----------\nconst frame = Buffer.concat([\n    Buffer.from([HEADER]),    // type + route\n    Buffer.from([PATH_LEN]),  // 1\n    //Buffer.from([ID_HASH]),   // path\n    Buffer.from([CHANNEL_HASH]),\n    payload\n]);\n\n// ---------- OUTPUT ----------\nmsg.payload = frame.toString(\"hex\").toUpperCase();\nreturn msg;\n",
        "outputs": 1,
        "timeout": 0,
        "noerr": 0,
        "initialize": "",
        "finalize": "",
        "libs": [],
        "x": 450,
        "y": 820,
        "wires": [
            [
                "1ff66e8e768b561a",
                "ffd4b146ceb39d2f"
            ]
        ]
    },
    {
        "id": "6ee5fbbd064ab4e7",
        "type": "inject",
        "z": "b4b9bf4d7980865a",
        "name": "",
        "props": [
            {
                "p": "payload"
            }
        ],
        "repeat": "",
        "crontab": "",
        "once": false,
        "onceDelay": 0.1,
        "topic": "",
        "payload": "un bot qui parle à un autre bot n'importe nawak!!!!",
        "payloadType": "str",
        "x": 370,
        "y": 780,
        "wires": [
            [
                "af2b5e7cb31ab1ca"
            ]
        ]
    },
    {
        "id": "57c4cff541230e44",
        "type": "mqtt-broker",
        "name": "local",
        "broker": "127.0.0.1",
        "port": "1883",
        "clientid": "NodeRedOVH",
        "autoConnect": true,
        "usetls": false,
        "protocolVersion": "4",
        "keepalive": "60",
        "cleansession": true,
        "autoUnsubscribe": true,
        "birthTopic": "",
        "birthQos": "0",
        "birthPayload": "",
        "birthMsg": {},
        "closeTopic": "",
        "closeQos": "0",
        "closePayload": "",
        "closeMsg": {},
        "willTopic": "",
        "willQos": "0",
        "willPayload": "",
        "willMsg": {},
        "userProps": "",
        "sessionExpiry": ""
    }
]

avec cela si on post un string sur le mqtt dans le topic meshcore/bot on envoie ce text sur le channel frblabla chiffré

si des gens sont interessé pour continuer avec moi, au plaisir le prochain but est d’integrer ce qu’il faut pour piloter des « sensor »
car même si quelques capteurs sont déjà integrés
image
je voudrais aussi pouvoir piloter des choses, et aussi integrer un capteur de niveau liquide ( hauteur d’une rivière) ou le mcp23017 qui permet 16 entrées sorties

ah j’oubliais si vous voulez nous rejoindre sur une messagerie instantanée pour de l’aide a mettre en marche votre réseau, nous sommes actif sur télégram

et comme les locaux de jedom sont à lyon, saché messieur que vous êtes couvert par nos réseau si vous voulez faire des essais

Salut,

Pas compris vraiment l’objectif j’avoue, quelles sont les use cases mais concernant l’intégration mqtt, perso je suis convaincu par l’auto-discovery :wink:
ca rendra de facto l’intégration avec HA ou Jeedom automatique sans avoir rien d’autre à faire puisque dans les 2 cas, c’est déjà supporté par les plateformes.

Bonsoir,
L’objectif est de se baser sur le protocole meshcore pour pouvoir faire de la domotique longue distance. Actuellement est en rase campagne j’ai besoin pour des amis de piloter des pompes ou des clôtures électriques pour le bétail ou avoir des relevés d’information de matériel agricole. Aujourd’hui il n’y a rien d’exploitable dans la domotique en terme de longue distance. Je le fais aussi pour les besoins de l’usine dans laquelle je travaille pour des relevés notamment de température ou de fermeture d’accès.
Mais très franchement le fait de passer par 'nodered c’est assez moche, et je voulais partager mon expérience car je suis persuadé que des utilisateurs notamment de la GTB pour être intéressé. Je pense notamment à des fabricants de village de vie serait intéressé pour des relevés ou pilotage de système de chauffage et température sur des villages de vie séparés de plusieurs kilomètres. La domotique longue distance sans fil peut avoir beaucoup d’applications.
Et à l’occasion peut-être que quelqu’un comme @MrGreen qui voulait travailler sur du Lora pour être intéressé afin de monter un plugin.
J’aime bien partager mes infos car souvent on apprend aussi des autres.
Donc j’espère rester dans l’esprit de jeedom et donc dans l’esprit de partage et d’échange

1 « J'aime »