package decoder import ( "fmt" "strings" "time" "google.golang.org/protobuf/proto" meshtreampb "meshstream/generated/meshstream" pb "meshstream/generated/meshtastic" ) // ParseTopic parses a Meshtastic MQTT topic into its components func ParseTopic(topic string) (*meshtreampb.TopicInfo, error) { info := &meshtreampb.TopicInfo{ FullTopic: topic, } // 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 info, fmt.Errorf("invalid topic format: %s", topic) } // Find protocol version and format indices by looking for "2" followed by "e" 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 info, fmt.Errorf("invalid topic format, missing version/format: %s", topic) } // Extract region path (all segments between "msh" and version) if versionIndex > 1 { info.RegionPath = strings.Join(parts[1:versionIndex], "/") } // Extract version and format info.Version = parts[versionIndex] info.Format = parts[formatIndex] // Extract channel and user ID channelIndex := formatIndex + 1 userIdIndex := channelIndex + 1 if channelIndex < len(parts) { info.Channel = parts[channelIndex] } if userIdIndex < len(parts) { info.UserId = parts[userIdIndex] } return info, nil } // maxPayloadBytes caps the encrypted payload size we will attempt to decrypt. // Meshtastic radio packets are capped at ~256 bytes; this limit blocks malformed // or oversized messages from consuming CPU on the decode path. const maxPayloadBytes = 4096 // DecodeEncodedMessage decodes a binary encoded message (format "e") func DecodeEncodedMessage(payload []byte) (*pb.ServiceEnvelope, error) { if len(payload) > maxPayloadBytes { return nil, fmt.Errorf("OVERSIZED_PAYLOAD") } var serviceEnvelope pb.ServiceEnvelope if err := proto.Unmarshal(payload, &serviceEnvelope); err != nil { return nil, fmt.Errorf("DECODE_ERROR") } return &serviceEnvelope, nil } // DecodeMessage creates a Data object from a binary encoded message func DecodeMessage(payload []byte, topicInfo *meshtreampb.TopicInfo) *meshtreampb.Data { data := &meshtreampb.Data{ // Add reception timestamp (Unix timestamp in seconds) RxTime: uint64(time.Now().Unix()), } // First decode the envelope envelope, err := DecodeEncodedMessage(payload) if err != nil { data.DecodeError = err.Error() return data } // Extract envelope fields data.ChannelId = envelope.GetChannelId() data.GatewayId = envelope.GetGatewayId() // Extract mesh packet fields if available packet := envelope.GetPacket() if packet == nil { data.DecodeError = "NO_PACKET" return data } // Extract mesh packet fields data.Id = packet.GetId() data.From = packet.GetFrom() data.To = packet.GetTo() data.HopLimit = packet.GetHopLimit() data.HopStart = packet.GetHopStart() data.WantAck = packet.GetWantAck() data.Priority = packet.GetPriority().String() data.ViaMqtt = packet.GetViaMqtt() data.NextHop = packet.GetNextHop() data.RelayNode = packet.GetRelayNode() data.RxSnr = packet.GetRxSnr() data.RxRssi = packet.GetRxRssi() // Process the payload if packet.GetDecoded() != nil { // Packet has already been decoded decodeDataPayload(data, packet.GetDecoded()) } else if packet.GetEncrypted() != nil { // Packet is encrypted, try to decrypt it decodeEncryptedPayload(data, packet.GetEncrypted(), envelope.GetChannelId(), packet.GetId(), packet.GetFrom()) } else { data.DecodeError = "NO_PAYLOAD" } return data } // decodeDataPayload extracts information from a Data message func decodeDataPayload(data *meshtreampb.Data, pbData *pb.Data) { // Extract data fields data.PortNum = pbData.GetPortnum() data.RequestId = pbData.GetRequestId() data.ReplyId = pbData.GetReplyId() data.Emoji = pbData.GetEmoji() data.Dest = pbData.GetDest() data.Source = pbData.GetSource() data.WantResponse = pbData.GetWantResponse() // Process the payload based on port type payload := pbData.GetPayload() switch pbData.GetPortnum() { case pb.PortNum_TEXT_MESSAGE_APP: // Text message - store as string data.Payload = &meshtreampb.Data_TextMessage{ TextMessage: string(payload), } case pb.PortNum_TEXT_MESSAGE_COMPRESSED_APP: // Compressed text - store the raw bytes data.Payload = &meshtreampb.Data_CompressedText{ CompressedText: payload, } case pb.PortNum_POSITION_APP: // Position data var position pb.Position if err := proto.Unmarshal(payload, &position); err != nil { data.DecodeError = "PARSE_ERROR" } else { data.Payload = &meshtreampb.Data_Position{ Position: &position, } } case pb.PortNum_NODEINFO_APP: // Node information var user pb.User if err := proto.Unmarshal(payload, &user); err != nil { data.DecodeError = "PARSE_ERROR" } else { data.Payload = &meshtreampb.Data_NodeInfo{ NodeInfo: &user, } } case pb.PortNum_TELEMETRY_APP: // Telemetry data var telemetry pb.Telemetry if err := proto.Unmarshal(payload, &telemetry); err != nil { data.DecodeError = "PARSE_ERROR" } else { data.Payload = &meshtreampb.Data_Telemetry{ Telemetry: &telemetry, } } case pb.PortNum_WAYPOINT_APP: // Waypoint data var waypoint pb.Waypoint if err := proto.Unmarshal(payload, &waypoint); err != nil { data.DecodeError = "PARSE_ERROR" } else { data.Payload = &meshtreampb.Data_Waypoint{ Waypoint: &waypoint, } } case pb.PortNum_MAP_REPORT_APP: // Map report data var mapReport pb.MapReport if err := proto.Unmarshal(payload, &mapReport); err != nil { data.DecodeError = "PARSE_ERROR" } else { data.Payload = &meshtreampb.Data_MapReport{ MapReport: &mapReport, } } case pb.PortNum_TRACEROUTE_APP: // Traceroute data var routeDiscovery pb.RouteDiscovery if err := proto.Unmarshal(payload, &routeDiscovery); err != nil { data.DecodeError = "PARSE_ERROR" } else { data.Payload = &meshtreampb.Data_RouteDiscovery{ RouteDiscovery: &routeDiscovery, } } case pb.PortNum_NEIGHBORINFO_APP: // Neighbor information data var neighborInfo pb.NeighborInfo if err := proto.Unmarshal(payload, &neighborInfo); err != nil { data.DecodeError = "PARSE_ERROR" } else { data.Payload = &meshtreampb.Data_NeighborInfo{ NeighborInfo: &neighborInfo, } } case pb.PortNum_REMOTE_HARDWARE_APP: // Remote hardware data var hardware pb.HardwareMessage if err := proto.Unmarshal(payload, &hardware); err != nil { data.DecodeError = "PARSE_ERROR" } else { data.Payload = &meshtreampb.Data_RemoteHardware{ RemoteHardware: &hardware, } } case pb.PortNum_ROUTING_APP: // Routing data var routing pb.Routing if err := proto.Unmarshal(payload, &routing); err != nil { data.DecodeError = "PARSE_ERROR" } else { data.Payload = &meshtreampb.Data_Routing{ Routing: &routing, } } case pb.PortNum_ADMIN_APP: // Admin data var admin pb.AdminMessage if err := proto.Unmarshal(payload, &admin); err != nil { data.DecodeError = "PARSE_ERROR" } else { data.Payload = &meshtreampb.Data_Admin{ Admin: &admin, } } case pb.PortNum_PAXCOUNTER_APP: // Paxcount data var paxcount pb.Paxcount if err := proto.Unmarshal(payload, &paxcount); err != nil { data.DecodeError = "PARSE_ERROR" } else { data.Payload = &meshtreampb.Data_Paxcounter{ Paxcounter: &paxcount, } } default: // For other types, just store the raw bytes data.Payload = &meshtreampb.Data_BinaryData{ BinaryData: payload, } } } // decodeEncryptedPayload tries to decrypt and decode encrypted payloads func decodeEncryptedPayload(data *meshtreampb.Data, encrypted []byte, channelId string, packetId, fromNode uint32) { // Attempt to decrypt the payload using the channel key if channelId == "" { data.DecodeError = "NO_CHANNEL_ID" return } channelKey := GetChannelKey(channelId) decrypted, err := XOR(encrypted, channelKey, packetId, fromNode) if err != nil { data.DecodeError = "DECRYPT_FAILED" return } // Try to parse as a Data message var pbData pb.Data if err := proto.Unmarshal(decrypted, &pbData); err != nil { // If we can't parse as Data, check if it's ASCII text if IsASCII(decrypted) { data.PortNum = pb.PortNum_TEXT_MESSAGE_APP data.Payload = &meshtreampb.Data_TextMessage{ TextMessage: string(decrypted), } } else { // Check if this channel is configured - if not, likely a private message if !IsChannelConfigured(channelId) { data.DecodeError = "PRIVATE_CHANNEL" } else { data.DecodeError = "PARSE_ERROR" } data.Payload = &meshtreampb.Data_BinaryData{ BinaryData: decrypted, } } } else { // Successfully decoded the payload decodeDataPayload(data, &pbData) } } // IsASCII checks if the given byte array contains only ASCII characters func IsASCII(data []byte) bool { for _, b := range data { // Check for non-control ASCII characters and common control characters if (b < 32 && b != 9 && b != 10 && b != 13) || b > 126 { return false } } return true }