A Python library for decoding MeshCore mesh networking packets with full cryptographic support. Complete Python implementation of the MeshCore Packet Decoder by Michael Hart.
- Packet Decoding: Decode MeshCore packets
- Built-in Decryption: Decrypt GroupText, TextMessage, Request, Response, Path, and AnonRequest payloads
- Developer Friendly: Python-first with full type hints and data classes
Packet and payload layouts follow the MeshCore protocol as described in the local docs:
- docs/packet_format.md – Packet layout (header, transport codes, path, payload), route types, payload types, payload versions
- docs/payloads.md – Payload formats (Advert, Ack, Request/Response, TextMessage, Path, Control, Group text/datagram, etc.)
Supported payload types include: Request, Response, TextMessage, Ack, Advert, GroupText, GroupData, AnonRequest, Path, Trace, Multipart, Control (DISCOVER_REQ / DISCOVER_RESP), reserved (0x0C–0x0E), and RawCustom. Request types include GetStats, Keepalive, GetTelemetryData, GetMinMaxAvgData, GetAccessList, GetNeighbours, and GetOwnerInfo.
pip install -r requirements.txtpip install meshcoredecoderpycryptodome>=3.19.0- Core cryptography (AES, HMAC)cryptography>=41.0.0- Ed25519 signature supportclick>=8.1.0- CLI improvements
from meshcoredecoder import MeshCoreDecoder
from meshcoredecoder.types.enums import PayloadType
from meshcoredecoder.utils.enum_names import get_route_type_name, get_payload_type_name, get_device_role_name
import json
# Decode a MeshCore packet
hex_data = '11007E76...'
packet = MeshCoreDecoder.decode(hex_data)
print(f"Route Type: {get_route_type_name(packet.route_type)}")
print(f"Payload Type: {get_payload_type_name(packet.payload_type)}")
print(f"Message Hash: {packet.message_hash}")
if packet.payload_type == PayloadType.Advert and packet.payload.get('decoded'):
advert = packet.payload['decoded']
print(f"Device Name: {advert.app_data.get('name')}")
print(f"Device Role: {get_device_role_name(advert.app_data.get('device_role'))}")
if advert.app_data.get('location'):
location = advert.app_data['location']
print(f"Location: {location['latitude']}, {location['longitude']}")Here's what a complete decoded packet looks like:
from meshcoredecoder import MeshCoreDecoder
import json
hex_data = '11007E766...'
packet = MeshCoreDecoder.decode(hex_data)
packet_dict = packet.to_dict()
print(json.dumps(packet_dict, indent=2, default=str))Output:
{
"messageHash": "F9C060FE",
"routeType": 1,
"payloadType": 4,
"payloadVersion": 0,
"pathLength": 0,
"path": null,
"payload": {
"raw": "7E7662676F7F0850A8A355BAAFBFC1EB7B4174C340442D7D7161C9474A2C94006CE7CF682E58408DD8FCC51906ECA98EBF94A037886BDADE7ECD09FD92B839491DF3809C9454F5286D1D3370AC31A34593D569E9A042A3B41FD331DFFB7E18599CE1E60992A076D50238C5B8F85757375354522F50756765744D65736820436F75676172",
"decoded": {
"type": 4,
"version": 0,
"isValid": true,
"publicKey": "7E7662676F7F0850A8A355BAAFBFC1EB7B4174C340442D7D7161C9474A2C9400",
"timestamp": 1758455660,
"signature": "2E58408DD8FCC51906ECA98EBF94A037886BDADE7ECD09FD92B839491DF3809C9454F5286D1D3370AC31A34593D569E9A042A3B41FD331DFFB7E18599CE1E609",
"appData": {
"flags": 146,
"deviceRole": 2,
"hasLocation": true,
"hasName": true,
"location": {
"latitude": 47.543968,
"longitude": -122.108616
},
"name": "WW7STR/PugetMesh Cougar"
}
}
},
"totalBytes": 134,
"isValid": true
}Simply provide your channel secret keys and the library handles everything else:
from meshcoredecoder import MeshCoreDecoder
from meshcoredecoder.crypto import MeshCoreKeyStore
from meshcoredecoder.types.crypto import DecryptionOptions
from meshcoredecoder.types.enums import PayloadType
# Create a key store with channel secret keys
key_store = MeshCoreKeyStore({
'channel_secrets': [
'8b3387e9c5cdea6ac9e5edbaa115cd72', # Public channel (channel hash 11)
'ff2b7d74e8d20f71505bda9ea8d59a1c', # A different channel's secret
]
})
group_text_hex_data = '...' # Your encrypted GroupText packet hex
# Decode encrypted GroupText message
options = DecryptionOptions(key_store=key_store)
encrypted_packet = MeshCoreDecoder.decode(group_text_hex_data, options)
if encrypted_packet.payload_type == PayloadType.GroupText and encrypted_packet.payload.get('decoded'):
group_text = encrypted_packet.payload['decoded']
if group_text.decrypted:
print(f"Sender: {group_text.decrypted.get('sender')}")
print(f"Message: {group_text.decrypted.get('message')}")
print(f"Timestamp: {group_text.decrypted.get('timestamp')}")
else:
print('Message encrypted (no key available)')The library automatically:
- Calculates channel hashes from your secret keys using SHA256
- Handles hash collisions (multiple keys with same first byte) by trying all matching keys
- Verifies message authenticity using HMAC-SHA256
- Decrypts using AES-128 ECB
from meshcoredecoder import MeshCoreDecoder
# Verify Ed25519 signatures
packet = MeshCoreDecoder.decode_with_verification(hex_data)
if packet.payload.get('decoded'):
advert = packet.payload['decoded']
if hasattr(advert, 'signature_valid'):
print(f"Signature Valid: {advert.signature_valid}")Trace packets include Signal-to-Noise Ratio (SNR) values collected at each hop along the path. The library automatically extracts and correlates SNR values with path hashes:
from meshcoredecoder import MeshCoreDecoder
from meshcoredecoder.types.enums import PayloadType
trace_hex_data = '...' # Your Trace packet hex
packet = MeshCoreDecoder.decode(trace_hex_data)
if packet.payload_type == PayloadType.Trace and packet.payload.get('decoded'):
trace = packet.payload['decoded']
# Get path with SNR per hop
path_with_snr = trace.get_path_with_snr()
for hop_info in path_with_snr:
print(f"Hop {hop_info['hop']}: Node {hop_info['nodeHash']} → SNR: {hop_info['snr']:.1f}dB")
# Or access SNR values directly
if trace.snr_values:
print(f"SNR values: {trace.snr_values}")Note: SNR values are stored in the packet's path field as signed int8 values multiplied by 4, and are automatically converted to dB by the decoder. Each SNR value corresponds to the signal quality at that hop in the path.
SNR (Signal-to-Noise Ratio) data is available from multiple packet types:
- TRACE Packets – SNR values for each hop in the path
- GetStats / GetNeighbours Response Packets – Full neighbor table with SNR values (requires decryption)
- DISCOVER_RESP Control Packets – SNR in discovery responses (unencrypted; decode with
PayloadType.Control)
GetStats is a request/response protocol used to query a repeater node for its neighbor table and statistics. Here's how it works:
1. Sending a GetStats Request:
- Create a Request packet with
RequestType.GetStats(0x01) - The request payload contains:
- Destination hash (1 byte): First byte of target repeater's public key
- Source hash (1 byte): First byte of your public key
- Cipher MAC (2 bytes): MAC for encrypted data
- Ciphertext: Encrypted data containing:
- Timestamp (4 bytes, Unix timestamp)
- Request type (1 byte): e.g. 0x01 GetStats, 0x06 GetNeighbours, 0x07 GetOwnerInfo (see docs/payloads.md)
- Request data (optional, depends on request type)
2. Receiving a Neighbor List Response:
- The target repeater responds with a Response packet
- The response payload contains (encrypted):
- sender_timestamp (4 bytes): Unix timestamp when response was created
- neighbours_count (2 bytes): Total number of neighbors available
- results_count (2 bytes): Number of neighbors in this packet
- Neighbor entries: Array of neighbor entries, each containing:
- pubkey_prefix (1-32 bytes): Public key prefix (length depends on request)
- heard_seconds_ago (4 bytes): Seconds since neighbor was last heard
- snr (1 byte): Signal-to-Noise Ratio (int8_t, direct value)
3. Decoding the Response: The decoder automatically extracts neighbor table data when you provide the appropriate node key for decryption.
Response payloads from neighbor list requests contain the full neighbor table with SNR values. This is the most comprehensive source of neighbor/SNR data. When decrypted with the appropriate node key, the decoder automatically extracts neighbor information:
from meshcoredecoder import MeshCoreDecoder
from meshcoredecoder.types.enums import PayloadType
from meshcoredecoder.types.crypto import DecryptionOptions
from meshcoredecoder.crypto.key_manager import MeshCoreKeyStore
from datetime import datetime
# Create a key store with node keys
# Format: PUBKEY:PRIVKEY or PUBKEY,PRIVKEY
# The public key should match the sender's public key for decryption
key_store = MeshCoreKeyStore()
key_store.add_node_key(
public_key='969605...', # 32-byte Ed25519 public key (hex)
private_key='1010fe...' # 64-byte Ed25519 private key (hex)
)
# Decode a Response packet with decryption
response_hex = '06002E...'
options = DecryptionOptions(key_store=key_store)
response_packet = MeshCoreDecoder.decode(response_hex, options)
if response_packet.payload_type == PayloadType.Response and response_packet.payload.get('decoded'):
response = response_packet.payload['decoded']
# Access decrypted response data
if response.decrypted:
if 'totalNeighborCount' in response.decrypted:
print(f"Total neighbors available: {response.decrypted['totalNeighborCount']}")
if 'neighborCount' in response.decrypted:
print(f"Neighbors in this packet: {response.decrypted['neighborCount']}")
# Access neighbor table with SNR values
if response.neighbors:
print(f"\nFound {len(response.neighbors)} neighbors:")
for i, neighbor in enumerate(response.neighbors, 1):
print(f"\n Neighbor {i}:")
print(f" Node ID (pubkey prefix): {neighbor.node_id}")
print(f" SNR: {neighbor.snr:.1f} dB")
print(f" Timestamp: {datetime.fromtimestamp(neighbor.advert_timestamp).isoformat()}")
# Calculate time since last heard
from datetime import datetime, timezone
now = datetime.now(timezone.utc).timestamp()
seconds_ago = int(now - neighbor.heard_timestamp) if neighbor.heard_timestamp > 0 else 0
print(f" Last heard: {seconds_ago} seconds ago")
# Access NeighborEntry properties
# Each neighbor is a NeighborEntry object with:
# - node_id: str (hex string of pubkey prefix)
# - advert_timestamp: int (Unix timestamp)
# - heard_timestamp: int (Unix timestamp)
# - snr: float (SNR in dB, direct int8_t value)
# Convert to dictionary for JSON serialization
if response.neighbors:
neighbors_json = [neighbor.to_dict() for neighbor in response.neighbors]
print(f"\nNeighbors as JSON:")
import json
print(json.dumps(neighbors_json, indent=2))Response Packet Structure (after decryption):
Byte 0-3: sender_timestamp (uint32_t, little-endian)
Byte 4-5: neighbours_count (uint16_t, little-endian) - total available
Byte 6-7: results_count (uint16_t, little-endian) - in this packet
Byte 8+: Array of neighbor entries, each entry:
- pubkey_prefix (variable length, 1-32 bytes)
- heard_seconds_ago (uint32_t, little-endian, 4 bytes)
- snr (int8_t, 1 byte)
Neighbor Entry Format:
- pubkey_prefix: First N bytes of the neighbor's public key (typically 1 or 4 bytes in compressed format)
- heard_seconds_ago: Seconds since the neighbor was last heard (relative to sender_timestamp)
- snr: Signal-to-noise ratio as signed int8_t (range: -128 to 127 dB)
Timestamp Calculation:
timestamp = sender_timestamp - heard_seconds_ago
Neighbor List Request/Response Flow:
- Client sends encrypted Request packet with neighbor list request to target repeater
- Repeater receives request, decrypts it, and processes the request
- Repeater responds with encrypted Response packet containing:
- sender_timestamp: When the response was created
- neighbours_count: Total number of neighbors available
- results_count: Number of neighbors in this packet
- Neighbor entries: Array of neighbor data with pubkey_prefix, heard_seconds_ago, and SNR
- Client decrypts response using node key (sender's public key + client's private key) to access neighbor table data
How Repeaters Define Neighbors:
- Repeaters detect neighbors through zero-hop ADVERT packets broadcast periodically
- When a repeater receives an advertisement, it stores:
- Node ID: Public key prefix (1-32 bytes, depending on request)
- Heard Timestamp: Calculated as
sender_timestamp - heard_seconds_ago - Advert Timestamp: Same as heard_timestamp
- SNR: Signal-to-Noise Ratio (stored directly as int8_t)
SNR Storage Format:
- Format:
int8_t(signed 8-bit integer) - Encoding: Direct value (not multiplied)
- Range: -128 dB to 127 dB
- Usage:
snr_dB = raw_value(no conversion needed)
Comparison of SNR Data Sources:
| Source | SNR Data | Additional Data | Requires Decryption |
|---|---|---|---|
| TRACE Packets | ✅ Path SNR per hop | Path hashes, trace tag | ❌ No |
| Response Packets | ✅ Neighbor SNR | Full neighbor table (pubkey prefixes, timestamps) | ✅ Yes (node key) |
| DISCOVER_RESP (Control) | ✅ SNR | Node info, tag, pubkey (8 or 32 bytes) | ❌ No |
Note: Response packets are the only source that provides the complete neighbor table with pubkey prefixes, timestamps, and SNR values. TRACE packets provide SNR for the specific path taken. Control packets with sub_type DISCOVER_RESP provide SNR and node pubkey (or prefix) without decryption.
NeighborEntry Object:
Each neighbor in response.neighbors is a NeighborEntry object with the following properties:
class NeighborEntry:
node_id: str # Pubkey prefix as hex string (1-32 bytes, typically 1 or 4 bytes)
advert_timestamp: int # Unix timestamp (same as heard_timestamp)
heard_timestamp: int # Unix timestamp (calculated from sender_timestamp - heard_seconds_ago)
snr: float # SNR in dB (direct int8_t value, range: -128 to 127)
def to_dict() -> Dict # Convert to dictionary for JSON serializationExample: Filtering and Processing Neighbors:
# Filter neighbors by SNR threshold
strong_neighbors = [n for n in response.neighbors if n.snr > 10.0]
print(f"Neighbors with SNR > 10 dB: {len(strong_neighbors)}")
# Sort by SNR (strongest first)
sorted_neighbors = sorted(response.neighbors, key=lambda n: n.snr, reverse=True)
print("\nNeighbors sorted by SNR (strongest first):")
for neighbor in sorted_neighbors[:5]: # Top 5
print(f" {neighbor.node_id[:8]}...: {neighbor.snr:.1f} dB")
# Find recently heard neighbors (within last hour)
from datetime import datetime, timezone, timedelta
one_hour_ago = (datetime.now(timezone.utc) - timedelta(hours=1)).timestamp()
recent = [n for n in response.neighbors if n.heard_timestamp > one_hour_ago]
print(f"\nNeighbors heard in last hour: {len(recent)}")Using the CLI to Decode Response Packets:
# Decode a response packet with neighbor data
python cli.py decode <packet_hex> --node-key <PUBKEY>:<PRIVKEY>
# Example:
python cli.py decode 06002E... --node-key 969605...:1010fe...The CLI will automatically:
- Decrypt the response using the provided node key
- Parse the neighbor entries
- Display total neighbors available and neighbors in the packet
- Show each neighbor's pubkey prefix, SNR, and timestamps
Each packet is: header (1 byte) → optional transport_codes (4 bytes for Transport Flood/Direct) → path_length (1 byte) → path (0–64 bytes) → payload (0–184 bytes). The header encodes route type (bits 0–1), payload type (bits 2–5), and payload version (bits 6–7). Path length is variable (e.g. 0, 1, or 2 bytes of path data are all valid). Full field descriptions and tables are in docs/packet_format.md.
For detailed packet analysis and debugging, use analyze_structure() to get byte-level breakdowns:
from meshcoredecoder import MeshCoreDecoder
print('=== Packet Breakdown ===')
hex_data = '11007E7662...'
print(f"Packet length: {len(hex_data)}")
print(f"Expected bytes: {len(hex_data) / 2}")
structure = MeshCoreDecoder.analyze_structure(hex_data)
print('\nMain segments:')
for i, seg in enumerate(structure.segments):
print(f"{i+1}. {seg.name} (bytes {seg.start_byte}-{seg.end_byte}): {seg.value}")
print('\nPayload segments:')
for i, seg in enumerate(structure.payload['segments']):
print(f"{i+1}. {seg.name} (bytes {seg.start_byte}-{seg.end_byte}): {seg.value}")
print(f" Description: {seg.description}")Output:
=== Packet Breakdown ===
Packet length: 268
Expected bytes: 134
Main segments:
1. Header (bytes 0-0): 0x11
2. Path Length (bytes 1-1): 0x00
3. Payload (bytes 2-133): 7E7662676F7F...
Payload segments:
1. Public Key (bytes 0-31): 7E7662676F7F0850A8A355BAAFBFC1EB7B4174C340442D7D7161C9474A2C9400
Description: Ed25519 public key
2. Timestamp (bytes 32-35): 6CE7CF68
Description: 1758455660 (2025-09-21T11:54:20Z)
3. Signature (bytes 36-99): 2E58408DD8FCC51906ECA98EBF94A037886BDADE7ECD09FD92B839491DF3809C9454F5286D1D3370AC31A34593D569E9A042A3B41FD331DFFB7E18599CE1E609
Description: Ed25519 signature
4. App Flags (bytes 100-100): 92
Description: Binary: 10010010 | Bits 0-3 (Role): Room server | Bit 4 (Location): Yes | Bit 5 (Feature1): No | Bit 6 (Feature2): No | Bit 7 (Name): Yes
5. Latitude (bytes 101-104): A076D502
Description: 47.543968° (47.543968)
6. Longitude (bytes 105-108): 38C5B8F8
Description: -122.108616° (-122.108616)
7. Node Name (bytes 109-131): 5757375354522F50756765744D65736820436F75676172
Description: Node name: "WW7STR/PugetMesh Cougar"
The analyze_structure() method provides:
- Header breakdown with bit-level field analysis
- Byte-accurate segments with start/end positions
- Payload field parsing for all supported packet types
- Human-readable descriptions for each field
The library includes MeshCore-compatible Ed25519 key derivation using the exact orlp/ed25519 algorithm:
from meshcoredecoder.crypto import derive_public_key, validate_key_pair
# Derive public key from MeshCore private key (64-byte format)
private_key = '18469d614044...'
public_key = derive_public_key(private_key)
print('Derived Public Key:', public_key)
# Output: 4852B693645...
# Validate a key pair
is_valid = validate_key_pair(private_key, public_key)
print('Key pair valid:', is_valid) # TrueFor quick analysis from the terminal, use the CLI:
# Analyze a packet
python cli.py decode 11007E7662676...
# With decryption (provide channel secrets)
python cli.py decode 150011C3C... --key 8b3387e9c...
# Show detailed structure analysis
python cli.py decode --structure 11007E7662676F7...
# JSON output
python cli.py decode --json 11007E7662676F7F085...
# Derive public key from MeshCore private key
python cli.py derive-key 18469d6140447f77...
# Validate key pair
python cli.py validate-key 18469d6140447f77de13... 4852b69...Here are examples of how to decode all supported packet types using the Python API in your own scripts. These examples use real test packets from the test suite.
First, set up your keys for decryption:
from meshcoredecoder import MeshCoreDecoder
from meshcoredecoder.crypto import MeshCoreKeyStore
from meshcoredecoder.types.crypto import DecryptionOptions
from meshcoredecoder.types.enums import PayloadType
from datetime import datetime
# Test keys (truncated for readability)
NODE_KEY = "2e5c4e32...:1010fe34..."
PEER_KEY = "969605c0..."
CHANNEL_KEY = "9cd8fcf2..."
# Create key store for encrypted packets
key_store = MeshCoreKeyStore()
key_store.add_node_key(
"2e5c4e32...",
"1010fe34..."
)
key_store.add_peer_key("969605c0...")
key_store.add_channel_secret("9cd8fcf2...")
decryption_options = DecryptionOptions(key_store=key_store)packet_hex = "0200962E8AD0F2E571B007FF696A06BFEE285ADB5394" # ... (truncated)
packet = MeshCoreDecoder.decode(packet_hex, decryption_options)
if packet.payload_type == PayloadType.Request:
request = packet.payload['decoded']
print(f"Request Type: {request.request_type}")
print(f"Timestamp: {datetime.fromtimestamp(request.timestamp)}")packet_hex = "06002E963EB..." # ... (truncated)
packet = MeshCoreDecoder.decode(packet_hex, decryption_options)
if packet.payload_type == PayloadType.Response:
response = packet.payload['decoded']
if response.decrypted and response.decrypted.get('content', {}).get('type') == 'stats':
print("Response Type: Stats")
print(f"Tag: {response.decrypted['tag']}")
# Stats data is in response.decrypted['content']['stats_data']packet_hex = "0200962E..." # ... (truncated)
packet = MeshCoreDecoder.decode(packet_hex, decryption_options)
if packet.payload_type == PayloadType.Request:
request = packet.payload['decoded']
if request.request_type == 0x06: # GetNeighbours
print(f"Request Type: GetNeighbours")
print(f"Count: {request.request_data.get('count')}")
print(f"Offset: {request.request_data.get('offset')}")packet_hex = "06002E960D..." # ... (truncated)
packet = MeshCoreDecoder.decode(packet_hex, decryption_options)
if packet.payload_type == PayloadType.Response:
response = packet.payload['decoded']
if response.decrypted:
content = response.decrypted.get('content', {})
if content.get('type') == 'neighbours':
print(f"Total Neighbours: {content.get('neighbours_count')}")
print(f"Results in Response: {content.get('results_count')}")
for i, neighbor in enumerate(content.get('neighbors', []), 1):
print(f" {i}. {neighbor.get('pubkey_prefix')} - SNR: {neighbor.get('snr')/4.0:.2f} dB")packet_hex = "020096..." # ... (truncated)
packet = MeshCoreDecoder.decode(packet_hex, decryption_options)
if packet.payload_type == PayloadType.Request:
request = packet.payload['decoded']
if request.request_type == 0x03: # GetTelemetryData
print(f"Request Type: GetTelemetryData")
print(f"Permission Mask: {request.request_data.get('permission_mask_hex')}")packet_hex = "06002E9..." # ... (truncated)
packet = MeshCoreDecoder.decode(packet_hex, decryption_options)
if packet.payload_type == PayloadType.Response:
response = packet.payload['decoded']
if response.decrypted:
content = response.decrypted.get('content', {})
if content.get('type') == 'telemetry':
print("Response Type: Telemetry")
print(f"Telemetry Data: {content.get('telemetry_data')}")
# Parse LPP format telemetry datapacket_hex = "1107D978C2..." # ... (truncated)
packet = MeshCoreDecoder.decode(packet_hex)
if packet.payload_type == PayloadType.Advert:
advert = packet.payload['decoded']
print(f"Device Name: {advert.app_data.get('name')}")
print(f"Device Role: {advert.app_data.get('device_role')}")
if advert.app_data.get('location'):
loc = advert.app_data['location']
print(f"Location: {loc['latitude']}, {loc['longitude']}")
print(f"Timestamp: {datetime.fromtimestamp(advert.timestamp)}")packet_hex = "0D055..."
packet = MeshCoreDecoder.decode(packet_hex)
if packet.payload_type == PayloadType.Ack:
ack = packet.payload['decoded']
print(f"Checksum: {ack.checksum}") # 4-byte CRC (hex)packet_hex = "260..."
packet = MeshCoreDecoder.decode(packet_hex)
if packet.payload_type == PayloadType.Trace:
trace = packet.payload['decoded']
print(f"Trace Tag: 0x{trace.trace_tag:08X}")
print(f"Path Hashes: {' → '.join(trace.path_hashes)}")
if trace.snr_values:
print("SNR Values:")
for i, snr in enumerate(trace.snr_values, 1):
print(f" Hop {i}: {snr:.1f} dB")# Control payloads are unencrypted. Sub-types include DISCOVER_REQ (0x8) and DISCOVER_RESP (0x9).
packet_hex = "2D00..." # Flood + Control + path_len=0 + payload (flags + data)
packet = MeshCoreDecoder.decode(packet_hex)
if packet.payload_type == PayloadType.Control and packet.payload.get('decoded'):
ctrl = packet.payload['decoded']
print(f"Sub-type: 0x{ctrl.sub_type:x}")
if ctrl.parsed.get('sub_type_name') == 'DISCOVER_REQ':
print(f" prefix_only: {ctrl.parsed.get('prefix_only')}")
print(f" type_filter: {ctrl.parsed.get('type_filter_hex')}")
print(f" tag: {ctrl.parsed.get('tag_hex')}")
elif ctrl.parsed.get('sub_type_name') == 'DISCOVER_RESP':
print(f" SNR: {ctrl.parsed.get('snr_db')} dB")
print(f" tag: {ctrl.parsed.get('tag_hex')}")
print(f" pubkey: {ctrl.parsed.get('pubkey', '')[:16]}...")packet_hex = "15062B0..." # ... (truncated)
packet = MeshCoreDecoder.decode(packet_hex, decryption_options)
if packet.payload_type == PayloadType.GroupText:
group_text = packet.payload['decoded']
if group_text.decrypted:
print(f"Sender: {group_text.decrypted.get('sender')}")
print(f"Message: {group_text.decrypted.get('message')}")
print(f"Timestamp: {datetime.fromtimestamp(group_text.decrypted.get('timestamp', 0))}")
else:
print("Message encrypted (no key available)")packet_hex = "0A002..." # ... (truncated)
packet = MeshCoreDecoder.decode(packet_hex, decryption_options)
if packet.payload_type == PayloadType.TextMessage:
text_msg = packet.payload['decoded']
if text_msg.decrypted:
print(f"Message: {text_msg.decrypted.get('message')}")
print(f"Text Type: {text_msg.decrypted.get('text_type')}")
print(f"Timestamp: {datetime.fromtimestamp(text_msg.decrypted.get('timestamp', 0))}")packet_hex = "2105D..."
packet = MeshCoreDecoder.decode(packet_hex, decryption_options)
if packet.payload_type == PayloadType.Path:
path = packet.payload['decoded']
print(f"Destination Hash: {path.destination_hash}")
print(f"Source Hash: {path.source_hash}")
if path.decrypted:
print(f"Decrypted Path Data: {path.decrypted}")
else:
print("Path encrypted (decryption may have failed)")packet_hex = "1E013..." # ... (truncated)
packet = MeshCoreDecoder.decode(packet_hex, decryption_options)
if packet.payload_type == PayloadType.AnonRequest:
anon_req = packet.payload['decoded']
print(f"Sender Public Key: {anon_req.sender_public_key}")
print(f"Destination Hash: {anon_req.destination_hash}")
if anon_req.decrypted:
print(f"Decrypted Request Data: {anon_req.decrypted}")packet_hex = "05002..."
packet = MeshCoreDecoder.decode(packet_hex, decryption_options)
if packet.payload_type == PayloadType.Response:
response = packet.payload['decoded']
if response.decrypted:
content = response.decrypted.get('content', {})
if content.get('type') == 'login_response':
print(f"Response Code: 0x{content.get('response_code'):02x}")
print(f"Is Admin: {bool(content.get('is_admin'))}")
print(f"Permissions: 0x{content.get('permissions'):02x}")
print(f"Firmware Version: {content.get('firmware_version')}")Here's a complete example that decodes multiple packet types:
from meshcoredecoder import MeshCoreDecoder
from meshcoredecoder.crypto import MeshCoreKeyStore
from meshcoredecoder.types.crypto import DecryptionOptions
from meshcoredecoder.types.enums import PayloadType
# Setup keys (truncated for readability)
key_store = MeshCoreKeyStore()
key_store.add_node_key("2e5c4e32...", "1010fe34...")
key_store.add_peer_key("969605c0...")
key_store.add_channel_secret("9cd8fcf2...")
options = DecryptionOptions(key_store=key_store)
# Example packets (hex strings truncated for readability)
packets = [
("Advert", "1107D978...", None),
("GroupText", "15062B...", options),
("GET_STATS Response", "06002E96...", options),
]
for name, hex_data, opts in packets:
print(f"\n=== {name} ===")
packet = MeshCoreDecoder.decode(hex_data, opts)
print(f"Route Type: {packet.route_type}")
print(f"Payload Type: {packet.payload_type}")
print(f"Message Hash: {packet.message_hash}")
# Process based on payload type...To release a new version and upload to PyPI:
-
Bump the version in these files (keep them in sync):
pyproject.toml→version = "X.Y.Z"meshcoredecoder/__init__.py→__version__ = "X.Y.Z"setup.py→version="X.Y.Z"index.py→__version__ = "X.Y.Z"
-
Install build tools (if needed):
pip install build twine
-
Build the package:
python -m build
This creates
dist/meshcoredecoder-X.Y.Z.tar.gzanddist/meshcoredecoder_X.Y.Z-*.whl. -
Upload to PyPI (requires a PyPI account and API token):
twine upload dist/*Use your PyPI username and an API token as the password. For Test PyPI first:
twine upload --repository testpypi dist/* -
Tag the release (optional):
git tag vX.Y.Z git push origin vX.Y.Z
MIT License
Copyright (c) 2025 Michael Hart michaelhart@michaelhart.me (https://github.com/michaelhart)
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.