meshtastic: support
Some checks failed
Run tests / test (1.25) (push) Failing after 1m1s
Run tests / test (stable) (push) Failing after 1m0s

This commit is contained in:
2026-03-06 09:24:56 +01:00
parent e6bda98b92
commit e2b69d92fd
39 changed files with 26113 additions and 1 deletions

View File

@@ -0,0 +1,591 @@
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
}