EN

Companion-Protokoll

Companion-Protokoll

  • Zuletzt aktualisiert: 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).

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

Offizielle Bibliotheken

Bitte beachten Sie die folgenden Repositories für bestehende Companion-Protokoll-Bibliotheken von MeshCore:

Wichtiger Sicherheitshinweis

Alle in dieser Anleitung gezeigten Geheimnisse (Secrets), Hashes und kryptografischen Werte sind nur Beispielwerte.

  • Alle Hexadezimalwerte, öffentlichen Schlüssel (Public Keys) und Hashes dienen nur zu Demonstrationszwecken.
  • Verwenden Sie niemals Beispielgeheimnisse in der Produktion.
  • Generieren Sie immer neue kryptografisch sichere Zufallsgeheimnisse.
  • Bitte implementieren Sie geeignete Sicherheitsverfahren in Ihrer Implementierung.
  • Diese Anleitung dient ausschließlich der Protokolldokumentation.

Inhaltsverzeichnis

  1. BLE-Verbindung
  2. Paketstruktur
  3. Befehle
  4. Kanalverwaltung
  5. Nachrichtenverarbeitung
  6. Antwort-Parsing
  7. Beispielhafter Implementierungsablauf
  8. Bewährte Verfahren
  9. Fehlerbehebung
---

BLE-Verbindung

Dienst (Service) und Eigenschaften (Characteristics)

MeshCore Companion-Geräte stellen einen BLE-Dienst mit den folgenden UUIDs (Universally Unique Identifiers) zur Verfügung:

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

Verbindungsschritte

  1. Nach Geräten suchen
- Suchen Sie nach BLE-Geräten, die die MeshCore-Dienst-UUID bewerben. - Filtern Sie optional nach Gerätenamen (enthält typischerweise das Präfix "MeshCore"). - Notieren Sie die MAC-Adresse des Geräts für die erneute Verbindung.
  1. Mit GATT verbinden
- Stellen Sie eine Verbindung zum Gerät über die ermittelte MAC-Adresse her. - Warten Sie, bis die Verbindung hergestellt ist.
  1. Dienste und Eigenschaften entdecken
- Entdecken Sie den Dienst mit der UUID 6E400001-B5A3-F393-E0A9-E50E24DCCA9E. - Entdecken Sie die RX-Eigenschaft 6E400002-B5A3-F393-E0A9-E50E24DCCA9E. - Ihre App schreibt hierhin, die Firmware liest von hier. - Entdecken Sie die TX-Eigenschaft 6E400003-B5A3-F393-E0A9-E50E24DCCA9E. - Die Firmware schreibt hierhin, Ihre App liest von hier.
  1. Benachrichtigungen aktivieren
- Abonnieren Sie Benachrichtigungen für die TX-Eigenschaft, um Daten von der Firmware zu empfangen.
  1. Erste Befehle senden
- Senden Sie CMD_APP_START, um Ihre App bei der Firmware zu identifizieren und Funk-Einstellungen zu erhalten. - Senden Sie CMD_DEVICE_QUERY, um Geräteinformationen abzurufen und unterstützte Protokollversionen auszuhandeln. - Senden Sie CMD_SET_DEVICE_TIME, um die Firmware-Uhr einzustellen. - Senden Sie CMD_GET_CONTACTS, um alle Kontakte abzurufen. - Senden Sie CMD_GET_CHANNEL mehrmals, um alle Kanal-Slots abzurufen. - Senden Sie CMD_SYNC_NEXT_MESSAGE, um die nächste in der Firmware gespeicherte Nachricht abzurufen. - Richten Sie Listener für Push-Codes ein, wie PUSH_CODE_MSG_WAITING oder PUSH_CODE_ADVERT. - Im Abschnitt Befehle finden Sie Informationen zu anderen Befehlen. Hinweis: MeshCore-Geräte können sich nach Inaktivitätsperioden trennen. Implementieren Sie eine automatische Wiederverbindungslogik mit exponentiellem Backoff (wiederholte Versuche mit zunehmenden Wartezeiten).

BLE-Schreibtyp

Beim Schreiben von Befehlen an die RX-Eigenschaft geben Sie den Schreibtyp an:

  • Schreiben mit Antwort (Standard): Wartet auf eine Bestätigung vom Gerät.
  • Schreiben ohne Antwort: Schneller, aber ohne Bestätigung.
Plattformspezifisch:
  • Android: Verwenden Sie BluetoothGattCharacteristic.WRITE_TYPE_DEFAULT oder WRITE_TYPE_NO_RESPONSE.
  • iOS: Verwenden Sie CBCharacteristicWriteType.withResponse oder .withoutResponse.
  • Python (bleak): Verwenden Sie write_gatt_char() mit response=True oder False.
Empfehlung: Verwenden Sie für Zuverlässigkeit "Schreiben mit Antwort".

MTU (Maximum Transmission Unit)

Die Standard-BLE-MTU beträgt 23 Bytes (20 Bytes Nutzdaten). Für größere Befehle wie SET_CHANNEL (50 Bytes) müssen Sie möglicherweise:

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

Befehlssequenzierung

Kritisch: Befehle müssen in der richtigen Reihenfolge gesendet werden:
  1. Nach der Verbindung:
- Warten Sie, bis die BLE-Verbindung hergestellt ist. - Warten Sie, bis Dienste/Eigenschaften entdeckt wurden. - Warten Sie, bis Benachrichtigungen aktiviert sind. - Jetzt können Sie Befehle sicher an die Firmware senden.
  1. Befehl-Antwort-Abgleich:
- Senden Sie immer nur einen Befehl. - Warten Sie auf eine Antwort, bevor Sie einen weiteren Befehl senden. - Verwenden Sie einen Timeout (typischerweise 5 Sekunden). - Gleichen Sie die Antwort mit dem Befehl nach Typ ab (z.B. CMD_GET_CHANNEL → RESP_CODE_CHANNEL_INFO).

Befehlswarteschlangenverwaltung

Für einen zuverlässigen Betrieb implementieren Sie eine Befehlswarteschlange.

Warteschlangenstruktur:
  • Verwalten Sie eine Warteschlange von ausstehenden Befehlen.
  • Verfolgen Sie, welcher Befehl derzeit auf eine Antwort wartet.
  • Senden Sie den nächsten Befehl erst nach Erhalt der Antwort oder nach einem Timeout.
Fehlerbehandlung:
  • Bei Timeout: Aktuellen Befehl löschen, nächsten in der Warteschlange verarbeiten.
  • Bei Fehler: Fehler protokollieren, aktuellen Befehl löschen, nächsten verarbeiten.
---

Paketstruktur

Das MeshCore-Protokoll verwendet ein Binärformat mit der folgenden Struktur:

  • Befehle: Von der App an die Firmware über die RX-Eigenschaft gesendet.
  • Antworten: Von der Firmware über TX-Eigenschaftsbenachrichtigungen empfangen.
  • Alle Mehrbyte-Integer: Little-Endian-Byte-Reihenfolge (außer CayenneLPP, das Big-Endian ist).
  • Alle Zeichenketten: 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: Initialisiert die Kommunikation mit dem Gerät. Muss als erstes nach der Verbindung gesendet werden. Befehlsformat:
CLI
Byte 0: 0x01
Bytes 1-7: Reserviert (wird derzeit von der Firmware ignoriert)
Bytes 8+: Anwendungsname (UTF-8, optional)
Beispiel (hexadezimal):
CLI
01 00 00 00 00 00 00 00 6d 63 63 6c 69
Antwort: PACKET_SELF_INFO (0x05)

---

2. Geräteabfrage

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

---

3. Kanalinformationen abrufen

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

---

4. Kanal einstellen

Zweck: Erstellen oder Aktualisieren eines Kanals auf dem Gerät. Befehlsformat:
CLI
Byte 0: 0x20
Byte 1: Kanalindex (0 .. max_channels-1)
Bytes 2-33: Kanalname (32 Bytes, UTF-8, mit Null-Bytes aufgefüllt)
Bytes 34-49: Geheimnis (16 Bytes)
Gesamtlänge: 50 Bytes Kanalindex:
  • Gültiger Bereich ist 0 bis max_channels - 1. Lesen Sie max_channels aus Byte 3 der
PACKET_DEVICE_INFO-Antwort; gehen Sie nicht von 8 aus. Builds werden mit MAX_GROUP_CHANNELS von 1, 8 oder 40 geliefert, je nach Platine.
  • Index 0: Von der Firmware mit dem integrierten "Public"-Kanal vorab belegt.
Er kann wie jeder andere Slot überschrieben werden.
  • Ein Index außerhalb des Bereichs wird mit PACKET_ERROR / ERR_CODE_NOT_FOUND abgelehnt.
Kanalname:
  • UTF-8-kodiert
  • Maximal 32 Bytes
  • Mit Null-Bytes (0x00) aufgefüllt, falls kürzer
Geheimnis-Feld (16 Bytes):
  • Immer ein echter 16-Byte-Schlüssel; die Firmware leitet den Kanal-Hash von
SHA256(secret) ab (BaseChatMesh.cpp:936-942). Es gibt keine "kein Geheimnis"-Form.
  • Schreiben Sie kein Geheimnis, das nur aus Null-Bytes besteht. SHA256 über 16 Null-Bytes ist ein fester,
global identischer Wert, daher ist ein Null-Geheimnis-Kanal ein bekannter Kanal mit einem AES-Schlüssel, der nur aus Null-Bytes besteht, den jeder Knoten lesen kann – kein privater. Die Firmware überspringt *unbenannte* Slots beim Abgleich von eingehendem Gruppenverkehr aus genau diesem Grund (BaseChatMesh.cpp:392-401), aber ein Slot, der einen Namen und ein Null-Geheimnis erhalten hat, wird nicht übersprungen und wird Gruppenverkehr mit Null-Schlüssel von jedem Knoten im Mesh absorbieren.
  • Der integrierte öffentliche Kanal verwendet den bekannten Schlüssel
izOH6cXN6mrJ5e26oRXNcg== (base64), d.h. 8b3387e9c5cdea6ac9e5edbaa115cd72 in Hex. Beispiel (Kanal "YourChannelName" mit Geheimnis an Index 1 erstellen):
CLI
20 01 53 4D 53 00 00 ... (Name auf 32 Bytes aufgefüllt)
    [16 Bytes Geheimnis]
Hinweis: Die 32-Byte-Geheimnis-Variante wird nicht unterstützt und gibt PACKET_ERROR zurück. Antwort: PACKET_OK (0x00) bei Erfolg, PACKET_ERROR (0x01) bei Fehler.

---

5. Kanalnachricht senden

Zweck: Eine Textnachricht an einen Kanal senden. Befehlsformat:
CLI
Byte 0: 0x03
Byte 1: 0x00
Byte 2: Kanalindex (0 .. max_channels-1)
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 zum Zeitstempel 1234567890 senden):
CLI
03 00 01 D2 02 96 49 48 65 6C 6C 6F
Antwort: PACKET_MSG_SENT (0x06) bei Erfolg.

---

6. Kanal-Datagramm senden

Zweck: Ein binäres Datagramm an einen Kanal senden. Im Gegensatz zu Kanal-Textnachrichten tragen Datagramme keine eingebaute Absenderidentität und keinen Zeitstempel – Anwendungen, die diese benötigen, müssen sie im binären Payload kodieren. Befehlsformat:
CLI
Byte 0:                         0x3E
Byte 1:                         Kanalindex (0 .. max_channels-1)
Byte 2:                         Pfadlänge (0xFF = Flood, sonst tatsächliche Pfadlänge)
Bytes 3 .. 2+Pfadlänge:          Pfad (entfällt, wenn Pfadlänge == 0xFF)
Nächste 2 Bytes (Little-Endian): Datentyp (`data_type`, uint16)
Verbleibende Bytes:             Binärer Payload (variable Länge)
Beispiel (Flood, DATA_TYPE_DEV, Payload A1 B2 C3, Kanal 1):
CLI
3E 01 FF FF FF A1 B2 C3
Datentyp / Transport-Mapping:
  • 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 0x0001–0xFFFE sind für registrierte Anwendungs-/Community-Namensräume verfügbar. Siehe die Tabelle Registrierte data_type-Werte unten.
Grenzwerte:
  • Die maximale Payload-Länge beträgt 165 Bytes — `MAX_GROUP_DATA_LENGTH =
MAX_PACKET_PAYLOAD - CIPHER_BLOCK_SIZE - 3 (src/MeshCore.h:21`). Dies ist die Funk-seitige Begrenzung und die, die Clients durchsetzen sollten.
  • Zwei verschiedene Grenzen werden überprüft, und sie stimmen nicht überein. Die Host-Frame-Prüfung
verwendet das größere MAX_CHANNEL_DATA_LENGTH = MAX_FRAME_SIZE - 9 = 167 (companion_radio/MyMesh.cpp:1265); die Funk-seitige Prüfung verwendet 165 (BaseChatMesh.cpp:544).
  • Payloads über 167 werden mit ERR_CODE_ILLEGAL_ARG (6) abgelehnt.
  • Payloads von 166 oder 167 bestehen die Frame-Prüfung, schlagen dann aber bei der Funk-seitigen
Prüfung fehl und geben ERR_CODE_TABLE_FULL (3) zurück. Trotz der üblichen Bedeutung dieses Codes kann ein solcher Sendevorgang niemals erfolgreich sein — versuchen Sie es nicht erneut. Halten Sie Payloads bei 165 Bytes oder weniger, und dieser Fall kann nicht auftreten. Antwort: PACKET_OK (0x00) bei Erfolg, oder PACKET_ERROR (0x01) mit einem der folgenden Fehler:
  • ERR_CODE_NOT_FOUND (2) — unbekannter channel_idx
  • ERR_CODE_ILLEGAL_ARG (6) — ungültige path_len, reservierter data_type (0x0000), oder Payload größer als MAX_CHANNEL_DATA_LENGTH (167)
  • ERR_CODE_TABLE_FULL (3) — die Warteschlange für ausgehende Nachrichten ist voll (später erneut versuchen), oder ein Payload von 166-167 Bytes, der die Funk-seitige Begrenzung überschreitet (permanent; siehe Grenzwerte oben)
Eingehende Datagramme werden dem Host über RESP_CODE_CHANNEL_DATA_RECV (0x1B) zugestellt; siehe Kanal-Datagramm empfangen.

Registrierte data_type-Werte

data_type ist ein Anwendungsbezeichner, kein Payload-Formatbezeichner. Jeder registrierte Wert identifiziert eine Anwendung, die ihre eigenen internen Payload-Schemas besitzt. Die Firmware inspiziert den Payload-Inhalt nicht — data_type wird undurchsichtig transportiert.
WertKonstanteZweck
0x0000DATA_TYPE_RESERVEDReserviert; ungültig beim Senden
0x0001 – 0x00FF—Reserviert für interne Nutzung
0x0100 – 0xFEFF—Registrierte Anwendungs-Namensräume (siehe nummern-zuteilungen)
0xFF00 – 0xFFFE—Testen/Entwicklung; keine Registrierung erforderlich
0xFFFFDATA_TYPE_DEVEntwickler-/Experimentier-Namensraum

Um eine neue Anwendung zu registrieren, reichen Sie einen PR (Pull Request) ein, der eine Zeile zur Tabelle in docs/number_allocations.md hinzufügt. Interne Unterformate innerhalb einer zugewiesenen Anwendungs-ID gehören dieser Anwendung und werden weder in der MeshCore-Firmware noch in diesem Dokument verfolgt.

---

Kanal-Datagramm empfangen

Eingehende Gruppendatagramme (Funk-Ebene 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 (Signed int8, skaliert ×4 — durch 4.0 teilen, um dB zu erhalten)
Bytes 2-3:              Reserviert (Clients MÜSSEN ignorieren)
Byte 4:                 Kanalindex (0 .. max_channels-1)
Byte 5:                 Pfadlänge (tatsächliche Pfadlänge bei Flood, sonst 0xFF für direkt)
Bytes 6-7:              Datentyp (uint16 Little-Endian)
Byte 8:                 Datenlänge
Bytes 9 .. 8+Datenlänge:  Payload
Pfad-Bytes werden nicht weitergeleitet: Nur path_len wird im Empfangs-Frame gemeldet – der Pfad selbst wird nicht auf den Host kopiert. Es gibt keine Pfad-Bytes zwischen Byte 5 und dem data_type-Feld bei Bytes 6–7, unabhängig von path_len. Semantik der Pfadlänge unterscheidet sich zwischen Senden und Empfangen:
Richtungpath_len = 0xFFpath_len ≠ 0xFF
SendenNetzwerk flutenDirekter Weg; der kodierte Pfad folgt (niedrige 6 Bits = Hash-Anzahl, obere 2 Bits + 1 = Hash-Größe; On-Wire-Byte-Anzahl = hash_count × hash_size)
EmpfangenPaket kam über direkten Weg anPaket wurde geflutet; dies ist das kodierte pkt->path_len Feld wie beobachtet (keine Pfad-Bytes folgen)

Mit anderen Worten, die Bedeutung von 0xFF ist zwischen den beiden Richtungen invertiert, und beim Empfang trägt das Feld nur Metadaten – niemals einen routbaren Pfad. path_len ist ein kodiertes Byte (siehe Packet::isValidPathLen / Packet::writePath in src/Packet.cpp), keine rohe Byte-Anzahl.

Hinweis: Das Gerät kann auch PACKET_MESSAGES_WAITING (0x83) ausgeben, um den Host darüber zu informieren, dass Datagramme in der Warteschlange liegen; fragen Sie mit CMD_SYNC_NEXT_MESSAGE (0x0A) ab, um sie abzurufen. 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. Nachricht abrufen

Zweck: Die nächste in der Warteschlange befindliche Nachricht vom Gerät anfordern. Befehlsformat:
CLI
Byte 0: 0x0A
Beispiel (hexadezimal):
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: Fragen Sie diesen Befehl regelmäßig ab, um in der Warteschlange befindliche Nachrichten abzurufen. Das Gerät kann auch PACKET_MESSAGES_WAITING (0x83) als Benachrichtigung senden, wenn Nachrichten verfügbar sind.

---

8. Batterie und Speicher abrufen

Zweck: Batteriespannung und Speichernutzung des Geräts abfragen. Befehlsformat:
CLI
Byte 0: 0x14
Beispiel (hexadezimal):
CLI
14
Antwort: PACKET_BATTERY (0x0C) mit Batteriespannung in 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-Gruppenchat verwendet.
  1. Hashtag-Kanäle
- Verwendet einen geheimen Schlüssel, der vom Kanalnamen abgeleitet wird. - Dies sind die ersten 16 Bytes von sha256("#test"). - Zum Beispiel hat der Hashtag-Kanal #test den Schlüssel: 9cd8fcf22a47333b591d96a2b848b73f. - Der Verkehr ist in der Luft verschlüsselt, aber jeder, der den Kanalnamen kennt oder errät, kann den Schlüssel ableiten. Hashtag-Kanäle sollten nicht als privat behandelt werden. - Wird als themenbasierter öffentlicher Gruppenchat verwendet, getrennt vom Standard-Öffentlichen Kanal.
  1. Private Kanäle
- Verwendet einen zufällig generierten 16-Byte-Geheimschlüssel. - Nachrichten sollten als privat zwischen denjenigen betrachtet werden, die das Geheimnis kennen. - Benutzer sollten den Schlüssel geheim halten und nur mit denen teilen, mit denen sie kommunizieren möchten. - Wird als sicherer privater Gruppenchat verwendet.

Kanal-Lebenszyklus

  1. Kanal einstellen:
- Alle Kanal-Slots abrufen und einen mit leerem Namen und einem Geheimnis, das nur aus Null-Bytes besteht, finden. - Ein 16-Byte-Geheimnis generieren oder bereitstellen. - CMD_SET_CHANNEL mit Namen und einem 16-Byte-Geheimnis senden.
  1. Kanal abrufen:
- CMD_GET_CHANNEL mit Kanalindex senden. - RESP_CODE_CHANNEL_INFO-Antwort parsen.
  1. Kanal löschen:
- CMD_SET_CHANNEL mit leerem Namen und einem Geheimnis, das nur aus Null-Bytes besteht, senden. - Oder mit einem neuen Kanal überschreiben.

---

Nachrichtenverarbeitung

Nachrichten empfangen

Nachrichten werden über die TX-Eigenschaft (Benachrichtigungen) 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) – Zeigt an, dass Nachrichten in der Warteschlange liegen

Kontaktnachrichtenformat

Standardformat (PACKET_CONTACT_MSG_RECV, 0x07):
CLI
Byte 0: 0x07 (Pakettyp)
Bytes 1-6: Public Key Präfix (6 Bytes, hexadezimal)
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 (Signed Byte, multipliziert mit 4)
Bytes 2-3: Reserviert
Bytes 4-9: Public Key Präfix (6 Bytes, hexadezimal)
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
    
    # Auf V3-Format prüfen
    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
    }

Kanalnachrichtenformat

Standardformat (PACKET_CHANNEL_MSG_RECV, 0x08):
CLI
Byte 0: 0x08 (Pakettyp)
Byte 1: Kanalindex (0 .. max_channels-1)
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 (Signed Byte, multipliziert mit 4)
Bytes 2-3: Reserviert
Byte 4: Kanalindex (0 .. max_channels-1)
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
    
    # Auf V3-Format prüfen
    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

Verwenden Sie den SEND_CHANNEL_MESSAGE-Befehl (siehe Befehle).

Wichtig:
  • Nachrichten sind auf 133 Zeichen gemäß MeshCore-Spezifikation begrenzt.
  • Lange Nachrichten sollten in Teile (Chunks) aufgeteilt werden.
  • Fügen Sie einen Chunk-Indikator hinzu (z.B. "[1/3] Nachrichtentext").
---

Antwort-Parsing

Terminologie

Dieses Dokument verwendet eine spezifikationsgetreue Namenskonvention (PACKET_*) für Bytes, die die Firmware an den Host zurücksendet. Im Firmware-Quellcode sind diese Werte nach Zweck in 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).
Byte-Werte sind maßgebend; Namen sind Aliase. Beim Lesen des Firmware-Quellcodes entsprechen RESP_CODE_X / PUSH_CODE_X den PACKET_X dieses Dokuments mit demselben numerischen Wert.

Pakettypen

WertNameBeschreibung
0x00PACKET_OKBefehl erfolgreich ausgeführt
0x01PACKET_ERRORBefehl fehlgeschlagen
0x02PACKET_CONTACT_STARTAnfang der Kontaktliste
0x03PACKET_CONTACTKontaktinformationen
0x04PACKET_CONTACT_ENDEnde der Kontaktliste
0x05PACKET_SELF_INFOGerät-Selbstinformationen
0x06PACKET_MSG_SENTBestätigung, dass Nachricht gesendet wurde
0x07PACKET_CONTACT_MSG_RECVKontaktnachricht (Standard)
0x08PACKET_CHANNEL_MSG_RECVKanalnachricht (Standard)
0x09PACKET_CURRENT_TIMEAntwort auf aktuelle Zeit
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_ADVERTISEMENTWerbepaket
0x82PACKET_ACKBestätigung
0x83PACKET_MESSAGES_WAITINGBenachrichtigung über wartende Nachrichten
0x88PACKET_LOG_DATARF-Logdaten (können ignoriert werden)

Antworten parsen

PACKET_OK (0x00):
CLI
Byte 0: 0x00
Bytes 1-4: Optionaler Wert (32-Bit Little-Endian Integer)
PACKET_ERROR (0x01):
CLI
Byte 0: 0x01
Byte 1: Fehlercode (optional)
PACKET_CHANNEL_INFO (0x12):
CLI
Byte 0: 0x12
Byte 1: Kanalindex
Bytes 2-33: Kanalname (32 Bytes, Null-terminiert)
Bytes 34-49: Geheimnis (16 Bytes)
Hinweis: Das Gerät gibt den 16-Byte-Kanal-Geheimschlüssel in dieser Antwort zurück. PACKET_DEVICE_INFO (0x0D):
CLI
Byte 0: 0x0D
Byte 1: Firmware-Version (uint8)
Bytes 2+: Variable Länge basierend auf Firmware-Version

Für Firmware-Version >= 3:
Byte 2: Max Kontakte Roh (uint8, tatsächlich = Wert * 2)
Byte 3: Max Kanäle (uint8)
Bytes 4-7: BLE PIN (32-Bit Little-Endian)
Bytes 8-19: Firmware-Build (12 Bytes, UTF-8, Null-gefüllt)
Bytes 20-59: Modell (40 Bytes, UTF-8, Null-gefüllt)
Bytes 60-79: Version (20 Bytes, UTF-8, Null-gefüllt)
Byte 80: Client Repeat aktiviert/bevorzugt (Firmware v9+)
Byte 81: Path Hash Mode (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: Genutzter 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: Werbetyp
Byte 2: TX-Leistung
Byte 3: Max. TX-Leistung
Bytes 4-35: Öffentlicher Schlüssel (32 Bytes, hexadezimal)
Bytes 36-39: Werbebreitengrad (32-Bit Little-Endian, geteilt durch 1e6)
Bytes 40-43: Werbelängengrad (32-Bit Little-Endian, geteilt durch 1e6)
Byte 44: Multi-ACKs
Byte 45: Werbe-Standortrichtlinie
Byte 46: Telemetrie-Modus (Bitfeld)
Byte 47: Manuell Kontakte hinzufügen (boolean)
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
Byte 57: Funk-Kodierrate
Bytes 58+: Gerätename (UTF-8, variable Länge, kein Null-Terminator 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: Routen-Flag (0 = direkt, 1 = Flood)
Bytes 2-5: Tag / Erwarteter 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, hexadezimal)

Fehlercodes

PACKET_ERROR (0x01) enthält einen ein Byte großen Fehlercode in Byte 1. Die Werte entsprechen den ERR_CODE_*-Konstanten, die in examples/companion_radio/MyMesh.cpp definiert sind:
CodeKonstante (Firmware)Beschreibung
1ERR_CODE_UNSUPPORTED_CMDUnbekanntes oder nicht unterstütztes Befehlsbyte / 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 gültig (z.B. Iterator läuft bereits)
5ERR_CODE_FILE_IO_ERRORDateisystem- oder Speicher-I/O-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. Überprüfen Sie immer Byte 1 der PACKET_ERROR-Antwort und behandeln Sie unbekannte Codes als generische Fehler.

Frame-Behandlung

BLE-Implementierungen reihen auf der Firmware-Ebene einen Protokoll-Frame pro BLE-Schreib-/Benachrichtigung ein und liefern ihn aus.

  • Apps sollten jeden Charakteristik-Schreib-/Benachrichtigungsvorgang als genau einen Companion-Protokoll-Frame behandeln.
  • Apps sollten weiterhin die Frame-Längen vor dem Parsen validieren.
  • Zukünftige Transportmittel oder Firmware-Revisionen können abweichen, daher sollte man nicht von festen Payload-Größen für variablengroße Antworten ausgehen.

Antwort-Behandlung

  1. Befehl-Antwort-Muster:
- Befehl über RX-Eigenschaft senden. - Auf Antwort über TX-Eigenschaft (Benachrichtigung) warten. - Antwort mit dem Befehl über Sequenznummern oder Befehlstyp abgleichen. - Timeout behandeln (typischerweise 5 Sekunden). - Befehlswarteschlange verwenden, um gleichzeitige Befehle zu verhindern.
  1. Asynchrone Nachrichten:
- Das Gerät kann jederzeit Nachrichten über die TX-Eigenschaft senden. - PACKET_MESSAGES_WAITING (0x83) behandeln, indem der GET_MESSAGE-Befehl abgefragt wird. - Eingehende Nachrichten parsen und an die entsprechenden Handler weiterleiten. - Frame-Länge vor dem Dekodieren validieren.
  1. Antwortabgleich:
- Antworten mit Befehlen nach erwartetem Pakettyp abgleichen: - APP_START → PACKET_SELF_INFO - DEVICE_QUERY → PACKET_DEVICE_INFO - GET_CHANNEL → PACKET_CHANNEL_INFO - SET_CHANNEL → PACKET_OK oder PACKET_ERROR - SEND_CHANNEL_MESSAGE → PACKET_MSG_SENT - GET_MESSAGE → PACKET_CHANNEL_MSG_RECV, PACKET_CONTACT_MSG_RECV, PACKET_CHANNEL_DATA_RECV oder PACKET_NO_MORE_MSGS - SEND_CHANNEL_DATA → PACKET_OK oder PACKET_ERROR - GET_BATTERY → PACKET_BATTERY
  1. Timeout-Behandlung:
- Standard-Timeout: 5 Sekunden pro Befehl. - Bei Timeout: Fehler protokollieren, aktuellen Befehl löschen, zum nächsten in der Warteschlange übergehen. - Einige Befehle können länger dauern (z.B. SET_CHANNEL kann 1-2 Sekunden benötigen). - Längere Timeout-Werte für Kanaloperationen in Betracht ziehen.
  1. Fehlerbehebung:
- Bei PACKET_ERROR: Fehlercode protokollieren, aktuellen Befehl löschen. - Bei Verbindungsverlust: Befehlswarteschlange löschen, Wiederverbindung versuchen. - Bei ungültiger Antwort: Warnung protokollieren, aktuellen Befehl löschen, fortfahren.

---

Beispielhafter Implementierungsablauf

Initialisierung

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

# 2. Mit BLE GATT verbinden
gatt = connect_to_device(device)

# 3. Dienste und Eigenschaften entdecken
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. Benachrichtigungen für TX-Eigenschaft 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-Geheimnis generieren
secret_16_bytes = generate_secret(16)  # CSPRNG verwenden
secret_hex = secret_16_bytes.hex()

# 2. SET_CHANNEL-Befehl erstellen
channel_name = "IhrKanalName"
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. Geheimnis lokal speichern
store_channel_secret(channel_index, secret_hex)

Nachricht senden

CLI
# 1. Kanalnachrichten-Befehl erstellen
channel_index = 1
message = "Hallo, 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 abrufen
        send_command(rx_char, build_get_message())

---

Bewährte Verfahren

  1. Verbindungsmanagement:
- Implementieren Sie automatische Wiederverbindung mit exponentiellem Backoff. - Behandeln Sie Verbindungsabbrüche elegant. - Speichern Sie die Adresse des zuletzt verbundenen Geräts für eine schnelle Wiederverbindung.
  1. Geheimnisverwaltung:
- Verwenden Sie immer kryptografisch sichere Zufallszahlengeneratoren (CSPRNGs). - Speichern Sie Geheimnisse sicher (verschlüsselter Speicher). - Protokollieren oder übertragen Sie Geheimnisse niemals im Klartext.
  1. Nachrichtenverarbeitung:
- Senden Sie CMD_SYNC_NEXT_MESSAGE, wenn PUSH_CODE_MSG_WAITING empfangen wird. - Implementieren Sie die Nachrichten-Deduplizierung, um zu vermeiden, dass dieselbe Nachricht zweimal angezeigt wird.
  1. Kanalverwaltung:
- Rufen Sie alle Kanal-Slots ab, auch wenn Sie einen leeren Slot finden. - Speichern Sie idealerweise neue Kanäle im ersten leeren Slot.
  1. Fehlerbehandlung:
- Implementieren Sie Timeouts für alle Befehle (typischerweise 5 Sekunden). - Behandeln Sie RESP_CODE_ERR-Antworten entsprechend.

---

Fehlerbehebung

Verbindungsprobleme

  • Gerät nicht gefunden: Stellen Sie sicher, dass das Gerät eingeschaltet ist und sendet (advertising).
  • Verbindungs-Timeout: Überprüfen Sie Bluetooth-Berechtigungen und die Gerätenähe.
  • GATT-Fehler: Stellen Sie eine korrekte Dienst-/Eigenschaften-Erkennung sicher.

Befehlsprobleme

  • Keine Antwort: Überprüfen Sie, ob Benachrichtigungen aktiviert sind und der Verbindungsstatus korrekt ist.
  • Fehlerantworten: Überprüfen Sie das Befehlsformat und den Fehlercode.
  • Timeout: Erhöhen Sie den Timeout-Wert oder versuchen Sie es erneut.

Nachrichtenprobleme

  • Nachrichten werden nicht empfangen: Fragen Sie den GET_MESSAGE-Befehl regelmäßig ab.
  • Doppelte Nachrichten: Implementieren Sie eine Nachrichten-Deduplizierung, indem Sie Zeitstempel/Inhalt als eindeutige ID verwenden.
  • Nachrichten-Trunkierung: Senden Sie lange Nachrichten als separate, kürzere Nachrichten.

Quelle: docs.meshcore.io