# 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/