diff --git a/CLAUDE.md b/CLAUDE.md index 865f78c..b0fc42c 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -1 +1,52 @@ -- Never run the program, you may build it and run tests, but I will run the program manually. \ No newline at end of file +# Meshtastic MQTT Protocol Structure + +## Topic Structure + +The Meshtastic MQTT topic structure follows this pattern: +``` +msh/REGION/2/e/CHANNELNAME/USERID +``` + +- `msh`: Fixed prefix for all Meshtastic topics +- `REGION`: Geographic region code (e.g., `US`, `EU`, `AU`) +- `2`: Protocol version +- `e`: Encrypted channel indicator (versions before 2.3.0 used `/c/` instead) +- `CHANNELNAME`: The channel name used in the Meshtastic network +- `USERID`: Unique identifier for the device, formats include: + - `!` followed by hex characters for MAC address based IDs + - `+` followed by phone number for Signal-based IDs + +## Message Types + +### Protobuf Messages (Binary) +Topic pattern: `msh/REGION/2/e/CHANNELNAME/USERID` +- Raw MeshPacket transmissions in protobuf format +- Can be encrypted or unencrypted based on channel settings + +### JSON Messages +Topic pattern: `msh/REGION/2/json/CHANNELNAME/USERID` +- Structured JSON payloads with fields like: + - `id`: Message ID + - `from`: Node ID of sender + - `to`: Node ID of recipient (or blank for broadcast) + - `payload`: The actual message content + - `timestamp`: Time the message was received + +Note: JSON format is not supported on nRF52 platform devices. + +### Special Topics +- MQTT Downlink: `msh/REGION/2/json/mqtt/` + - Used for sending instructions to gateway nodes + +## Important Notes + +- The public MQTT server implements a zero-hop policy (only direct messages) +- JSON messages may include specific data types like: + - NodeInfo + - Telemetry + - Text Messages + - Position Updates +- Position data on public servers has reduced precision for privacy +- Binary messages use the protocol buffers format defined in the Meshtastic codebase + +This corrects the previous assumption that the topic structure was `msh/REGION/STATE/NAME`, which was incorrect. \ No newline at end of file diff --git a/decoder/decoder.go b/decoder/decoder.go index 97088f2..3bf89d2 100644 --- a/decoder/decoder.go +++ b/decoder/decoder.go @@ -19,7 +19,9 @@ const ( // DecodedPacket contains information about a decoded packet type DecodedPacket struct { Topic string + Region string Channel string + UserID string Type PacketType JSONData map[string]interface{} FromNode string @@ -38,20 +40,40 @@ func DecodePacket(topic string, payload []byte) (*DecodedPacket, error) { } // Extract channel and other info from topic + // Format: msh/REGION/2/e/CHANNELNAME/USERID + // or: msh/REGION/2/json/CHANNELNAME/USERID parts := strings.Split(topic, "/") if len(parts) < 4 { return packet, fmt.Errorf("invalid topic format: %s", topic) } - // Set channel info (typically msh/REGION/STATE/NAME) - packet.Channel = strings.Join(parts[1:4], "/") + // Set the region + if len(parts) >= 2 { + packet.Region = parts[1] + } - // Try to determine type from topic structure + // Extract channel name - should be part 4 for binary or part 5 for JSON if len(parts) >= 5 { - typeIndicator := parts[4] - - switch typeIndicator { - case "json": + if parts[3] == "e" || parts[3] == "c" { + // Binary format: msh/REGION/2/e/CHANNELNAME/USERID + if len(parts) >= 5 { + packet.Channel = parts[4] + } + if len(parts) >= 6 { + packet.UserID = parts[5] + } + // This is a binary protobuf packet + packet.Type = TypeBinary + } else if parts[3] == "json" { + // JSON format: msh/REGION/2/json/CHANNELNAME/USERID + if len(parts) >= 5 { + packet.Channel = parts[4] + } + if len(parts) >= 6 { + packet.UserID = parts[5] + } + + // This is a JSON packet packet.Type = TypeJSON if err := json.Unmarshal(payload, &packet.JSONData); err != nil { return packet, fmt.Errorf("failed to parse JSON: %v", err) @@ -70,19 +92,8 @@ func DecodePacket(topic string, payload []byte) (*DecodedPacket, error) { if ts, ok := packet.JSONData["timestamp"].(string); ok { packet.Timestamp = ts } - - case "binary": - // Binary protobuf payload - packet.Type = TypeBinary - // Note: Actual protobuf decoding would be done here - - case "text": - // Plain text payload - packet.Type = TypeText - packet.Text = string(payload) - - default: - // Try to infer type from payload + } else { + // Unknown format, try to infer from content if len(payload) > 0 && payload[0] == '{' { // Looks like JSON packet.Type = TypeJSON @@ -113,7 +124,7 @@ func DecodePacket(topic string, payload []byte) (*DecodedPacket, error) { } } } - + return packet, nil } @@ -139,7 +150,13 @@ 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: %s\n", packet.Region)) 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: diff --git a/main.go b/main.go index c9ba299..a33dd1b 100644 --- a/main.go +++ b/main.go @@ -67,7 +67,10 @@ func main() { log.Fatalf("Error connecting to MQTT broker: %v", token.Error()) } - // Subscribe to topic + // Subscribe to all topics for this region + // This will capture: + // - msh/US/CA/Motherlode/2/e/# (binary protobuf data) + // - msh/US/CA/Motherlode/2/json/# (JSON formatted data) topic := mqttTopicPrefix + "/#" token := client.Subscribe(topic, 0, nil) token.Wait()