Fixed code smells
Some checks failed
Run tests / test (1.25) (push) Failing after 15s
Run tests / test (stable) (push) Failing after 17s

This commit is contained in:
2026-02-22 21:14:58 +01:00
parent 3bcbaf2135
commit 32f6c38c13
17 changed files with 1593 additions and 22 deletions

173
protocol/meshtastic/node.go Normal file
View File

@@ -0,0 +1,173 @@
package meshtastic
import (
"crypto/rand"
"encoding/binary"
"fmt"
"math"
"math/big"
"strconv"
"strings"
)
type NodeID uint32
const (
// BroadcastNodeID is the special NodeID used when broadcasting a packet to a channel.
BroadcastNodeID NodeID = math.MaxUint32
// BroadcastNodeIDNoLora is a special broadcast address that excludes LoRa transmission.
// Used for MQTT-only broadcasts. This is ^all with the NO_LORA flag (0x40) cleared.
BroadcastNodeIDNoLora NodeID = math.MaxUint32 ^ 0x40
// ReservedNodeIDThreshold is the threshold at which NodeIDs are considered reserved. Random NodeIDs should not
// be generated below this threshold.
// Source: https://github.com/meshtastic/firmware/blob/d1ea58975755e146457a8345065e4ca357555275/src/mesh/NodeDB.cpp#L461
reservedNodeIDThreshold NodeID = 4
)
// ParseNodeID parses a NodeID from various string formats:
// - "!abcd1234" (Meshtastic format with ! prefix)
// - "0xabcd1234" (hex with 0x prefix)
// - "abcd1234" (plain hex)
// - "12345678" (decimal)
func ParseNodeID(s string) (NodeID, error) {
s = strings.TrimSpace(s)
if s == "" {
return 0, fmt.Errorf("empty node ID string")
}
// Handle !prefix format
if strings.HasPrefix(s, "!") {
s = s[1:]
n, err := strconv.ParseUint(s, 16, 32)
if err != nil {
return 0, fmt.Errorf("invalid node ID %q: %w", s, err)
}
return NodeID(n), nil
}
// Handle 0x prefix
if strings.HasPrefix(s, "0x") || strings.HasPrefix(s, "0X") {
n, err := strconv.ParseUint(s[2:], 16, 32)
if err != nil {
return 0, fmt.Errorf("invalid node ID %q: %w", s, err)
}
return NodeID(n), nil
}
// Try hex first if it looks like hex (contains a-f)
sLower := strings.ToLower(s)
if strings.ContainsAny(sLower, "abcdef") {
n, err := strconv.ParseUint(s, 16, 32)
if err != nil {
return 0, fmt.Errorf("invalid node ID %q: %w", s, err)
}
return NodeID(n), nil
}
// Try decimal
n, err := strconv.ParseUint(s, 10, 32)
if err != nil {
// Fall back to hex for 8-char strings
if len(s) == 8 {
n, err = strconv.ParseUint(s, 16, 32)
if err != nil {
return 0, fmt.Errorf("invalid node ID %q: %w", s, err)
}
return NodeID(n), nil
}
return 0, fmt.Errorf("invalid node ID %q: %w", s, err)
}
return NodeID(n), nil
}
// Uint32 returns the underlying uint32 value of the NodeID.
func (n NodeID) Uint32() uint32 {
return uint32(n)
}
// String converts the NodeID to a hex formatted string.
// This is typically how NodeIDs are displayed in Meshtastic UIs.
func (n NodeID) String() string {
return fmt.Sprintf("!%08x", uint32(n))
}
// Bytes converts the NodeID to a byte slice
func (n NodeID) Bytes() []byte {
bytes := make([]byte, 4) // uint32 is 4 bytes
binary.BigEndian.PutUint32(bytes, n.Uint32())
return bytes
}
// DefaultLongName returns the default long node name based on the NodeID.
// Source: https://github.com/meshtastic/firmware/blob/d1ea58975755e146457a8345065e4ca357555275/src/mesh/NodeDB.cpp#L382
func (n NodeID) DefaultLongName() string {
bytes := make([]byte, 4) // uint32 is 4 bytes
binary.BigEndian.PutUint32(bytes, n.Uint32())
return fmt.Sprintf("Meshtastic %04x", bytes[2:])
}
// DefaultShortName returns the default short node name based on the NodeID.
// Last two bytes of the NodeID represented in hex.
// Source: https://github.com/meshtastic/firmware/blob/d1ea58975755e146457a8345065e4ca357555275/src/mesh/NodeDB.cpp#L382
func (n NodeID) DefaultShortName() string {
bytes := make([]byte, 4) // uint32 is 4 bytes
binary.BigEndian.PutUint32(bytes, n.Uint32())
return fmt.Sprintf("%04x", bytes[2:])
}
// UnmarshalText implements encoding.TextUnmarshaler for use with config parsers like Viper.
func (n *NodeID) UnmarshalText(text []byte) error {
parsed, err := ParseNodeID(string(text))
if err != nil {
return err
}
*n = parsed
return nil
}
// MarshalText implements encoding.TextMarshaler.
func (n NodeID) MarshalText() ([]byte, error) {
return []byte(n.String()), nil
}
// IsReservedID returns true if this is a reserved or broadcast NodeID.
func (n NodeID) IsReservedID() bool {
return n < reservedNodeIDThreshold || n >= BroadcastNodeIDNoLora
}
// IsBroadcast returns true if this is any form of broadcast address.
func (n NodeID) IsBroadcast() bool {
return n == BroadcastNodeID || n == BroadcastNodeIDNoLora
}
// ToMacAddress returns a MAC address string derived from the NodeID.
// This creates a locally administered unicast MAC address.
func (n NodeID) ToMacAddress() string {
bytes := n.Bytes()
// Use 0x02 as the first octet (locally administered, unicast)
// Then 0x00 as padding, followed by the 4 bytes of the NodeID
return fmt.Sprintf("02:00:%02x:%02x:%02x:%02x", bytes[0], bytes[1], bytes[2], bytes[3])
}
// RandomNodeID returns a randomised NodeID.
// It's recommended to call this the first time a node is started and persist the result.
//
// Hardware meshtastic nodes first try a NodeID of the last four bytes of the BLE MAC address. If that ID is already in
// use or invalid, a random NodeID is generated.
// Source: https://github.com/meshtastic/firmware/blob/d1ea58975755e146457a8345065e4ca357555275/src/mesh/NodeDB.cpp#L466
func RandomNodeID() (NodeID, error) {
// Generates a random uint32 between reservedNodeIDThreshold and math.MaxUint32
randomInt, err := rand.Int(
rand.Reader,
big.NewInt(
int64(math.MaxUint32-reservedNodeIDThreshold.Uint32()),
),
)
if err != nil {
return NodeID(0), fmt.Errorf("reading entropy: %w", err)
}
r := uint32(randomInt.Uint64()) + reservedNodeIDThreshold.Uint32()
return NodeID(r), nil
}

View File

@@ -0,0 +1,54 @@
package meshtastic
import (
"encoding/binary"
"errors"
)
const (
minPacketSize = 4 + 4 + 4 + 1 + 1 + 1 + 1
maxPayloadSize = 237
)
var (
// ErrInvalidPacket signals the source buffer does not contain a valid packet.
ErrInvalidPacket = errors.New("meshtastic: invalid packet")
)
type Packet struct {
Destination NodeID
Source NodeID
ID uint32
Flags uint8
ChannelHash uint8
NextHop uint8
RelayNode uint8
PayloadLength int
Payload [maxPayloadSize]byte
}
func (packet *Packet) Decode(data []byte) error {
if len(data) < minPacketSize {
return ErrInvalidPacket
}
packet.Destination = parseNodeID(data[0:])
packet.Source = parseNodeID(data[4:])
packet.ID = binary.LittleEndian.Uint32(data[8:])
packet.Flags = data[12]
packet.ChannelHash = data[13]
packet.NextHop = data[14]
packet.RelayNode = data[15]
packet.PayloadLength = len(data[16:])
copy(packet.Payload[:], data[16:])
return nil
}
func (packet *Packet) HopLimit() int {
return
}
func parseNodeID(data []byte) NodeID {
return NodeID(binary.LittleEndian.Uint32(data))
}