2026-06-11 08:48:18 +00:00
<!doctype html>
< html lang = "en" class = "no-js" >
< head >
< meta charset = "utf-8" >
< meta name = "viewport" content = "width=device-width,initial-scale=1" >
< meta name = "description" content = "Documentation for the open source MeshCore firmware" >
< link rel = "canonical" href = "https://meshcore-dev.github.io/meshcore/companion_protocol/" >
< link rel = "prev" href = "../cli_commands/" >
< link rel = "next" href = "../docs/" >
< link rel = "icon" href = "../assets/images/favicon.png" >
< meta name = "generator" content = "mkdocs-1.6.1, mkdocs-material-9.7.6" >
< title > Companion Protocol - MeshCore Docs</ title >
< link rel = "stylesheet" href = "../assets/stylesheets/main.484c7ddc.min.css" >
< link rel = "preconnect" href = "https://fonts.gstatic.com" crossorigin >
< link rel = "stylesheet" href = "https://fonts.googleapis.com/css?family=Roboto:300,300i,400,400i,700,700i%7CRoboto+Mono:400,400i,700,700i&display=fallback" >
< style >: root { --md-text-font : "Roboto" ; --md-code-font : "Roboto Mono" }</ style >
< link rel = "stylesheet" href = "../_stylesheets/extra.css" >
< script > __md_scope = new URL ( ".." , location ), __md_hash = e =>[... e ]. reduce ((( e , _ )=>( e << 5 ) - e + _ . charCodeAt ( 0 )), 0 ), __md_get = ( e , _ = localStorage , t = __md_scope )=> JSON . parse ( _ . getItem ( t . pathname + "." + e )), __md_set = ( e , _ , t = localStorage , a = __md_scope )=>{ try { t . setItem ( a . pathname + "." + e , JSON . stringify ( _ ))} catch ( e ){}}</ script >
</ head >
< body dir = "ltr" >
< input class = "md-toggle" data-md-toggle = "drawer" type = "checkbox" id = "__drawer" autocomplete = "off" >
< input class = "md-toggle" data-md-toggle = "search" type = "checkbox" id = "__search" autocomplete = "off" >
< label class = "md-overlay" for = "__drawer" ></ label >
< div data-md-component = "skip" >
< a href = "#companion-protocol" class = "md-skip" >
Skip to content
</ a >
</ div >
< div data-md-component = "announce" >
</ div >
< header class = "md-header md-header--shadow" data-md-component = "header" >
< nav class = "md-header__inner md-grid" aria-label = "Header" >
< a href = ".." title = "MeshCore Docs" class = "md-header__button md-logo" aria-label = "MeshCore Docs" data-md-component = "logo" >
< img src = "../_assets/meshcore.svg" alt = "logo" >
</ a >
< label class = "md-header__button md-icon" for = "__drawer" >
< svg xmlns = "http://www.w3.org/2000/svg" viewBox = "0 0 24 24" >< path d = "M3 6h18v2H3zm0 5h18v2H3zm0 5h18v2H3z" /></ svg >
</ label >
< div class = "md-header__title" data-md-component = "header-title" >
< div class = "md-header__ellipsis" >
< div class = "md-header__topic" >
< span class = "md-ellipsis" >
MeshCore Docs
</ span >
</ div >
< div class = "md-header__topic" data-md-component = "header-topic" >
< span class = "md-ellipsis" >
Companion Protocol
</ span >
</ div >
</ div >
</ div >
< script > var palette = __md_get ( "__palette" ); if ( palette && palette . color ){ if ( "(prefers-color-scheme)" === palette . color . media ){ var media = matchMedia ( "(prefers-color-scheme: light)" ), input = document . querySelector ( media . matches ? "[data-md-color-media='(prefers-color-scheme: light)']" : "[data-md-color-media='(prefers-color-scheme: dark)']" ); palette . color . media = input . getAttribute ( "data-md-color-media" ), palette . color . scheme = input . getAttribute ( "data-md-color-scheme" ), palette . color . primary = input . getAttribute ( "data-md-color-primary" ), palette . color . accent = input . getAttribute ( "data-md-color-accent" )} for ( var [ key , value ] of Object . entries ( palette . color )) document . body . setAttribute ( "data-md-color-" + key , value )}</ script >
< label class = "md-header__button md-icon" for = "__search" >
< svg xmlns = "http://www.w3.org/2000/svg" viewBox = "0 0 24 24" >< path d = "M9.5 3A6.5 6.5 0 0 1 16 9.5c0 1.61-.59 3.09-1.56 4.23l.27.27h.79l5 5-1.5 1.5-5-5v-.79l-.27-.27A6.52 6.52 0 0 1 9.5 16 6.5 6.5 0 0 1 3 9.5 6.5 6.5 0 0 1 9.5 3m0 2C7 5 5 7 5 9.5S7 14 9.5 14 14 12 14 9.5 12 5 9.5 5" /></ svg >
</ label >
< div class = "md-search" data-md-component = "search" role = "dialog" >
< label class = "md-search__overlay" for = "__search" ></ label >
< div class = "md-search__inner" role = "search" >
< form class = "md-search__form" name = "search" >
< input type = "text" class = "md-search__input" name = "query" aria-label = "Search" placeholder = "Search" autocapitalize = "off" autocorrect = "off" autocomplete = "off" spellcheck = "false" data-md-component = "search-query" required >
< label class = "md-search__icon md-icon" for = "__search" >
< svg xmlns = "http://www.w3.org/2000/svg" viewBox = "0 0 24 24" >< path d = "M9.5 3A6.5 6.5 0 0 1 16 9.5c0 1.61-.59 3.09-1.56 4.23l.27.27h.79l5 5-1.5 1.5-5-5v-.79l-.27-.27A6.52 6.52 0 0 1 9.5 16 6.5 6.5 0 0 1 3 9.5 6.5 6.5 0 0 1 9.5 3m0 2C7 5 5 7 5 9.5S7 14 9.5 14 14 12 14 9.5 12 5 9.5 5" /></ svg >
< svg xmlns = "http://www.w3.org/2000/svg" viewBox = "0 0 24 24" >< path d = "M20 11v2H8l5.5 5.5-1.42 1.42L4.16 12l7.92-7.92L13.5 5.5 8 11z" /></ svg >
</ label >
< nav class = "md-search__options" aria-label = "Search" >
< button type = "reset" class = "md-search__icon md-icon" title = "Clear" aria-label = "Clear" tabindex = "-1" >
< svg xmlns = "http://www.w3.org/2000/svg" viewBox = "0 0 24 24" >< path d = "M19 6.41 17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12z" /></ svg >
</ button >
</ nav >
< div class = "md-search__suggest" data-md-component = "search-suggest" ></ div >
</ form >
< div class = "md-search__output" >
< div class = "md-search__scrollwrap" tabindex = "0" data-md-scrollfix >
< div class = "md-search-result" data-md-component = "search-result" >
< div class = "md-search-result__meta" >
Initializing search
</ div >
< ol class = "md-search-result__list" role = "presentation" ></ ol >
</ div >
</ div >
</ div >
</ div >
</ div >
< div class = "md-header__source" >
< a href = "https://github.com/meshcore-dev/meshcore/" title = "Go to repository" class = "md-source" data-md-component = "source" >
< div class = "md-source__icon md-icon" >
< svg xmlns = "http://www.w3.org/2000/svg" viewBox = "0 0 448 512" > <!--! Font Awesome Free 7.1.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) Copyright 2025 Fonticons, Inc.--> < path d = "M439.6 236.1 244 40.5c-5.4-5.5-12.8-8.5-20.4-8.5s-15 3-20.4 8.4L162.5 81l51.5 51.5c27.1-9.1 52.7 16.8 43.4 43.7l49.7 49.7c34.2-11.8 61.2 31 35.5 56.7-26.5 26.5-70.2-2.9-56-37.3L240.3 199v121.9c25.3 12.5 22.3 41.8 9.1 55-6.4 6.4-15.2 10.1-24.3 10.1s-17.8-3.6-24.3-10.1c-17.6-17.6-11.1-46.9 11.2-56v-123c-20.8-8.5-24.6-30.7-18.6-45L142.6 101 8.5 235.1C3 240.6 0 247.9 0 255.5s3 15 8.5 20.4l195.6 195.7c5.4 5.4 12.7 8.4 20.4 8.4s15-3 20.4-8.4l194.7-194.7c5.4-5.4 8.4-12.8 8.4-20.4s-3-15-8.4-20.4" /></ svg >
</ div >
< div class = "md-source__repository" >
meshcore-dev/meshcore
</ div >
</ a >
</ div >
</ nav >
</ header >
< div class = "md-container" data-md-component = "container" >
< main class = "md-main" data-md-component = "main" >
< div class = "md-main__inner md-grid" >
< div class = "md-sidebar md-sidebar--primary" data-md-component = "sidebar" data-md-type = "navigation" >
< div class = "md-sidebar__scrollwrap" >
< div class = "md-sidebar__inner" >
< nav class = "md-nav md-nav--primary" aria-label = "Navigation" data-md-level = "0" >
< label class = "md-nav__title" for = "__drawer" >
< a href = ".." title = "MeshCore Docs" class = "md-nav__button md-logo" aria-label = "MeshCore Docs" data-md-component = "logo" >
< img src = "../_assets/meshcore.svg" alt = "logo" >
</ a >
MeshCore Docs
</ label >
< div class = "md-nav__source" >
< a href = "https://github.com/meshcore-dev/meshcore/" title = "Go to repository" class = "md-source" data-md-component = "source" >
< div class = "md-source__icon md-icon" >
< svg xmlns = "http://www.w3.org/2000/svg" viewBox = "0 0 448 512" > <!--! Font Awesome Free 7.1.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) Copyright 2025 Fonticons, Inc.--> < path d = "M439.6 236.1 244 40.5c-5.4-5.5-12.8-8.5-20.4-8.5s-15 3-20.4 8.4L162.5 81l51.5 51.5c27.1-9.1 52.7 16.8 43.4 43.7l49.7 49.7c34.2-11.8 61.2 31 35.5 56.7-26.5 26.5-70.2-2.9-56-37.3L240.3 199v121.9c25.3 12.5 22.3 41.8 9.1 55-6.4 6.4-15.2 10.1-24.3 10.1s-17.8-3.6-24.3-10.1c-17.6-17.6-11.1-46.9 11.2-56v-123c-20.8-8.5-24.6-30.7-18.6-45L142.6 101 8.5 235.1C3 240.6 0 247.9 0 255.5s3 15 8.5 20.4l195.6 195.7c5.4 5.4 12.7 8.4 20.4 8.4s15-3 20.4-8.4l194.7-194.7c5.4-5.4 8.4-12.8 8.4-20.4s-3-15-8.4-20.4" /></ svg >
</ div >
< div class = "md-source__repository" >
meshcore-dev/meshcore
</ div >
</ a >
</ div >
< ul class = "md-nav__list" data-md-scrollfix >
< li class = "md-nav__item" >
< a href = ".." class = "md-nav__link" >
< span class = "md-ellipsis" >
Introduction
</ span >
</ a >
</ li >
< li class = "md-nav__item" >
< a href = "../cli_commands/" class = "md-nav__link" >
< span class = "md-ellipsis" >
CLI Commands
</ span >
</ a >
</ li >
< li class = "md-nav__item md-nav__item--active" >
< input class = "md-nav__toggle md-toggle" type = "checkbox" id = "__toc" >
< label class = "md-nav__link md-nav__link--active" for = "__toc" >
< span class = "md-ellipsis" >
Companion Protocol
</ span >
< span class = "md-nav__icon md-icon" ></ span >
</ label >
< a href = "./" class = "md-nav__link md-nav__link--active" >
< span class = "md-ellipsis" >
Companion Protocol
</ span >
</ a >
< nav class = "md-nav md-nav--secondary" aria-label = "Table of contents" >
< label class = "md-nav__title" for = "__toc" >
< span class = "md-nav__icon md-icon" ></ span >
Table of contents
</ label >
< ul class = "md-nav__list" data-md-component = "toc" data-md-scrollfix >
< li class = "md-nav__item" >
< a href = "#official-libraries" class = "md-nav__link" >
< span class = "md-ellipsis" >
Official Libraries
</ span >
</ a >
</ li >
< li class = "md-nav__item" >
< a href = "#important-security-note" class = "md-nav__link" >
< span class = "md-ellipsis" >
Important Security Note
</ span >
</ a >
</ li >
< li class = "md-nav__item" >
< a href = "#table-of-contents" class = "md-nav__link" >
< span class = "md-ellipsis" >
Table of Contents
</ span >
</ a >
</ li >
< li class = "md-nav__item" >
< a href = "#ble-connection" class = "md-nav__link" >
< span class = "md-ellipsis" >
BLE Connection
</ span >
</ a >
< nav class = "md-nav" aria-label = "BLE Connection" >
< ul class = "md-nav__list" >
< li class = "md-nav__item" >
< a href = "#service-and-characteristics" class = "md-nav__link" >
< span class = "md-ellipsis" >
Service and Characteristics
</ span >
</ a >
</ li >
< li class = "md-nav__item" >
< a href = "#connection-steps" class = "md-nav__link" >
< span class = "md-ellipsis" >
Connection Steps
</ span >
</ a >
</ li >
< li class = "md-nav__item" >
< a href = "#ble-write-type" class = "md-nav__link" >
< span class = "md-ellipsis" >
BLE Write Type
</ span >
</ a >
</ li >
< li class = "md-nav__item" >
< a href = "#mtu-maximum-transmission-unit" class = "md-nav__link" >
< span class = "md-ellipsis" >
MTU (Maximum Transmission Unit)
</ span >
</ a >
</ li >
< li class = "md-nav__item" >
< a href = "#command-sequencing" class = "md-nav__link" >
< span class = "md-ellipsis" >
Command Sequencing
</ span >
</ a >
</ li >
< li class = "md-nav__item" >
< a href = "#command-queue-management" class = "md-nav__link" >
< span class = "md-ellipsis" >
Command Queue Management
</ span >
</ a >
</ li >
</ ul >
</ nav >
</ li >
< li class = "md-nav__item" >
< a href = "#packet-structure" class = "md-nav__link" >
< span class = "md-ellipsis" >
Packet Structure
</ span >
</ a >
</ li >
< li class = "md-nav__item" >
< a href = "#commands" class = "md-nav__link" >
< span class = "md-ellipsis" >
Commands
</ span >
</ a >
< nav class = "md-nav" aria-label = "Commands" >
< ul class = "md-nav__list" >
< li class = "md-nav__item" >
< a href = "#1-app-start" class = "md-nav__link" >
< span class = "md-ellipsis" >
1. App Start
</ span >
</ a >
</ li >
< li class = "md-nav__item" >
< a href = "#2-device-query" class = "md-nav__link" >
< span class = "md-ellipsis" >
2. Device Query
</ span >
</ a >
</ li >
< li class = "md-nav__item" >
< a href = "#3-get-channel-info" class = "md-nav__link" >
< span class = "md-ellipsis" >
3. Get Channel Info
</ span >
</ a >
</ li >
< li class = "md-nav__item" >
< a href = "#4-set-channel" class = "md-nav__link" >
< span class = "md-ellipsis" >
4. Set Channel
</ span >
</ a >
</ li >
< li class = "md-nav__item" >
< a href = "#5-send-channel-message" class = "md-nav__link" >
< span class = "md-ellipsis" >
5. Send Channel Message
</ span >
</ a >
</ li >
< li class = "md-nav__item" >
< a href = "#6-send-channel-data-datagram" class = "md-nav__link" >
< span class = "md-ellipsis" >
6. Send Channel Data Datagram
</ span >
</ a >
< nav class = "md-nav" aria-label = "6. Send Channel Data Datagram" >
< ul class = "md-nav__list" >
< li class = "md-nav__item" >
< a href = "#registered-data_type-values" class = "md-nav__link" >
< span class = "md-ellipsis" >
Registered data_type values
</ span >
</ a >
</ li >
</ ul >
</ nav >
</ li >
< li class = "md-nav__item" >
< a href = "#receive-channel-data-datagram" class = "md-nav__link" >
< span class = "md-ellipsis" >
Receive Channel Data Datagram
</ span >
</ a >
</ li >
< li class = "md-nav__item" >
< a href = "#7-get-message" class = "md-nav__link" >
< span class = "md-ellipsis" >
7. Get Message
</ span >
</ a >
</ li >
< li class = "md-nav__item" >
< a href = "#8-get-battery-and-storage" class = "md-nav__link" >
< span class = "md-ellipsis" >
8. Get Battery and Storage
</ span >
</ a >
</ li >
</ ul >
</ nav >
</ li >
< li class = "md-nav__item" >
< a href = "#channel-management" class = "md-nav__link" >
< span class = "md-ellipsis" >
Channel Management
</ span >
</ a >
< nav class = "md-nav" aria-label = "Channel Management" >
< ul class = "md-nav__list" >
< li class = "md-nav__item" >
< a href = "#channel-types" class = "md-nav__link" >
< span class = "md-ellipsis" >
Channel Types
</ span >
</ a >
</ li >
< li class = "md-nav__item" >
< a href = "#channel-lifecycle" class = "md-nav__link" >
< span class = "md-ellipsis" >
Channel Lifecycle
</ span >
</ a >
</ li >
</ ul >
</ nav >
</ li >
< li class = "md-nav__item" >
< a href = "#message-handling" class = "md-nav__link" >
< span class = "md-ellipsis" >
Message Handling
</ span >
</ a >
< nav class = "md-nav" aria-label = "Message Handling" >
< ul class = "md-nav__list" >
< li class = "md-nav__item" >
< a href = "#receiving-messages" class = "md-nav__link" >
< span class = "md-ellipsis" >
Receiving Messages
</ span >
</ a >
</ li >
< li class = "md-nav__item" >
< a href = "#contact-message-format" class = "md-nav__link" >
< span class = "md-ellipsis" >
Contact Message Format
</ span >
</ a >
</ li >
< li class = "md-nav__item" >
< a href = "#channel-message-format" class = "md-nav__link" >
< span class = "md-ellipsis" >
Channel Message Format
</ span >
</ a >
</ li >
< li class = "md-nav__item" >
< a href = "#sending-messages" class = "md-nav__link" >
< span class = "md-ellipsis" >
Sending Messages
</ span >
</ a >
</ li >
</ ul >
</ nav >
</ li >
< li class = "md-nav__item" >
< a href = "#response-parsing" class = "md-nav__link" >
< span class = "md-ellipsis" >
Response Parsing
</ span >
</ a >
< nav class = "md-nav" aria-label = "Response Parsing" >
< ul class = "md-nav__list" >
< li class = "md-nav__item" >
< a href = "#terminology" class = "md-nav__link" >
< span class = "md-ellipsis" >
Terminology
</ span >
</ a >
</ li >
< li class = "md-nav__item" >
< a href = "#packet-types" class = "md-nav__link" >
< span class = "md-ellipsis" >
Packet Types
</ span >
</ a >
</ li >
< li class = "md-nav__item" >
< a href = "#parsing-responses" class = "md-nav__link" >
< span class = "md-ellipsis" >
Parsing Responses
</ span >
</ a >
</ li >
< li class = "md-nav__item" >
< a href = "#error-codes" class = "md-nav__link" >
< span class = "md-ellipsis" >
Error Codes
</ span >
</ a >
</ li >
< li class = "md-nav__item" >
< a href = "#frame-handling" class = "md-nav__link" >
< span class = "md-ellipsis" >
Frame Handling
</ span >
</ a >
</ li >
< li class = "md-nav__item" >
< a href = "#response-handling" class = "md-nav__link" >
< span class = "md-ellipsis" >
Response Handling
</ span >
</ a >
</ li >
</ ul >
</ nav >
</ li >
< li class = "md-nav__item" >
< a href = "#example-implementation-flow" class = "md-nav__link" >
< span class = "md-ellipsis" >
Example Implementation Flow
</ span >
</ a >
< nav class = "md-nav" aria-label = "Example Implementation Flow" >
< ul class = "md-nav__list" >
< li class = "md-nav__item" >
< a href = "#initialization" class = "md-nav__link" >
< span class = "md-ellipsis" >
Initialization
</ span >
</ a >
</ li >
< li class = "md-nav__item" >
< a href = "#creating-a-private-channel" class = "md-nav__link" >
< span class = "md-ellipsis" >
Creating a Private Channel
</ span >
</ a >
</ li >
< li class = "md-nav__item" >
< a href = "#sending-a-message" class = "md-nav__link" >
< span class = "md-ellipsis" >
Sending a Message
</ span >
</ a >
</ li >
< li class = "md-nav__item" >
< a href = "#receiving-messages_1" class = "md-nav__link" >
< span class = "md-ellipsis" >
Receiving Messages
</ span >
</ a >
</ li >
</ ul >
</ nav >
</ li >
< li class = "md-nav__item" >
< a href = "#best-practices" class = "md-nav__link" >
< span class = "md-ellipsis" >
Best Practices
</ span >
</ a >
</ li >
< li class = "md-nav__item" >
< a href = "#troubleshooting" class = "md-nav__link" >
< span class = "md-ellipsis" >
Troubleshooting
</ span >
</ a >
< nav class = "md-nav" aria-label = "Troubleshooting" >
< ul class = "md-nav__list" >
< li class = "md-nav__item" >
< a href = "#connection-issues" class = "md-nav__link" >
< span class = "md-ellipsis" >
Connection Issues
</ span >
</ a >
</ li >
< li class = "md-nav__item" >
< a href = "#command-issues" class = "md-nav__link" >
< span class = "md-ellipsis" >
Command Issues
</ span >
</ a >
</ li >
< li class = "md-nav__item" >
< a href = "#message-issues" class = "md-nav__link" >
< span class = "md-ellipsis" >
Message Issues
</ span >
</ a >
</ li >
</ ul >
</ nav >
</ li >
</ ul >
</ nav >
</ li >
< li class = "md-nav__item" >
< a href = "../docs/" class = "md-nav__link" >
< span class = "md-ellipsis" >
Local Documentation
</ span >
</ a >
</ li >
< li class = "md-nav__item" >
< a href = "../faq/" class = "md-nav__link" >
< span class = "md-ellipsis" >
Frequently Asked Questions
</ span >
</ a >
</ li >
< li class = "md-nav__item" >
< a href = "../kiss_modem_protocol/" class = "md-nav__link" >
< span class = "md-ellipsis" >
MeshCore KISS Modem Protocol
</ span >
</ a >
</ li >
< li class = "md-nav__item" >
< a href = "../nrf52_power_management/" class = "md-nav__link" >
< span class = "md-ellipsis" >
nRF52 Power Management
</ span >
</ a >
</ li >
< li class = "md-nav__item" >
< a href = "../number_allocations/" class = "md-nav__link" >
< span class = "md-ellipsis" >
Number Allocations
</ span >
</ a >
</ li >
< li class = "md-nav__item" >
< a href = "../packet_format/" class = "md-nav__link" >
< span class = "md-ellipsis" >
Packet Format
</ span >
</ a >
</ li >
< li class = "md-nav__item" >
< a href = "../payloads/" class = "md-nav__link" >
< span class = "md-ellipsis" >
Payload Format
</ span >
</ a >
</ li >
< li class = "md-nav__item" >
< a href = "../qr_codes/" class = "md-nav__link" >
< span class = "md-ellipsis" >
QR Codes
</ span >
</ a >
</ li >
< li class = "md-nav__item" >
< a href = "../stats_binary_frames/" class = "md-nav__link" >
< span class = "md-ellipsis" >
Stats Binary Frame Structures
</ span >
</ a >
</ li >
< li class = "md-nav__item" >
< a href = "../terminal_chat_cli/" class = "md-nav__link" >
< span class = "md-ellipsis" >
Terminal Chat CLI
</ span >
</ a >
</ li >
< li class = "md-nav__item md-nav__item--nested" >
< input class = "md-nav__toggle md-toggle " type = "checkbox" id = "__nav_14" >
< label class = "md-nav__link" for = "__nav_14" id = "__nav_14_label" tabindex = "0" >
< span class = "md-ellipsis" >
2026-06-14 22:06:34 +00:00
Design
2026-06-11 08:48:18 +00:00
</ span >
< span class = "md-nav__icon md-icon" ></ span >
</ label >
< nav class = "md-nav" data-md-level = "1" aria-labelledby = "__nav_14_label" aria-expanded = "false" >
< label class = "md-nav__title" for = "__nav_14" >
< span class = "md-nav__icon md-icon" ></ span >
2026-06-14 22:06:34 +00:00
Design
</ label >
< ul class = "md-nav__list" data-md-scrollfix >
2026-06-15 07:23:54 +00:00
< li class = "md-nav__item" >
< a href = "../design/nearby_redesign/" class = "md-nav__link" >
< span class = "md-ellipsis" >
Nearby Nodes — analiza i propozycja uporządkowania
</ span >
</ a >
</ li >
2026-06-14 22:06:34 +00:00
< li class = "md-nav__item" >
< a href = "../design/trail_redesign/" class = "md-nav__link" >
< span class = "md-ellipsis" >
Trail (Tools › Trail) — analiza i propozycja uporządkowania
</ span >
</ a >
</ li >
</ ul >
</ nav >
</ li >
< li class = "md-nav__item md-nav__item--nested" >
< input class = "md-nav__toggle md-toggle " type = "checkbox" id = "__nav_15" >
< label class = "md-nav__link" for = "__nav_15" id = "__nav_15_label" tabindex = "0" >
< span class = "md-ellipsis" >
Solo features
</ span >
< span class = "md-nav__icon md-icon" ></ span >
</ label >
< nav class = "md-nav" data-md-level = "1" aria-labelledby = "__nav_15_label" aria-expanded = "false" >
< label class = "md-nav__title" for = "__nav_15" >
< span class = "md-nav__icon md-icon" ></ span >
2026-06-11 08:48:18 +00:00
Solo features
</ label >
< ul class = "md-nav__list" data-md-scrollfix >
< li class = "md-nav__item md-nav__item--nested" >
2026-06-14 22:06:34 +00:00
< input class = "md-nav__toggle md-toggle " type = "checkbox" id = "__nav_15_1" >
2026-06-11 08:48:18 +00:00
2026-06-14 22:06:34 +00:00
< label class = "md-nav__link" for = "__nav_15_1" id = "__nav_15_1_label" tabindex = "0" >
2026-06-11 08:48:18 +00:00
< span class = "md-ellipsis" >
Clock screen
</ span >
< span class = "md-nav__icon md-icon" ></ span >
</ label >
2026-06-14 22:06:34 +00:00
< nav class = "md-nav" data-md-level = "2" aria-labelledby = "__nav_15_1_label" aria-expanded = "false" >
< label class = "md-nav__title" for = "__nav_15_1" >
2026-06-11 08:48:18 +00:00
< span class = "md-nav__icon md-icon" ></ span >
Clock screen
</ label >
< ul class = "md-nav__list" data-md-scrollfix >
< li class = "md-nav__item" >
< a href = "../solo_features/clock_screen/clock_screen/" class = "md-nav__link" >
< span class = "md-ellipsis" >
Clock screen
</ span >
</ a >
</ li >
</ ul >
</ nav >
</ li >
< li class = "md-nav__item md-nav__item--nested" >
2026-06-14 22:06:34 +00:00
< input class = "md-nav__toggle md-toggle " type = "checkbox" id = "__nav_15_2" >
2026-06-11 08:48:18 +00:00
2026-06-14 22:06:34 +00:00
< label class = "md-nav__link" for = "__nav_15_2" id = "__nav_15_2_label" tabindex = "0" >
2026-06-11 08:48:18 +00:00
< span class = "md-ellipsis" >
Favourites dial
</ span >
< span class = "md-nav__icon md-icon" ></ span >
</ label >
2026-06-14 22:06:34 +00:00
< nav class = "md-nav" data-md-level = "2" aria-labelledby = "__nav_15_2_label" aria-expanded = "false" >
< label class = "md-nav__title" for = "__nav_15_2" >
2026-06-11 08:48:18 +00:00
< span class = "md-nav__icon md-icon" ></ span >
Favourites dial
</ label >
< ul class = "md-nav__list" data-md-scrollfix >
< li class = "md-nav__item" >
< a href = "../solo_features/favourites_dial/favourites_dial/" class = "md-nav__link" >
< span class = "md-ellipsis" >
Favourites dial
</ span >
</ a >
</ li >
</ ul >
</ nav >
</ li >
< li class = "md-nav__item md-nav__item--nested" >
2026-06-14 22:06:34 +00:00
< input class = "md-nav__toggle md-toggle " type = "checkbox" id = "__nav_15_3" >
2026-06-11 08:48:18 +00:00
2026-06-14 22:06:34 +00:00
< label class = "md-nav__link" for = "__nav_15_3" id = "__nav_15_3_label" tabindex = "0" >
2026-06-11 08:48:18 +00:00
< span class = "md-ellipsis" >
Message screen
</ span >
< span class = "md-nav__icon md-icon" ></ span >
</ label >
2026-06-14 22:06:34 +00:00
< nav class = "md-nav" data-md-level = "2" aria-labelledby = "__nav_15_3_label" aria-expanded = "false" >
< label class = "md-nav__title" for = "__nav_15_3" >
2026-06-11 08:48:18 +00:00
< span class = "md-nav__icon md-icon" ></ span >
Message screen
</ label >
< ul class = "md-nav__list" data-md-scrollfix >
< li class = "md-nav__item" >
< a href = "../solo_features/message_screen/message_screen/" class = "md-nav__link" >
< span class = "md-ellipsis" >
Message screen
</ span >
</ a >
</ li >
</ ul >
</ nav >
</ li >
< li class = "md-nav__item md-nav__item--nested" >
2026-06-14 22:06:34 +00:00
< input class = "md-nav__toggle md-toggle " type = "checkbox" id = "__nav_15_4" >
2026-06-11 08:48:18 +00:00
2026-06-14 22:06:34 +00:00
< label class = "md-nav__link" for = "__nav_15_4" id = "__nav_15_4_label" tabindex = "0" >
2026-06-11 08:48:18 +00:00
< span class = "md-ellipsis" >
Screen lock
</ span >
< span class = "md-nav__icon md-icon" ></ span >
</ label >
2026-06-14 22:06:34 +00:00
< nav class = "md-nav" data-md-level = "2" aria-labelledby = "__nav_15_4_label" aria-expanded = "false" >
< label class = "md-nav__title" for = "__nav_15_4" >
2026-06-11 08:48:18 +00:00
< span class = "md-nav__icon md-icon" ></ span >
Screen lock
</ label >
< ul class = "md-nav__list" data-md-scrollfix >
< li class = "md-nav__item" >
< a href = "../solo_features/screen_lock/screen_lock/" class = "md-nav__link" >
< span class = "md-ellipsis" >
Screen lock
</ span >
</ a >
</ li >
</ ul >
</ nav >
</ li >
< li class = "md-nav__item md-nav__item--nested" >
2026-06-14 22:06:34 +00:00
< input class = "md-nav__toggle md-toggle " type = "checkbox" id = "__nav_15_5" >
2026-06-11 08:48:18 +00:00
2026-06-14 22:06:34 +00:00
< label class = "md-nav__link" for = "__nav_15_5" id = "__nav_15_5_label" tabindex = "0" >
2026-06-11 08:48:18 +00:00
< span class = "md-ellipsis" >
Settings screen
</ span >
< span class = "md-nav__icon md-icon" ></ span >
</ label >
2026-06-14 22:06:34 +00:00
< nav class = "md-nav" data-md-level = "2" aria-labelledby = "__nav_15_5_label" aria-expanded = "false" >
< label class = "md-nav__title" for = "__nav_15_5" >
2026-06-11 08:48:18 +00:00
< span class = "md-nav__icon md-icon" ></ span >
Settings screen
</ label >
< ul class = "md-nav__list" data-md-scrollfix >
< li class = "md-nav__item" >
< a href = "../solo_features/settings_screen/settings_screen/" class = "md-nav__link" >
< span class = "md-ellipsis" >
Settings screen
</ span >
</ a >
</ li >
</ ul >
</ nav >
</ li >
< li class = "md-nav__item md-nav__item--nested" >
2026-06-14 22:06:34 +00:00
< input class = "md-nav__toggle md-toggle " type = "checkbox" id = "__nav_15_6" >
2026-06-11 08:48:18 +00:00
2026-06-14 22:06:34 +00:00
< label class = "md-nav__link" for = "__nav_15_6" id = "__nav_15_6_label" tabindex = "0" >
2026-06-11 08:48:18 +00:00
< span class = "md-ellipsis" >
Tools screen
</ span >
< span class = "md-nav__icon md-icon" ></ span >
</ label >
2026-06-14 22:06:34 +00:00
< nav class = "md-nav" data-md-level = "2" aria-labelledby = "__nav_15_6_label" aria-expanded = "false" >
< label class = "md-nav__title" for = "__nav_15_6" >
2026-06-11 08:48:18 +00:00
< span class = "md-nav__icon md-icon" ></ span >
Tools screen
</ label >
< ul class = "md-nav__list" data-md-scrollfix >
< li class = "md-nav__item" >
< a href = "../solo_features/tools_screen/tools_screen/" class = "md-nav__link" >
< span class = "md-ellipsis" >
Tools screen
</ span >
</ a >
</ li >
</ ul >
</ nav >
</ li >
</ ul >
</ nav >
</ li >
</ ul >
</ nav >
</ div >
</ div >
</ div >
< div class = "md-sidebar md-sidebar--secondary" data-md-component = "sidebar" data-md-type = "toc" >
< div class = "md-sidebar__scrollwrap" >
< div class = "md-sidebar__inner" >
< nav class = "md-nav md-nav--secondary" aria-label = "Table of contents" >
< label class = "md-nav__title" for = "__toc" >
< span class = "md-nav__icon md-icon" ></ span >
Table of contents
</ label >
< ul class = "md-nav__list" data-md-component = "toc" data-md-scrollfix >
< li class = "md-nav__item" >
< a href = "#official-libraries" class = "md-nav__link" >
< span class = "md-ellipsis" >
Official Libraries
</ span >
</ a >
</ li >
< li class = "md-nav__item" >
< a href = "#important-security-note" class = "md-nav__link" >
< span class = "md-ellipsis" >
Important Security Note
</ span >
</ a >
</ li >
< li class = "md-nav__item" >
< a href = "#table-of-contents" class = "md-nav__link" >
< span class = "md-ellipsis" >
Table of Contents
</ span >
</ a >
</ li >
< li class = "md-nav__item" >
< a href = "#ble-connection" class = "md-nav__link" >
< span class = "md-ellipsis" >
BLE Connection
</ span >
</ a >
< nav class = "md-nav" aria-label = "BLE Connection" >
< ul class = "md-nav__list" >
< li class = "md-nav__item" >
< a href = "#service-and-characteristics" class = "md-nav__link" >
< span class = "md-ellipsis" >
Service and Characteristics
</ span >
</ a >
</ li >
< li class = "md-nav__item" >
< a href = "#connection-steps" class = "md-nav__link" >
< span class = "md-ellipsis" >
Connection Steps
</ span >
</ a >
</ li >
< li class = "md-nav__item" >
< a href = "#ble-write-type" class = "md-nav__link" >
< span class = "md-ellipsis" >
BLE Write Type
</ span >
</ a >
</ li >
< li class = "md-nav__item" >
< a href = "#mtu-maximum-transmission-unit" class = "md-nav__link" >
< span class = "md-ellipsis" >
MTU (Maximum Transmission Unit)
</ span >
</ a >
</ li >
< li class = "md-nav__item" >
< a href = "#command-sequencing" class = "md-nav__link" >
< span class = "md-ellipsis" >
Command Sequencing
</ span >
</ a >
</ li >
< li class = "md-nav__item" >
< a href = "#command-queue-management" class = "md-nav__link" >
< span class = "md-ellipsis" >
Command Queue Management
</ span >
</ a >
</ li >
</ ul >
</ nav >
</ li >
< li class = "md-nav__item" >
< a href = "#packet-structure" class = "md-nav__link" >
< span class = "md-ellipsis" >
Packet Structure
</ span >
</ a >
</ li >
< li class = "md-nav__item" >
< a href = "#commands" class = "md-nav__link" >
< span class = "md-ellipsis" >
Commands
</ span >
</ a >
< nav class = "md-nav" aria-label = "Commands" >
< ul class = "md-nav__list" >
< li class = "md-nav__item" >
< a href = "#1-app-start" class = "md-nav__link" >
< span class = "md-ellipsis" >
1. App Start
</ span >
</ a >
</ li >
< li class = "md-nav__item" >
< a href = "#2-device-query" class = "md-nav__link" >
< span class = "md-ellipsis" >
2. Device Query
</ span >
</ a >
</ li >
< li class = "md-nav__item" >
< a href = "#3-get-channel-info" class = "md-nav__link" >
< span class = "md-ellipsis" >
3. Get Channel Info
</ span >
</ a >
</ li >
< li class = "md-nav__item" >
< a href = "#4-set-channel" class = "md-nav__link" >
< span class = "md-ellipsis" >
4. Set Channel
</ span >
</ a >
</ li >
< li class = "md-nav__item" >
< a href = "#5-send-channel-message" class = "md-nav__link" >
< span class = "md-ellipsis" >
5. Send Channel Message
</ span >
</ a >
</ li >
< li class = "md-nav__item" >
< a href = "#6-send-channel-data-datagram" class = "md-nav__link" >
< span class = "md-ellipsis" >
6. Send Channel Data Datagram
</ span >
</ a >
< nav class = "md-nav" aria-label = "6. Send Channel Data Datagram" >
< ul class = "md-nav__list" >
< li class = "md-nav__item" >
< a href = "#registered-data_type-values" class = "md-nav__link" >
< span class = "md-ellipsis" >
Registered data_type values
</ span >
</ a >
</ li >
</ ul >
</ nav >
</ li >
< li class = "md-nav__item" >
< a href = "#receive-channel-data-datagram" class = "md-nav__link" >
< span class = "md-ellipsis" >
Receive Channel Data Datagram
</ span >
</ a >
</ li >
< li class = "md-nav__item" >
< a href = "#7-get-message" class = "md-nav__link" >
< span class = "md-ellipsis" >
7. Get Message
</ span >
</ a >
</ li >
< li class = "md-nav__item" >
< a href = "#8-get-battery-and-storage" class = "md-nav__link" >
< span class = "md-ellipsis" >
8. Get Battery and Storage
</ span >
</ a >
</ li >
</ ul >
</ nav >
</ li >
< li class = "md-nav__item" >
< a href = "#channel-management" class = "md-nav__link" >
< span class = "md-ellipsis" >
Channel Management
</ span >
</ a >
< nav class = "md-nav" aria-label = "Channel Management" >
< ul class = "md-nav__list" >
< li class = "md-nav__item" >
< a href = "#channel-types" class = "md-nav__link" >
< span class = "md-ellipsis" >
Channel Types
</ span >
</ a >
</ li >
< li class = "md-nav__item" >
< a href = "#channel-lifecycle" class = "md-nav__link" >
< span class = "md-ellipsis" >
Channel Lifecycle
</ span >
</ a >
</ li >
</ ul >
</ nav >
</ li >
< li class = "md-nav__item" >
< a href = "#message-handling" class = "md-nav__link" >
< span class = "md-ellipsis" >
Message Handling
</ span >
</ a >
< nav class = "md-nav" aria-label = "Message Handling" >
< ul class = "md-nav__list" >
< li class = "md-nav__item" >
< a href = "#receiving-messages" class = "md-nav__link" >
< span class = "md-ellipsis" >
Receiving Messages
</ span >
</ a >
</ li >
< li class = "md-nav__item" >
< a href = "#contact-message-format" class = "md-nav__link" >
< span class = "md-ellipsis" >
Contact Message Format
</ span >
</ a >
</ li >
< li class = "md-nav__item" >
< a href = "#channel-message-format" class = "md-nav__link" >
< span class = "md-ellipsis" >
Channel Message Format
</ span >
</ a >
</ li >
< li class = "md-nav__item" >
< a href = "#sending-messages" class = "md-nav__link" >
< span class = "md-ellipsis" >
Sending Messages
</ span >
</ a >
</ li >
</ ul >
</ nav >
</ li >
< li class = "md-nav__item" >
< a href = "#response-parsing" class = "md-nav__link" >
< span class = "md-ellipsis" >
Response Parsing
</ span >
</ a >
< nav class = "md-nav" aria-label = "Response Parsing" >
< ul class = "md-nav__list" >
< li class = "md-nav__item" >
< a href = "#terminology" class = "md-nav__link" >
< span class = "md-ellipsis" >
Terminology
</ span >
</ a >
</ li >
< li class = "md-nav__item" >
< a href = "#packet-types" class = "md-nav__link" >
< span class = "md-ellipsis" >
Packet Types
</ span >
</ a >
</ li >
< li class = "md-nav__item" >
< a href = "#parsing-responses" class = "md-nav__link" >
< span class = "md-ellipsis" >
Parsing Responses
</ span >
</ a >
</ li >
< li class = "md-nav__item" >
< a href = "#error-codes" class = "md-nav__link" >
< span class = "md-ellipsis" >
Error Codes
</ span >
</ a >
</ li >
< li class = "md-nav__item" >
< a href = "#frame-handling" class = "md-nav__link" >
< span class = "md-ellipsis" >
Frame Handling
</ span >
</ a >
</ li >
< li class = "md-nav__item" >
< a href = "#response-handling" class = "md-nav__link" >
< span class = "md-ellipsis" >
Response Handling
</ span >
</ a >
</ li >
</ ul >
</ nav >
</ li >
< li class = "md-nav__item" >
< a href = "#example-implementation-flow" class = "md-nav__link" >
< span class = "md-ellipsis" >
Example Implementation Flow
</ span >
</ a >
< nav class = "md-nav" aria-label = "Example Implementation Flow" >
< ul class = "md-nav__list" >
< li class = "md-nav__item" >
< a href = "#initialization" class = "md-nav__link" >
< span class = "md-ellipsis" >
Initialization
</ span >
</ a >
</ li >
< li class = "md-nav__item" >
< a href = "#creating-a-private-channel" class = "md-nav__link" >
< span class = "md-ellipsis" >
Creating a Private Channel
</ span >
</ a >
</ li >
< li class = "md-nav__item" >
< a href = "#sending-a-message" class = "md-nav__link" >
< span class = "md-ellipsis" >
Sending a Message
</ span >
</ a >
</ li >
< li class = "md-nav__item" >
< a href = "#receiving-messages_1" class = "md-nav__link" >
< span class = "md-ellipsis" >
Receiving Messages
</ span >
</ a >
</ li >
</ ul >
</ nav >
</ li >
< li class = "md-nav__item" >
< a href = "#best-practices" class = "md-nav__link" >
< span class = "md-ellipsis" >
Best Practices
</ span >
</ a >
</ li >
< li class = "md-nav__item" >
< a href = "#troubleshooting" class = "md-nav__link" >
< span class = "md-ellipsis" >
Troubleshooting
</ span >
</ a >
< nav class = "md-nav" aria-label = "Troubleshooting" >
< ul class = "md-nav__list" >
< li class = "md-nav__item" >
< a href = "#connection-issues" class = "md-nav__link" >
< span class = "md-ellipsis" >
Connection Issues
</ span >
</ a >
</ li >
< li class = "md-nav__item" >
< a href = "#command-issues" class = "md-nav__link" >
< span class = "md-ellipsis" >
Command Issues
</ span >
</ a >
</ li >
< li class = "md-nav__item" >
< a href = "#message-issues" class = "md-nav__link" >
< span class = "md-ellipsis" >
Message Issues
</ span >
</ a >
</ li >
</ ul >
</ nav >
</ li >
</ ul >
</ nav >
</ div >
</ div >
</ div >
< div class = "md-content" data-md-component = "content" >
< article class = "md-content__inner md-typeset" >
< a href = "https://github.com/meshcore-dev/meshcore/edit/main/docs/companion_protocol.md" title = "Edit this page" class = "md-content__button md-icon" rel = "edit" >
< svg xmlns = "http://www.w3.org/2000/svg" viewBox = "0 0 24 24" >< path d = "M10 20H6V4h7v5h5v3.1l2-2V8l-6-6H6c-1.1 0-2 .9-2 2v16c0 1.1.9 2 2 2h4zm10.2-7c.1 0 .3.1.4.2l1.3 1.3c.2.2.2.6 0 .8l-1 1-2.1-2.1 1-1c.1-.1.2-.2.4-.2m0 3.9L14.1 23H12v-2.1l6.1-6.1z" /></ svg >
</ a >
< h1 id = "companion-protocol" > Companion Protocol</ h1 >
< ul >
< li >< strong > Last Updated</ strong > : 2026-03-08</ li >
< li >< strong > Protocol Version</ strong > : Companion Firmware v1.12.0+</ li >
</ ul >
< blockquote >
< p > NOTE: This document is still in development. Some information may be inaccurate.</ p >
</ blockquote >
< p > This document provides a comprehensive guide for communicating with MeshCore devices over Bluetooth Low Energy (BLE).</ p >
< p > It is platform-agnostic and can be used for Android, iOS, Python, JavaScript, or any other platform that supports BLE.</ p >
< h2 id = "official-libraries" > Official Libraries</ h2 >
< p > Please see the following repos for existing MeshCore Companion Protocol libraries.</ p >
< ul >
< li > JavaScript: < a href = "https://github.com/meshcore-dev/meshcore.js" > https://github.com/meshcore-dev/meshcore.js</ a ></ li >
< li > Python: < a href = "https://github.com/meshcore-dev/meshcore_py" > https://github.com/meshcore-dev/meshcore_py</ a ></ li >
</ ul >
< h2 id = "important-security-note" > Important Security Note</ h2 >
< p > All secrets, hashes, and cryptographic values shown in this guide are example values only.</ p >
< ul >
< li > All hex values, public keys and hashes are for demonstration purposes only</ li >
< li > Never use example secrets in production</ li >
< li > Always generate new cryptographically secure random secrets</ li >
< li > Please implement proper security practices in your implementation</ li >
< li > This guide is for protocol documentation only</ li >
</ ul >
< h2 id = "table-of-contents" > Table of Contents</ h2 >
< ol >
< li >< a href = "#ble-connection" > BLE Connection</ a ></ li >
< li >< a href = "#packet-structure" > Packet Structure</ a ></ li >
< li >< a href = "#commands" > Commands</ a ></ li >
< li >< a href = "#channel-management" > Channel Management</ a ></ li >
< li >< a href = "#message-handling" > Message Handling</ a ></ li >
< li >< a href = "#response-parsing" > Response Parsing</ a ></ li >
< li >< a href = "#example-implementation-flow" > Example Implementation Flow</ a ></ li >
< li >< a href = "#best-practices" > Best Practices</ a ></ li >
< li >< a href = "#troubleshooting" > Troubleshooting</ a ></ li >
</ ol >
< hr />
< h2 id = "ble-connection" > BLE Connection</ h2 >
< h3 id = "service-and-characteristics" > Service and Characteristics</ h3 >
< p > MeshCore Companion devices expose a BLE service with the following UUIDs:</ p >
< ul >
< li >< strong > Service UUID</ strong > : < code > 6E400001-B5A3-F393-E0A9-E50E24DCCA9E</ code ></ li >
< li >< strong > RX Characteristic</ strong > (App → Firmware): < code > 6E400002-B5A3-F393-E0A9-E50E24DCCA9E</ code ></ li >
< li >< strong > TX Characteristic</ strong > (Firmware → App): < code > 6E400003-B5A3-F393-E0A9-E50E24DCCA9E</ code ></ li >
</ ul >
< h3 id = "connection-steps" > Connection Steps</ h3 >
< ol >
< li >
< p >< strong > Scan for Devices</ strong ></ p >
< ul >
< li > Scan for BLE devices advertising the MeshCore Service UUID</ li >
< li > Optionally filter by device name (typically contains "MeshCore" prefix)</ li >
< li > Note the device MAC address for reconnection</ li >
</ ul >
</ li >
< li >
< p >< strong > Connect to GATT</ strong ></ p >
< ul >
< li > Connect to the device using the discovered MAC address</ li >
< li > Wait for connection to be established</ li >
</ ul >
</ li >
< li >
< p >< strong > Discover Services and Characteristics</ strong ></ p >
< ul >
< li > Discover the service with UUID < code > 6E400001-B5A3-F393-E0A9-E50E24DCCA9E</ code ></ li >
< li > Discover the RX characteristic < code > 6E400002-B5A3-F393-E0A9-E50E24DCCA9E</ code >< ul >
< li > Your app writes to this, the firmware reads from this</ li >
</ ul >
</ li >
< li > Discover the TX characteristic < code > 6E400003-B5A3-F393-E0A9-E50E24DCCA9E</ code >< ul >
< li > The firmware writes to this, your app reads from this</ li >
</ ul >
</ li >
</ ul >
</ li >
< li >
< p >< strong > Enable Notifications</ strong ></ p >
< ul >
< li > Subscribe to notifications on the TX characteristic to receive data from the firmware</ li >
</ ul >
</ li >
< li >
< p >< strong > Send Initial Commands</ strong ></ p >
< ul >
< li > Send < code > CMD_APP_START</ code > to identify your app to firmware and get radio settings</ li >
< li > Send < code > CMD_DEVICE_QUERY</ code > to fetch device info and negotiate supported protocol versions</ li >
< li > Send < code > CMD_SET_DEVICE_TIME</ code > to set the firmware clock</ li >
< li > Send < code > CMD_GET_CONTACTS</ code > to fetch all contacts</ li >
< li > Send < code > CMD_GET_CHANNEL</ code > multiple times to fetch all channel slots</ li >
< li > Send < code > CMD_SYNC_NEXT_MESSAGE</ code > to fetch the next message stored in firmware</ li >
< li > Setup listeners for push codes, such as < code > PUSH_CODE_MSG_WAITING</ code > or < code > PUSH_CODE_ADVERT</ code ></ li >
< li > See < a href = "#commands" > Commands</ a > section for information on other commands</ li >
</ ul >
</ li >
</ ol >
< p >< strong > Note</ strong > : MeshCore devices may disconnect after periods of inactivity. Implement auto-reconnect logic with exponential backoff.</ p >
< h3 id = "ble-write-type" > BLE Write Type</ h3 >
< p > When writing commands to the RX characteristic, specify the write type:</ p >
< ul >
< li >< strong > Write with Response</ strong > (default): Waits for acknowledgment from device</ li >
< li >< strong > Write without Response</ strong > : Faster but no acknowledgment</ li >
</ ul >
< p >< strong > Platform-specific</ strong > :</ p >
< ul >
< li >< strong > Android</ strong > : Use < code > BluetoothGattCharacteristic.WRITE_TYPE_DEFAULT</ code > or < code > WRITE_TYPE_NO_RESPONSE</ code ></ li >
< li >< strong > iOS</ strong > : Use < code > CBCharacteristicWriteType.withResponse</ code > or < code > .withoutResponse</ code ></ li >
< li >< strong > Python (bleak)</ strong > : Use < code > write_gatt_char()</ code > with < code > response=True</ code > or < code > False</ code ></ li >
</ ul >
< p >< strong > Recommendation</ strong > : Use write with response for reliability.</ p >
< h3 id = "mtu-maximum-transmission-unit" > MTU (Maximum Transmission Unit)</ h3 >
< p > The default BLE MTU is 23 bytes (20 bytes payload). For larger commands like < code > SET_CHANNEL</ code > (50 bytes), you may need to:</ p >
< ol >
< li >< strong > Request Larger MTU</ strong > : Request MTU of 512 bytes if supported< ul >
< li > Android: < code > gatt.requestMtu(512)</ code ></ li >
< li > iOS: < code > peripheral.maximumWriteValueLength(for:)</ code ></ li >
< li > Python (bleak): MTU is negotiated automatically</ li >
</ ul >
</ li >
</ ol >
< h3 id = "command-sequencing" > Command Sequencing</ h3 >
< p >< strong > Critical</ strong > : Commands must be sent in the correct sequence:</ p >
< ol >
< li >
< p >< strong > After Connection</ strong > :</ p >
< ul >
< li > Wait for BLE connection to be established</ li >
< li > Wait for services/characteristics to be discovered</ li >
< li > Wait for notifications to be enabled</ li >
< li > Now you can safely send commands to the firmware</ li >
</ ul >
</ li >
< li >
< p >< strong > Command-Response Matching</ strong > :</ p >
< ul >
< li > Send one command at a time</ li >
< li > Wait for a response before sending another command</ li >
< li > Use a timeout (typically 5 seconds)</ li >
< li > Match response to command by type (e.g: < code > CMD_GET_CHANNEL</ code > → < code > RESP_CODE_CHANNEL_INFO</ code > )</ li >
</ ul >
</ li >
</ ol >
< h3 id = "command-queue-management" > Command Queue Management</ h3 >
< p > For reliable operation, implement a command queue.</ p >
< p >< strong > Queue Structure</ strong > :</ p >
< ul >
< li > Maintain a queue of pending commands</ li >
< li > Track which command is currently waiting for a response</ li >
< li > Only send next command after receiving response or timeout</ li >
</ ul >
< p >< strong > Error Handling</ strong > :</ p >
< ul >
< li > On timeout, clear current command, process next in queue</ li >
< li > On error, log error, clear current command, process next</ li >
</ ul >
< hr />
< h2 id = "packet-structure" > Packet Structure</ h2 >
< p > The MeshCore protocol uses a binary format with the following structure:</ p >
< ul >
< li >< strong > Commands</ strong > : Sent from app to firmware via RX characteristic</ li >
< li >< strong > Responses</ strong > : Received from firmware via TX characteristic notifications</ li >
< li >< strong > All multi-byte integers</ strong > : Little-endian byte order (except CayenneLPP which is Big-endian)</ li >
< li >< strong > All strings</ strong > : UTF-8 encoding</ li >
</ ul >
< p > Most packets follow this format:</ p >
< pre >< code > [Packet Type (1 byte)] [Data (variable length)]
</ code ></ pre >
< p > The first byte indicates the packet type (see < a href = "#response-parsing" > Response Parsing</ a > ).</ p >
< hr />
< h2 id = "commands" > Commands</ h2 >
< h3 id = "1-app-start" > 1. App Start</ h3 >
< p >< strong > Purpose</ strong > : Initialize communication with the device. Must be sent first after connection.</ p >
< p >< strong > Command Format</ strong > :</ p >
< pre >< code > Byte 0: 0x01
Bytes 1-7: Reserved (currently ignored by firmware)
Bytes 8+: Application name (UTF-8, optional)
</ code ></ pre >
< p >< strong > Example</ strong > (hex):</ p >
< pre >< code > 01 00 00 00 00 00 00 00 6d 63 63 6c 69
</ code ></ pre >
< p >< strong > Response</ strong > : < code > PACKET_SELF_INFO</ code > (0x05)</ p >
< hr />
< h3 id = "2-device-query" > 2. Device Query</ h3 >
< p >< strong > Purpose</ strong > : Query device information.</ p >
< p >< strong > Command Format</ strong > :</ p >
< pre >< code > Byte 0: 0x16
Byte 1: 0x03
</ code ></ pre >
< p >< strong > Example</ strong > (hex):</ p >
< pre >< code > 16 03
</ code ></ pre >
< p >< strong > Response</ strong > : < code > PACKET_DEVICE_INFO</ code > (0x0D) with device information</ p >
< hr />
< h3 id = "3-get-channel-info" > 3. Get Channel Info</ h3 >
< p >< strong > Purpose</ strong > : Retrieve information about a specific channel.</ p >
< p >< strong > Command Format</ strong > :</ p >
< pre >< code > Byte 0: 0x1F
Byte 1: Channel Index (0-7)
</ code ></ pre >
< p >< strong > Example</ strong > (get channel 1):</ p >
< pre >< code > 1F 01
</ code ></ pre >
< p >< strong > Response</ strong > : < code > PACKET_CHANNEL_INFO</ code > (0x12) with channel details</ p >
< hr />
< h3 id = "4-set-channel" > 4. Set Channel</ h3 >
< p >< strong > Purpose</ strong > : Create or update a channel on the device.</ p >
< p >< strong > Command Format</ strong > :</ p >
< pre >< code > Byte 0: 0x20
Byte 1: Channel Index (0-7)
Bytes 2-33: Channel Name (32 bytes, UTF-8, null-padded)
Bytes 34-49: Secret (16 bytes)
</ code ></ pre >
< p >< strong > Total Length</ strong > : 50 bytes</ p >
< p >< strong > Channel Index</ strong > :
- Index 0: Reserved for public channels (no secret)
- Indices 1-7: Available for private channels</ p >
< p >< strong > Channel Name</ strong > :
- UTF-8 encoded
- Maximum 32 bytes
- Padded with null bytes (0x00) if shorter</ p >
< p >< strong > Secret Field</ strong > (16 bytes):
- For < strong > private channels</ strong > : 16-byte secret
- For < strong > public channels</ strong > : All zeros (0x00)</ p >
< p >< strong > Example</ strong > (create channel "YourChannelName" at index 1 with secret):</ p >
< pre >< code > 20 01 53 4D 53 00 00 ... (name padded to 32 bytes)
[16 bytes of secret]
</ code ></ pre >
< p >< strong > Note</ strong > : The 32-byte secret variant is unsupported and returns < code > PACKET_ERROR</ code > .</ p >
< p >< strong > Response</ strong > : < code > PACKET_OK</ code > (0x00) on success, < code > PACKET_ERROR</ code > (0x01) on failure</ p >
< hr />
< h3 id = "5-send-channel-message" > 5. Send Channel Message</ h3 >
< p >< strong > Purpose</ strong > : Send a text message to a channel.</ p >
< p >< strong > Command Format</ strong > :</ p >
< pre >< code > Byte 0: 0x03
Byte 1: 0x00
Byte 2: Channel Index (0-7)
Bytes 3-6: Timestamp (32-bit little-endian Unix timestamp, seconds)
Bytes 7+: Message Text (UTF-8, variable length)
</ code ></ pre >
< p >< strong > Timestamp</ strong > : Unix timestamp in seconds (32-bit unsigned integer, little-endian)</ p >
< p >< strong > Example</ strong > (send "Hello" to channel 1 at timestamp 1234567890):</ p >
< pre >< code > 03 00 01 D2 02 96 49 48 65 6C 6C 6F
</ code ></ pre >
< p >< strong > Response</ strong > : < code > PACKET_MSG_SENT</ code > (0x06) on success</ p >
< hr />
< h3 id = "6-send-channel-data-datagram" > 6. Send Channel Data Datagram</ h3 >
< p >< strong > Purpose</ strong > : Send a binary datagram to a channel. Unlike channel text messages, datagrams carry no built-in sender identity and no timestamp — applications needing either must encode them inside the binary payload.</ p >
< p >< strong > Command Format</ strong > :</ p >
< pre >< code > Byte 0: 0x3E
Byte 1: Channel Index (0-7)
Byte 2: Path Length (0xFF = flood, otherwise actual path length)
Bytes 3 .. 2+path_len: Path (omitted when path_len == 0xFF)
Next 2 bytes (little-endian): Data Type (`data_type`, uint16)
Remaining bytes: Binary payload (variable length)
</ code ></ pre >
< p >< strong > Example</ strong > (flood, < code > DATA_TYPE_DEV</ code > , payload < code > A1 B2 C3</ code > , channel 1):</ p >
< pre >< code > 3E 01 FF FF FF A1 B2 C3
</ code ></ pre >
< p >< strong > Data Type / Transport Mapping</ strong > :
- < code > 0x0000</ code > (< code > DATA_TYPE_RESERVED</ code > ) is invalid and rejected with < code > PACKET_ERROR</ code > .
- < code > 0xFFFF</ code > (< code > DATA_TYPE_DEV</ code > ) is the developer namespace for experimenting and developing apps.
- Values < code > 0x0001</ code > – < code > 0xFFFE</ code > are available for registered application/community namespaces. See the < a href = "#registered-data_type-values" > Registered data_type values</ a > table below.</ p >
< p >< strong > Limits</ strong > :
- Maximum payload length is < code > MAX_CHANNEL_DATA_LENGTH = MAX_FRAME_SIZE - 9 = 163</ code > bytes.
- Larger payloads are rejected with < code > PACKET_ERROR</ code > (< code > ERR_CODE_ILLEGAL_ARG</ code > ).</ p >
< p >< strong > Response</ strong > : < code > PACKET_OK</ code > (0x00) on success, or < code > PACKET_ERROR</ code > (0x01) with one of:
- < code > ERR_CODE_NOT_FOUND</ code > (2) — unknown < code > channel_idx</ code >
- < code > ERR_CODE_ILLEGAL_ARG</ code > (6) — invalid < code > path_len</ code > , reserved < code > data_type</ code > (< code > 0x0000</ code > ), or payload larger than < code > MAX_CHANNEL_DATA_LENGTH</ code >
- < code > ERR_CODE_TABLE_FULL</ code > (3) — outbound send queue is full; retry later</ p >
< p >< strong > Inbound datagrams</ strong > are delivered to the host via < code > RESP_CODE_CHANNEL_DATA_RECV</ code > (0x1B); see < a href = "#receive-channel-data-datagram" > Receive Channel Data Datagram</ a > .</ p >
< h4 id = "registered-data_type-values" > Registered < code > data_type</ code > values</ h4 >
< p >< code > data_type</ code > is an < strong > application identifier</ strong > , not a payload-format identifier. Each registered value identifies an application that owns its own internal payload schemas. The firmware does not inspect payload contents — < code > data_type</ code > is transported opaquely.</ p >
< table >
< thead >
< tr >
< th > Value</ th >
< th > Constant</ th >
< th > Purpose</ th >
</ tr >
</ thead >
< tbody >
< tr >
< td > 0x0000</ td >
< td >< code > DATA_TYPE_RESERVED</ code ></ td >
< td > Reserved; invalid on send</ td >
</ tr >
< tr >
< td > 0x0001 – 0x00FF</ td >
< td > —</ td >
< td > Reserved for internal use</ td >
</ tr >
< tr >
< td > 0x0100 – 0xFEFF</ td >
< td > —</ td >
< td > Registered application namespaces (see < a href = "../number_allocations/" > number_allocations.md</ a > )</ td >
</ tr >
< tr >
< td > 0xFF00 – 0xFFFE</ td >
< td > —</ td >
< td > Testing/development; no registration required</ td >
</ tr >
< tr >
< td > 0xFFFF</ td >
< td >< code > DATA_TYPE_DEV</ code ></ td >
< td > Developer/experimental namespace</ td >
</ tr >
</ tbody >
</ table >
< p > To register a new application, submit a PR adding a row to the table in < a href = "../number_allocations/" > docs/number_allocations.md</ a > . Internal sub-formats within an allocated application ID are owned by that application and are not tracked in MeshCore firmware or this document.</ p >
< hr />
< h3 id = "receive-channel-data-datagram" > Receive Channel Data Datagram</ h3 >
< p > Inbound group datagrams (radio-level < code > PAYLOAD_TYPE_GRP_DATA</ code > , 0x06) are forwarded to the host as < code > RESP_CODE_CHANNEL_DATA_RECV</ code > notifications.</ p >
< p >< strong > Frame Format</ strong > (< code > RESP_CODE_CHANNEL_DATA_RECV</ code > , 0x1B):</ p >
< pre >< code > Byte 0: 0x1B (packet type)
Byte 1: SNR (signed int8, scaled × 4 — divide by 4.0 to recover dB)
Bytes 2-3: Reserved (clients MUST ignore)
Byte 4: Channel Index (0-7)
Byte 5: Path Length (actual path length when flooded, otherwise 0xFF for direct)
Bytes 6-7: Data Type (uint16 little-endian)
Byte 8: Data Length
Bytes 9 .. 8+data_len: Payload
</ code ></ pre >
< p >< strong > Path bytes are not forwarded</ strong > : Only < code > path_len</ code > is reported in the receive frame — the path itself is not copied to the host. There are no path bytes between byte 5 and the data_type field at bytes 6– 7, regardless of < code > path_len</ code > .</ p >
< p >< strong > Path Length semantics differ between send and receive</ strong > :</ p >
< table >
< thead >
< tr >
< th > Direction</ th >
< th >< code > path_len = 0xFF</ code ></ th >
< th >< code > path_len ≠ 0xFF</ code ></ th >
</ tr >
</ thead >
< tbody >
< tr >
< td > Send</ td >
< td > Flood the network</ td >
< td > Direct route; the encoded path follows (low 6 bits = hash count, top 2 bits + 1 = hash size; on-wire byte count = < code > hash_count × hash_size</ code > )</ td >
</ tr >
< tr >
< td > Receive</ td >
< td > Packet arrived via direct route</ td >
< td > Packet was flooded; this is the encoded < code > pkt-> path_len</ code > field as observed (no path bytes follow)</ td >
</ tr >
</ tbody >
</ table >
< p > In other words, the meaning of < code > 0xFF</ code > is inverted between the two directions, and on receive the field carries metadata only — never a routable path. < code > path_len</ code > is an encoded byte (see < code > Packet::isValidPathLen</ code > / < code > Packet::writePath</ code > in < code > src/Packet.cpp</ code > ), not a raw byte count.</ p >
< p >< strong > Note</ strong > : The device may also emit < code > PACKET_MESSAGES_WAITING</ code > (0x83) to notify the host that datagrams are queued; poll with < code > CMD_SYNC_NEXT_MESSAGE</ code > (0x0A) to retrieve them.</ p >
< p >< strong > Parsing Pseudocode</ strong > :</ p >
< pre >< code class = "language-python" > 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),
}
</ code ></ pre >
< hr />
< h3 id = "7-get-message" > 7. Get Message</ h3 >
< p >< strong > Purpose</ strong > : Request the next queued message from the device.</ p >
< p >< strong > Command Format</ strong > :</ p >
< pre >< code > Byte 0: 0x0A
</ code ></ pre >
< p >< strong > Example</ strong > (hex):</ p >
< pre >< code > 0A
</ code ></ pre >
< p >< strong > Response</ strong > :
- < code > PACKET_CHANNEL_MSG_RECV</ code > (0x08) or < code > PACKET_CHANNEL_MSG_RECV_V3</ code > (0x11) for channel messages
- < code > PACKET_CONTACT_MSG_RECV</ code > (0x07) or < code > PACKET_CONTACT_MSG_RECV_V3</ code > (0x10) for contact messages
- < code > PACKET_CHANNEL_DATA_RECV</ code > (0x1B) for channel data datagrams
- < code > PACKET_NO_MORE_MSGS</ code > (0x0A) if no messages available</ p >
< p >< strong > Note</ strong > : Poll this command periodically to retrieve queued messages. The device may also send < code > PACKET_MESSAGES_WAITING</ code > (0x83) as a notification when messages are available.</ p >
< hr />
< h3 id = "8-get-battery-and-storage" > 8. Get Battery and Storage</ h3 >
< p >< strong > Purpose</ strong > : Query device battery voltage and storage usage.</ p >
< p >< strong > Command Format</ strong > :</ p >
< pre >< code > Byte 0: 0x14
</ code ></ pre >
< p >< strong > Example</ strong > (hex):</ p >
< pre >< code > 14
</ code ></ pre >
< p >< strong > Response</ strong > : < code > PACKET_BATTERY</ code > (0x0C) with battery millivolts and storage information</ p >
< hr />
< h2 id = "channel-management" > Channel Management</ h2 >
< h3 id = "channel-types" > Channel Types</ h3 >
< ol >
< li >< strong > Public Channel</ strong >< ul >
< li > Uses a publicly known 16-byte key: < code > 8b3387e9c5cdea6ac9e5edbaa115cd72</ code ></ li >
< li > Anyone can join this channel, messages should be considered public</ li >
< li > Used as the default public group chat</ li >
</ ul >
</ li >
< li >< strong > Hashtag Channels</ strong >< ul >
< li > Uses a secret key derived from the channel name</ li >
< li > It is the first 16 bytes of < code > sha256("#test")</ code ></ li >
< li > For example hashtag channel < code > #test</ code > has the key: < code > 9cd8fcf22a47333b591d96a2b848b73f</ code ></ li >
< li > Used as a topic based public group chat, separate from the default public channel</ li >
</ ul >
</ li >
< li >< strong > Private Channels</ strong >< ul >
< li > Uses a randomly generated 16-byte secret key</ li >
< li > Messages should be considered private between those that know the secret</ li >
< li > Users should keep the key secret, and only share with those you want to communicate with</ li >
< li > Used as a secure private group chat</ li >
</ ul >
</ li >
</ ol >
< h3 id = "channel-lifecycle" > Channel Lifecycle</ h3 >
< ol >
< li >< strong > Set Channel</ strong > :< ul >
< li > Fetch all channel slots, and find one with empty name and all-zero secret</ li >
< li > Generate or provide a 16-byte secret</ li >
< li > Send < code > CMD_SET_CHANNEL</ code > with name and a 16-byte secret</ li >
</ ul >
</ li >
< li >< strong > Get Channel</ strong > :< ul >
< li > Send < code > CMD_GET_CHANNEL</ code > with channel index</ li >
< li > Parse < code > RESP_CODE_CHANNEL_INFO</ code > response</ li >
</ ul >
</ li >
< li >< strong > Delete Channel</ strong > :< ul >
< li > Send < code > CMD_SET_CHANNEL</ code > with empty name and all-zero secret</ li >
< li > Or overwrite with a new channel</ li >
</ ul >
</ li >
</ ol >
< hr />
< h2 id = "message-handling" > Message Handling</ h2 >
< h3 id = "receiving-messages" > Receiving Messages</ h3 >
< p > Messages are received via the TX characteristic (notifications). The device sends:</ p >
< ol >
< li >< strong > Channel Messages</ strong > :</ li >
< li >< code > PACKET_CHANNEL_MSG_RECV</ code > (0x08) - Standard format</ li >
< li >
< p >< code > PACKET_CHANNEL_MSG_RECV_V3</ code > (0x11) - Version 3 with SNR</ p >
</ li >
< li >
< p >< strong > Contact Messages</ strong > :</ p >
</ li >
< li >< code > PACKET_CONTACT_MSG_RECV</ code > (0x07) - Standard format</ li >
< li >
< p >< code > PACKET_CONTACT_MSG_RECV_V3</ code > (0x10) - Version 3 with SNR</ p >
</ li >
< li >
< p >< strong > Notifications</ strong > :</ p >
</ li >
< li >< code > PACKET_MESSAGES_WAITING</ code > (0x83) - Indicates messages are queued</ li >
</ ol >
< h3 id = "contact-message-format" > Contact Message Format</ h3 >
< p >< strong > Standard Format</ strong > (< code > PACKET_CONTACT_MSG_RECV</ code > , 0x07):</ p >
< pre >< code > Byte 0: 0x07 (packet type)
Bytes 1-6: Public Key Prefix (6 bytes, hex)
Byte 7: Path Length
Byte 8: Text Type
Bytes 9-12: Timestamp (32-bit little-endian)
Bytes 13-16: Signature (4 bytes, only if txt_type == 2)
Bytes 17+: Message Text (UTF-8)
</ code ></ pre >
< p >< strong > V3 Format</ strong > (< code > PACKET_CONTACT_MSG_RECV_V3</ code > , 0x10):</ p >
< pre >< code > Byte 0: 0x10 (packet type)
Byte 1: SNR (signed byte, multiplied by 4)
Bytes 2-3: Reserved
Bytes 4-9: Public Key Prefix (6 bytes, hex)
Byte 10: Path Length
Byte 11: Text Type
Bytes 12-15: Timestamp (32-bit little-endian)
Bytes 16-19: Signature (4 bytes, only if txt_type == 2)
Bytes 20+: Message Text (UTF-8)
</ code ></ pre >
< p >< strong > Parsing Pseudocode</ strong > :</ p >
< pre >< code class = "language-python" > def parse_contact_message(data):
packet_type = data[0]
offset = 1
# Check for V3 format
if packet_type == 0x10: # V3
snr_byte = data[offset]
snr = ((snr_byte if snr_byte < 128 else snr_byte - 256) / 4.0)
offset += 3 # Skip SNR + reserved
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
# If txt_type == 2, skip 4-byte signature
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
}
</ code ></ pre >
< h3 id = "channel-message-format" > Channel Message Format</ h3 >
< p >< strong > Standard Format</ strong > (< code > PACKET_CHANNEL_MSG_RECV</ code > , 0x08):</ p >
< pre >< code > Byte 0: 0x08 (packet type)
Byte 1: Channel Index (0-7)
Byte 2: Path Length
Byte 3: Text Type
Bytes 4-7: Timestamp (32-bit little-endian)
Bytes 8+: Message Text (UTF-8)
</ code ></ pre >
< p >< strong > V3 Format</ strong > (< code > PACKET_CHANNEL_MSG_RECV_V3</ code > , 0x11):</ p >
< pre >< code > Byte 0: 0x11 (packet type)
Byte 1: SNR (signed byte, multiplied by 4)
Bytes 2-3: Reserved
Byte 4: Channel Index (0-7)
Byte 5: Path Length
Byte 6: Text Type
Bytes 7-10: Timestamp (32-bit little-endian)
Bytes 11+: Message Text (UTF-8)
</ code ></ pre >
< p >< strong > Parsing Pseudocode</ strong > :</ p >
< pre >< code class = "language-python" > def parse_channel_message(data):
packet_type = data[0]
offset = 1
# Check for V3 format
if packet_type == 0x11: # V3
snr_byte = data[offset]
snr = ((snr_byte if snr_byte < 128 else snr_byte - 256) / 4.0)
offset += 3 # Skip SNR + reserved
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
}
</ code ></ pre >
< h3 id = "sending-messages" > Sending Messages</ h3 >
< p > Use the < code > SEND_CHANNEL_MESSAGE</ code > command (see < a href = "#commands" > Commands</ a > ).</ p >
< p >< strong > Important</ strong > :
- Messages are limited to 133 characters per MeshCore specification
- Long messages should be split into chunks
- Include a chunk indicator (e.g., "[1/3] message text")</ p >
< hr />
< h2 id = "response-parsing" > Response Parsing</ h2 >
< h3 id = "terminology" > Terminology</ h3 >
< p > This document uses a spec-level naming convention (< code > PACKET_*</ code > ) for bytes the firmware sends back to the host. In the firmware source these same values are split across two < code > #define</ code > families by purpose:</ p >
< ul >
< li >< code > RESP_CODE_*</ code > — direct replies to a command (e.g. < code > RESP_CODE_CHANNEL_DATA_RECV</ code > = < code > PACKET_CHANNEL_DATA_RECV</ code > = 0x1B).</ li >
< li >< code > PUSH_CODE_*</ code > — asynchronous notifications not tied to a specific command (e.g. < code > PUSH_CODE_MSG_WAITING</ code > = < code > PACKET_MESSAGES_WAITING</ code > = 0x83).</ li >
</ ul >
< p > Byte values are authoritative; names are aliases. When reading firmware source, < code > RESP_CODE_X</ code > / < code > PUSH_CODE_X</ code > correspond to this doc's < code > PACKET_X</ code > of the same numeric value.</ p >
< h3 id = "packet-types" > Packet Types</ h3 >
< table >
< thead >
< tr >
< th > Value</ th >
< th > Name</ th >
< th > Description</ th >
</ tr >
</ thead >
< tbody >
< tr >
< td > 0x00</ td >
< td > PACKET_OK</ td >
< td > Command succeeded</ td >
</ tr >
< tr >
< td > 0x01</ td >
< td > PACKET_ERROR</ td >
< td > Command failed</ td >
</ tr >
< tr >
< td > 0x02</ td >
< td > PACKET_CONTACT_START</ td >
< td > Start of contact list</ td >
</ tr >
< tr >
< td > 0x03</ td >
< td > PACKET_CONTACT</ td >
< td > Contact information</ td >
</ tr >
< tr >
< td > 0x04</ td >
< td > PACKET_CONTACT_END</ td >
< td > End of contact list</ td >
</ tr >
< tr >
< td > 0x05</ td >
< td > PACKET_SELF_INFO</ td >
< td > Device self-information</ td >
</ tr >
< tr >
< td > 0x06</ td >
< td > PACKET_MSG_SENT</ td >
< td > Message sent confirmation</ td >
</ tr >
< tr >
< td > 0x07</ td >
< td > PACKET_CONTACT_MSG_RECV</ td >
< td > Contact message (standard)</ td >
</ tr >
< tr >
< td > 0x08</ td >
< td > PACKET_CHANNEL_MSG_RECV</ td >
< td > Channel message (standard)</ td >
</ tr >
< tr >
< td > 0x09</ td >
< td > PACKET_CURRENT_TIME</ td >
< td > Current time response</ td >
</ tr >
< tr >
< td > 0x0A</ td >
< td > PACKET_NO_MORE_MSGS</ td >
< td > No more messages available</ td >
</ tr >
< tr >
< td > 0x0C</ td >
< td > PACKET_BATTERY</ td >
< td > Battery level</ td >
</ tr >
< tr >
< td > 0x0D</ td >
< td > PACKET_DEVICE_INFO</ td >
< td > Device information</ td >
</ tr >
< tr >
< td > 0x10</ td >
< td > PACKET_CONTACT_MSG_RECV_V3</ td >
< td > Contact message (V3 with SNR)</ td >
</ tr >
< tr >
< td > 0x11</ td >
< td > PACKET_CHANNEL_MSG_RECV_V3</ td >
< td > Channel message (V3 with SNR)</ td >
</ tr >
< tr >
< td > 0x12</ td >
< td > PACKET_CHANNEL_INFO</ td >
< td > Channel information</ td >
</ tr >
< tr >
< td > 0x1B</ td >
< td > PACKET_CHANNEL_DATA_RECV</ td >
< td > Channel data datagram</ td >
</ tr >
< tr >
< td > 0x80</ td >
< td > PACKET_ADVERTISEMENT</ td >
< td > Advertisement packet</ td >
</ tr >
< tr >
< td > 0x82</ td >
< td > PACKET_ACK</ td >
< td > Acknowledgment</ td >
</ tr >
< tr >
< td > 0x83</ td >
< td > PACKET_MESSAGES_WAITING</ td >
< td > Messages waiting notification</ td >
</ tr >
< tr >
< td > 0x88</ td >
< td > PACKET_LOG_DATA</ td >
< td > RF log data (can be ignored)</ td >
</ tr >
</ tbody >
</ table >
< h3 id = "parsing-responses" > Parsing Responses</ h3 >
< p >< strong > PACKET_OK</ strong > (0x00):</ p >
< pre >< code > Byte 0: 0x00
Bytes 1-4: Optional value (32-bit little-endian integer)
</ code ></ pre >
< p >< strong > PACKET_ERROR</ strong > (0x01):</ p >
< pre >< code > Byte 0: 0x01
Byte 1: Error code (optional)
</ code ></ pre >
< p >< strong > PACKET_CHANNEL_INFO</ strong > (0x12):</ p >
< pre >< code > Byte 0: 0x12
Byte 1: Channel Index
Bytes 2-33: Channel Name (32 bytes, null-terminated)
Bytes 34-49: Secret (16 bytes)
</ code ></ pre >
< p >< strong > Note</ strong > : The device returns the 16-byte channel secret in this response.</ p >
< p >< strong > PACKET_DEVICE_INFO</ strong > (0x0D):</ p >
< pre >< code > Byte 0: 0x0D
Byte 1: Firmware Version (uint8)
Bytes 2+: Variable length based on firmware version
For firmware version > = 3:
Byte 2: Max Contacts Raw (uint8, actual = value * 2)
Byte 3: Max Channels (uint8)
Bytes 4-7: BLE PIN (32-bit little-endian)
Bytes 8-19: Firmware Build (12 bytes, UTF-8, null-padded)
Bytes 20-59: Model (40 bytes, UTF-8, null-padded)
Bytes 60-79: Version (20 bytes, UTF-8, null-padded)
Byte 80: Client repeat enabled/preferred (firmware v9+)
Byte 81: Path hash mode (firmware v10+)
</ code ></ pre >
< p >< strong > Parsing Pseudocode</ strong > :</ p >
< pre >< code class = "language-python" > 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
</ code ></ pre >
< p >< strong > PACKET_BATTERY</ strong > (0x0C):</ p >
< pre >< code > Byte 0: 0x0C
Bytes 1-2: Battery Voltage (16-bit little-endian, millivolts)
Bytes 3-6: Used Storage (32-bit little-endian, KB)
Bytes 7-10: Total Storage (32-bit little-endian, KB)
</ code ></ pre >
< p >< strong > Parsing Pseudocode</ strong > :</ p >
< pre >< code class = "language-python" > 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
</ code ></ pre >
< p >< strong > PACKET_SELF_INFO</ strong > (0x05):</ p >
< pre >< code > Byte 0: 0x05
Byte 1: Advertisement Type
Byte 2: TX Power
Byte 3: Max TX Power
Bytes 4-35: Public Key (32 bytes, hex)
Bytes 36-39: Advertisement Latitude (32-bit little-endian, divided by 1e6)
Bytes 40-43: Advertisement Longitude (32-bit little-endian, divided by 1e6)
Byte 44: Multi ACKs
Byte 45: Advertisement Location Policy
Byte 46: Telemetry Mode (bitfield)
Byte 47: Manual Add Contacts (bool)
Bytes 48-51: Radio Frequency (32-bit little-endian, divided by 1000.0)
Bytes 52-55: Radio Bandwidth (32-bit little-endian, divided by 1000.0)
Byte 56: Radio Spreading Factor
Byte 57: Radio Coding Rate
Bytes 58+: Device Name (UTF-8, variable length, no null terminator required)
</ code ></ pre >
< p >< strong > Parsing Pseudocode</ strong > :</ p >
< pre >< code class = "language-python" > 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
</ code ></ pre >
< p >< strong > PACKET_MSG_SENT</ strong > (0x06):</ p >
< pre >< code > Byte 0: 0x06
Byte 1: Route Flag (0 = direct, 1 = flood)
Bytes 2-5: Tag / Expected ACK (4 bytes, little-endian)
Bytes 6-9: Suggested Timeout (32-bit little-endian, milliseconds)
</ code ></ pre >
< p >< strong > PACKET_ACK</ strong > (0x82):</ p >
< pre >< code > Byte 0: 0x82
Bytes 1-6: ACK Code (6 bytes, hex)
</ code ></ pre >
< h3 id = "error-codes" > Error Codes</ h3 >
< p >< code > PACKET_ERROR</ code > (0x01) carries a single-byte error code in byte 1. Values match the < code > ERR_CODE_*</ code > constants defined in < code > examples/companion_radio/MyMesh.cpp</ code > :</ p >
< table >
< thead >
< tr >
< th > Code</ th >
< th > Constant (firmware)</ th >
< th > Description</ th >
</ tr >
</ thead >
< tbody >
< tr >
< td > 1</ td >
< td >< code > ERR_CODE_UNSUPPORTED_CMD</ code ></ td >
< td > Unknown or unsupported command byte / sub-command</ td >
</ tr >
< tr >
< td > 2</ td >
< td >< code > ERR_CODE_NOT_FOUND</ code ></ td >
< td > Target not found (channel, contact, message, etc.)</ td >
</ tr >
< tr >
< td > 3</ td >
< td >< code > ERR_CODE_TABLE_FULL</ code ></ td >
< td > Internal queue or table is full — retry later</ td >
</ tr >
< tr >
< td > 4</ td >
< td >< code > ERR_CODE_BAD_STATE</ code ></ td >
< td > Operation not valid in current device state (e.g. iterator already running)</ td >
</ tr >
< tr >
< td > 5</ td >
< td >< code > ERR_CODE_FILE_IO_ERROR</ code ></ td >
< td > Filesystem or storage I/O failure</ td >
</ tr >
< tr >
< td > 6</ td >
< td >< code > ERR_CODE_ILLEGAL_ARG</ code ></ td >
< td > Invalid argument (bad length, out-of-range value, reserved field, etc.)</ td >
</ tr >
</ tbody >
</ table >
< p >< strong > Note</ strong > : Error codes may vary by firmware version. Always check byte 1 of < code > PACKET_ERROR</ code > response, and treat unknown codes as generic errors.</ p >
< h3 id = "frame-handling" > Frame Handling</ h3 >
< p > BLE implementations enqueue and deliver one protocol frame per BLE write/notification at the firmware layer.</ p >
< ul >
< li > Apps should treat each characteristic write/notification as exactly one companion protocol frame</ li >
< li > Apps should still validate frame lengths before parsing</ li >
< li > Future transports or firmware revisions may differ, so avoid assuming fixed payload sizes for variable-length responses</ li >
</ ul >
< h3 id = "response-handling" > Response Handling</ h3 >
< ol >
< li >< strong > Command-Response Pattern</ strong > :</ li >
< li > Send command via RX characteristic</ li >
< li > Wait for response via TX characteristic (notification)</ li >
< li > Match response to command using sequence numbers or command type</ li >
< li > Handle timeout (typically 5 seconds)</ li >
< li >
< p > Use command queue to prevent concurrent commands</ p >
</ li >
< li >
< p >< strong > Asynchronous Messages</ strong > :</ p >
</ li >
< li > Device may send messages at any time via TX characteristic</ li >
< li > Handle < code > PACKET_MESSAGES_WAITING</ code > (0x83) by polling < code > GET_MESSAGE</ code > command</ li >
< li > Parse incoming messages and route to appropriate handlers</ li >
< li >
< p > Validate frame length before decoding</ p >
</ li >
< li >
< p >< strong > Response Matching</ strong > :</ p >
</ li >
< li >
< p > Match responses to commands by expected packet type:</ p >
< ul >
< li >< code > APP_START</ code > → < code > PACKET_SELF_INFO</ code ></ li >
< li >< code > DEVICE_QUERY</ code > → < code > PACKET_DEVICE_INFO</ code ></ li >
< li >< code > GET_CHANNEL</ code > → < code > PACKET_CHANNEL_INFO</ code ></ li >
< li >< code > SET_CHANNEL</ code > → < code > PACKET_OK</ code > or < code > PACKET_ERROR</ code ></ li >
< li >< code > SEND_CHANNEL_MESSAGE</ code > → < code > PACKET_MSG_SENT</ code ></ li >
< li >< code > GET_MESSAGE</ code > → < code > PACKET_CHANNEL_MSG_RECV</ code > , < code > PACKET_CONTACT_MSG_RECV</ code > , < code > PACKET_CHANNEL_DATA_RECV</ code > , or < code > PACKET_NO_MORE_MSGS</ code ></ li >
< li >< code > SEND_CHANNEL_DATA</ code > → < code > PACKET_OK</ code > or < code > PACKET_ERROR</ code ></ li >
< li >< code > GET_BATTERY</ code > → < code > PACKET_BATTERY</ code ></ li >
</ ul >
</ li >
< li >
< p >< strong > Timeout Handling</ strong > :</ p >
</ li >
< li > Default timeout: 5 seconds per command</ li >
< li > On timeout: Log error, clear current command, proceed to next in queue</ li >
< li > Some commands may take longer (e.g., < code > SET_CHANNEL</ code > may need 1-2 seconds)</ li >
< li >
< p > Consider longer timeout for channel operations</ p >
</ li >
< li >
< p >< strong > Error Recovery</ strong > :</ p >
</ li >
< li > On < code > PACKET_ERROR</ code > : Log error code, clear current command</ li >
< li > On connection loss: Clear command queue, attempt reconnection</ li >
< li > On invalid response: Log warning, clear current command, proceed</ li >
</ ol >
< hr />
< h2 id = "example-implementation-flow" > Example Implementation Flow</ h2 >
< h3 id = "initialization" > Initialization</ h3 >
< pre >< code class = "language-python" > # 1. Scan for MeshCore device
device = scan_for_device(" MeshCore" )
# 2. Connect to BLE GATT
gatt = connect_to_device(device)
# 3. Discover services and characteristics
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. Enable notifications on TX characteristic
enable_notifications(tx_char, on_notification_received)
# 5. Send AppStart command
send_command(rx_char, build_app_start())
wait_for_response(PACKET_SELF_INFO)
</ code ></ pre >
< h3 id = "creating-a-private-channel" > Creating a Private Channel</ h3 >
< pre >< code class = "language-python" > # 1. Generate 16-byte secret
secret_16_bytes = generate_secret(16) # Use CSPRNG
secret_hex = secret_16_bytes.hex()
# 2. Build SET_CHANNEL command
channel_name = " YourChannelName"
channel_index = 1 # Use 1-7 for private channels
command = build_set_channel(channel_index, channel_name, secret_16_bytes)
# 3. Send command
send_command(rx_char, command)
response = wait_for_response(PACKET_OK)
# 4. Store secret locally
store_channel_secret(channel_index, secret_hex)
</ code ></ pre >
< h3 id = "sending-a-message" > Sending a Message</ h3 >
< pre >< code class = "language-python" > # 1. Build channel message command
channel_index = 1
message = " Hello, MeshCore!"
timestamp = int(time.time())
command = build_channel_message(channel_index, message, timestamp)
# 2. Send command
send_command(rx_char, command)
response = wait_for_response(PACKET_MSG_SENT)
</ code ></ pre >
< h3 id = "receiving-messages_1" > Receiving Messages</ h3 >
< pre >< code class = "language-python" > 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:
# Poll for messages
send_command(rx_char, build_get_message())
</ code ></ pre >
< hr />
< h2 id = "best-practices" > Best Practices</ h2 >
< ol >
< li >< strong > Connection Management</ strong > :</ li >
< li > Implement auto-reconnect with exponential backoff</ li >
< li > Handle disconnections gracefully</ li >
< li >
< p > Store last connected device address for quick reconnection</ p >
</ li >
< li >
< p >< strong > Secret Management</ strong > :</ p >
</ li >
< li > Always use cryptographically secure random number generators</ li >
< li > Store secrets securely (encrypted storage)</ li >
< li >
< p > Never log or transmit secrets in plain text</ p >
</ li >
< li >
< p >< strong > Message Handling</ strong > :</ p >
</ li >
< li > Send < code > CMD_SYNC_NEXT_MESSAGE</ code > when < code > PUSH_CODE_MSG_WAITING</ code > is received</ li >
< li >
< p > Implement message deduplication to avoid displaying the same message twice</ p >
</ li >
< li >
< p >< strong > Channel Management</ strong > :</ p >
< ul >
< li > Fetch all channel slots even if you encounter an empty slot</ li >
< li > Ideally save new channels into the first empty slot</ li >
</ ul >
</ li >
< li >
< p >< strong > Error Handling</ strong > :</ p >
</ li >
< li > Implement timeouts for all commands (typically 5 seconds)</ li >
< li > Handle < code > RESP_CODE_ERR</ code > responses appropriately</ li >
</ ol >
< hr />
< h2 id = "troubleshooting" > Troubleshooting</ h2 >
< h3 id = "connection-issues" > Connection Issues</ h3 >
< ul >
< li >< strong > Device not found</ strong > : Ensure device is powered on and advertising</ li >
< li >< strong > Connection timeout</ strong > : Check Bluetooth permissions and device proximity</ li >
< li >< strong > GATT errors</ strong > : Ensure proper service/characteristic discovery</ li >
</ ul >
< h3 id = "command-issues" > Command Issues</ h3 >
< ul >
< li >< strong > No response</ strong > : Verify notifications are enabled, check connection state</ li >
< li >< strong > Error responses</ strong > : Verify command format and check error code</ li >
< li >< strong > Timeout</ strong > : Increase timeout value or try again</ li >
</ ul >
< h3 id = "message-issues" > Message Issues</ h3 >
< ul >
< li >< strong > Messages not received</ strong > : Poll < code > GET_MESSAGE</ code > command periodically</ li >
< li >< strong > Duplicate messages</ strong > : Implement message deduplication using timestamp/content as a unique id</ li >
< li >< strong > Message truncation</ strong > : Send long messages as separate shorter messages</ li >
</ ul >
</ article >
</ div >
< script > var target = document . getElementById ( location . hash . slice ( 1 )); target && target . name && ( target . checked = target . name . startsWith ( "__tabbed_" ))</ script >
</ div >
</ main >
< footer class = "md-footer" >
< div class = "md-footer-meta md-typeset" >
< div class = "md-footer-meta__inner md-grid" >
< div class = "md-copyright" >
Made with
< a href = "https://squidfunk.github.io/mkdocs-material/" target = "_blank" rel = "noopener" >
Material for MkDocs
</ a >
</ div >
</ div >
</ div >
</ footer >
</ div >
< div class = "md-dialog" data-md-component = "dialog" >
< div class = "md-dialog__inner md-typeset" ></ div >
</ div >
< script id = "__config" type = "application/json" >{ "annotate" : null , "base" : ".." , "features" : [ "content.action.edit" , "content.code.copy" , "search.highlight" , "search.suggest" ], "search" : "../assets/javascripts/workers/search.2c215733.min.js" , "tags" : null , "translations" : { "clipboard.copied" : "Copied to clipboard" , "clipboard.copy" : "Copy to clipboard" , "search.result.more.one" : "1 more on this page" , "search.result.more.other" : "# more on this page" , "search.result.none" : "No matching documents" , "search.result.one" : "1 matching document" , "search.result.other" : "# matching documents" , "search.result.placeholder" : "Type to start searching" , "search.result.term.missing" : "Missing" , "select.version" : "Select version" }, "version" : null }</ script >
< script src = "../assets/javascripts/bundle.79ae519e.min.js" ></ script >
</ body >
</ html >