deploy: c724f8cc9f
This commit is contained in:
@@ -23,7 +23,7 @@
|
||||
|
||||
|
||||
<link rel="icon" href="../assets/images/favicon.png">
|
||||
<meta name="generator" content="mkdocs-1.6.1, mkdocs-material-9.7.4">
|
||||
<meta name="generator" content="mkdocs-1.6.1, mkdocs-material-9.7.5">
|
||||
|
||||
|
||||
|
||||
@@ -545,10 +545,10 @@
|
||||
</li>
|
||||
|
||||
<li class="md-nav__item">
|
||||
<a href="#7-get-battery" class="md-nav__link">
|
||||
<a href="#7-get-battery-and-storage" class="md-nav__link">
|
||||
<span class="md-ellipsis">
|
||||
|
||||
7. Get Battery
|
||||
7. Get Battery and Storage
|
||||
|
||||
</span>
|
||||
</a>
|
||||
@@ -706,10 +706,10 @@
|
||||
</li>
|
||||
|
||||
<li class="md-nav__item">
|
||||
<a href="#partial-packet-handling" class="md-nav__link">
|
||||
<a href="#frame-handling" class="md-nav__link">
|
||||
<span class="md-ellipsis">
|
||||
|
||||
Partial Packet Handling
|
||||
Frame Handling
|
||||
|
||||
</span>
|
||||
</a>
|
||||
@@ -1337,10 +1337,10 @@
|
||||
</li>
|
||||
|
||||
<li class="md-nav__item">
|
||||
<a href="#7-get-battery" class="md-nav__link">
|
||||
<a href="#7-get-battery-and-storage" class="md-nav__link">
|
||||
<span class="md-ellipsis">
|
||||
|
||||
7. Get Battery
|
||||
7. Get Battery and Storage
|
||||
|
||||
</span>
|
||||
</a>
|
||||
@@ -1498,10 +1498,10 @@
|
||||
</li>
|
||||
|
||||
<li class="md-nav__item">
|
||||
<a href="#partial-packet-handling" class="md-nav__link">
|
||||
<a href="#frame-handling" class="md-nav__link">
|
||||
<span class="md-ellipsis">
|
||||
|
||||
Partial Packet Handling
|
||||
Frame Handling
|
||||
|
||||
</span>
|
||||
</a>
|
||||
@@ -1673,7 +1673,7 @@
|
||||
|
||||
<h1 id="companion-protocol">Companion Protocol</h1>
|
||||
<ul>
|
||||
<li><strong>Last Updated</strong>: 2026-01-03</li>
|
||||
<li><strong>Last Updated</strong>: 2026-03-08</li>
|
||||
<li><strong>Protocol Version</strong>: Companion Firmware v1.12.0+</li>
|
||||
</ul>
|
||||
<blockquote>
|
||||
@@ -1783,7 +1783,7 @@
|
||||
</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> (66 bytes), you may need to:</p>
|
||||
<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>
|
||||
@@ -1846,13 +1846,13 @@
|
||||
<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
|
||||
Byte 1: 0x03
|
||||
Bytes 2-10: "mccli" (ASCII, null-padded to 9 bytes)
|
||||
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 03 6d 63 63 6c 69 00 00 00 00
|
||||
<pre><code>01 00 00 00 00 00 00 00 6d 63 63 6c 69
|
||||
</code></pre>
|
||||
<p><strong>Response</strong>: <code>PACKET_OK</code> (0x00)</p>
|
||||
<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>
|
||||
@@ -1875,7 +1875,6 @@ Byte 1: Channel Index (0-7)
|
||||
<pre><code>1F 01
|
||||
</code></pre>
|
||||
<p><strong>Response</strong>: <code>PACKET_CHANNEL_INFO</code> (0x12) with channel details</p>
|
||||
<p><strong>Note</strong>: The device does not return channel secrets for security reasons. Store secrets locally when creating channels.</p>
|
||||
<hr />
|
||||
<h3 id="4-set-channel">4. Set Channel</h3>
|
||||
<p><strong>Purpose</strong>: Create or update a channel on the device.</p>
|
||||
@@ -1883,9 +1882,9 @@ Byte 1: Channel Index (0-7)
|
||||
<pre><code>Byte 0: 0x20
|
||||
Byte 1: Channel Index (0-7)
|
||||
Bytes 2-33: Channel Name (32 bytes, UTF-8, null-padded)
|
||||
Bytes 34-65: Secret (32 bytes)
|
||||
Bytes 34-49: Secret (16 bytes)
|
||||
</code></pre>
|
||||
<p><strong>Total Length</strong>: 66 bytes</p>
|
||||
<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>
|
||||
@@ -1893,13 +1892,14 @@ Bytes 34-65: Secret (32 bytes)
|
||||
- UTF-8 encoded
|
||||
- Maximum 32 bytes
|
||||
- Padded with null bytes (0x00) if shorter</p>
|
||||
<p><strong>Secret Field</strong> (32 bytes):
|
||||
- For <strong>private channels</strong>: 32-byte secret
|
||||
<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)
|
||||
[32 bytes of secret]
|
||||
[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>
|
||||
@@ -1931,15 +1931,15 @@ Bytes 7+: Message Text (UTF-8, variable length)
|
||||
- <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="7-get-battery">7. Get Battery</h3>
|
||||
<p><strong>Purpose</strong>: Query device battery level.</p>
|
||||
<h3 id="7-get-battery-and-storage">7. 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 percentage</p>
|
||||
<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>
|
||||
@@ -1970,7 +1970,7 @@ Bytes 7+: Message Text (UTF-8, variable length)
|
||||
<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 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>
|
||||
@@ -1987,7 +1987,7 @@ Bytes 7+: Message Text (UTF-8, variable length)
|
||||
<hr />
|
||||
<h2 id="message-handling">Message Handling</h2>
|
||||
<h3 id="receiving-messages">Receiving Messages</h3>
|
||||
<p>Messages are received via the RX characteristic (notifications). The device sends:</p>
|
||||
<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>
|
||||
@@ -2239,9 +2239,9 @@ Byte 1: Error code (optional)
|
||||
<pre><code>Byte 0: 0x12
|
||||
Byte 1: Channel Index
|
||||
Bytes 2-33: Channel Name (32 bytes, null-terminated)
|
||||
Bytes 34-65: Secret (32 bytes, but device typically only returns 20 bytes total)
|
||||
Bytes 34-49: Secret (16 bytes)
|
||||
</code></pre>
|
||||
<p><strong>Note</strong>: The device may not return the full 66-byte packet. Parse what is available. The secret field is typically not returned for security reasons.</p>
|
||||
<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)
|
||||
@@ -2254,6 +2254,8 @@ 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):
|
||||
@@ -2275,9 +2277,7 @@ Bytes 60-79: Version (20 bytes, UTF-8, null-padded)
|
||||
</code></pre>
|
||||
<p><strong>PACKET_BATTERY</strong> (0x0C):</p>
|
||||
<pre><code>Byte 0: 0x0C
|
||||
Bytes 1-2: Battery Level (16-bit little-endian, percentage 0-100)
|
||||
|
||||
Optional (if data size > 3):
|
||||
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>
|
||||
@@ -2286,14 +2286,12 @@ Bytes 7-10: Total Storage (32-bit little-endian, KB)
|
||||
if len(data) < 3:
|
||||
return None
|
||||
|
||||
level = int.from_bytes(data[1:3], 'little')
|
||||
info = {'level': level}
|
||||
mv = int.from_bytes(data[1:3], 'little')
|
||||
info = {'battery_mv': mv}
|
||||
|
||||
if len(data) > 3:
|
||||
used_kb = int.from_bytes(data[3:7], 'little')
|
||||
total_kb = int.from_bytes(data[7:11], 'little')
|
||||
info['used_kb'] = used_kb
|
||||
info['total_kb'] = total_kb
|
||||
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>
|
||||
@@ -2313,7 +2311,7 @@ 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, null-terminated)
|
||||
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):
|
||||
@@ -2360,9 +2358,9 @@ Bytes 58+: Device Name (UTF-8, variable length, null-terminated)
|
||||
</code></pre>
|
||||
<p><strong>PACKET_MSG_SENT</strong> (0x06):</p>
|
||||
<pre><code>Byte 0: 0x06
|
||||
Byte 1: Message Type
|
||||
Bytes 2-5: Expected ACK (4 bytes, hex)
|
||||
Bytes 6-9: Suggested Timeout (32-bit little-endian, seconds)
|
||||
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
|
||||
@@ -2421,70 +2419,18 @@ Bytes 1-6: ACK Code (6 bytes, hex)
|
||||
</tbody>
|
||||
</table>
|
||||
<p><strong>Note</strong>: Error codes may vary by firmware version. Always check byte 1 of <code>PACKET_ERROR</code> response.</p>
|
||||
<h3 id="partial-packet-handling">Partial Packet Handling</h3>
|
||||
<p>BLE notifications may arrive in chunks, especially for larger packets. Implement buffering:</p>
|
||||
<p><strong>Implementation</strong>:</p>
|
||||
<pre><code class="language-python">class PacketBuffer:
|
||||
def __init__(self):
|
||||
self.buffer = bytearray()
|
||||
self.expected_length = None
|
||||
|
||||
def add_data(self, data):
|
||||
self.buffer.extend(data)
|
||||
|
||||
# Check if we have a complete packet
|
||||
if len(self.buffer) >= 1:
|
||||
packet_type = self.buffer[0]
|
||||
|
||||
# Determine expected length based on packet type
|
||||
expected = self.get_expected_length(packet_type)
|
||||
|
||||
if expected is not None and len(self.buffer) >= expected:
|
||||
# Complete packet
|
||||
packet = bytes(self.buffer[:expected])
|
||||
self.buffer = self.buffer[expected:]
|
||||
return packet
|
||||
elif expected is None:
|
||||
# Variable length packet - try to parse what we have
|
||||
# Some packets have minimum length requirements
|
||||
if self.can_parse_partial(packet_type):
|
||||
return self.try_parse_partial()
|
||||
|
||||
return None # Incomplete packet
|
||||
|
||||
def get_expected_length(self, packet_type):
|
||||
# Fixed-length packets
|
||||
fixed_lengths = {
|
||||
0x00: 5, # PACKET_OK (minimum)
|
||||
0x01: 2, # PACKET_ERROR (minimum)
|
||||
0x0A: 1, # PACKET_NO_MORE_MSGS
|
||||
0x14: 3, # PACKET_BATTERY (minimum)
|
||||
}
|
||||
return fixed_lengths.get(packet_type)
|
||||
|
||||
def can_parse_partial(self, packet_type):
|
||||
# Some packets can be parsed partially
|
||||
return packet_type in [0x12, 0x08, 0x11, 0x07, 0x10, 0x05, 0x0D]
|
||||
|
||||
def try_parse_partial(self):
|
||||
# Try to parse with available data
|
||||
# Return packet if successfully parsed, None otherwise
|
||||
# This is packet-type specific
|
||||
pass
|
||||
</code></pre>
|
||||
<p><strong>Usage</strong>:</p>
|
||||
<pre><code class="language-python">buffer = PacketBuffer()
|
||||
|
||||
def on_notification_received(data):
|
||||
packet = buffer.add_data(data)
|
||||
if packet:
|
||||
parse_and_handle_packet(packet)
|
||||
</code></pre>
|
||||
<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 TX characteristic</li>
|
||||
<li>Wait for response via RX characteristic (notification)</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>
|
||||
@@ -2493,11 +2439,11 @@ def on_notification_received(data):
|
||||
<li>
|
||||
<p><strong>Asynchronous Messages</strong>:</p>
|
||||
</li>
|
||||
<li>Device may send messages at any time via RX characteristic</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>Buffer partial packets until complete</p>
|
||||
<p>Validate frame length before decoding</p>
|
||||
</li>
|
||||
<li>
|
||||
<p><strong>Response Matching</strong>:</p>
|
||||
@@ -2505,7 +2451,7 @@ def on_notification_received(data):
|
||||
<li>
|
||||
<p>Match responses to commands by expected packet type:</p>
|
||||
<ul>
|
||||
<li><code>APP_START</code> → <code>PACKET_OK</code></li>
|
||||
<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>
|
||||
@@ -2540,37 +2486,32 @@ device = scan_for_device("MeshCore")
|
||||
gatt = connect_to_device(device)
|
||||
|
||||
# 3. Discover services and characteristics
|
||||
service = discover_service(gatt, "0000ff00-0000-1000-8000-00805f9b34fb")
|
||||
rx_char = discover_characteristic(service, "0000ff01-0000-1000-8000-00805f9b34fb")
|
||||
tx_char = discover_characteristic(service, "0000ff02-0000-1000-8000-00805f9b34fb")
|
||||
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 RX characteristic
|
||||
enable_notifications(rx_char, on_notification_received)
|
||||
# 4. Enable notifications on TX characteristic
|
||||
enable_notifications(tx_char, on_notification_received)
|
||||
|
||||
# 5. Send AppStart command
|
||||
send_command(tx_char, build_app_start())
|
||||
wait_for_response(PACKET_OK)
|
||||
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. Expand secret to 32 bytes using SHA-512
|
||||
import hashlib
|
||||
sha512_hash = hashlib.sha512(secret_16_bytes).digest()
|
||||
secret_32_bytes = sha512_hash[:32]
|
||||
|
||||
# 3. Build SET_CHANNEL command
|
||||
# 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_32_bytes)
|
||||
command = build_set_channel(channel_index, channel_name, secret_16_bytes)
|
||||
|
||||
# 4. Send command
|
||||
send_command(tx_char, command)
|
||||
# 3. Send command
|
||||
send_command(rx_char, command)
|
||||
response = wait_for_response(PACKET_OK)
|
||||
|
||||
# 5. Store secret locally (device won't return it)
|
||||
# 4. Store secret locally
|
||||
store_channel_secret(channel_index, secret_hex)
|
||||
</code></pre>
|
||||
<h3 id="sending-a-message">Sending a Message</h3>
|
||||
@@ -2581,7 +2522,7 @@ timestamp = int(time.time())
|
||||
command = build_channel_message(channel_index, message, timestamp)
|
||||
|
||||
# 2. Send command
|
||||
send_command(tx_char, command)
|
||||
send_command(rx_char, command)
|
||||
response = wait_for_response(PACKET_MSG_SENT)
|
||||
</code></pre>
|
||||
<h3 id="receiving-messages_1">Receiving Messages</h3>
|
||||
@@ -2593,7 +2534,7 @@ response = wait_for_response(PACKET_MSG_SENT)
|
||||
handle_channel_message(message)
|
||||
elif packet_type == PACKET_MESSAGES_WAITING:
|
||||
# Poll for messages
|
||||
send_command(tx_char, build_get_message())
|
||||
send_command(rx_char, build_get_message())
|
||||
</code></pre>
|
||||
<hr />
|
||||
<h2 id="best-practices">Best Practices</h2>
|
||||
|
||||
Reference in New Issue
Block a user