Skip to content

Instantly share code, notes, and snippets.

@cpatulea
Last active March 7, 2026 19:03
Show Gist options
  • Select an option

  • Save cpatulea/8a1ac685f9c1de9b3deafd93bcce7931 to your computer and use it in GitHub Desktop.

Select an option

Save cpatulea/8a1ac685f9c1de9b3deafd93bcce7931 to your computer and use it in GitHub Desktop.
meshtastic2mattermost.go
// GOARCH=arm64 go build -ldflags="-s -w" -o meshtastic2mattermost && rsync -avzP meshtastic2mattermost root@bobcat.lab:lora-meshtastic/
//
//go:generate protoc --go_out=meshpb/ -I=./protobufs ./protobufs/meshtastic/mesh.proto ./protobufs/meshtastic/config.proto ./protobufs/meshtastic/channel.proto ./protobufs/meshtastic/portnums.proto ./protobufs/meshtastic/telemetry.proto ./protobufs/meshtastic/module_config.proto ./protobufs/meshtastic/xmodem.proto
package main
import (
"bytes"
"crypto/aes"
"crypto/cipher"
"encoding/base64"
"encoding/binary"
"encoding/json"
"fmt"
"io"
"log"
"net"
"os"
"strings"
mattermost "github.com/mattermost/mattermost-server/v6/model"
meshpb "github.com/meshtastic/go/generated"
"google.golang.org/protobuf/proto"
)
// https://github.com/Lora-net/sx1302_hal/blob/master/packet_forwarder/PROTOCOL.md
type Upstream struct {
RXPK []struct {
Stat int32
RSSI int32
LSNR float32
Data string
}
}
// https://github.com/meshtastic/firmware/blob/234a56446b6352490a2e30c5faec0860e8f17e27/src/mesh/RadioInterface.h#L26
type MeshtasticPacketHeader struct {
To uint32
From uint32
Id uint32
Flags uint8
Channel uint8
NextHop uint8
RelayNode uint8
}
const (
PACKET_FLAGS_HOP_LIMIT_MASK = 0x07
PACKET_FLAGS_WANT_ACK_MASK = 0x08
PACKET_FLAGS_VIA_MQTT_MASK = 0x10
PACKET_FLAGS_HOP_START_MASK = 0xE0
PACKET_FLAGS_HOP_START_SHIFT = 5
)
func ParseLoRaPacket(jsonText []byte, w io.Writer) (bool, bool) {
ok := true
show := true
var up Upstream
if err := json.Unmarshal(jsonText, &up); err != nil {
fmt.Fprintf(w, "unmarshal json: %v\n", err)
ok = false
return ok, show
}
for i, pk := range up.RXPK {
if i != 0 {
fmt.Fprintf(w, "\n")
}
var crc string
switch pk.Stat {
case 1:
crc = "OK"
case -1:
crc = "fail"
case 0:
crc = "no CRC"
default:
crc = "(other)"
}
if crc == "fail" {
show = false
}
fmt.Fprintf(w, "CRC: %s RSSI: %d dBm SNR: %.01f dB\n", crc, pk.RSSI, pk.LSNR)
var err error
var packet []byte
if packet, err = base64.StdEncoding.DecodeString(pk.Data); err != nil {
fmt.Fprintf(w, "rxpk %d: base64 decode: %v\n", i, err)
ok = false
continue
}
reader := bytes.NewReader(packet)
var header MeshtasticPacketHeader
if err = binary.Read(reader, binary.LittleEndian, &header); err != nil {
fmt.Fprintf(w, "rxpk %d: header decode: %v\n", i, err)
continue
}
fmt.Fprintf(w, "To: %08X From: %08X Id: %08X Flags: %02X ",
header.To, header.From, header.Id, header.Flags)
fmt.Fprintf(w, "(Hop Limit: %d", header.Flags&PACKET_FLAGS_HOP_LIMIT_MASK)
header.Flags &= ^uint8(PACKET_FLAGS_HOP_LIMIT_MASK)
fmt.Fprintf(w, ", Hop Start: %d", (header.Flags&PACKET_FLAGS_HOP_START_MASK)>>PACKET_FLAGS_HOP_START_SHIFT)
header.Flags &= ^uint8(PACKET_FLAGS_HOP_START_MASK)
if header.Flags&PACKET_FLAGS_WANT_ACK_MASK != 0 {
fmt.Fprintf(w, ", Want Ack")
header.Flags &= ^uint8(PACKET_FLAGS_WANT_ACK_MASK)
}
if header.Flags&PACKET_FLAGS_VIA_MQTT_MASK != 0 {
fmt.Fprintf(w, ", Via MQTT")
header.Flags &= ^uint8(PACKET_FLAGS_VIA_MQTT_MASK)
}
if header.Flags != 0 {
fmt.Fprintf(w, ", %02X", header.Flags)
}
fmt.Fprintf(w, ") ")
fmt.Fprintf(w, "Channel: %d ", header.Channel)
if header.NextHop != 0 {
fmt.Fprintf(w, "Next Hop: %d ", header.NextHop)
}
if header.RelayNode != 0 {
fmt.Fprintf(w, "Relay Node: %d ", header.RelayNode)
}
fmt.Fprintf(w, "\n")
// https://github.com/meshtastic/firmware/blob/53f189fff4b05b171d6f2500e17d6d14da1e6403/src/mesh/Router.cpp#L302
// https://github.com/meshtastic/meshtastic/blob/master/docs/about/overview/encryption/index.mdx
// https://github.com/meshtastic/firmware/blob/53f189fff4b05b171d6f2500e17d6d14da1e6403/src/mesh/Channels.cpp#L206
defaultpsk := []byte{0xd4, 0xf1, 0xbb, 0x3a, 0x20, 0x29, 0x07, 0x59,
0xf0, 0xbc, 0xff, 0xab, 0xcf, 0x4e, 0x69, 0x01}
block, err := aes.NewCipher(defaultpsk)
if err != nil {
panic(err)
}
if len(defaultpsk) != aes.BlockSize {
panic("wrong psk size")
}
// https://github.com/meshtastic/firmware/blob/master/src/mesh/CryptoEngine.cpp#L243
packetId := uint64(header.Id)
extraNonce := uint32(0)
nonce := &bytes.Buffer{}
if err := binary.Write(nonce, binary.LittleEndian, &packetId); err != nil {
panic(err)
}
if err := binary.Write(nonce, binary.LittleEndian, &header.From); err != nil {
panic(err)
}
if err := binary.Write(nonce, binary.LittleEndian, &extraNonce); err != nil {
panic(err)
}
if nonce.Len() != 16 {
panic("wrong nonce length")
}
stream := cipher.NewCTR(block, nonce.Bytes())
var payload []byte
if payload, err = io.ReadAll(reader); err != nil {
fmt.Fprintf(w, "rxpk %d: read payload: %v\n", i, err)
continue
}
stream.XORKeyStream(payload, payload)
// fmt.Fprintf(w, "Payload: %q\n", string(payload))
// fmt.Fprintf(w, "Hex: %s\n", hex.EncodeToString(payload))
data := &meshpb.Data{}
if err := proto.Unmarshal(payload, data); err != nil {
fmt.Fprintf(w, "rxpk %d: proto unmarshal: %v (wrong encryption key?)\n", i, err)
show = false
continue
}
// "portnum:..."
dataPayload := data.GetPayload()
data.Payload = nil
fmt.Fprintf(w, "%+v ", data)
// https://github.com/meshtastic/js/blob/d6f8a0bd4e0d469e3662135ce16680514787b84d/src/meshDevice.ts#L1012
//
// Intervals: https://meshtastic.org/docs/overview/mesh-algo/#regular-broadcast-intervals
switch data.Portnum {
case meshpb.PortNum_TEXT_MESSAGE_APP:
fmt.Fprintf(w, ": %q\n", string(dataPayload))
case meshpb.PortNum_POSITION_APP:
show = false
break
position := &meshpb.Position{}
if err := proto.Unmarshal(dataPayload, position); err != nil {
fmt.Fprintf(w, "rxpk %d: proto unmarshal Position: %v\n", i, err)
continue
}
fmt.Fprintf(w, ": %+v\n", position)
fmt.Fprintf(w, "https://www.google.com/maps?q=%.07f,%.07f\n",
float32(position.GetLatitudeI())/1e7, float32(position.GetLongitudeI())/1e7)
case meshpb.PortNum_NODEINFO_APP:
show = false
break
user := &meshpb.User{}
if err := proto.Unmarshal(dataPayload, user); err != nil {
fmt.Fprintf(w, "rxpk %d: proto unmarshal User: %v\n", i, err)
continue
}
fmt.Fprintf(w, ": %+v\n", user)
case meshpb.PortNum_TELEMETRY_APP:
show = false
break
telemetry := &meshpb.Telemetry{}
if err := proto.Unmarshal(dataPayload, telemetry); err != nil {
fmt.Fprintf(w, "rxpk %d: proto unmarshal Telemetry: %v\n", i, err)
continue
}
fmt.Fprintf(w, ": %+v\n", telemetry)
default:
show = false
break
data.Portnum = meshpb.PortNum(0)
data.Payload = dataPayload
fmt.Fprintf(w, "%+v\n", data)
}
}
fmt.Fprintf(w, "\n")
return ok, show
}
func main() {
pc, err := net.ListenPacket("udp", "127.0.0.1:1680")
if err != nil {
log.Fatal(err)
}
defer pc.Close()
mm := mattermost.NewAPIv4Client("https://test.foulab.org")
token, err := os.ReadFile("/root/lora-meshtastic/mattermost-token.txt")
if err != nil {
log.Fatal(err)
}
mm.SetToken(strings.TrimSpace(string(token)))
log.Printf("Started\n")
for {
buf := make([]byte, 1024)
n, _, err := pc.ReadFrom(buf)
if err != nil {
log.Printf("ReadFrom: %v\n", err)
continue
}
buf = buf[:n]
if buf[0] == 2 && buf[3] == 0 {
text := &bytes.Buffer{}
_, show := ParseLoRaPacket(buf[12:], text)
textString := strings.Trim(text.String(), "\n")
if textString != "" {
log.Printf("%s\n", strings.ReplaceAll(textString, "\n", " "))
if !show {
continue
}
post := &mattermost.Post{}
// https://test.foulab.org/foulab/channels/bobcat
post.ChannelId = "gfc44j6yhjb9iqoi55badwy6ie"
post.Message = "**Received packet:** " + textString
_, resp, err := mm.CreatePost(post)
if err != nil {
log.Printf("CreatePost err: %v\n", err)
}
if resp != nil {
log.Printf("CreatePost resp: %+v\n", resp)
}
}
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment