Files
meshstream/decoder/decoder.go

323 lines
9.2 KiB
Go

package decoder
import (
"encoding/json"
"fmt"
"strings"
"unicode/utf8"
)
// PacketType represents the type of a Meshtastic packet
type PacketType string
const (
TypeJSON PacketType = "json"
TypeEncoded PacketType = "encoded"
TypeText PacketType = "text"
)
// DecodedPacket contains information about a decoded packet
type DecodedPacket struct {
// Topic structure fields
Topic string
RegionPath string
Version string
Format string
Channel string
UserID string
Type PacketType
// JSON message fields
JSONData map[string]interface{}
FromNode string
ToNode string
Text string
Timestamp string
// Encoded message fields
ChannelID string
GatewayID string
PacketID string
// Raw data
RawData []byte
}
// DecodePacket attempts to decode a packet from MQTT
func DecodePacket(topic string, payload []byte) (*DecodedPacket, error) {
packet := &DecodedPacket{
Topic: topic,
RawData: payload,
JSONData: make(map[string]interface{}),
}
// Extract topic components
// Format: msh/REGION_PATH/VERSION/FORMAT/CHANNELNAME/USERID
// Example: msh/US/CA/Motherlode/2/e/LongFast/!abcd1234
// Example: msh/US/CA/Motherlode/2/json/LongFast/!abcd1234
parts := strings.Split(topic, "/")
if len(parts) < 4 {
return packet, fmt.Errorf("invalid topic format: %s", topic)
}
// Find protocol version and format indices by looking for "2" followed by "e", "c", or "json"
versionIndex := -1
formatIndex := -1
for i := 1; i < len(parts)-1; i++ {
if parts[i] == "2" {
// Found the version
versionIndex = i
formatIndex = i + 1
break
}
}
if versionIndex == -1 || formatIndex >= len(parts) {
// Could not find proper version/format markers
return packet, fmt.Errorf("invalid topic format, missing version/format: %s", topic)
}
// Extract region path (all segments between "msh" and version)
if versionIndex > 1 {
packet.RegionPath = strings.Join(parts[1:versionIndex], "/")
}
// Extract version and format
packet.Version = parts[versionIndex]
packet.Format = parts[formatIndex]
// Process based on format type
channelIndex := formatIndex + 1
userIdIndex := channelIndex + 1
if channelIndex < len(parts) {
packet.Channel = parts[channelIndex]
}
if userIdIndex < len(parts) {
packet.UserID = parts[userIdIndex]
}
// Process based on format type
if packet.Format == "e" {
// Encoded protobuf packet (using ServiceEnvelope)
packet.Type = TypeEncoded
// For encoded packets, try to parse if the payload looks like a JSON ServiceEnvelope
// (Some gateways present ServiceEnvelope in JSON format)
if len(payload) > 0 && payload[0] == '{' {
var serviceEnvelope map[string]interface{}
if err := json.Unmarshal(payload, &serviceEnvelope); err == nil {
// Successfully parsed as JSON ServiceEnvelope
packet.JSONData = serviceEnvelope
// Extract ServiceEnvelope metadata fields
if channelId, ok := serviceEnvelope["channel_id"].(string); ok {
packet.ChannelID = channelId
}
if gatewayId, ok := serviceEnvelope["gateway_id"].(string); ok {
packet.GatewayID = gatewayId
}
// Try to extract data from the packet field
if packetData, ok := serviceEnvelope["packet"].(map[string]interface{}); ok {
if id, ok := packetData["id"].(float64); ok {
packet.PacketID = fmt.Sprintf("%d", int(id))
}
if from, ok := packetData["from"].(float64); ok {
packet.FromNode = fmt.Sprintf("%d", int(from))
}
if to, ok := packetData["to"].(float64); ok {
packet.ToNode = fmt.Sprintf("%d", int(to))
}
// Try to extract decoded payload if available
if decoded, ok := packetData["decoded"].(map[string]interface{}); ok {
if payload, ok := decoded["payload"].(string); ok {
packet.Text = payload
}
}
}
}
}
// Note: For binary protocol buffer decoding we would need to use the generated protobuf code,
// but we'll defer that for now and just track that this is an encoded packet.
} else if packet.Format == "json" {
// JSON format
packet.Type = TypeJSON
if err := json.Unmarshal(payload, &packet.JSONData); err != nil {
return packet, fmt.Errorf("failed to parse JSON: %v", err)
}
// Extract common fields
if from, ok := packet.JSONData["from"].(string); ok {
packet.FromNode = from
}
if to, ok := packet.JSONData["to"].(string); ok {
packet.ToNode = to
}
if text, ok := packet.JSONData["payload"].(string); ok {
packet.Text = text
}
if ts, ok := packet.JSONData["timestamp"].(string); ok {
packet.Timestamp = ts
}
} else {
// Unknown format, try to infer from content
if len(payload) > 0 && payload[0] == '{' {
// Looks like JSON
packet.Type = TypeJSON
if err := json.Unmarshal(payload, &packet.JSONData); err == nil {
// Successfully parsed as JSON
// Extract common fields
if from, ok := packet.JSONData["from"].(string); ok {
packet.FromNode = from
}
if to, ok := packet.JSONData["to"].(string); ok {
packet.ToNode = to
}
if text, ok := packet.JSONData["payload"].(string); ok {
packet.Text = text
}
if ts, ok := packet.JSONData["timestamp"].(string); ok {
packet.Timestamp = ts
}
}
} else if utf8.Valid(payload) && !containsBinaryData(payload) {
// Probably text
packet.Type = TypeText
packet.Text = string(payload)
} else {
// Encoded but not in JSON format
packet.Type = TypeEncoded
}
}
return packet, nil
}
// containsBinaryData does a simple check to see if a byte slice likely contains binary data
// by checking for control characters that aren't common in text
func containsBinaryData(data []byte) bool {
for _, b := range data {
// Skip common control characters
if b == '\n' || b == '\r' || b == '\t' {
continue
}
// If we find a control character, it's probably binary data
if b < 32 || b > 126 {
return true
}
}
return false
}
// FormatPacket formats a decoded packet for display
func FormatPacket(packet *DecodedPacket) string {
var builder strings.Builder
builder.WriteString(fmt.Sprintf("Topic: %s\n", packet.Topic))
// Show basic topic structure
builder.WriteString(fmt.Sprintf("Region Path: %s\n", packet.RegionPath))
if packet.Version != "" {
builder.WriteString(fmt.Sprintf("Version: %s\n", packet.Version))
}
if packet.Format != "" {
builder.WriteString(fmt.Sprintf("Format: %s\n", packet.Format))
}
if packet.Channel != "" {
builder.WriteString(fmt.Sprintf("Channel: %s\n", packet.Channel))
}
if packet.UserID != "" {
builder.WriteString(fmt.Sprintf("User ID: %s\n", packet.UserID))
}
switch packet.Type {
case TypeJSON:
builder.WriteString("Type: JSON\n")
if packet.FromNode != "" {
builder.WriteString(fmt.Sprintf("From: %s\n", packet.FromNode))
}
if packet.ToNode != "" {
builder.WriteString(fmt.Sprintf("To: %s\n", packet.ToNode))
}
if packet.Text != "" {
builder.WriteString(fmt.Sprintf("Text: %s\n", packet.Text))
}
if packet.Timestamp != "" {
builder.WriteString(fmt.Sprintf("Timestamp: %s\n", packet.Timestamp))
}
// Format remaining JSON data
jsonBytes, _ := json.MarshalIndent(packet.JSONData, "", " ")
builder.WriteString(fmt.Sprintf("Data: %s\n", jsonBytes))
case TypeEncoded:
builder.WriteString("Type: Encoded (ServiceEnvelope)\n")
// Display ServiceEnvelope metadata if available
if packet.ChannelID != "" {
builder.WriteString(fmt.Sprintf("Channel ID: %s\n", packet.ChannelID))
}
if packet.GatewayID != "" {
builder.WriteString(fmt.Sprintf("Gateway ID: %s\n", packet.GatewayID))
}
if packet.PacketID != "" {
builder.WriteString(fmt.Sprintf("Packet ID: %s\n", packet.PacketID))
}
if packet.FromNode != "" {
builder.WriteString(fmt.Sprintf("From Node: %s\n", packet.FromNode))
}
if packet.ToNode != "" {
builder.WriteString(fmt.Sprintf("To Node: %s\n", packet.ToNode))
}
if packet.Text != "" {
builder.WriteString(fmt.Sprintf("Payload: %s\n", packet.Text))
}
// If we were able to parse as JSON, show the data
if len(packet.JSONData) > 0 {
jsonBytes, _ := json.MarshalIndent(packet.JSONData, "", " ")
builder.WriteString(fmt.Sprintf("Service Envelope: %s\n", jsonBytes))
} else {
// Show raw binary data
builder.WriteString(fmt.Sprintf("Raw Data (%d bytes): %x\n", len(packet.RawData), packet.RawData))
}
case TypeText:
builder.WriteString("Type: Text\n")
builder.WriteString(fmt.Sprintf("Content: %s\n", packet.Text))
default:
// Debug case for unknown packet types
builder.WriteString(fmt.Sprintf("Type: UNKNOWN (%s)\n", packet.Type))
builder.WriteString("---DEBUG INFO---\n")
builder.WriteString(fmt.Sprintf("Raw Payload (%d bytes): %x\n", len(packet.RawData), packet.RawData))
// Try to show as string if possible
if len(packet.RawData) > 0 {
builder.WriteString(fmt.Sprintf("As String: %s\n", string(packet.RawData)))
}
// Topic parts
topicParts := strings.Split(packet.Topic, "/")
builder.WriteString("Topic Parts:\n")
for i, part := range topicParts {
builder.WriteString(fmt.Sprintf(" [%d]: %s\n", i, part))
}
// Show any JSON data if present
if len(packet.JSONData) > 0 {
jsonBytes, _ := json.MarshalIndent(packet.JSONData, "", " ")
builder.WriteString(fmt.Sprintf("JSON Data: %s\n", jsonBytes))
}
}
return builder.String()
}