# Meshtastic BLE Protocol > About Meshtastic Communication Protocol With BLE To Mobile. [Source Doc](https://meshtastic.org/) > :::warning > 以下有經過Claude AI協助解釋,可能會有不正確的內容,還請見諒。 > ::: [TOC] # 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 訊息。 ```cpp LOG_INFO("To Radio onwrite\n"); ``` 2. 獲取藍牙特性的值 (pCharacteristic->getValue())。 ```cpp auto val = pCharacteristic->getValue(); ``` 3. 調用 bluetoothPhoneAPI->handleToRadio() 函數,將獲取到的藍牙特性值傳遞過去。 ```cpp bluetoothPhoneAPI->handleToRadio(val.data(), val.length()); ``` - `NimbleBluetoothFromRadioCallback::onRead()`:負責處理藍牙通訊的 FromRadio Services。當手機讀取此特性值時,會觸發此Callback。 (ID: `2c55e69e-4993-11ed-b878-0242ac120002`) 1. 記錄一個 "From Radio onread" 的 log 訊息。 ```cpp LOG_INFO("From Radio onread\n"); ``` 2. 定義一個大小為 meshtastic_FromRadio_size 的Buffer fromRadioBytes。並調用 bluetoothPhoneAPI->getFromRadio() 函數,將無線電設備發送的資料拷貝到 fromRadioBytes Buffer,並獲取實際拷貝的字節數 numBytes。 ```cpp uint8_t fromRadioBytes[meshtastic_FromRadio_size]; size_t numBytes = bluetoothPhoneAPI->getFromRadio(fromRadioBytes); ``` 3. 使用 fromRadioBytes Buffer資料,宣告字串 fromRadioByteString,並將 fromRadioByteString 寫入到藍牙 FromRadio 特性值中,以便讓手機或其他支援BLE的程式讀取。 ```cpp std::string fromRadioByteString(fromRadioBytes, fromRadioBytes + numBytes); pCharacteristic->setValue(fromRadioByteString); ``` ## PhoneAPI.cpp - `PhoneAPI::handleToRadio()`:處理從手機向無線電發送的 ToRadio 協定訊息。 1. 當手機與無線電設備 (ESP32) 保持通訊時, 觸發EVENT_CONTACT_FROM_PHONE, 以防止無線電設備 (ESP32) 進入睡眠模式。 ```cpp powerFSM.trigger(EVENT_CONTACT_FROM_PHONE); // As long as the phone keeps talking to us, don't let the radio go to sleep ``` 2. 將接收到的 ToRadio 訊息解碼到 toRadioScratch 結構體中。 ```cpp pb_decode_from_bytes(buf, bufLength, &meshtastic_ToRadio_msg, &toRadioScratch); ``` 3. 依據 toRadioScratch 中的 which_payload_variant 欄位,分別處理不同類型的 ToRadio 訊息: - `meshtastic_ToRadio_packet_tag`: 呼叫 handleToRadioPacket() 函數處理封包。 ```cpp case meshtastic_ToRadio_packet_tag: return handleToRadioPacket(toRadioScratch.packet); ``` - `meshtastic_ToRadio_want_config_id_tag`: 取得設定檔 ID (config_nonce),並呼叫 handleStartConfig() 函數。 ```cpp 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`: 關閉與手機的連接。 ```cpp case meshtastic_ToRadio_disconnect_tag: LOG_INFO("Disconnecting from phone\n"); close(); break; ``` - `meshtastic_ToRadio_xmodemPacket_tag`: 呼叫 xModem.handlePacket() 處理 xmodem 封包。 - 或許是更新韌體的地方 (`2024.4.13 blackcat`) ```cpp 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無效。 ```cpp #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`: 記錄已收到手機的心跳訊號。 ```cpp case meshtastic_ToRadio_heartbeat_tag: LOG_DEBUG("Got client heartbeat\n"); break; ``` 4. 如果無法解碼 ToRadio 訊息,會記錄錯誤訊息。 ```cpp 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的字節數。 ```cpp // 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](https://buf.build/meshtastic/protobufs/docs/main) ## Code Generate - 可以用meshtastic專案的bat,也可以用[這裡](https://github.com/protocolbuffers/protobuf/releases/download/v26.1/protoc-26.1-win64.zip)的檔案。 - 環境變數:`<Top Folder>\protoc\bin` ### Python ```powershell protoc.exe --python_out=out -I=../protobufs ../protobufs/meshtastic/*.proto ``` ### C# ```powershell protoc.exe --csharp_out=out --proto_path=../protobufs ../protobufs/meshtastic/*.proto ``` ### C++ - `meshtastic專案資料夾/bin/regen-protos.bat` ```powershell protoc.exe --experimental_allow_proto3_optional "--nanopb_out=-S.cpp -v:out\" -I=..\protobufs ..\protobufs\meshtastic\*.proto ``` ## MeshPacket ```protobuf 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 ```protobuf 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 ```protobuf 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 ## Print Code - 修改了`NimbleBluetooth.cpp`,將封包透過Hex印出: ```cpp= 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); } }; ``` - https://github.com/nanopb/nanopb/ - 用在Radio端進行解碼 ## **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 - https://meshtastic.org/docs/overview/mesh-algo/ - https://tsunhua.github.io/it/common/protobuf-intro/ - https://protobuf.dev/getting-started/pythontutorial/ - https://protobuf.dev/getting-started/csharptutorial/