Try   HackMD

Meshtastic BLE Protocol

About Meshtastic Communication Protocol With BLE To Mobile. Source Doc

以下有經過Claude AI協助解釋,可能會有不正確的內容,還請見諒。

BLE Services

  • Mesh Service
    • UUID: 6ba1b218-15a8-461f-9fa8-5dcae273eafd
    • Characterstatic:
      • ToRadioCharacteristic
        • UUID: f75c76d2-129e-4dad-a1dd-7866124401e7
        • Property: Write
      • FromRadioCharacteristic
        • UUID: 2c55e69e-4993-11ed-b878-0242ac120002
        • Property: Read
      • fromNumCharacteristic
        • UUID: ed9da18c-a800-4f66-a670-aa7547e34453
        • Property: Read / Notify
  • Battery Service
    • UUID: 0x2a19
    • Characterstatic:
      • BatteryCharacteristic: 電池%數
        • UUID: 0x2A19
        • Property: Read / Notify
        • Descriptor:
          • UUID: 0x2904
          • Format: UInt8
          • Namespace: 1
          • Unit: 0x27AD (Percentage)

Code

NimbleBluetooth.cpp

  • NimbleBluetoothToRadioCallback::onWrite():負責處理藍牙通訊的 ToRadio Services。當該特性值發生變化時,會觸發此Callback。 (ID: f75c76d2-129e-4dad-a1dd-7866124401e7)
    1. 記錄一個 "To Radio onwrite" 的 log 訊息。
    LOG_INFO("To Radio onwrite\n");
    
    1. 獲取藍牙特性的值 (pCharacteristic->getValue())。
    ​​​​auto val = pCharacteristic->getValue();
    
    1. 調用 bluetoothPhoneAPI->handleToRadio() 函數,將獲取到的藍牙特性值傳遞過去。
    ​​​​bluetoothPhoneAPI->handleToRadio(val.data(), val.length());
    
  • NimbleBluetoothFromRadioCallback::onRead():負責處理藍牙通訊的 FromRadio Services。當手機讀取此特性值時,會觸發此Callback。 (ID: 2c55e69e-4993-11ed-b878-0242ac120002)
    1. 記錄一個 "From Radio onread" 的 log 訊息。
    LOG_INFO("From Radio onread\n");
    
    1. 定義一個大小為 meshtastic_FromRadio_size 的Buffer fromRadioBytes。並調用 bluetoothPhoneAPI->getFromRadio() 函數,將無線電設備發送的資料拷貝到 fromRadioBytes Buffer,並獲取實際拷貝的字節數 numBytes。
    uint8_t fromRadioBytes[meshtastic_FromRadio_size];
    ​size_t numBytes = bluetoothPhoneAPI->getFromRadio(fromRadioBytes);
    
    1. 使用 fromRadioBytes Buffer資料,宣告字串 fromRadioByteString,並將 fromRadioByteString 寫入到藍牙 FromRadio 特性值中,以便讓手機或其他支援BLE的程式讀取。
    ​std::string fromRadioByteString(fromRadioBytes, fromRadioBytes + numBytes);
    ​pCharacteristic->setValue(fromRadioByteString);
    

PhoneAPI.cpp

  • PhoneAPI::handleToRadio():處理從手機向無線電發送的 ToRadio 協定訊息。
    1. 當手機與無線電設備 (ESP32) 保持通訊時, 觸發EVENT_CONTACT_FROM_PHONE, 以防止無線電設備 (ESP32) 進入睡眠模式。
    ​​​​powerFSM.trigger(EVENT_CONTACT_FROM_PHONE); // As long as the phone keeps talking to us, don't let the radio go to sleep
    
    1. 將接收到的 ToRadio 訊息解碼到 toRadioScratch 結構體中。
    ​​​​pb_decode_from_bytes(buf, bufLength, &meshtastic_ToRadio_msg, &toRadioScratch);
    
    1. 依據 toRadioScratch 中的 which_payload_variant 欄位,分別處理不同類型的 ToRadio 訊息:
      • meshtastic_ToRadio_packet_tag: 呼叫 handleToRadioPacket() 函數處理封包。
      ​​​​​​​case meshtastic_ToRadio_packet_tag:
      ​​​​​​​ return handleToRadioPacket(toRadioScratch.packet);
      
      • meshtastic_ToRadio_want_config_id_tag: 取得設定檔 ID (config_nonce),並呼叫 handleStartConfig() 函數。
      ​​​​​​​case meshtastic_ToRadio_want_config_id_tag:
      ​​​​​​​ config_nonce = toRadioScratch.want_config_id;
      ​​​​​​​ LOG_INFO("Client wants config, nonce=%u\n", config_nonce);
      ​​​​​​​ handleStartConfig();
      ​​​​​​​ break;
      
      • meshtastic_ToRadio_disconnect_tag: 關閉與手機的連接。
      ​​​​​​​case meshtastic_ToRadio_disconnect_tag:
      ​​​​​​​ LOG_INFO("Disconnecting from phone\n");
      ​​​​​​​ close();
      ​​​​​​​ break;
      
      • meshtastic_ToRadio_xmodemPacket_tag: 呼叫 xModem.handlePacket() 處理 xmodem 封包。
      • 或許是更新韌體的地方 (2024.4.13 blackcat)
      ​​​​​​​case meshtastic_ToRadio_xmodemPacket_tag:
      ​​​​​​​ LOG_INFO("Got xmodem packet\n");
      ​​​​​​​ xModem.handlePacket(toRadioScratch.xmodemPacket);
      ​​​​​​​ break;
      
      • meshtastic_ToRadio_mqttClientProxyMessage_tag: 呼叫 mqtt->onClientProxyReceive() 處理 MQTT 代理訊息。
      • 若 MQTT 代理沒有啟用,此Function無效。
      ​​​​​​​#if !MESHTASTIC_EXCLUDE_MQTT
      ​​​​​​​case meshtastic_ToRadio_mqttClientProxyMessage_tag:
      ​​​​​​​ LOG_INFO("Got MqttClientProxy message\n");
      ​​​​​​​ if (mqtt && moduleConfig.mqtt.proxy_to_client_enabled && moduleConfig.mqtt.enabled &&
      ​​​​​​​     (channels.anyMqttEnabled() || moduleConfig.mqtt.map_reporting_enabled)) {
      ​​​​​​​     mqtt->onClientProxyReceive(toRadioScratch.mqttClientProxyMessage);
      ​​​​​​​ } else {
      ​​​​​​​     LOG_WARN("MqttClientProxy received but proxy is not enabled, no channels have up/downlink, or map reporting "
      ​​​​​​​                 "not enabled\n");
      ​​​​​​​ }
      ​​​​​​​ break;
      ​​​​​​​#endif
      
      • meshtastic_ToRadio_heartbeat_tag: 記錄已收到手機的心跳訊號。
      ​​​​​​​case meshtastic_ToRadio_heartbeat_tag:
      ​​​​​​​  LOG_DEBUG("Got client heartbeat\n");
      ​​​​​​​  break;
      
    2. 如果無法解碼 ToRadio 訊息,會記錄錯誤訊息。
    ​​​​LOG_ERROR("Error: ignoring malformed toradio\n");
    
  • PhoneAPI::getFromRadio():處理從無線電向手機發送的 FromRadio 協定訊息。
    1. 根據當前的狀態 (state 變數),對應不同的訊息類型進行處理。
      • STATE_SEND_MY_INFO:發送個人資訊。
      • STATE_SEND_METADATA:發送設備 (這裡指無線電) 的原始資料。
      • STATE_SEND_NODEINFO:發送節點資訊。
      • STATE_SEND_CHANNELS:發送頻道配置。
      • STATE_SEND_CONFIG:發送設備 (這裡指無線電) 設定資訊。
        • meshtastic_Config_device_tag:裝置資訊
        • meshtastic_Config_position_tag:GPS定位
        • meshtastic_Config_power_tag:省電
        • meshtastic_Config_network_tag:WiFi網路
        • meshtastic_Config_display_tag:螢幕 (如SSD1306 / SSD1308等)
        • meshtastic_Config_lora_tag:LoRa模組 (如SX1262 / 1276 / 1278等)
        • meshtastic_Config_bluetooth_tag:藍芽
      • STATE_SEND_MODULECONFIG:發送功能模組的設定資訊。
        • meshtastic_ModuleConfig_mqtt_tag:MQTT網路
        • meshtastic_ModuleConfig_serial_tag:序列埠 (如TTL / RS232等)
        • meshtastic_ModuleConfig_external_notification_tag:外部裝置輸出 (如LED / 蜂鳴器等)
        • meshtastic_ModuleConfig_store_forward_tag:告訴路由節點暫時存儲 / 轉發訊息給其他裝置
          • 這裡需要再修一下 (2024.4.13 blackcat)
        • meshtastic_ModuleConfig_range_test_tag:兩節點之間的距離測試
        • meshtastic_ModuleConfig_telemetry_tag:遙測模組 (如BME280、SHT31等 Sensor)
        • meshtastic_ModuleConfig_canned_message_tag:事先設定好預發送的訊息
        • meshtastic_ModuleConfig_audio_tag:音頻資料
        • meshtastic_ModuleConfig_remote_hardware_tag:遙控其他節點的GPIO開/關
        • meshtastic_ModuleConfig_neighbor_info_tag:鄰近節點的資訊
        • meshtastic_ModuleConfig_detection_sensor_tag:開關 / 壓力感測器 / 紅外線感測器 (輸出訊號為Low或者High)
        • meshtastic_ModuleConfig_ambient_lighting_tag:漸變燈 / 呼吸燈
        • meshtastic_ModuleConfig_paxcounter_tag:透過WiFi或者藍芽 MAC位置計算該周圍的人數
      • STATE_SEND_COMPLETE_ID:發送設定完成的標識。
      • STATE_SEND_PACKETS:發送從Mesh網路接收的封包。
    2. 將要發送的資訊打包成 FromRadio 格式,並返回實際寫入Buffer的字節數。
    // Do we have a message from the mesh?
    ​​​​if (fromRadioScratch.which_payload_variant != 0) {
    ​​​​    // Encapsulate as a FromRadio packet
    ​​​​    size_t numbytes = pb_encode_to_bytes(buf, meshtastic_FromRadio_size, &meshtastic_FromRadio_msg, &fromRadioScratch);
    
    ​​​​    LOG_DEBUG("encoding toPhone packet to phone variant=%d, %d bytes\n", fromRadioScratch.which_payload_variant, numbytes);
    ​​​​    return numbytes;
    ​​​​}
    

Protobuf Struct

Code Generate

  • 可以用meshtastic專案的bat,也可以用這裡的檔案。
    • 環境變數:<Top Folder>\protoc\bin

Python

protoc.exe --python_out=out -I=../protobufs ../protobufs/meshtastic/*.proto

C#

protoc.exe --csharp_out=out --proto_path=../protobufs ../protobufs/meshtastic/*.proto

C++

  • meshtastic專案資料夾/bin/regen-protos.bat
protoc.exe --experimental_allow_proto3_optional "--nanopb_out=-S.cpp -v:out\" -I=..\protobufs ..\protobufs\meshtastic\*.proto

MeshPacket

message MeshPacket {
  /*
   * The priority of this message for sending.
   * Higher priorities are sent first (when managing the transmit queue).
   * This field is never sent over the air, it is only used internally inside of a local device node.
   * API clients (either on the local node or connected directly to the node)
   * can set this parameter if necessary.
   * (values must be <= 127 to keep protobuf field to one byte in size.
   * Detailed background on this field:
   * I noticed a funny side effect of lora being so slow: Usually when making
   * a protocol there isn’t much need to use message priority to change the order
   * of transmission (because interfaces are fairly fast).
   * But for lora where packets can take a few seconds each, it is very important
   * to make sure that critical packets are sent ASAP.
   * In the case of meshtastic that means we want to send protocol acks as soon as possible
   * (to prevent unneeded retransmissions), we want routing messages to be sent next,
   * then messages marked as reliable and finally 'background' packets like periodic position updates.
   * So I bit the bullet and implemented a new (internal - not sent over the air)
   * field in MeshPacket called 'priority'.
   * And the transmission queue in the router object is now a priority queue.
   */
  enum Priority {
    /*
     * Treated as Priority.DEFAULT
     */
    UNSET = 0;
    /*
     * TODO: REPLACE
     */
    MIN = 1;
    /*
     * Background position updates are sent with very low priority -
     * if the link is super congested they might not go out at all
     */
    BACKGROUND = 10;
    /*
     * This priority is used for most messages that don't have a priority set
     */
    DEFAULT = 64;
    /*
     * If priority is unset but the message is marked as want_ack,
     * assume it is important and use a slightly higher priority
     */
    RELIABLE = 70;
    /*
     * Ack/naks are sent with very high priority to ensure that retransmission
     * stops as soon as possible
     */
    ACK = 120;
    /*
     * TODO: REPLACE
     */
    MAX = 127;
  }
  /*
   * Identify if this is a delayed packet
   */
  enum Delayed {
    /*
     * If unset, the message is being sent in real time.
     */
    NO_DELAY = 0;
    /*
     * The message is delayed and was originally a broadcast
     */
    DELAYED_BROADCAST = 1;
    /*
     * The message is delayed and was originally a direct message
     */
    DELAYED_DIRECT = 2;
  }
  /*
   * The sending node number.
   * Note: Our crypto implementation uses this field as well.
   * See [crypto](/docs/overview/encryption) for details.
   */
  fixed32 from = 1;
  /*
   * The (immediate) destination for this packet
   */
  fixed32 to = 2;
  /*
   * (Usually) If set, this indicates the index in the secondary_channels table that this packet was sent/received on.
   * If unset, packet was on the primary channel.
   * A particular node might know only a subset of channels in use on the mesh.
   * Therefore channel_index is inherently a local concept and meaningless to send between nodes.
   * Very briefly, while sending and receiving deep inside the device Router code, this field instead
   * contains the 'channel hash' instead of the index.
   * This 'trick' is only used while the payload_variant is an 'encrypted'.
   */
  uint32 channel = 3;
  /*
   * Internally to the mesh radios we will route SubPackets encrypted per [this](docs/developers/firmware/encryption).
   * However, when a particular node has the correct
   * key to decode a particular packet, it will decode the payload into a SubPacket protobuf structure.
   * Software outside of the device nodes will never encounter a packet where
   * "decoded" is not populated (i.e. any encryption/decryption happens before reaching the applications)
   * The numeric IDs for these fields were selected to keep backwards compatibility with old applications.
   */
  oneof payload_variant {
    /*
     * TODO: REPLACE
     */
    Data decoded = 4;
    /*
     * TODO: REPLACE
     */
    bytes encrypted = 5;
  }
  /*
   * A unique ID for this packet.
   * Always 0 for no-ack packets or non broadcast packets (and therefore take zero bytes of space).
   * Otherwise a unique ID for this packet, useful for flooding algorithms.
   * ID only needs to be unique on a _per sender_ basis, and it only
   * needs to be unique for a few minutes (long enough to last for the length of
   * any ACK or the completion of a mesh broadcast flood).
   * Note: Our crypto implementation uses this id as well.
   * See [crypto](/docs/overview/encryption) for details.
   */
  fixed32 id = 6;
  /*
   * The time this message was received by the esp32 (secs since 1970).
   * Note: this field is _never_ sent on the radio link itself (to save space) Times
   * are typically not sent over the mesh, but they will be added to any Packet
   * (chain of SubPacket) sent to the phone (so the phone can know exact time of reception)
   */
  fixed32 rx_time = 7;
  /*
   * *Never* sent over the radio links.
   * Set during reception to indicate the SNR of this packet.
   * Used to collect statistics on current link quality.
   */
  float rx_snr = 8;
  /*
   * If unset treated as zero (no forwarding, send to adjacent nodes only)
   * if 1, allow hopping through one node, etc...
   * For our usecase real world topologies probably have a max of about 3.
   * This field is normally placed into a few of bits in the header.
   */
  uint32 hop_limit = 9;
  /*
   * This packet is being sent as a reliable message, we would prefer it to arrive at the destination.
   * We would like to receive a ack packet in response.
   * Broadcasts messages treat this flag specially: Since acks for broadcasts would
   * rapidly flood the channel, the normal ack behavior is suppressed.
   * Instead, the original sender listens to see if at least one node is rebroadcasting this packet (because naive flooding algorithm).
   * If it hears that the odds (given typical LoRa topologies) the odds are very high that every node should eventually receive the message.
   * So FloodingRouter.cpp generates an implicit ack which is delivered to the original sender.
   * If after some time we don't hear anyone rebroadcast our packet, we will timeout and retransmit, using the regular resend logic.
   * Note: This flag is normally sent in a flag bit in the header when sent over the wire
   */
  bool want_ack = 10;
  /*
   * The priority of this message for sending.
   * See MeshPacket.Priority description for more details.
   */
  Priority priority = 11;
  /*
   * rssi of received packet. Only sent to phone for dispay purposes.
   */
  int32 rx_rssi = 12;
  /*
   * Describe if this message is delayed
   */
  Delayed delayed = 13 [deprecated = true];
  /*
   * Describes whether this packet passed via MQTT somewhere along the path it currently took.
   */
  bool via_mqtt = 14;
  /*
   * Hop limit with which the original packet started. Sent via LoRa using three bits in the unencrypted header.
   * When receiving a packet, the difference between hop_start and hop_limit gives how many hops it traveled.
   */
  uint32 hop_start = 15;
}

FromRadio

message FromRadio {
  /*
   * The packet id, used to allow the phone to request missing read packets from the FIFO,
   * see our bluetooth docs
   */
  uint32 id = 1;
  /*
   * Log levels, chosen to match python logging conventions.
   */
  oneof payload_variant {
    /*
     * Log levels, chosen to match python logging conventions.
     */
    MeshPacket packet = 2;
    /*
     * Tells the phone what our node number is, can be -1 if we've not yet joined a mesh.
     * NOTE: This ID must not change - to keep (minimal) compatibility with <1.2 version of android apps.
     */
    MyNodeInfo my_info = 3;
    /*
     * One packet is sent for each node in the on radio DB
     * starts over with the first node in our DB
     */
    NodeInfo node_info = 4;
    /*
     * Include a part of the config (was: RadioConfig radio)
     */
    Config config = 5;
    /*
     * Set to send debug console output over our protobuf stream
     */
    LogRecord log_record = 6;
    /*
     * Sent as true once the device has finished sending all of the responses to want_config
     * recipient should check if this ID matches our original request nonce, if
     * not, it means your config responses haven't started yet.
     * NOTE: This ID must not change - to keep (minimal) compatibility with <1.2 version of android apps.
     */
    uint32 config_complete_id = 7;
    /*
     * Sent to tell clients the radio has just rebooted.
     * Set to true if present.
     * Not used on all transports, currently just used for the serial console.
     * NOTE: This ID must not change - to keep (minimal) compatibility with <1.2 version of android apps.
     */
    bool rebooted = 8;
    /*
     * Include module config
     */
    ModuleConfig moduleConfig = 9;
    /*
     * One packet is sent for each channel
     */
    Channel channel = 10;
    /*
     * Queue status info
     */
    QueueStatus queueStatus = 11;
    /*
     * File Transfer Chunk
     */
    XModem xmodemPacket = 12;
    /*
     * Device metadata message
     */
    DeviceMetadata metadata = 13;
    /*
     * MQTT Client Proxy Message (device sending to client / phone for publishing to MQTT)
     */
    MqttClientProxyMessage mqttClientProxyMessage = 14;
  }
}

ToRadio

message ToRadio {
  /*
   * Log levels, chosen to match python logging conventions.
   */
  oneof payload_variant {
    /*
     * Send this packet on the mesh
     */
    MeshPacket packet = 1;
    /*
     * Phone wants radio to send full node db to the phone, This is
     * typically the first packet sent to the radio when the phone gets a
     * bluetooth connection. The radio will respond by sending back a
     * MyNodeInfo, a owner, a radio config and a series of
     * FromRadio.node_infos, and config_complete
     * the integer you write into this field will be reported back in the
     * config_complete_id response this allows clients to never be confused by
     * a stale old partially sent config.
     */
    uint32 want_config_id = 3;
    /*
     * Tell API server we are disconnecting now.
     * This is useful for serial links where there is no hardware/protocol based notification that the client has dropped the link.
     * (Sending this message is optional for clients)
     */
    bool disconnect = 4;
    /*
     * File Transfer Chunk
     */
    XModem xmodemPacket = 5;
    /*
     * MQTT Client Proxy Message (for client / phone subscribed to MQTT sending to device)
     */
    MqttClientProxyMessage mqttClientProxyMessage = 6;
    /*
     * Heartbeat message (used to keep the device connection awake on serial)
     */
    Heartbeat heartbeat = 7;
  }
}

Raw Message

  • 修改了NimbleBluetooth.cpp,將封包透過Hex印出:
class NimbleBluetoothToRadioCallback : public NimBLECharacteristicCallbacks { virtual void onWrite(NimBLECharacteristic *pCharacteristic) { LOG_INFO("To Radio onwrite\n"); auto val = pCharacteristic->getValue(); for(int i = 0; i < val.length(); i++) LOG_DEBUG("%2x ", val[i]); LOG_DEBUG("\n"); bluetoothPhoneAPI->handleToRadio(val.data(), val.length()); } }; class NimbleBluetoothFromRadioCallback : public NimBLECharacteristicCallbacks { virtual void onRead(NimBLECharacteristic *pCharacteristic) { LOG_INFO("From Radio onread\n"); uint8_t fromRadioBytes[meshtastic_FromRadio_size]; size_t numBytes = bluetoothPhoneAPI->getFromRadio(fromRadioBytes); std::string fromRadioByteString(fromRadioBytes, fromRadioBytes + numBytes); for(int i = 0; i < fromRadioByteString.length(); i++) LOG_DEBUG("%2x ", fromRadioByteString[i]); LOG_DEBUG("\n"); pCharacteristic->setValue(fromRadioByteString); } };

ToRadioCharacteristic

  • SEND_PACKETS:發送Hello的訊息到Mesh網路
0a 17 15 ff ff ff ff 22 09 08 01 12 05 48 65 6c 6c 6f 35 5b 06 ec 94 50 01
  • 0a 17: 表示這是一個 ToRadio 訊息,長度為 0x17(23)個字節。
  • 15 ff ff ff ff: 這是一個 Packet 類型的訊息,具有 4 個字節的目的地 ID。
  • 22 09: 是協議版本號。
  • 08 01: 表示這是一個 DATA_MESSAGE 類型的封包。
  • 12 05: 表示接下來有 5 個字節的荷載資料。
  • 48 65 6c 6c 6f: 這就是荷載資料,解碼出來是 "Hello" 字串。
  • 35 5b 06 ec 94 50 01: 這是一些其他的封包資訊,如來源 ID、crc 校驗碼等。

Reference