package meshtastic import ( "encoding/binary" "encoding/json" "testing" meshtasticpb "git.maze.io/go/ham/protocol/meshtastic/pb" "google.golang.org/protobuf/proto" ) func TestPacketDecodeTextPayload(t *testing.T) { dataMsg := &meshtasticpb.Data{ Portnum: meshtasticpb.PortNum_TEXT_MESSAGE_APP, Payload: []byte("hello mesh"), } encodedData, err := proto.Marshal(dataMsg) if err != nil { t.Fatalf("marshaling data protobuf: %v", err) } rawPacket := make([]byte, 16+len(encodedData)) binary.LittleEndian.PutUint32(rawPacket[0:], 0x01020304) binary.LittleEndian.PutUint32(rawPacket[4:], 0x05060708) binary.LittleEndian.PutUint32(rawPacket[8:], 0x090a0b0c) rawPacket[12] = 0x01 rawPacket[13] = 0x02 rawPacket[14] = 0x03 rawPacket[15] = 0x04 copy(rawPacket[16:], encodedData) var packet Packet if err := packet.Decode(rawPacket); err != nil { t.Fatalf("decoding packet: %v", err) } if packet.Data == nil { t.Fatal("expected Data protobuf to be decoded") } if packet.Data.GetPortnum() != meshtasticpb.PortNum_TEXT_MESSAGE_APP { t.Fatalf("unexpected portnum: got %v", packet.Data.GetPortnum()) } if packet.TextPayload != "hello mesh" { t.Fatalf("unexpected text payload: got %q", packet.TextPayload) } if packet.DecodedPayload != nil { t.Fatalf("expected no decoded protobuf payload for text app, got %T", packet.DecodedPayload) } } func TestPacketDecodeRejectsOversizedPayload(t *testing.T) { rawPacket := make([]byte, 16+maxPayloadSize+1) var packet Packet err := packet.Decode(rawPacket) if err != ErrInvalidPacket { t.Fatalf("expected ErrInvalidPacket, got %v", err) } } func TestPacketJSONSerialization(t *testing.T) { dataMsg := &meshtasticpb.Data{ Portnum: meshtasticpb.PortNum_TEXT_MESSAGE_APP, Payload: []byte("test message"), } encodedData, err := proto.Marshal(dataMsg) if err != nil { t.Fatalf("marshaling data protobuf: %v", err) } rawPacket := make([]byte, 16+len(encodedData)) binary.LittleEndian.PutUint32(rawPacket[0:], 0x12345678) // source binary.LittleEndian.PutUint32(rawPacket[4:], 0xabcdef01) // destination binary.LittleEndian.PutUint32(rawPacket[8:], 0x99887766) // id rawPacket[12] = 0x11 // flags rawPacket[13] = 0x22 // channelHash rawPacket[14] = 0x33 // nextHop rawPacket[15] = 0x44 // relayNode copy(rawPacket[16:], encodedData) var packet Packet if err := packet.Decode(rawPacket); err != nil { t.Fatalf("decoding packet: %v", err) } jsonBytes, err := json.Marshal(&packet) if err != nil { t.Fatalf("marshaling to JSON: %v", err) } var decoded map[string]interface{} if err := json.Unmarshal(jsonBytes, &decoded); err != nil { t.Fatalf("unmarshaling JSON: %v", err) } // Check camelCase field names if _, ok := decoded["destination"]; !ok { t.Error("missing 'destination' field") } if _, ok := decoded["source"]; !ok { t.Error("missing 'source' field") } if _, ok := decoded["channelHash"]; !ok { t.Error("missing 'channelHash' field") } if _, ok := decoded["nextHop"]; !ok { t.Error("missing 'nextHop' field") } if _, ok := decoded["relayNode"]; !ok { t.Error("missing 'relayNode' field") } // Check raw payload field if _, ok := decoded["raw"]; !ok { t.Error("missing 'raw' field") } // Check text payload if textPayload, ok := decoded["textPayload"].(string); !ok || textPayload != "test message" { t.Errorf("expected textPayload='test message', got %v", decoded["textPayload"]) } // Verify PayloadLength and Payload are not in JSON if _, ok := decoded["PayloadLength"]; ok { t.Error("PayloadLength should not be in JSON output") } if _, ok := decoded["Payload"]; ok { t.Error("Payload should not be in JSON output") } } func TestProtobufMessagesCamelCaseJSON(t *testing.T) { // Test that protobuf messages serialize with camelCase JSON tags pos := &meshtasticpb.Position{ LatitudeI: proto.Int32(379208045), LongitudeI: proto.Int32(-1223928905), Altitude: proto.Int32(120), Time: 1234567890, } jsonBytes, err := json.Marshal(pos) if err != nil { t.Fatalf("marshaling Position to JSON: %v", err) } var decoded map[string]interface{} if err := json.Unmarshal(jsonBytes, &decoded); err != nil { t.Fatalf("unmarshaling JSON: %v", err) } // Check camelCase field names (not snake_case) if _, ok := decoded["latitudeI"]; !ok { t.Error("missing camelCase 'latitudeI' field") } if _, ok := decoded["longitudeI"]; !ok { t.Error("missing camelCase 'longitudeI' field") } if _, ok := decoded["altitude"]; !ok { t.Error("missing 'altitude' field") } // Check that snake_case fields are NOT present if _, ok := decoded["latitude_i"]; ok { t.Error("found snake_case 'latitude_i' field, expected camelCase 'latitudeI'") } if _, ok := decoded["longitude_i"]; ok { t.Error("found snake_case 'longitude_i' field, expected camelCase 'longitudeI'") } // Test User message with underscored fields user := &meshtasticpb.User{ LongName: "Test User", ShortName: "TU", HwModel: meshtasticpb.HardwareModel_TBEAM, } jsonBytes, err = json.Marshal(user) if err != nil { t.Fatalf("marshaling User to JSON: %v", err) } decoded = make(map[string]interface{}) if err := json.Unmarshal(jsonBytes, &decoded); err != nil { t.Fatalf("unmarshaling JSON: %v", err) } // Check camelCase field names if _, ok := decoded["longName"]; !ok { t.Error("missing camelCase 'longName' field") } if _, ok := decoded["shortName"]; !ok { t.Error("missing camelCase 'shortName' field") } if _, ok := decoded["hwModel"]; !ok { t.Error("missing camelCase 'hwModel' field") } // Check that snake_case fields are NOT present if _, ok := decoded["long_name"]; ok { t.Error("found snake_case 'long_name' field, expected camelCase 'longName'") } if _, ok := decoded["short_name"]; ok { t.Error("found snake_case 'short_name' field, expected camelCase 'shortName'") } if _, ok := decoded["hw_model"]; ok { t.Error("found snake_case 'hw_model' field, expected camelCase 'hwModel'") } } // Test POSITION_APP portnum decoding func TestPacketDecodePositionApp(t *testing.T) { pos := &meshtasticpb.Position{ LatitudeI: proto.Int32(379208045), LongitudeI: proto.Int32(-1223928905), Altitude: proto.Int32(120), Time: 1234567890, } dataMsg := &meshtasticpb.Data{ Portnum: meshtasticpb.PortNum_POSITION_APP, Payload: encodeProto(t, pos), } packet := decodeTestPacket(t, dataMsg) if packet.Data == nil { t.Fatal("expected Data protobuf to be decoded") } if packet.Data.GetPortnum() != meshtasticpb.PortNum_POSITION_APP { t.Fatalf("unexpected portnum: got %v", packet.Data.GetPortnum()) } decodedPos, ok := packet.DecodedPayload.(*meshtasticpb.Position) if !ok { t.Fatalf("expected *Position, got %T", packet.DecodedPayload) } if decodedPos.GetLatitudeI() != 379208045 { t.Errorf("unexpected latitude: got %d", decodedPos.GetLatitudeI()) } if decodedPos.GetLongitudeI() != -1223928905 { t.Errorf("unexpected longitude: got %d", decodedPos.GetLongitudeI()) } if decodedPos.GetAltitude() != 120 { t.Errorf("unexpected altitude: got %d", decodedPos.GetAltitude()) } } // Test NODEINFO_APP portnum decoding func TestPacketDecodeNodeInfoApp(t *testing.T) { user := &meshtasticpb.User{ LongName: "Mesh Node", ShortName: "MN", HwModel: meshtasticpb.HardwareModel_TBEAM, IsLicensed: true, Macaddr: []byte{0xaa, 0xbb, 0xcc, 0xdd, 0xee, 0xff}, } dataMsg := &meshtasticpb.Data{ Portnum: meshtasticpb.PortNum_NODEINFO_APP, Payload: encodeProto(t, user), } packet := decodeTestPacket(t, dataMsg) if packet.Data == nil { t.Fatal("expected Data protobuf to be decoded") } if packet.Data.GetPortnum() != meshtasticpb.PortNum_NODEINFO_APP { t.Fatalf("unexpected portnum: got %v", packet.Data.GetPortnum()) } decodedUser, ok := packet.DecodedPayload.(*meshtasticpb.User) if !ok { t.Fatalf("expected *User, got %T", packet.DecodedPayload) } if decodedUser.GetLongName() != "Mesh Node" { t.Errorf("unexpected long name: got %s", decodedUser.GetLongName()) } if decodedUser.GetShortName() != "MN" { t.Errorf("unexpected short name: got %s", decodedUser.GetShortName()) } if decodedUser.GetHwModel() != meshtasticpb.HardwareModel_TBEAM { t.Errorf("unexpected hardware model: got %v", decodedUser.GetHwModel()) } } // Test TELEMETRY_APP portnum decoding func TestPacketDecodeTelemetryApp(t *testing.T) { batteryLevel := uint32(85) voltage := float32(12.5) channelUtil := float32(45.3) airUtilTx := float32(15.2) telemetry := &meshtasticpb.Telemetry{ Time: 1234567890, Variant: &meshtasticpb.Telemetry_DeviceMetrics_{ DeviceMetrics: &meshtasticpb.DeviceMetrics{ BatteryLevel: &batteryLevel, Voltage: &voltage, ChannelUtilization: &channelUtil, AirUtilTx: &airUtilTx, }, }, } dataMsg := &meshtasticpb.Data{ Portnum: meshtasticpb.PortNum_TELEMETRY_APP, Payload: encodeProto(t, telemetry), } packet := decodeTestPacket(t, dataMsg) if packet.Data == nil { t.Fatal("expected Data protobuf to be decoded") } if packet.Data.GetPortnum() != meshtasticpb.PortNum_TELEMETRY_APP { t.Fatalf("unexpected portnum: got %v", packet.Data.GetPortnum()) } decodedTelem, ok := packet.DecodedPayload.(*meshtasticpb.Telemetry) if !ok { t.Fatalf("expected *Telemetry, got %T", packet.DecodedPayload) } if decodedTelem.GetTime() != 1234567890 { t.Errorf("unexpected time: got %d", decodedTelem.GetTime()) } deviceMetrics := decodedTelem.GetDeviceMetrics() if deviceMetrics == nil { t.Fatal("expected DeviceMetrics to be set") } if deviceMetrics.GetBatteryLevel() != 85 { t.Errorf("unexpected battery level: got %d", deviceMetrics.GetBatteryLevel()) } } // Test ROUTING_APP portnum decoding func TestPacketDecodeRoutingApp(t *testing.T) { routing := &meshtasticpb.Routing{ Variant: &meshtasticpb.Routing_RouteReply_{ RouteReply: &meshtasticpb.Routing_RouteReply{}, }, } dataMsg := &meshtasticpb.Data{ Portnum: meshtasticpb.PortNum_ROUTING_APP, Payload: encodeProto(t, routing), } packet := decodeTestPacket(t, dataMsg) if packet.Data == nil { t.Fatal("expected Data protobuf to be decoded") } if packet.Data.GetPortnum() != meshtasticpb.PortNum_ROUTING_APP { t.Fatalf("unexpected portnum: got %v", packet.Data.GetPortnum()) } decodedRouting, ok := packet.DecodedPayload.(*meshtasticpb.Routing) if !ok { t.Fatalf("expected *Routing, got %T", packet.DecodedPayload) } if decodedRouting.GetRouteReply() == nil { t.Fatal("expected RouteReply to be set") } } // Test ADMIN_APP portnum decoding func TestPacketDecodeAdminApp(t *testing.T) { // AdminMessage has a sessionPasskey field admin := &meshtasticpb.AdminMessage{ SessionPasskey: []byte{0x01, 0x02, 0x03, 0x04}, } dataMsg := &meshtasticpb.Data{ Portnum: meshtasticpb.PortNum_ADMIN_APP, Payload: encodeProto(t, admin), } packet := decodeTestPacket(t, dataMsg) if packet.Data == nil { t.Fatal("expected Data protobuf to be decoded") } if packet.Data.GetPortnum() != meshtasticpb.PortNum_ADMIN_APP { t.Fatalf("unexpected portnum: got %v", packet.Data.GetPortnum()) } decodedAdmin, ok := packet.DecodedPayload.(*meshtasticpb.AdminMessage) if !ok { t.Fatalf("expected *AdminMessage, got %T", packet.DecodedPayload) } if len(decodedAdmin.GetSessionPasskey()) != 4 { t.Errorf("unexpected session passkey length: got %d", len(decodedAdmin.GetSessionPasskey())) } } // Test WAYPOINT_APP portnum decoding func TestPacketDecodeWaypointApp(t *testing.T) { latI := int32(379208045) lonI := int32(-1223928905) waypoint := &meshtasticpb.Waypoint{ Id: 1001, Name: "Home Sweet Home", Description: "My house", LatitudeI: &latI, LongitudeI: &lonI, Expire: 1234567890, } dataMsg := &meshtasticpb.Data{ Portnum: meshtasticpb.PortNum_WAYPOINT_APP, Payload: encodeProto(t, waypoint), } packet := decodeTestPacket(t, dataMsg) if packet.Data == nil { t.Fatal("expected Data protobuf to be decoded") } if packet.Data.GetPortnum() != meshtasticpb.PortNum_WAYPOINT_APP { t.Fatalf("unexpected portnum: got %v", packet.Data.GetPortnum()) } decodedWaypoint, ok := packet.DecodedPayload.(*meshtasticpb.Waypoint) if !ok { t.Fatalf("expected *Waypoint, got %T", packet.DecodedPayload) } if decodedWaypoint.GetName() != "Home Sweet Home" { t.Errorf("unexpected waypoint name: got %s", decodedWaypoint.GetName()) } if decodedWaypoint.GetId() != 1001 { t.Errorf("unexpected waypoint id: got %d", decodedWaypoint.GetId()) } } // Test NEIGHBORINFO_APP portnum decoding func TestPacketDecodeNeighborInfoApp(t *testing.T) { neighborInfo := &meshtasticpb.NeighborInfo{ NodeId: 12345, NodeBroadcastIntervalSecs: 60, } dataMsg := &meshtasticpb.Data{ Portnum: meshtasticpb.PortNum_NEIGHBORINFO_APP, Payload: encodeProto(t, neighborInfo), } packet := decodeTestPacket(t, dataMsg) if packet.Data == nil { t.Fatal("expected Data protobuf to be decoded") } if packet.Data.GetPortnum() != meshtasticpb.PortNum_NEIGHBORINFO_APP { t.Fatalf("unexpected portnum: got %v", packet.Data.GetPortnum()) } decodedNeighbor, ok := packet.DecodedPayload.(*meshtasticpb.NeighborInfo) if !ok { t.Fatalf("expected *NeighborInfo, got %T", packet.DecodedPayload) } if decodedNeighbor.GetNodeId() != 12345 { t.Errorf("unexpected node id: got %d", decodedNeighbor.GetNodeId()) } } // Test NODE_STATUS_APP portnum decoding func TestPacketDecodeNodeStatusApp(t *testing.T) { statusMsg := &meshtasticpb.StatusMessage{ Status: "Device online", } dataMsg := &meshtasticpb.Data{ Portnum: meshtasticpb.PortNum_NODE_STATUS_APP, Payload: encodeProto(t, statusMsg), } packet := decodeTestPacket(t, dataMsg) if packet.Data == nil { t.Fatal("expected Data protobuf to be decoded") } if packet.Data.GetPortnum() != meshtasticpb.PortNum_NODE_STATUS_APP { t.Fatalf("unexpected portnum: got %v", packet.Data.GetPortnum()) } decodedStatus, ok := packet.DecodedPayload.(*meshtasticpb.StatusMessage) if !ok { t.Fatalf("expected *StatusMessage, got %T", packet.DecodedPayload) } if decodedStatus.GetStatus() != "Device online" { t.Errorf("unexpected status: got %s", decodedStatus.GetStatus()) } } // Test REMOTE_HARDWARE_APP portnum decoding func TestPacketDecodeRemoteHardwareApp(t *testing.T) { hwMsg := &meshtasticpb.HardwareMessage{ Type: meshtasticpb.HardwareMessage_READ_GPIOS, GpioMask: 0xFF, } dataMsg := &meshtasticpb.Data{ Portnum: meshtasticpb.PortNum_REMOTE_HARDWARE_APP, Payload: encodeProto(t, hwMsg), } packet := decodeTestPacket(t, dataMsg) if packet.Data == nil { t.Fatal("expected Data protobuf to be decoded") } if packet.Data.GetPortnum() != meshtasticpb.PortNum_REMOTE_HARDWARE_APP { t.Fatalf("unexpected portnum: got %v", packet.Data.GetPortnum()) } decodedHw, ok := packet.DecodedPayload.(*meshtasticpb.HardwareMessage) if !ok { t.Fatalf("expected *HardwareMessage, got %T", packet.DecodedPayload) } if decodedHw.GetType() != meshtasticpb.HardwareMessage_READ_GPIOS { t.Errorf("unexpected hardware message type: got %v", decodedHw.GetType()) } } // Test text payload portnums func TestPacketDecodeTextPayloads(t *testing.T) { textPortNums := map[meshtasticpb.PortNum]string{ meshtasticpb.PortNum_TEXT_MESSAGE_APP: "text message app", meshtasticpb.PortNum_ALERT_APP: "alert message", meshtasticpb.PortNum_DETECTION_SENSOR_APP: "detection sensor", meshtasticpb.PortNum_REPLY_APP: "reply message", meshtasticpb.PortNum_RANGE_TEST_APP: "range test message", } for portNum, expectedText := range textPortNums { t.Run(portNum.String(), func(t *testing.T) { dataMsg := &meshtasticpb.Data{ Portnum: portNum, Payload: []byte(expectedText), } packet := decodeTestPacket(t, dataMsg) if packet.Data == nil { t.Fatal("expected Data protobuf to be decoded") } if packet.Data.GetPortnum() != portNum { t.Fatalf("unexpected portnum: got %v", packet.Data.GetPortnum()) } if packet.TextPayload != expectedText { t.Errorf("unexpected text payload: got %q, want %q", packet.TextPayload, expectedText) } if packet.DecodedPayload != nil { t.Errorf("expected no decoded payload for text portnum, got %T", packet.DecodedPayload) } }) } } // Helper function to encode a proto.Message func encodeProto(t *testing.T, msg proto.Message) []byte { encoded, err := proto.Marshal(msg) if err != nil { t.Fatalf("marshaling protobuf: %v", err) } return encoded } // Helper function to create and decode a test packet func decodeTestPacket(t *testing.T, dataMsg *meshtasticpb.Data) *Packet { encodedData := encodeProto(t, dataMsg) rawPacket := make([]byte, 16+len(encodedData)) binary.LittleEndian.PutUint32(rawPacket[0:], 0x12345678) // source binary.LittleEndian.PutUint32(rawPacket[4:], 0xabcdef01) // destination binary.LittleEndian.PutUint32(rawPacket[8:], 0x99887766) // id rawPacket[12] = 0x11 // flags rawPacket[13] = 0x22 // channelHash rawPacket[14] = 0x33 // nextHop rawPacket[15] = 0x44 // relayNode copy(rawPacket[16:], encodedData) var packet Packet if err := packet.Decode(rawPacket); err != nil { t.Fatalf("decoding packet: %v", err) } return &packet }