Better handling of map reports

This commit is contained in:
Daniel Pupius
2025-04-20 17:18:26 -07:00
parent c28957e34d
commit f10111e37d
3 changed files with 208 additions and 181 deletions

View File

@@ -164,10 +164,10 @@ func TestDecodeMessageWithMapPayload(t *testing.T) {
}
// Format the output and check it contains expected components
formattedOutput := FormatTopicAndMapData(topicInfo, decodedPacket)
formattedOutput := FormatTopicAndPacket(topicInfo, decodedPacket)
if !strings.Contains(formattedOutput, "Map Report") {
t.Error("Expected formatted output to contain 'Map Report'")
if !strings.Contains(formattedOutput, "Map Report Data") {
t.Error("Expected formatted output to contain 'Map Report Data'")
}
if !strings.Contains(formattedOutput, "Format: map") {

View File

@@ -5,23 +5,23 @@ import (
"fmt"
"strings"
"time"
"google.golang.org/protobuf/encoding/protojson"
"google.golang.org/protobuf/proto"
pb "meshstream/proto/generated/meshtastic"
)
// FormatTelemetryMessage formats a Telemetry message
func FormatTelemetryMessage(telemetry *pb.Telemetry) string {
var builder strings.Builder
if telemetry == nil {
return "Error: nil telemetry data"
}
builder.WriteString("Telemetry Data:\n")
if telemetry.GetTime() != 0 {
tm := time.Unix(int64(telemetry.GetTime()), 0)
builder.WriteString(fmt.Sprintf(" Time: %s\n", tm.Format(time.RFC3339)))
@@ -30,54 +30,54 @@ func FormatTelemetryMessage(telemetry *pb.Telemetry) string {
// Check which telemetry type we have
if env := telemetry.GetEnvironmentMetrics(); env != nil {
builder.WriteString(" Environment Telemetry:\n")
temp := env.GetTemperature()
if temp != 0 {
builder.WriteString(fmt.Sprintf(" Temperature: %.2f °C\n", temp))
}
humidity := env.GetRelativeHumidity()
if humidity != 0 {
builder.WriteString(fmt.Sprintf(" Humidity: %.2f %%\n", humidity))
}
pressure := env.GetBarometricPressure()
if pressure != 0 {
builder.WriteString(fmt.Sprintf(" Pressure: %.2f hPa\n", pressure))
}
gasRes := env.GetGasResistance()
if gasRes != 0 {
builder.WriteString(fmt.Sprintf(" Gas Resistance: %.2f MΩ\n", gasRes))
}
iaq := env.GetIaq()
if iaq != 0 {
builder.WriteString(fmt.Sprintf(" Air Quality (IAQ): %d\n", iaq))
}
distance := env.GetDistance()
if distance != 0 {
builder.WriteString(fmt.Sprintf(" Distance: %.1f mm\n", distance))
}
lux := env.GetLux()
if lux != 0 {
builder.WriteString(fmt.Sprintf(" Light: %.1f lux\n", lux))
}
windDir := env.GetWindDirection()
if windDir != 0 {
directions := []string{"N", "NE", "E", "SE", "S", "SW", "W", "NW"}
dirIndex := int(windDir / 45) % 8
dirIndex := int(windDir/45) % 8
builder.WriteString(fmt.Sprintf(" Wind Direction: %d° (%s)\n", windDir, directions[dirIndex]))
}
windSpeed := env.GetWindSpeed()
if windSpeed != 0 {
builder.WriteString(fmt.Sprintf(" Wind Speed: %.1f m/s\n", windSpeed))
}
windGust := env.GetWindGust()
if windGust != 0 {
builder.WriteString(fmt.Sprintf(" Wind Gust: %.1f m/s\n", windGust))
@@ -86,32 +86,32 @@ func FormatTelemetryMessage(telemetry *pb.Telemetry) string {
if power := telemetry.GetPowerMetrics(); power != nil {
builder.WriteString(" Power Telemetry:\n")
ch1Volt := power.GetCh1Voltage()
if ch1Volt != 0 {
builder.WriteString(fmt.Sprintf(" Channel 1 Voltage: %.2f V\n", ch1Volt))
}
ch1Curr := power.GetCh1Current()
if ch1Curr != 0 {
builder.WriteString(fmt.Sprintf(" Channel 1 Current: %.2f mA\n", ch1Curr))
}
ch2Volt := power.GetCh2Voltage()
if ch2Volt != 0 {
builder.WriteString(fmt.Sprintf(" Channel 2 Voltage: %.2f V\n", ch2Volt))
}
ch2Curr := power.GetCh2Current()
if ch2Curr != 0 {
builder.WriteString(fmt.Sprintf(" Channel 2 Current: %.2f mA\n", ch2Curr))
}
ch3Volt := power.GetCh3Voltage()
if ch3Volt != 0 {
builder.WriteString(fmt.Sprintf(" Channel 3 Voltage: %.2f V\n", ch3Volt))
}
ch3Curr := power.GetCh3Current()
if ch3Curr != 0 {
builder.WriteString(fmt.Sprintf(" Channel 3 Current: %.2f mA\n", ch3Curr))
@@ -120,61 +120,61 @@ func FormatTelemetryMessage(telemetry *pb.Telemetry) string {
if air := telemetry.GetAirQualityMetrics(); air != nil {
builder.WriteString(" Air Quality Telemetry:\n")
pm10 := air.GetPm10Standard()
if pm10 != 0 {
builder.WriteString(fmt.Sprintf(" PM 1.0: %d µg/m³\n", pm10))
}
pm25 := air.GetPm25Standard()
if pm25 != 0 {
builder.WriteString(fmt.Sprintf(" PM 2.5: %d µg/m³\n", pm25))
}
pm100 := air.GetPm100Standard()
if pm100 != 0 {
builder.WriteString(fmt.Sprintf(" PM 10.0: %d µg/m³\n", pm100))
}
co2 := air.GetCo2()
if co2 != 0 {
builder.WriteString(fmt.Sprintf(" CO2: %d ppm\n", co2))
}
// VOC field not available in this version of the proto
}
// Device metrics
if device := telemetry.GetDeviceMetrics(); device != nil {
builder.WriteString(" Device Metrics:\n")
batLevel := device.GetBatteryLevel()
if batLevel != 0 {
builder.WriteString(fmt.Sprintf(" Battery Level: %d%%\n", batLevel))
}
voltage := device.GetVoltage()
if voltage != 0 {
builder.WriteString(fmt.Sprintf(" Voltage: %.2f V\n", voltage))
}
chanUtil := device.GetChannelUtilization()
if chanUtil != 0 {
builder.WriteString(fmt.Sprintf(" Channel Utilization: %.2f%%\n", chanUtil))
}
airUtil := device.GetAirUtilTx()
if airUtil != 0 {
builder.WriteString(fmt.Sprintf(" Air Utilization TX: %.2f%%\n", airUtil))
}
uptime := device.GetUptimeSeconds()
if uptime != 0 {
uptimeDur := time.Duration(uptime) * time.Second
builder.WriteString(fmt.Sprintf(" Uptime: %s\n", uptimeDur))
}
}
// Local stats
if stats := telemetry.GetLocalStats(); stats != nil {
builder.WriteString(" Local Statistics:\n")
@@ -204,21 +204,21 @@ func FormatTelemetryMessage(telemetry *pb.Telemetry) string {
builder.WriteString(fmt.Sprintf(" Total Nodes: %d\n", stats.GetNumTotalNodes()))
}
}
// Health metrics
if health := telemetry.GetHealthMetrics(); health != nil {
builder.WriteString(" Health Metrics:\n")
heartBpm := health.GetHeartBpm()
if heartBpm != 0 {
builder.WriteString(fmt.Sprintf(" Heart Rate: %d bpm\n", heartBpm))
}
spo2 := health.GetSpO2()
if spo2 != 0 {
builder.WriteString(fmt.Sprintf(" SpO2: %d%%\n", spo2))
}
temp := health.GetTemperature()
if temp != 0 {
builder.WriteString(fmt.Sprintf(" Body Temperature: %.1f °C\n", temp))
@@ -238,7 +238,7 @@ func FormatTelemetryMessage(telemetry *pb.Telemetry) string {
// FormatPositionMessage formats a Position message
func FormatPositionMessage(position *pb.Position) string {
var builder strings.Builder
if position == nil {
return "Error: nil position data"
}
@@ -252,10 +252,10 @@ func FormatPositionMessage(position *pb.Position) string {
// representing the position multiplied by 1e7 (10 million)
lat := float64(position.GetLatitudeI()) / 10000000.0
lon := float64(position.GetLongitudeI()) / 10000000.0
builder.WriteString(fmt.Sprintf(" Latitude: %.7f\n", lat))
builder.WriteString(fmt.Sprintf(" Longitude: %.7f\n", lon))
// Google Maps link for convenience
builder.WriteString(fmt.Sprintf(" Google Maps: https://maps.google.com/?q=%.7f,%.7f\n", lat, lon))
} else {
@@ -271,12 +271,12 @@ func FormatPositionMessage(position *pb.Position) string {
if position.GetTime() != 0 {
builder.WriteString(fmt.Sprintf(" Time: %d\n", position.GetTime()))
}
if position.GetTimestamp() != 0 {
// Convert UNIX timestamp to readable time
tm := time.Unix(int64(position.GetTimestamp()), 0)
builder.WriteString(fmt.Sprintf(" Timestamp: %s\n", tm.Format(time.RFC3339)))
if position.GetTimestampMillisAdjust() != 0 {
builder.WriteString(fmt.Sprintf(" Millis adjustment: %d\n", position.GetTimestampMillisAdjust()))
}
@@ -286,7 +286,7 @@ func FormatPositionMessage(position *pb.Position) string {
if position.GetLocationSource() != pb.Position_LOC_UNSET {
builder.WriteString(fmt.Sprintf(" Location Source: %s\n", position.GetLocationSource()))
}
if position.GetAltitudeSource() != pb.Position_ALT_UNSET {
builder.WriteString(fmt.Sprintf(" Altitude Source: %s\n", position.GetAltitudeSource()))
}
@@ -327,37 +327,37 @@ func FormatPositionMessage(position *pb.Position) string {
// FormatNodeInfoMessage formats a User message (used by the NODEINFO_APP port)
func FormatNodeInfoMessage(user *pb.User) string {
var builder strings.Builder
if user == nil {
return "Error: nil user data"
}
builder.WriteString("Node Information:\n")
if user.GetId() != "" {
builder.WriteString(fmt.Sprintf(" ID: %s\n", user.GetId()))
}
if user.GetLongName() != "" {
builder.WriteString(fmt.Sprintf(" Name: %s\n", user.GetLongName()))
}
if user.GetShortName() != "" {
builder.WriteString(fmt.Sprintf(" Short Name: %s\n", user.GetShortName()))
}
if user.GetHwModel() != pb.HardwareModel_UNSET {
builder.WriteString(fmt.Sprintf(" Hardware: %s\n", user.GetHwModel()))
}
if user.GetIsLicensed() {
builder.WriteString(" Licensed HAM operator\n")
}
if user.GetRole() != 0 {
builder.WriteString(fmt.Sprintf(" Role: %s\n", user.GetRole()))
}
if len(user.GetPublicKey()) > 0 {
builder.WriteString(fmt.Sprintf(" Public Key: %x\n", user.GetPublicKey()))
}
@@ -375,47 +375,47 @@ func FormatNodeInfoMessage(user *pb.User) string {
// FormatWaypointMessage formats a Waypoint message
func FormatWaypointMessage(waypoint *pb.Waypoint) string {
var builder strings.Builder
if waypoint == nil {
return "Error: nil waypoint data"
}
builder.WriteString("Waypoint:\n")
if waypoint.GetId() != 0 {
builder.WriteString(fmt.Sprintf(" ID: %d\n", waypoint.GetId()))
}
if waypoint.GetName() != "" {
builder.WriteString(fmt.Sprintf(" Name: %s\n", waypoint.GetName()))
}
if waypoint.GetDescription() != "" {
builder.WriteString(fmt.Sprintf(" Description: %s\n", waypoint.GetDescription()))
}
if waypoint.GetLatitudeI() != 0 && waypoint.GetLongitudeI() != 0 {
// Convert the integer coordinates to floating-point degrees
lat := float64(waypoint.GetLatitudeI()) / 10000000.0
lon := float64(waypoint.GetLongitudeI()) / 10000000.0
builder.WriteString(fmt.Sprintf(" Latitude: %.7f\n", lat))
builder.WriteString(fmt.Sprintf(" Longitude: %.7f\n", lon))
// Google Maps link for convenience
builder.WriteString(fmt.Sprintf(" Google Maps: https://maps.google.com/?q=%.7f,%.7f\n", lat, lon))
}
if waypoint.GetIcon() != 0 {
builder.WriteString(fmt.Sprintf(" Icon: %d\n", waypoint.GetIcon()))
}
if waypoint.GetExpire() != 0 {
// Convert UNIX timestamp to readable time
tm := time.Unix(int64(waypoint.GetExpire()), 0)
builder.WriteString(fmt.Sprintf(" Expires: %s\n", tm.Format(time.RFC3339)))
}
if waypoint.GetLockedTo() != 0 {
builder.WriteString(fmt.Sprintf(" Locked to node: %d\n", waypoint.GetLockedTo()))
}
@@ -435,19 +435,19 @@ func FormatDataMessage(data *pb.Data) string {
if data == nil {
return "Error: nil data"
}
var builder strings.Builder
builder.WriteString(fmt.Sprintf("Data (Port: %s):\n", data.GetPortnum()))
// Format payload based on port type
payload := data.GetPayload()
switch data.GetPortnum() {
case pb.PortNum_TEXT_MESSAGE_APP:
builder.WriteString(fmt.Sprintf(" Text Message: %s\n", string(payload)))
case pb.PortNum_TEXT_MESSAGE_COMPRESSED_APP:
builder.WriteString(fmt.Sprintf(" Compressed Text Message (%d bytes): %x\n", len(payload), payload))
case pb.PortNum_TELEMETRY_APP:
var telemetry pb.Telemetry
if err := proto.Unmarshal(payload, &telemetry); err == nil {
@@ -456,7 +456,7 @@ func FormatDataMessage(data *pb.Data) string {
builder.WriteString(fmt.Sprintf(" Failed to parse telemetry: %v\n", err))
builder.WriteString(fmt.Sprintf(" Raw bytes (%d): %x\n", len(payload), payload))
}
case pb.PortNum_POSITION_APP:
var position pb.Position
if err := proto.Unmarshal(payload, &position); err == nil {
@@ -465,7 +465,7 @@ func FormatDataMessage(data *pb.Data) string {
builder.WriteString(fmt.Sprintf(" Failed to parse position: %v\n", err))
builder.WriteString(fmt.Sprintf(" Raw bytes (%d): %x\n", len(payload), payload))
}
case pb.PortNum_NODEINFO_APP:
var user pb.User
if err := proto.Unmarshal(payload, &user); err == nil {
@@ -474,7 +474,7 @@ func FormatDataMessage(data *pb.Data) string {
builder.WriteString(fmt.Sprintf(" Failed to parse node info: %v\n", err))
builder.WriteString(fmt.Sprintf(" Raw bytes (%d): %x\n", len(payload), payload))
}
case pb.PortNum_WAYPOINT_APP:
var waypoint pb.Waypoint
if err := proto.Unmarshal(payload, &waypoint); err == nil {
@@ -483,7 +483,7 @@ func FormatDataMessage(data *pb.Data) string {
builder.WriteString(fmt.Sprintf(" Failed to parse waypoint: %v\n", err))
builder.WriteString(fmt.Sprintf(" Raw bytes (%d): %x\n", len(payload), payload))
}
default:
// For unknown types, show raw binary data
builder.WriteString(fmt.Sprintf(" Binary data (%d bytes): %x\n", len(payload), payload))
@@ -492,7 +492,7 @@ func FormatDataMessage(data *pb.Data) string {
builder.WriteString(fmt.Sprintf(" As Text: %s\n", string(payload)))
}
}
// Show additional Data fields
if data.GetRequestId() != 0 {
builder.WriteString(fmt.Sprintf(" Request ID: %d\n", data.GetRequestId()))
@@ -512,7 +512,86 @@ func FormatDataMessage(data *pb.Data) string {
if data.GetWantResponse() {
builder.WriteString(" Wants Response: Yes\n")
}
return builder.String()
}
// FormatMapReportMessage formats a MapReport message
func FormatMapReportMessage(mapReport *pb.MapReport) string {
var builder strings.Builder
if mapReport == nil {
return "Error: nil map report data"
}
builder.WriteString("Map Report Data:\n")
// Node information
if mapReport.GetLongName() != "" {
builder.WriteString(fmt.Sprintf(" Long Name: %s\n", mapReport.GetLongName()))
}
if mapReport.GetShortName() != "" {
builder.WriteString(fmt.Sprintf(" Short Name: %s\n", mapReport.GetShortName()))
}
if mapReport.GetRole() != 0 {
builder.WriteString(fmt.Sprintf(" Role: %s\n", mapReport.GetRole().String()))
}
if mapReport.GetHwModel() != pb.HardwareModel_UNSET {
builder.WriteString(fmt.Sprintf(" Hardware: %s\n", mapReport.GetHwModel().String()))
}
if mapReport.GetFirmwareVersion() != "" {
builder.WriteString(fmt.Sprintf(" Firmware: %s\n", mapReport.GetFirmwareVersion()))
}
if mapReport.GetRegion() != 0 {
builder.WriteString(fmt.Sprintf(" Region: %s\n", mapReport.GetRegion().String()))
}
if mapReport.GetModemPreset() != 0 {
builder.WriteString(fmt.Sprintf(" Modem Preset: %s\n", mapReport.GetModemPreset().String()))
}
builder.WriteString(fmt.Sprintf(" Default Channel: %v\n", mapReport.GetHasDefaultChannel()))
// Position information
if mapReport.GetLatitudeI() != 0 && mapReport.GetLongitudeI() != 0 {
// Convert the integer coordinates to floating-point degrees
// Meshtastic uses a format where the values are stored as integers
// representing the position multiplied by 1e7 (10 million)
lat := float64(mapReport.GetLatitudeI()) / 10000000.0
lon := float64(mapReport.GetLongitudeI()) / 10000000.0
builder.WriteString(fmt.Sprintf(" Latitude: %.7f\n", lat))
builder.WriteString(fmt.Sprintf(" Longitude: %.7f\n", lon))
builder.WriteString(fmt.Sprintf(" Position Precision: %d bits\n", mapReport.GetPositionPrecision()))
// Google Maps link for convenience
builder.WriteString(fmt.Sprintf(" Google Maps: https://maps.google.com/?q=%.7f,%.7f\n", lat, lon))
}
if mapReport.GetAltitude() != 0 {
builder.WriteString(fmt.Sprintf(" Altitude: %d meters\n", mapReport.GetAltitude()))
}
if mapReport.GetNumOnlineLocalNodes() != 0 {
builder.WriteString(fmt.Sprintf(" Online Local Nodes: %d\n", mapReport.GetNumOnlineLocalNodes()))
}
// Use protojson to generate a full JSON representation for debugging
marshaler := protojson.MarshalOptions{
Multiline: true,
Indent: " ",
}
jsonBytes, err := marshaler.Marshal(mapReport)
if err == nil {
builder.WriteString("\nFull Map Report Structure:\n")
builder.WriteString(string(jsonBytes))
}
return builder.String()
}
@@ -521,46 +600,52 @@ func FormatPayload(payload interface{}, portNum pb.PortNum) string {
if payload == nil {
return " No payload data\n"
}
switch portNum {
case pb.PortNum_TEXT_MESSAGE_APP:
// Text message
if text, ok := payload.(string); ok {
return fmt.Sprintf(" Text Message: %s\n", text)
}
case pb.PortNum_TEXT_MESSAGE_COMPRESSED_APP:
// Compressed text
if data, ok := payload.([]byte); ok {
return fmt.Sprintf(" Compressed Text Message (%d bytes): %x\n", len(data), data)
// TODO: Add decompression support
}
case pb.PortNum_TELEMETRY_APP:
// Telemetry data
if telemetry, ok := payload.(*pb.Telemetry); ok {
return FormatTelemetryMessage(telemetry)
}
case pb.PortNum_POSITION_APP:
// Position data
if position, ok := payload.(*pb.Position); ok {
return FormatPositionMessage(position)
}
case pb.PortNum_NODEINFO_APP:
// Node information
if user, ok := payload.(*pb.User); ok {
return FormatNodeInfoMessage(user)
}
case pb.PortNum_WAYPOINT_APP:
// Waypoint data
if waypoint, ok := payload.(*pb.Waypoint); ok {
return FormatWaypointMessage(waypoint)
}
case pb.PortNum_MAP_REPORT_APP:
// Map data
if mapReport, ok := payload.(*pb.MapReport); ok {
return FormatMapReportMessage(mapReport)
}
}
// Default formatting for unknown types
switch v := payload.(type) {
case string:
@@ -579,31 +664,31 @@ func FormatPayload(payload interface{}, portNum pb.PortNum) string {
// FormatDecodedPacket formats a DecodedPacket into a human-readable string
func FormatDecodedPacket(packet *DecodedPacket) string {
var builder strings.Builder
if packet == nil {
return "Error: nil packet"
}
if packet.DecodeError != nil {
builder.WriteString(fmt.Sprintf("Error decoding packet: %v\n", packet.DecodeError))
return builder.String()
}
// Envelope info
builder.WriteString("Packet:\n")
builder.WriteString(fmt.Sprintf(" Channel ID: %s\n", packet.ChannelID))
if packet.GatewayID != "" {
builder.WriteString(fmt.Sprintf(" Gateway ID: %s\n", packet.GatewayID))
}
// Mesh packet info
builder.WriteString(fmt.Sprintf(" ID: %d\n", packet.ID))
builder.WriteString(fmt.Sprintf(" From: %d\n", packet.From))
builder.WriteString(fmt.Sprintf(" To: %d\n", packet.To))
// Additional fields that might be interesting
builder.WriteString(fmt.Sprintf(" Port: %s\n", packet.PortNum))
if packet.HopLimit != 0 {
builder.WriteString(fmt.Sprintf(" Hop Limit: %d\n", packet.HopLimit))
}
@@ -622,7 +707,7 @@ func FormatDecodedPacket(packet *DecodedPacket) string {
if packet.RelayNode != 0 {
builder.WriteString(fmt.Sprintf(" Relay Node: %d\n", packet.RelayNode))
}
// Additional data fields
if packet.RequestID != 0 {
builder.WriteString(fmt.Sprintf(" Request ID: %d\n", packet.RequestID))
@@ -642,42 +727,42 @@ func FormatDecodedPacket(packet *DecodedPacket) string {
if packet.WantResponse {
builder.WriteString(" Wants Response: Yes\n")
}
// Format the payload
builder.WriteString("\n")
builder.WriteString(FormatPayload(packet.Payload, packet.PortNum))
return builder.String()
}
// FormatServiceEnvelope formats a ServiceEnvelope message into a human-readable string
func FormatServiceEnvelope(envelope *pb.ServiceEnvelope) string {
var builder strings.Builder
builder.WriteString("ServiceEnvelope:\n")
// Print basic envelope info
builder.WriteString(fmt.Sprintf(" Channel ID: %s\n", envelope.GetChannelId()))
builder.WriteString(fmt.Sprintf(" Gateway ID: %s\n", envelope.GetGatewayId()))
// Print MeshPacket info if available
if packet := envelope.GetPacket(); packet != nil {
builder.WriteString("\nMeshPacket:\n")
builder.WriteString(fmt.Sprintf(" ID: %d\n", packet.GetId()))
builder.WriteString(fmt.Sprintf(" From: %d\n", packet.GetFrom()))
builder.WriteString(fmt.Sprintf(" To: %d\n", packet.GetTo()))
// Output routing and hop information
builder.WriteString(fmt.Sprintf(" Hop Limit: %d\n", packet.GetHopLimit()))
builder.WriteString(fmt.Sprintf(" Hop Start: %d\n", packet.GetHopStart()))
builder.WriteString(fmt.Sprintf(" Want ACK: %v\n", packet.GetWantAck()))
builder.WriteString(fmt.Sprintf(" Priority: %s\n", packet.GetPriority()))
// Output if the packet was delivered via MQTT
if packet.GetViaMqtt() {
builder.WriteString(" Via MQTT: Yes\n")
}
// Relay and next hop info
if packet.GetNextHop() != 0 {
builder.WriteString(fmt.Sprintf(" Next Hop: %d\n", packet.GetNextHop()))
@@ -685,25 +770,25 @@ func FormatServiceEnvelope(envelope *pb.ServiceEnvelope) string {
if packet.GetRelayNode() != 0 {
builder.WriteString(fmt.Sprintf(" Relay Node: %d\n", packet.GetRelayNode()))
}
// Show public key information if available (for PKI-encrypted packets)
if len(packet.GetPublicKey()) > 0 {
builder.WriteString(fmt.Sprintf(" Public Key: %x\n", packet.GetPublicKey()))
builder.WriteString(fmt.Sprintf(" PKI Encrypted: %v\n", packet.GetPkiEncrypted()))
}
// Determine payload type
if packet.GetDecoded() != nil {
// For already decoded packets, use our specialized data formatter
builder.WriteString("\n")
builder.WriteString(FormatDataMessage(packet.GetDecoded()))
} else if packet.GetEncrypted() != nil {
// Encrypted payload information
builder.WriteString("\n Encrypted Payload:\n")
builder.WriteString(fmt.Sprintf(" Size: %d bytes\n", len(packet.GetEncrypted())))
builder.WriteString(fmt.Sprintf(" Channel: %d\n", packet.GetChannel()))
// Print the first few bytes of the encrypted payload for identification
if len(packet.GetEncrypted()) > 0 {
displayLen := len(packet.GetEncrypted())
@@ -712,23 +797,23 @@ func FormatServiceEnvelope(envelope *pb.ServiceEnvelope) string {
}
builder.WriteString(fmt.Sprintf(" First %d bytes: %x\n", displayLen, packet.GetEncrypted()[:displayLen]))
}
// If the packet has channel ID, it's using channel-based encryption
channelId := envelope.GetChannelId()
if channelId != "" {
builder.WriteString(fmt.Sprintf(" Encryption: Channel-based (Channel ID: %s)\n", channelId))
// Attempt to decrypt the payload using the channel key
channelKey := GetChannelKey(channelId)
builder.WriteString(fmt.Sprintf(" Using key (%d bytes): %x\n", len(channelKey), channelKey))
// Try to decrypt
decrypted, err := XOR(packet.GetEncrypted(), channelKey, packet.GetId(), packet.GetFrom())
if err != nil {
builder.WriteString(fmt.Sprintf(" Decryption error: %v\n", err))
} else {
builder.WriteString(fmt.Sprintf(" Decrypted (%d bytes): %x\n", len(decrypted), decrypted))
// Try to parse the decrypted payload as a Data message
var data pb.Data
if err := proto.Unmarshal(decrypted, &data); err == nil {
@@ -748,7 +833,7 @@ func FormatServiceEnvelope(envelope *pb.ServiceEnvelope) string {
}
}
}
// Use protojson to generate a full JSON representation for debugging
marshaler := protojson.MarshalOptions{
Multiline: true,
@@ -759,7 +844,7 @@ func FormatServiceEnvelope(envelope *pb.ServiceEnvelope) string {
builder.WriteString("\nFull Protobuf Structure:\n")
builder.WriteString(string(jsonBytes))
}
return builder.String()
}
@@ -777,9 +862,9 @@ func indent(s, prefix string) string {
// FormatJSONMessage formats a JSON message into a human-readable string
func FormatJSONMessage(jsonData map[string]interface{}) string {
var builder strings.Builder
builder.WriteString("JSON Message:\n")
// Extract and display common fields
if from, ok := jsonData["from"].(string); ok {
builder.WriteString(fmt.Sprintf(" From: %s\n", from))
@@ -793,21 +878,21 @@ func FormatJSONMessage(jsonData map[string]interface{}) string {
if timestamp, ok := jsonData["timestamp"].(string); ok {
builder.WriteString(fmt.Sprintf(" Timestamp: %s\n", timestamp))
}
// Format the full JSON for reference
jsonBytes, err := json.MarshalIndent(jsonData, " ", " ")
if err == nil {
builder.WriteString("\nFull JSON Structure:\n ")
builder.WriteString(string(jsonBytes))
}
return builder.String()
}
// FormatTopicAndPacket formats both topic information and a decoded packet
func FormatTopicAndPacket(topicInfo *TopicInfo, decodedPacket *DecodedPacket) string {
var builder strings.Builder
// Display topic information
builder.WriteString(fmt.Sprintf("Topic: %s\n", topicInfo.FullTopic))
builder.WriteString(fmt.Sprintf("Region Path: %s\n", topicInfo.RegionPath))
@@ -818,17 +903,17 @@ func FormatTopicAndPacket(topicInfo *TopicInfo, decodedPacket *DecodedPacket) st
builder.WriteString(fmt.Sprintf("User ID: %s\n", topicInfo.UserID))
}
builder.WriteString("\n")
// Format the decoded packet
builder.WriteString(FormatDecodedPacket(decodedPacket))
return builder.String()
}
// FormatTopicAndJSONData formats both topic information and decoded JSON data
func FormatTopicAndJSONData(topicInfo *TopicInfo, jsonData map[string]interface{}) string {
var builder strings.Builder
// Display topic information
builder.WriteString(fmt.Sprintf("Topic: %s\n", topicInfo.FullTopic))
builder.WriteString(fmt.Sprintf("Region Path: %s\n", topicInfo.RegionPath))
@@ -839,70 +924,17 @@ func FormatTopicAndJSONData(topicInfo *TopicInfo, jsonData map[string]interface{
builder.WriteString(fmt.Sprintf("User ID: %s\n", topicInfo.UserID))
}
builder.WriteString("\n")
// Format the JSON data
builder.WriteString(FormatJSONMessage(jsonData))
return builder.String()
}
// FormatTopicAndMapData formats topic information and map data
func FormatTopicAndMapData(topicInfo *TopicInfo, decodedPacket *DecodedPacket) string {
var builder strings.Builder
// Display topic information
builder.WriteString(fmt.Sprintf("Topic: %s\n", topicInfo.FullTopic))
builder.WriteString(fmt.Sprintf("Region Path: %s\n", topicInfo.RegionPath))
builder.WriteString(fmt.Sprintf("Version: %s\n", topicInfo.Version))
builder.WriteString(fmt.Sprintf("Format: %s\n", topicInfo.Format))
builder.WriteString(fmt.Sprintf("Channel: %s\n", topicInfo.Channel))
if topicInfo.UserID != "" {
builder.WriteString(fmt.Sprintf("User ID: %s\n", topicInfo.UserID))
}
builder.WriteString("\n")
// Check if there was an error decoding
if decodedPacket.DecodeError != nil {
builder.WriteString(fmt.Sprintf("Error decoding packet: %v\n", decodedPacket.DecodeError))
if decodedPacket.RawEnvelope != nil {
// Try to format whatever we got
builder.WriteString(FormatServiceEnvelope(decodedPacket.RawEnvelope))
}
return builder.String()
}
// Format map data
builder.WriteString("Map Report:\n")
// First show basic packet info
builder.WriteString(fmt.Sprintf(" Channel ID: %s\n", decodedPacket.ChannelID))
if decodedPacket.GatewayID != "" {
builder.WriteString(fmt.Sprintf(" Gateway ID: %s\n", decodedPacket.GatewayID))
}
// Node information
builder.WriteString(fmt.Sprintf(" From Node: %d\n", decodedPacket.From))
if decodedPacket.To != 0 && decodedPacket.To != 4294967295 { // 4294967295 is broadcast
builder.WriteString(fmt.Sprintf(" To Node: %d\n", decodedPacket.To))
} else {
builder.WriteString(" To: Broadcast\n")
}
// Check if this is a MAP_REPORT_APP port and format payload accordingly
if decodedPacket.PortNum == pb.PortNum_MAP_REPORT_APP {
// Format the payload based on its type
builder.WriteString(FormatPayload(decodedPacket.Payload, decodedPacket.PortNum))
} else {
builder.WriteString(fmt.Sprintf("\n Unexpected port number %s for map data\n", decodedPacket.PortNum))
}
return builder.String()
}
// FormatTopicAndRawData formats topic information and raw data for unsupported formats
func FormatTopicAndRawData(topicInfo *TopicInfo, payload []byte) string {
var builder strings.Builder
// Display topic information
builder.WriteString(fmt.Sprintf("Topic: %s\n", topicInfo.FullTopic))
builder.WriteString(fmt.Sprintf("Region Path: %s\n", topicInfo.RegionPath))
@@ -913,10 +945,10 @@ func FormatTopicAndRawData(topicInfo *TopicInfo, payload []byte) string {
builder.WriteString(fmt.Sprintf("User ID: %s\n", topicInfo.UserID))
}
builder.WriteString("\n")
// Display raw data
builder.WriteString(fmt.Sprintf("Unsupported format: %s\n", topicInfo.Format))
builder.WriteString(fmt.Sprintf("Raw Data (%d bytes): %x\n", len(payload), payload))
return builder.String()
}
}