EN

Companion-Protokoll

Companion-Protokoll

  • Letzte Aktualisierung: 2026-03-08
  • Protokollversion: Companion-Firmware v1.12.0+
HINWEIS: Dieses Dokument befindet sich noch in Entwicklung. Einige Informationen können ungenau sein.

Dieses Dokument bietet eine umfassende Anleitung zur Kommunikation mit MeshCore-Geräten über Bluetooth Low Energy (BLE, energiesparende Bluetooth-Variante).

Es ist plattformunabhängig und kann für Android, iOS, Python, JavaScript oder jede andere Plattform verwendet werden, die BLE unterstützt.

Offizielle Bibliotheken

Für bereits existierende Companion-Protokoll-Bibliotheken siehe folgende Repositories:

Wichtiger Sicherheitshinweis

Sämtliche Geheimnisse, Hashes und kryptographische Werte in diesem Dokument sind ausschließlich Beispielwerte.

  • Alle Hex-Werte, öffentlichen Schlüssel und Hashes dienen nur zur Veranschaulichung
  • Verwende niemals Beispiel-Geheimnisse in Produktivsystemen
  • Erzeuge stets neue, kryptographisch sichere Zufallswerte
  • Setze in deiner Implementierung immer ordnungsgemäße Sicherheitspraktiken um
  • Dieses Dokument dient ausschließlich der Protokolldokumentation

Inhaltsverzeichnis

  1. BLE-Verbindung
  2. Paketstruktur
  3. Befehle
  4. Kanalverwaltung
  5. Nachrichtenverarbeitung
  6. Antwort-Parsing
  7. Beispielhafte Implementierung
  8. Best Practices
  9. Fehlerbehebung
---

BLE-Verbindung

Service und Characteristics

MeshCore Companion-Geräte stellen einen BLE-Service mit folgenden UUIDs bereit:

  • Service-UUID: 6E400001-B5A3-F393-E0A9-E50E24DCCA9E
  • RX Characteristic (App → Firmware): 6E400002-B5A3-F393-E0A9-E50E24DCCA9E
  • TX Characteristic (Firmware → App): 6E400003-B5A3-F393-E0A9-E50E24DCCA9E

Verbindungsschritte

  1. Nach Geräten suchen
- Scanne nach BLE-Geräten, die die MeshCore-Service-UUID bewerben - Optional nach Gerätenamen filtern (enthält typischerweise das Präfix „MeshCore") - Die MAC-Adresse des Geräts für spätere Wiederverbindungen notieren
  1. GATT-Verbindung herstellen
- Über die ermittelte MAC-Adresse mit dem Gerät verbinden - Warten, bis die Verbindung vollständig aufgebaut ist
  1. Services und Characteristics ermitteln
- Den Service mit der UUID 6E400001-B5A3-F393-E0A9-E50E24DCCA9E suchen - Die RX Characteristic 6E400002-B5A3-F393-E0A9-E50E24DCCA9E suchen - Deine App schreibt hierhin, die Firmware liest daraus - Die TX Characteristic 6E400003-B5A3-F393-E0A9-E50E24DCCA9E suchen - Die Firmware schreibt hierhin, deine App liest daraus
  1. Benachrichtigungen aktivieren
- Notifications auf der TX Characteristic abonnieren, um Daten von der Firmware zu empfangen
  1. Initiale Befehle senden
- CMD_APP_START senden, um deine App gegenüber der Firmware zu identifizieren und die Radioeinstellungen zu erhalten - CMD_DEVICE_QUERY senden, um Geräteinformationen abzurufen und unterstützte Protokollversionen auszuhandeln - CMD_SET_DEVICE_TIME senden, um die Uhr der Firmware zu setzen - CMD_GET_CONTACTS senden, um alle Kontakte abzurufen - CMD_GET_CHANNEL mehrfach senden, um alle Kanal-Slots abzurufen - CMD_SYNC_NEXT_MESSAGE senden, um die nächste in der Firmware gespeicherte Nachricht abzurufen - Listener für Push-Codes einrichten, beispielsweise PUSH_CODE_MSG_WAITING oder PUSH_CODE_ADVERT - Siehe den Abschnitt Befehle für Informationen zu weiteren Befehlen Hinweis: MeshCore-Geräte können sich nach einer Phase der Inaktivität trennen. Implementiere eine automatische Wiederverbindungslogik mit exponentiellem Backoff (schrittweise Verlängerung der Wartezeit zwischen Verbindungsversuchen).

BLE-Schreibtyp

Beim Schreiben von Befehlen auf die RX Characteristic muss der Schreibtyp angegeben werden:

  • Write with Response (Standard): Wartet auf eine Bestätigung vom Gerät
  • Write without Response: Schneller, aber ohne Bestätigung
Plattformspezifisch:
  • Android: BluetoothGattCharacteristic.WRITE_TYPE_DEFAULT oder WRITE_TYPE_NO_RESPONSE verwenden
  • iOS: CBCharacteristicWriteType.withResponse oder .withoutResponse verwenden
  • Python (bleak): write_gatt_char() mit response=True oder False verwenden
Empfehlung: Verwende Write with Response für maximale Zuverlässigkeit.

MTU (Maximum Transmission Unit)

Die standardmäßige BLE-MTU (maximale Übertragungseinheit) beträgt 23 Bytes (20 Bytes Nutzlast). Für größere Befehle wie SET_CHANNEL (50 Bytes) muss möglicherweise Folgendes gemacht werden:

  1. Größere MTU anfordern: MTU von 512 Bytes anfordern, sofern unterstützt
- Android: gatt.requestMtu(512) - iOS: peripheral.maximumWriteValueLength(for:) - Python (bleak): Die MTU wird automatisch ausgehandelt

Befehlsreihenfolge

Wichtig: Befehle müssen in der richtigen Reihenfolge gesendet werden:
  1. Nach dem Verbindungsaufbau:
- Warten, bis die BLE-Verbindung hergestellt ist - Warten, bis Services/Characteristics erkannt wurden - Warten, bis Notifications aktiviert sind - Erst dann können Befehle sicher an die Firmware gesendet werden
  1. Befehls-Antwort-Zuordnung:
- Immer nur einen Befehl gleichzeitig senden - Auf die Antwort warten, bevor der nächste Befehl gesendet wird - Einen Timeout verwenden (typischerweise 5 Sekunden) - Die Antwort dem Befehl anhand des Typs zuordnen (z. B.: CMD_GET_CHANNELRESP_CODE_CHANNEL_INFO)

Befehls-Warteschlange

Für einen zuverlässigen Betrieb empfiehlt sich die Implementierung einer Befehls-Warteschlange (Command Queue).

Warteschlangenstruktur:
  • Eine Warteschlange ausstehender Befehle führen
  • Verfolgen, welcher Befehl gerade auf eine Antwort wartet
  • Den nächsten Befehl erst senden, nachdem die Antwort eingegangen ist oder der Timeout abgelaufen ist
Fehlerbehandlung:
  • Bei Timeout: Aktuellen Befehl verwerfen, nächsten aus der Warteschlange verarbeiten
  • Bei Fehler: Fehler protokollieren, aktuellen Befehl verwerfen, nächsten verarbeiten
---

Paketstruktur

Das MeshCore-Protokoll nutzt ein binäres Format mit folgender Struktur:

  • Befehle (Commands): Werden von der App über die RX Characteristic an die Firmware gesendet
  • Antworten (Responses): Werden von der Firmware über TX-Characteristic-Notifications empfangen
  • Alle mehrbytigen Ganzzahlen: Little-Endian-Bytereihenfolge (Ausnahme: CayenneLPP verwendet Big-Endian)
  • Alle Strings: UTF-8-Kodierung
Die meisten Pakete folgen diesem Format:
CLI
[Pakettyp (1 Byte)] [Daten (variable Länge)]

Das erste Byte gibt den Pakettyp an (siehe Antwort-Parsing).

---

Befehle

1. App Start

Zweck: Kommunikation mit dem Gerät initialisieren. Muss nach dem Verbindungsaufbau als erstes gesendet werden. Befehlsformat:
CLI
Byte 0: 0x01
Bytes 1-7: Reserviert (wird derzeit von der Firmware ignoriert)
Bytes 8+: Applikationsname (UTF-8, optional)
Beispiel (hex):
CLI
01 00 00 00 00 00 00 00 6d 63 63 6c 69
Antwort: PACKET_SELF_INFO (0x05)

---

2. Device Query

Zweck: Geräteinformationen abfragen. Befehlsformat:
CLI
Byte 0: 0x16
Byte 1: 0x03
Beispiel (hex):
CLI
16 03
Antwort: PACKET_DEVICE_INFO (0x0D) mit Geräteinformationen

---

3. Get Channel Info

Zweck: Informationen über einen bestimmten Kanal abrufen. Befehlsformat:
CLI
Byte 0: 0x1F
Byte 1: Kanal-Index (0-7)
Beispiel (Kanal 1 abfragen):
CLI
1F 01
Antwort: PACKET_CHANNEL_INFO (0x12) mit Kanaldetails

---

4. Set Channel

Zweck: Einen Kanal auf dem Gerät anlegen oder aktualisieren. Befehlsformat:
CLI
Byte 0: 0x20
Byte 1: Kanal-Index (0-7)
Bytes 2-33: Kanalname (32 Bytes, UTF-8, mit Null-Bytes aufgefüllt)
Bytes 34-49: Geheimnis / Secret (16 Bytes)
Gesamtlänge: 50 Bytes Kanal-Index:
  • Index 0: Reserviert für öffentliche Kanäle (kein Secret)
  • Index 1-7: Verfügbar für private Kanäle
Kanalname:
  • UTF-8-kodiert
  • Maximal 32 Bytes
  • Kürzere Namen werden mit Null-Bytes (0x00) aufgefüllt
Secret-Feld (16 Bytes):
  • Für private Kanäle: 16-Byte-Geheimnis
  • Für öffentliche Kanäle: Nur Nullen (0x00)
Beispiel (Kanal „YourChannelName" an Index 1 mit Secret erstellen):
CLI
20 01 53 4D 53 00 00 ... (Name auf 32 Bytes aufgefüllt)
    [16 Bytes Secret]
Hinweis: Die 32-Byte-Secret-Variante wird nicht unterstützt und liefert PACKET_ERROR zurück. Antwort: PACKET_OK (0x00) bei Erfolg, PACKET_ERROR (0x01) bei Fehler

---

5. Send Channel Message

Zweck: Eine Textnachricht an einen Kanal senden. Befehlsformat:
CLI
Byte 0: 0x03
Byte 1: 0x00
Byte 2: Kanal-Index (0-7)
Bytes 3-6: Zeitstempel (32-Bit Little-Endian Unix-Zeitstempel, Sekunden)
Bytes 7+: Nachrichtentext (UTF-8, variable Länge)
Zeitstempel: Unix-Zeitstempel in Sekunden (32-Bit vorzeichenlose Ganzzahl, Little-Endian) Beispiel („Hello" an Kanal 1 mit Zeitstempel 1234567890 senden):
CLI
03 00 01 D2 02 96 49 48 65 6C 6C 6F
Antwort: PACKET_MSG_SENT (0x06) bei Erfolg

---

6. Send Channel Data Datagram

Zweck: Ein binäres Datagramm an einen Kanal senden. Im Gegensatz zu Kanal-Textnachrichten enthalten Datagramme keine eingebaute Absenderidentität und keinen Zeitstempel – falls benötigt, müssen Anwendungen beides innerhalb der binären Nutzlast selbst kodieren. Befehlsformat:
CLI
Byte 0:                         0x3E
Byte 1:                         Kanal-Index (0-7)
Byte 2:                         Pfadlänge (0xFF = Flood, ansonsten tatsächliche Pfadlänge)
Bytes 3 .. 2+path_len:          Pfad (entfällt wenn path_len == 0xFF)
Nächste 2 Bytes (Little-Endian): Datentyp (`data_type`, uint16)
Verbleibende Bytes:             Binäre Nutzlast (variable Länge)
Beispiel (Flood, DATA_TYPE_DEV, Nutzlast A1 B2 C3, Kanal 1):
CLI
3E 01 FF FF FF A1 B2 C3
Datentyp / Transport-Zuordnung:
  • 0x0000 (DATA_TYPE_RESERVED) ist ungültig und wird mit PACKET_ERROR abgelehnt.
  • 0xFFFF (DATA_TYPE_DEV) ist der Entwickler-Namensraum zum Experimentieren und Entwickeln von Apps.
  • Werte 0x00010xFFFE stehen für registrierte Anwendungs-/Community-Namensräume zur Verfügung. Siehe die Tabelle Registrierte data_type-Werte weiter unten.
Beschränkungen:
  • Die maximale Nutzlastlänge beträgt MAX_CHANNEL_DATA_LENGTH = MAX_FRAME_SIZE - 9 = 163 Bytes.
  • Größere Nutzlasten werden mit PACKET_ERROR (ERR_CODE_ILLEGAL_ARG) abgelehnt.
Antwort: PACKET_OK (0x00) bei Erfolg, oder PACKET_ERROR (0x01) mit einem der folgenden Fehlercodes:
  • ERR_CODE_NOT_FOUND (2) — unbekannter channel_idx
  • ERR_CODE_ILLEGAL_ARG (6) — ungültige path_len, reservierter data_type (0x0000) oder Nutzlast größer als MAX_CHANNEL_DATA_LENGTH
  • ERR_CODE_TABLE_FULL (3) — die Sendewarteschlange ist voll; später erneut versuchen
Eingehende Datagramme werden über RESP_CODE_CHANNEL_DATA_RECV (0x1B) an den Host weitergeleitet; siehe Empfang von Kanal-Datagrammen.

Registrierte data_type-Werte

data_type ist ein Anwendungsbezeichner, kein Nutzlastformat-Bezeichner. Jeder registrierte Wert identifiziert eine Anwendung, die ihre eigenen internen Nutzlast-Schemata verwaltet. Die Firmware untersucht den Nutzlastinhalt nicht – data_type wird transparent transportiert.
WertKonstanteZweck
0x0000DATA_TYPE_RESERVEDReserviert; beim Senden ungültig
0x0001 – 0x00FFReserviert für interne Verwendung
0x0100 – 0xFEFFRegistrierte Anwendungs-Namensräume (siehe number_allocations.md)
0xFF00 – 0xFFFETest/Entwicklung; keine Registrierung erforderlich
0xFFFFDATA_TYPE_DEVEntwickler-/Experimentier-Namensraum

Um eine neue Anwendung zu registrieren, reiche einen PR (Pull Request) ein, der eine Zeile zur Tabelle in docs/number_allocations.md hinzufügt. Interne Unterformate innerhalb einer zugewiesenen Application-ID werden von der jeweiligen Anwendung verwaltet und weder in der MeshCore-Firmware noch in diesem Dokument erfasst.

---

Empfang von Kanal-Datagrammen

Eingehende Gruppen-Datagramme (Funkebene PAYLOAD_TYPE_GRP_DATA, 0x06) werden als RESP_CODE_CHANNEL_DATA_RECV-Benachrichtigungen an den Host weitergeleitet.

Frame-Format (RESP_CODE_CHANNEL_DATA_RECV, 0x1B):
CLI
Byte 0:                 0x1B (Pakettyp)
Byte 1:                 SNR (vorzeichenbehaftetes int8, ×4 skaliert — durch 4.0 teilen für dB)
Bytes 2-3:              Reserviert (Clients MÜSSEN diese ignorieren)
Byte 4:                 Kanal-Index (0-7)
Byte 5:                 Pfadlänge (tatsächliche Pfadlänge bei Flood, andernfalls 0xFF für direkt)
Bytes 6-7:              Datentyp (uint16 Little-Endian)
Byte 8:                 Datenlänge
Bytes 9 .. 8+data_len:  Nutzlast
Pfad-Bytes werden nicht weitergeleitet: Nur path_len wird im Empfangs-Frame mitgeteilt – der Pfad selbst wird nicht an den Host kopiert. Es befinden sich keine Pfad-Bytes zwischen Byte 5 und dem data_type-Feld an den Bytes 6–7, unabhängig von path_len. Die Semantik der Pfadlänge unterscheidet sich zwischen Senden und Empfangen:
Richtungpath_len = 0xFFpath_len ≠ 0xFF
SendenNetzwerk fluten (Flood)Direkte Route; der kodierte Pfad folgt (untere 6 Bits = Hash-Anzahl, obere 2 Bits + 1 = Hash-Größe; Byte-Anzahl auf dem Funk = hash_count × hash_size)
EmpfangenPaket kam über direkte Route anPaket wurde geflutet; dies ist das kodierte pkt->path_len-Feld wie beobachtet (keine Pfad-Bytes folgen)

Anders ausgedrückt: Die Bedeutung von 0xFF ist zwischen den beiden Richtungen invertiert, und beim Empfangen enthält das Feld nur Metadaten – niemals einen routbaren Pfad. path_len ist ein kodiertes Byte (siehe Packet::isValidPathLen / Packet::writePath in src/Packet.cpp), kein roher Byte-Zähler.

Hinweis: Das Gerät kann auch PACKET_MESSAGES_WAITING (0x83) senden, um den Host darüber zu benachrichtigen, dass Datagramme in der Warteschlange stehen; mit CMD_SYNC_NEXT_MESSAGE (0x0A) abrufbar. Parsing-Pseudocode:
CLI
def parse_channel_data_recv(data):
    if len(data) < 9:
        return None
    snr_byte = data[1]
    snr = (snr_byte if snr_byte < 128 else snr_byte - 256) / 4.0
    channel_idx = data[4]
    path_len = data[5]
    data_type = int.from_bytes(data[6:8], 'little')
    data_len = data[8]
    if 9 + data_len > len(data):
        return None
    payload = data[9:9 + data_len]
    return {
        'snr': snr,
        'channel_idx': channel_idx,
        'path_len': path_len,
        'data_type': data_type,
        'payload': bytes(payload),
    }

---

7. Get Message

Zweck: Die nächste in der Warteschlange stehende Nachricht vom Gerät anfordern. Befehlsformat:
CLI
Byte 0: 0x0A
Beispiel (hex):
CLI
0A
Antwort:
  • PACKET_CHANNEL_MSG_RECV (0x08) oder PACKET_CHANNEL_MSG_RECV_V3 (0x11) für Kanalnachrichten
  • PACKET_CONTACT_MSG_RECV (0x07) oder PACKET_CONTACT_MSG_RECV_V3 (0x10) für Kontaktnachrichten
  • PACKET_CHANNEL_DATA_RECV (0x1B) für Kanal-Datagramme
  • PACKET_NO_MORE_MSGS (0x0A) wenn keine Nachrichten verfügbar sind
Hinweis: Diesen Befehl regelmäßig abfragen, um wartende Nachrichten abzurufen. Das Gerät kann auch PACKET_MESSAGES_WAITING (0x83) als Benachrichtigung senden, wenn Nachrichten verfügbar sind.

---

8. Get Battery and Storage

Zweck: Batteriespannung und Speichernutzung des Geräts abfragen. Befehlsformat:
CLI
Byte 0: 0x14
Beispiel (hex):
CLI
14
Antwort: PACKET_BATTERY (0x0C) mit Batterie-Millivolt und Speicherinformationen

---

Kanalverwaltung

Kanaltypen

  1. Öffentlicher Kanal
- Verwendet einen öffentlich bekannten 16-Byte-Schlüssel: 8b3387e9c5cdea6ac9e5edbaa115cd72 - Jeder kann diesem Kanal beitreten, Nachrichten sollten als öffentlich betrachtet werden - Wird als Standard-Gruppen-Chat für öffentliche Kommunikation verwendet
  1. Hashtag-Kanäle
- Verwenden einen aus dem Kanalnamen abgeleiteten geheimen Schlüssel - Dieser besteht aus den ersten 16 Bytes von sha256("#test") - Beispiel: Der Hashtag-Kanal #test hat den Schlüssel: 9cd8fcf22a47333b591d96a2b848b73f - Wird als themenbasierter öffentlicher Gruppen-Chat verwendet, getrennt vom Standard-Öffentlichkeitskanal
  1. Private Kanäle
- Verwenden einen zufällig generierten 16-Byte-Geheimschlüssel - Nachrichten sollten als privat betrachtet werden, nur zugänglich für jene, die das Secret kennen - Nutzer sollten den Schlüssel geheim halten und nur mit gewünschten Kommunikationspartnern teilen - Wird als sicherer, privater Gruppen-Chat verwendet

Kanal-Lebenszyklus

  1. Kanal setzen:
- Alle Kanal-Slots abrufen und einen mit leerem Namen und Null-Secret finden - Einen 16-Byte-Secret-Schlüssel erzeugen oder bereitstellen - CMD_SET_CHANNEL mit Name und 16-Byte-Secret senden
  1. Kanal abrufen:
- CMD_GET_CHANNEL mit dem Kanal-Index senden - Die RESP_CODE_CHANNEL_INFO-Antwort parsen
  1. Kanal löschen:
- CMD_SET_CHANNEL mit leerem Namen und Null-Secret senden - Oder mit einem neuen Kanal überschreiben

---

Nachrichtenverarbeitung

Nachrichten empfangen

Nachrichten werden über die TX Characteristic (Notifications) empfangen. Das Gerät sendet:

  1. Kanalnachrichten:
- PACKET_CHANNEL_MSG_RECV (0x08) – Standardformat - PACKET_CHANNEL_MSG_RECV_V3 (0x11) – Version 3 mit SNR (Signal-Rausch-Verhältnis)
  1. Kontaktnachrichten:
- PACKET_CONTACT_MSG_RECV (0x07) – Standardformat - PACKET_CONTACT_MSG_RECV_V3 (0x10) – Version 3 mit SNR
  1. Benachrichtigungen:
- PACKET_MESSAGES_WAITING (0x83) – Signalisiert, dass Nachrichten in der Warteschlange stehen

Kontaktnachricht-Format

Standardformat (PACKET_CONTACT_MSG_RECV, 0x07):
CLI
Byte 0: 0x07 (Pakettyp)
Bytes 1-6: Öffentlicher Schlüssel – Präfix (6 Bytes, hex)
Byte 7: Pfadlänge
Byte 8: Texttyp
Bytes 9-12: Zeitstempel (32-Bit Little-Endian)
Bytes 13-16: Signatur (4 Bytes, nur wenn txt_type == 2)
Bytes 17+: Nachrichtentext (UTF-8)
V3-Format (PACKET_CONTACT_MSG_RECV_V3, 0x10):
CLI
Byte 0: 0x10 (Pakettyp)
Byte 1: SNR (vorzeichenbehaftetes Byte, mit 4 multipliziert)
Bytes 2-3: Reserviert
Bytes 4-9: Öffentlicher Schlüssel – Präfix (6 Bytes, hex)
Byte 10: Pfadlänge
Byte 11: Texttyp
Bytes 12-15: Zeitstempel (32-Bit Little-Endian)
Bytes 16-19: Signatur (4 Bytes, nur wenn txt_type == 2)
Bytes 20+: Nachrichtentext (UTF-8)
Parsing-Pseudocode:
CLI
def parse_contact_message(data):
    packet_type = data[0]
    offset = 1
    
    # Prüfe auf V3-Format
    if packet_type == 0x10:  # V3
        snr_byte = data[offset]
        snr = ((snr_byte if snr_byte < 128 else snr_byte - 256) / 4.0)
        offset += 3  # SNR + Reserviert überspringen
    
    pubkey_prefix = data[offset:offset+6].hex()
    offset += 6
    
    path_len = data[offset]
    txt_type = data[offset + 1]
    offset += 2
    
    timestamp = int.from_bytes(data[offset:offset+4], 'little')
    offset += 4
    
    # Wenn txt_type == 2, 4-Byte-Signatur überspringen
    if txt_type == 2:
        offset += 4
    
    message = data[offset:].decode('utf-8')
    
    return {
        'pubkey_prefix': pubkey_prefix,
        'path_len': path_len,
        'txt_type': txt_type,
        'timestamp': timestamp,
        'message': message,
        'snr': snr if packet_type == 0x10 else None
    }

Kanalnachricht-Format

Standardformat (PACKET_CHANNEL_MSG_RECV, 0x08):
CLI
Byte 0: 0x08 (Pakettyp)
Byte 1: Kanal-Index (0-7)
Byte 2: Pfadlänge
Byte 3: Texttyp
Bytes 4-7: Zeitstempel (32-Bit Little-Endian)
Bytes 8+: Nachrichtentext (UTF-8)
V3-Format (PACKET_CHANNEL_MSG_RECV_V3, 0x11):
CLI
Byte 0: 0x11 (Pakettyp)
Byte 1: SNR (vorzeichenbehaftetes Byte, mit 4 multipliziert)
Bytes 2-3: Reserviert
Byte 4: Kanal-Index (0-7)
Byte 5: Pfadlänge
Byte 6: Texttyp
Bytes 7-10: Zeitstempel (32-Bit Little-Endian)
Bytes 11+: Nachrichtentext (UTF-8)
Parsing-Pseudocode:
CLI
def parse_channel_message(data):
    packet_type = data[0]
    offset = 1
    
    # Prüfe auf V3-Format
    if packet_type == 0x11:  # V3
        snr_byte = data[offset]
        snr = ((snr_byte if snr_byte < 128 else snr_byte - 256) / 4.0)
        offset += 3  # SNR + Reserviert überspringen
    
    channel_idx = data[offset]
    path_len = data[offset + 1]
    txt_type = data[offset + 2]
    timestamp = int.from_bytes(data[offset+3:offset+7], 'little')
    message = data[offset+7:].decode('utf-8')
    
    return {
        'channel_idx': channel_idx,
        'timestamp': timestamp,
        'message': message,
        'snr': snr if packet_type == 0x11 else None
    }

Nachrichten senden

Verwende den Befehl SEND_CHANNEL_MESSAGE (siehe Befehle).

Wichtig:
  • Nachrichten sind gemäß MeshCore-Spezifikation auf 133 Zeichen begrenzt
  • Längere Nachrichten sollten in Teile aufgeteilt werden
  • Einen Teilindikator einfügen (z. B. „[1/3] Nachrichtentext")
---

Antwort-Parsing

Terminologie

In diesem Dokument wird eine spezifikationsbezogene Namenskonvention (PACKET_*) für die Bytes verwendet, die die Firmware an den Host zurücksendet. Im Firmware-Quellcode sind dieselben Werte je nach Zweck auf zwei #define-Familien aufgeteilt:

  • RESP_CODE_* — direkte Antworten auf einen Befehl (z. B. RESP_CODE_CHANNEL_DATA_RECV = PACKET_CHANNEL_DATA_RECV = 0x1B).
  • PUSH_CODE_* — asynchrone Benachrichtigungen, die nicht an einen bestimmten Befehl gebunden sind (z. B. PUSH_CODE_MSG_WAITING = PACKET_MESSAGES_WAITING = 0x83).
Die Byte-Werte sind maßgeblich; die Namen sind Aliase. Beim Lesen des Firmware-Quellcodes entsprechen RESP_CODE_X / PUSH_CODE_X dem PACKET_X dieses Dokuments mit demselben numerischen Wert.

Pakettypen

WertNameBeschreibung
0x00PACKET_OKBefehl erfolgreich ausgeführt
0x01PACKET_ERRORBefehl fehlgeschlagen
0x02PACKET_CONTACT_STARTBeginn der Kontaktliste
0x03PACKET_CONTACTKontaktinformation
0x04PACKET_CONTACT_ENDEnde der Kontaktliste
0x05PACKET_SELF_INFOEigene Geräteinformationen
0x06PACKET_MSG_SENTBestätigung: Nachricht gesendet
0x07PACKET_CONTACT_MSG_RECVKontaktnachricht (Standard)
0x08PACKET_CHANNEL_MSG_RECVKanalnachricht (Standard)
0x09PACKET_CURRENT_TIMEAktuelle Uhrzeit-Antwort
0x0APACKET_NO_MORE_MSGSKeine weiteren Nachrichten verfügbar
0x0CPACKET_BATTERYBatteriestand
0x0DPACKET_DEVICE_INFOGeräteinformationen
0x10PACKET_CONTACT_MSG_RECV_V3Kontaktnachricht (V3 mit SNR)
0x11PACKET_CHANNEL_MSG_RECV_V3Kanalnachricht (V3 mit SNR)
0x12PACKET_CHANNEL_INFOKanalinformationen
0x1BPACKET_CHANNEL_DATA_RECVKanal-Datagramm
0x80PACKET_ADVERTISEMENTAdvertisement-Paket (Ankündigung)
0x82PACKET_ACKEmpfangsbestätigung
0x83PACKET_MESSAGES_WAITINGBenachrichtigung: Nachrichten warten
0x88PACKET_LOG_DATARF-Logdaten (kann ignoriert werden)

Antworten parsen

PACKET_OK (0x00):
CLI
Byte 0: 0x00
Bytes 1-4: Optionaler Wert (32-Bit Little-Endian Ganzzahl)
PACKET_ERROR (0x01):
CLI
Byte 0: 0x01
Byte 1: Fehlercode (optional)
PACKET_CHANNEL_INFO (0x12):
CLI
Byte 0: 0x12
Byte 1: Kanal-Index
Bytes 2-33: Kanalname (32 Bytes, null-terminiert)
Bytes 34-49: Secret (16 Bytes)
Hinweis: Das Gerät gibt in dieser Antwort das 16-Byte-Kanal-Secret zurück. PACKET_DEVICE_INFO (0x0D):
CLI
Byte 0: 0x0D
Byte 1: Firmware-Version (uint8)
Bytes 2+: Variable Länge, abhängig von der Firmware-Version

Für Firmware-Version >= 3:
Byte 2: Max Contacts Raw (uint8, tatsächlich = Wert * 2)
Byte 3: Max Channels (uint8)
Bytes 4-7: BLE-PIN (32-Bit Little-Endian)
Bytes 8-19: Firmware-Build (12 Bytes, UTF-8, mit Null aufgefüllt)
Bytes 20-59: Modell (40 Bytes, UTF-8, mit Null aufgefüllt)
Bytes 60-79: Version (20 Bytes, UTF-8, mit Null aufgefüllt)
Byte 80: Client-Repeat aktiviert/bevorzugt (Firmware v9+)
Byte 81: Pfad-Hash-Modus (Firmware v10+)
Parsing-Pseudocode:
CLI
def parse_device_info(data):
    if len(data) < 2:
        return None
    
    fw_ver = data[1]
    info = {'fw_ver': fw_ver}
    
    if fw_ver >= 3 and len(data) >= 80:
        info['max_contacts'] = data[2] * 2
        info['max_channels'] = data[3]
        info['ble_pin'] = int.from_bytes(data[4:8], 'little')
        info['fw_build'] = data[8:20].decode('utf-8').rstrip('\x00').strip()
        info['model'] = data[20:60].decode('utf-8').rstrip('\x00').strip()
        info['ver'] = data[60:80].decode('utf-8').rstrip('\x00').strip()
    
    return info
PACKET_BATTERY (0x0C):
CLI
Byte 0: 0x0C
Bytes 1-2: Batteriespannung (16-Bit Little-Endian, Millivolt)
Bytes 3-6: Belegter Speicher (32-Bit Little-Endian, KB)
Bytes 7-10: Gesamtspeicher (32-Bit Little-Endian, KB)
Parsing-Pseudocode:
CLI
def parse_battery(data):
    if len(data) < 3:
        return None
    
    mv = int.from_bytes(data[1:3], 'little')
    info = {'battery_mv': mv}
    
    if len(data) >= 11:
        info['used_kb'] = int.from_bytes(data[3:7], 'little')
        info['total_kb'] = int.from_bytes(data[7:11], 'little')
    
    return info
PACKET_SELF_INFO (0x05):
CLI
Byte 0: 0x05
Byte 1: Advertisement-Typ
Byte 2: TX-Leistung (Sendeleistung)
Byte 3: Maximale TX-Leistung
Bytes 4-35: Öffentlicher Schlüssel (32 Bytes, hex)
Bytes 36-39: Advertisement-Breitengrad (32-Bit Little-Endian, geteilt durch 1e6)
Bytes 40-43: Advertisement-Längengrad (32-Bit Little-Endian, geteilt durch 1e6)
Byte 44: Multi-ACKs
Byte 45: Advertisement-Standortrichtlinie
Byte 46: Telemetrie-Modus (Bitfeld)
Byte 47: Kontakte manuell hinzufügen (bool)
Bytes 48-51: Funkfrequenz (32-Bit Little-Endian, geteilt durch 1000.0)
Bytes 52-55: Funkbandbreite (32-Bit Little-Endian, geteilt durch 1000.0)
Byte 56: Funk-Spreizfaktor (Spreading Factor)
Byte 57: Funk-Codierungsrate (Coding Rate)
Bytes 58+: Gerätename (UTF-8, variable Länge, Null-Terminierung nicht erforderlich)
Parsing-Pseudocode:
CLI
def parse_self_info(data):
    if len(data) < 36:
        return None
    
    offset = 1
    info = {
        'adv_type': data[offset],
        'tx_power': data[offset + 1],
        'max_tx_power': data[offset + 2],
        'public_key': data[offset + 3:offset + 35].hex()
    }
    offset += 35
    
    lat = int.from_bytes(data[offset:offset+4], 'little') / 1e6
    lon = int.from_bytes(data[offset+4:offset+8], 'little') / 1e6
    info['adv_lat'] = lat
    info['adv_lon'] = lon
    offset += 8
    
    info['multi_acks'] = data[offset]
    info['adv_loc_policy'] = data[offset + 1]
    telemetry_mode = data[offset + 2]
    info['telemetry_mode_env'] = (telemetry_mode >> 4) & 0b11
    info['telemetry_mode_loc'] = (telemetry_mode >> 2) & 0b11
    info['telemetry_mode_base'] = telemetry_mode & 0b11
    info['manual_add_contacts'] = data[offset + 3] > 0
    offset += 4
    
    freq = int.from_bytes(data[offset:offset+4], 'little') / 1000.0
    bw = int.from_bytes(data[offset+4:offset+8], 'little') / 1000.0
    info['radio_freq'] = freq
    info['radio_bw'] = bw
    info['radio_sf'] = data[offset + 8]
    info['radio_cr'] = data[offset + 9]
    offset += 10
    
    if offset < len(data):
        name_bytes = data[offset:]
        info['name'] = name_bytes.decode('utf-8').rstrip('\x00').strip()
    
    return info
PACKET_MSG_SENT (0x06):
CLI
Byte 0: 0x06
Byte 1: Route-Flag (0 = direkt, 1 = Flood)
Bytes 2-5: Tag / Erwartetes ACK (4 Bytes, Little-Endian)
Bytes 6-9: Vorgeschlagener Timeout (32-Bit Little-Endian, Millisekunden)
PACKET_ACK (0x82):
CLI
Byte 0: 0x82
Bytes 1-6: ACK-Code (6 Bytes, hex)

Fehlercodes

PACKET_ERROR (0x01) enthält in Byte 1 einen einzelnen Fehlercode-Byte. Die Werte entsprechen den ERR_CODE_*-Konstanten aus examples/companion_radio/MyMesh.cpp:
CodeKonstante (Firmware)Beschreibung
1ERR_CODE_UNSUPPORTED_CMDUnbekanntes oder nicht unterstütztes Befehls-Byte / Unterbefehl
2ERR_CODE_NOT_FOUNDZiel nicht gefunden (Kanal, Kontakt, Nachricht usw.)
3ERR_CODE_TABLE_FULLInterne Warteschlange oder Tabelle ist voll — später erneut versuchen
4ERR_CODE_BAD_STATEOperation im aktuellen Gerätezustand nicht zulässig (z. B. Iterator läuft bereits)
5ERR_CODE_FILE_IO_ERRORDateisystem- oder Speicher-E/A-Fehler
6ERR_CODE_ILLEGAL_ARGUngültiges Argument (falsche Länge, Wert außerhalb des Bereichs, reserviertes Feld usw.)
Hinweis: Fehlercodes können je nach Firmware-Version variieren. Prüfe stets Byte 1 der PACKET_ERROR-Antwort und behandle unbekannte Codes als generische Fehler.

Frame-Verarbeitung

BLE-Implementierungen reihen auf Firmware-Ebene je einen Protokoll-Frame pro BLE-Schreib-/Notification-Vorgang ein und liefern ihn aus.

  • Apps sollten jede Characteristic-Write/Notification als exakt einen Companion-Protokoll-Frame behandeln
  • Apps sollten dennoch Frame-Längen vor dem Parsen validieren
  • Zukünftige Transporte oder Firmware-Revisionen können abweichen – verlasse dich daher nicht auf feste Nutzlastgrößen bei Antworten variabler Länge

Antwortverarbeitung

  1. Befehls-Antwort-Muster:
- Befehl über RX Characteristic senden - Auf Antwort über TX Characteristic warten (Notification) - Antwort dem Befehl über Sequenznummern oder Befehlstyp zuordnen - Timeout behandeln (typischerweise 5 Sekunden) - Befehls-Warteschlange verwenden, um gleichzeitige Befehle zu vermeiden
  1. Asynchrone Nachrichten:
- Das Gerät kann jederzeit Nachrichten über die TX Characteristic senden - Bei PACKET_MESSAGES_WAITING (0x83) den GET_MESSAGE-Befehl abfragen - Eingehende Nachrichten parsen und an die zuständigen Handler weiterleiten - Frame-Länge vor dem Dekodieren validieren
  1. Antwort-Zuordnung:
- Antworten den Befehlen über den erwarteten Pakettyp zuordnen: - APP_STARTPACKET_SELF_INFO - DEVICE_QUERYPACKET_DEVICE_INFO - GET_CHANNELPACKET_CHANNEL_INFO - SET_CHANNELPACKET_OK oder PACKET_ERROR - SEND_CHANNEL_MESSAGEPACKET_MSG_SENT - GET_MESSAGEPACKET_CHANNEL_MSG_RECV, PACKET_CONTACT_MSG_RECV, PACKET_CHANNEL_DATA_RECV oder PACKET_NO_MORE_MSGS - SEND_CHANNEL_DATAPACKET_OK oder PACKET_ERROR - GET_BATTERYPACKET_BATTERY
  1. Timeout-Behandlung:
- Standard-Timeout: 5 Sekunden pro Befehl - Bei Timeout: Fehler protokollieren, aktuellen Befehl verwerfen, mit dem nächsten in der Warteschlange fortfahren - Manche Befehle können länger dauern (z. B. SET_CHANNEL kann 1–2 Sekunden benötigen) - Für Kanaloperationen einen längeren Timeout in Betracht ziehen
  1. Fehlerwiederherstellung:
- Bei PACKET_ERROR: Fehlercode protokollieren, aktuellen Befehl verwerfen - Bei Verbindungsverlust: Befehls-Warteschlange leeren, Wiederverbindung versuchen - Bei ungültiger Antwort: Warnung protokollieren, aktuellen Befehl verwerfen, fortfahren

---

Beispielhafte Implementierung

Initialisierung

CLI
# 1. Nach MeshCore-Gerät suchen
device = scan_for_device("MeshCore")

# 2. BLE-GATT-Verbindung herstellen
gatt = connect_to_device(device)

# 3. Services und Characteristics ermitteln
service = discover_service(gatt, "6E400001-B5A3-F393-E0A9-E50E24DCCA9E")
rx_char = discover_characteristic(service, "6E400002-B5A3-F393-E0A9-E50E24DCCA9E")
tx_char = discover_characteristic(service, "6E400003-B5A3-F393-E0A9-E50E24DCCA9E")

# 4. Notifications auf TX Characteristic aktivieren
enable_notifications(tx_char, on_notification_received)

# 5. AppStart-Befehl senden
send_command(rx_char, build_app_start())
wait_for_response(PACKET_SELF_INFO)

Privaten Kanal erstellen

CLI
# 1. 16-Byte-Secret erzeugen
secret_16_bytes = generate_secret(16)  # CSPRNG verwenden
secret_hex = secret_16_bytes.hex()

# 2. SET_CHANNEL-Befehl aufbauen
channel_name = "YourChannelName"
channel_index = 1  # 1-7 für private Kanäle verwenden
command = build_set_channel(channel_index, channel_name, secret_16_bytes)

# 3. Befehl senden
send_command(rx_char, command)
response = wait_for_response(PACKET_OK)

# 4. Secret lokal speichern
store_channel_secret(channel_index, secret_hex)

Nachricht senden

CLI
# 1. Kanalnachrichten-Befehl aufbauen
channel_index = 1
message = "Hello, MeshCore!"
timestamp = int(time.time())
command = build_channel_message(channel_index, message, timestamp)

# 2. Befehl senden
send_command(rx_char, command)
response = wait_for_response(PACKET_MSG_SENT)

Nachrichten empfangen

CLI
def on_notification_received(data):
    packet_type = data[0]
    
    if packet_type == PACKET_CHANNEL_MSG_RECV or packet_type == PACKET_CHANNEL_MSG_RECV_V3:
        message = parse_channel_message(data)
        handle_channel_message(message)
    elif packet_type == PACKET_MESSAGES_WAITING:
        # Nachrichten abfragen
        send_command(rx_char, build_get_message())

---

Best Practices

  1. Verbindungsmanagement:
- Automatische Wiederverbindung mit exponentiellem Backoff implementieren - Verbindungsabbrüche ordnungsgemäß behandeln - Die Adresse des zuletzt verbundenen Geräts für schnelle Wiederverbindung speichern
  1. Secret-Management:
- Immer kryptographisch sichere Zufallszahlengeneratoren verwenden - Secrets sicher speichern (verschlüsselter Speicher) - Secrets niemals im Klartext protokollieren oder übertragen
  1. Nachrichtenverarbeitung:
- CMD_SYNC_NEXT_MESSAGE senden, wenn PUSH_CODE_MSG_WAITING empfangen wird - Nachrichten-Deduplizierung implementieren, um die doppelte Anzeige derselben Nachricht zu vermeiden
  1. Kanalverwaltung:
- Alle Kanal-Slots abrufen, auch wenn ein leerer Slot angetroffen wird - Neue Kanäle idealerweise im ersten leeren Slot speichern
  1. Fehlerbehandlung:
- Timeouts für alle Befehle implementieren (typischerweise 5 Sekunden) - RESP_CODE_ERR-Antworten angemessen behandeln

---

Fehlerbehebung

Verbindungsprobleme

  • Gerät nicht gefunden: Sicherstellen, dass das Gerät eingeschaltet ist und sendet (Advertising)
  • Verbindungs-Timeout: Bluetooth-Berechtigungen und Gerätenähe prüfen
  • GATT-Fehler: Korrekte Service-/Characteristic-Erkennung sicherstellen

Befehlsprobleme

  • Keine Antwort: Prüfen, ob Notifications aktiviert sind und die Verbindung besteht
  • Fehlerantworten: Befehlsformat überprüfen und Fehlercode auswerten
  • Timeout: Timeout-Wert erhöhen oder erneut versuchen

Nachrichtenprobleme

  • Nachrichten werden nicht empfangen: Den GET_MESSAGE-Befehl regelmäßig abfragen
  • Doppelte Nachrichten: Nachrichten-Deduplizierung implementieren, z. B. anhand von Zeitstempel/Inhalt als eindeutiger Kennung
  • Abgeschnittene Nachrichten: Lange Nachrichten als separate, kürzere Nachrichten senden

Quelle: docs.meshcore.io