Last active
March 7, 2026 19:03
-
-
Save cpatulea/8a1ac685f9c1de9b3deafd93bcce7931 to your computer and use it in GitHub Desktop.
meshtastic2mattermost.go
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| // 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