aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorSmoke <[email protected]>2024-01-19 10:51:52 -1000
committerSmoke <[email protected]>2024-01-19 10:51:52 -1000
commit70bb2c77356d349165ba46ea98f8346284c2e44e (patch)
tree7a1f858ca12386f7bd9478550e29bf3c1af109b5
parent320bb2e1e7dfe5092ea1f6b65a9c6e53e58ce387 (diff)
updates
-rw-r--r--go.mod27
-rw-r--r--go.sum59
-rw-r--r--lora/helpers.go52
-rw-r--r--lora/stuff.go84
-rw-r--r--mqtt/client.go138
-rw-r--r--mqtt/node.go14
-rw-r--r--mqtt/util.go23
-rw-r--r--radio/aes.go72
-rw-r--r--radio/mqtt.go29
-rw-r--r--radio/radio.go177
-rw-r--r--readme.md1
-rw-r--r--transport/serial/serial.go244
-rw-r--r--transport/serial/usb.go62
-rw-r--r--util.go46
14 files changed, 1028 insertions, 0 deletions
diff --git a/go.mod b/go.mod
index eb6d13b..faec524 100644
--- a/go.mod
+++ b/go.mod
@@ -1,3 +1,30 @@
module github.com/crypto-smoke/meshtastic-go
go 1.21
+
+require (
+ buf.build/gen/go/meshtastic/protobufs/protocolbuffers/go v1.32.0-20240117225219-a9940c43223e.1
+ github.com/charmbracelet/log v0.3.1
+ github.com/eclipse/paho.mqtt.golang v1.4.3
+ github.com/kylelemons/godebug v1.1.0
+ go.bug.st/serial v1.6.1
+ google.golang.org/protobuf v1.32.0
+)
+
+require (
+ github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
+ github.com/charmbracelet/lipgloss v0.9.1 // indirect
+ github.com/creack/goselect v0.1.2 // indirect
+ github.com/go-logfmt/logfmt v0.6.0 // indirect
+ github.com/gorilla/websocket v1.5.0 // indirect
+ github.com/lucasb-eyer/go-colorful v1.2.0 // indirect
+ github.com/mattn/go-isatty v0.0.18 // indirect
+ github.com/mattn/go-runewidth v0.0.15 // indirect
+ github.com/muesli/reflow v0.3.0 // indirect
+ github.com/muesli/termenv v0.15.2 // indirect
+ github.com/rivo/uniseg v0.2.0 // indirect
+ golang.org/x/exp v0.0.0-20231006140011-7918f672742d // indirect
+ golang.org/x/net v0.8.0 // indirect
+ golang.org/x/sync v0.1.0 // indirect
+ golang.org/x/sys v0.13.0 // indirect
+)
diff --git a/go.sum b/go.sum
new file mode 100644
index 0000000..fe4e795
--- /dev/null
+++ b/go.sum
@@ -0,0 +1,59 @@
+buf.build/gen/go/meshtastic/protobufs/protocolbuffers/go v1.32.0-20240117225219-a9940c43223e.1 h1:6Y6LLHjUJsLAS01ZOZ/imBOXIek4ebWhZwadIx6el5I=
+buf.build/gen/go/meshtastic/protobufs/protocolbuffers/go v1.32.0-20240117225219-a9940c43223e.1/go.mod h1:6znL4Am/mtSqEWteEmKgLpgDI0t/Wx80bzMsTKlu2oA=
+github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k=
+github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8=
+github.com/charmbracelet/lipgloss v0.9.1 h1:PNyd3jvaJbg4jRHKWXnCj1akQm4rh8dbEzN1p/u1KWg=
+github.com/charmbracelet/lipgloss v0.9.1/go.mod h1:1mPmG4cxScwUQALAAnacHaigiiHB9Pmr+v1VEawJl6I=
+github.com/charmbracelet/log v0.3.1 h1:TjuY4OBNbxmHWSwO3tosgqs5I3biyY8sQPny/eCMTYw=
+github.com/charmbracelet/log v0.3.1/go.mod h1:OR4E1hutLsax3ZKpXbgUqPtTjQfrh1pG3zwHGWuuq8g=
+github.com/creack/goselect v0.1.2 h1:2DNy14+JPjRBgPzAd1thbQp4BSIihxcBf0IXhQXDRa0=
+github.com/creack/goselect v0.1.2/go.mod h1:a/NhLweNvqIYMuxcMOuWY516Cimucms3DglDzQP3hKY=
+github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
+github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/eclipse/paho.mqtt.golang v1.4.3 h1:2kwcUGn8seMUfWndX0hGbvH8r7crgcJguQNCyp70xik=
+github.com/eclipse/paho.mqtt.golang v1.4.3/go.mod h1:CSYvoAlsMkhYOXh/oKyxa8EcBci6dVkLCbo5tTC1RIE=
+github.com/go-logfmt/logfmt v0.6.0 h1:wGYYu3uicYdqXVgoYbvnkrPVXkuLM1p1ifugDMEdRi4=
+github.com/go-logfmt/logfmt v0.6.0/go.mod h1:WYhtIu8zTZfxdn5+rREduYbwxfcBr/Vr6KEVveWlfTs=
+github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
+github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
+github.com/google/go-cmp v0.5.8 h1:e6P7q2lk1O+qJJb4BtCQXlK8vWEO8V1ZeuEdJNOqZyg=
+github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
+github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc=
+github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
+github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
+github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
+github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY=
+github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
+github.com/mattn/go-isatty v0.0.18 h1:DOKFKCQ7FNG2L1rbrmstDN4QVRdS89Nkh85u68Uwp98=
+github.com/mattn/go-isatty v0.0.18/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
+github.com/mattn/go-runewidth v0.0.12/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk=
+github.com/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZgg3U=
+github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
+github.com/muesli/reflow v0.3.0 h1:IFsN6K9NfGtjeggFP+68I4chLZV2yIKsXJFNZ+eWh6s=
+github.com/muesli/reflow v0.3.0/go.mod h1:pbwTDkVPibjO2kyvBQRBxTWEEGDGq0FlB1BIKtnHY/8=
+github.com/muesli/termenv v0.15.2 h1:GohcuySI0QmI3wN8Ok9PtKGkgkFIk7y6Vpb5PvrY+Wo=
+github.com/muesli/termenv v0.15.2/go.mod h1:Epx+iuz8sNs7mNKhxzH4fWXGNpZwUaJKRS1noLXviQ8=
+github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
+github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
+github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
+github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY=
+github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
+github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
+github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
+go.bug.st/serial v1.6.1 h1:VSSWmUxlj1T/YlRo2J104Zv3wJFrjHIl/T3NeruWAHY=
+go.bug.st/serial v1.6.1/go.mod h1:UABfsluHAiaNI+La2iESysd9Vetq7VRdpxvjx7CmmOE=
+golang.org/x/exp v0.0.0-20231006140011-7918f672742d h1:jtJma62tbqLibJ5sFQz8bKtEM8rJBtfilJ2qTU199MI=
+golang.org/x/exp v0.0.0-20231006140011-7918f672742d/go.mod h1:ldy0pHrwJyGW56pPQzzkH36rKxoZW1tw7ZJpeKx+hdo=
+golang.org/x/net v0.8.0 h1:Zrh2ngAOFYneWTAIAPethzeaQLuHwhuBkuV6ZiRnUaQ=
+golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc=
+golang.org/x/sync v0.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o=
+golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.13.0 h1:Af8nKPmuFypiUBjVoU9V20FiaFXOcuZI21p0ycVYYGE=
+golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
+google.golang.org/protobuf v1.32.0 h1:pPC6BG5ex8PDFnkbrGU3EixyhKcQ2aDuBS36lqK/C7I=
+google.golang.org/protobuf v1.32.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
+gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
+gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
diff --git a/lora/helpers.go b/lora/helpers.go
new file mode 100644
index 0000000..c843cc3
--- /dev/null
+++ b/lora/helpers.go
@@ -0,0 +1,52 @@
+package lora
+
+import "fmt"
+
+// translated from https://sensing-labs.com/f-a-q/a-good-radio-level/
+
+// Define signal quality and diagnostic notes.
+type signalQuality string
+type diagnosticNote string
+
+const (
+ Good signalQuality = "GOOD"
+ Fair signalQuality = "FAIR"
+ Bad signalQuality = "BAD"
+)
+
+// getSignalQuality determines the signal quality based on RSSI and SNR.
+func getSignalQuality(rssi, snr float64) signalQuality {
+ // Define the boundaries for GOOD signal quality
+ if snr >= -7 && rssi >= -115 {
+ return Good
+ }
+ // Define the boundaries for FAIR signal quality
+ if snr >= -15 && rssi >= -126 {
+ return Fair
+ }
+ // If none of the above conditions are met, signal is BAD
+ return Bad
+}
+
+// getDiagnosticNotes provides recommendations based on RSSI and SNR values.
+func getDiagnosticNotes(rssi, snr float64) diagnosticNote {
+ if rssi >= -115 && snr >= -7 {
+ return "RF level is optimal to get a good reception reliability."
+ } else if rssi >= -126 && snr >= -15 {
+ return "RF level is not optimal but must be sufficient. Try to improve your device position if possible. You will have to monitor the stability of the RF level."
+ } else {
+ return "NOISY environment. Try to put device out of electromagnetic sources."
+ }
+}
+
+func Demo() {
+ // Example usage
+ rssi := -120.0 // RSSI value
+ snr := -10.0 // SNR value
+
+ quality := getSignalQuality(rssi, snr)
+ notes := getDiagnosticNotes(rssi, snr)
+
+ fmt.Printf("The signal quality is %s.\n", quality)
+ fmt.Printf("Diagnostic Notes: %s\n", notes)
+}
diff --git a/lora/stuff.go b/lora/stuff.go
new file mode 100644
index 0000000..1cee21a
--- /dev/null
+++ b/lora/stuff.go
@@ -0,0 +1,84 @@
+// Package loraradio provides functionality to determine values for a LoRa radio link,
+// including maximum data rate and link budget while accounting for receiver sensitivity.
+package lora
+
+import (
+ "fmt"
+ "math"
+)
+
+// TODO: needs cleanup. lots of gpt generated code
+
+// MaxDataRate calculates the maximum data rate for a LoRa radio link based on the provided parameters.
+func MaxDataRate(bandwidth, spreadingFactor, codeRate float64) float64 {
+ // Assuming the LoRa modulation's data rate equation:
+ // Data Rate = BW / (2^SF) * CR
+ // where SF is the spreading factor, BW is the bandwidth in Hz, and CR is the code rate.
+ return (bandwidth / math.Pow(2, spreadingFactor)) * codeRate * spreadingFactor
+}
+
+// Simplified link budget calculation per the Semtech calculator
+func LinkBudget(rxSensitivity, transmitPower float64) float64 {
+ return transmitPower - rxSensitivity
+}
+
+// SensitivityParams holds the parameters used for sensitivity calculations.
+type SensitivityParams struct {
+ Bandwidth float64 // in Hz
+ ImplementationL float64 // Implementation loss in dB, typically 1-3 dB
+ SpreadingFactor int // LoRa Spreading Factor
+}
+
+// SpreadingFactorData holds the data for each spreading factor.
+type SpreadingFactorData struct {
+ SF int
+ ChipsPerSymbol int
+ DemodulatorSNR float64
+}
+
+var SpreadingFactors = []SpreadingFactorData{
+ {SF: 5, ChipsPerSymbol: 32, DemodulatorSNR: -2.5},
+ {SF: 6, ChipsPerSymbol: 64, DemodulatorSNR: -5},
+ {SF: 7, ChipsPerSymbol: 128, DemodulatorSNR: -7.5},
+ {SF: 8, ChipsPerSymbol: 256, DemodulatorSNR: -10},
+ {SF: 9, ChipsPerSymbol: 512, DemodulatorSNR: -12.5},
+ {SF: 10, ChipsPerSymbol: 1024, DemodulatorSNR: -15},
+ {SF: 11, ChipsPerSymbol: 2048, DemodulatorSNR: -17.5},
+ {SF: 12, ChipsPerSymbol: 4096, DemodulatorSNR: -20},
+}
+
+// CalculateSensitivity calculates the LoRa receiver sensitivity based on the provided parameters.
+func CalculateSensitivity(params SensitivityParams, spreadingFactors []SpreadingFactorData) (float64, error) {
+ if params.Bandwidth <= 0 {
+ return 0, fmt.Errorf("bandwidth must be greater than 0")
+ }
+
+ // Find the SNR for the given spreading factor from the provided data.
+ var snr float64
+ found := false
+ for _, sfData := range spreadingFactors {
+ if sfData.SF == params.SpreadingFactor {
+ snr = sfData.DemodulatorSNR
+ found = true
+ break
+ }
+ }
+ if !found {
+ return 0, fmt.Errorf("spreading factor data not found for SF=%d", params.SpreadingFactor)
+ }
+
+ // Thermal noise in dBm for the given bandwidth at room temperature (290K).
+ thermalNoise := -174.0 // dBm/Hz
+
+ // Noise figure in dBm for the given bandwidth.
+ noiseFigure := thermalNoise + 10*math.Log10(params.Bandwidth)
+
+ // Receiver sensitivity calculation in dBm.
+ sensitivity := noiseFigure + snr + params.ImplementationL
+
+ return sensitivity, nil
+}
+func SNR(spreadingFactor int) float64 {
+ result := (float64(spreadingFactor) - 4) * -2.5
+ return result * 100 / 100
+}
diff --git a/mqtt/client.go b/mqtt/client.go
new file mode 100644
index 0000000..913b8af
--- /dev/null
+++ b/mqtt/client.go
@@ -0,0 +1,138 @@
+package mqtt
+
+import (
+ "errors"
+ "github.com/charmbracelet/log"
+ mqtt "github.com/eclipse/paho.mqtt.golang"
+ "strings"
+ "sync"
+ "time"
+)
+
+type Client struct {
+ server string
+ username string
+ password string
+ topicRoot string
+ clientID string
+ client mqtt.Client
+ sync.RWMutex
+ channelHandlers map[string][]HandlerFunc
+}
+
+type HandlerFunc func(message Message)
+
+var DefaultClient = Client{
+ server: "tcp://mqtt.meshtastic.org:1883",
+ username: "meshdev",
+ password: "large4cats",
+ topicRoot: "msh/2",
+
+ channelHandlers: make(map[string][]HandlerFunc),
+}
+
+func NewClient(url, username, password, rootTopic string) *Client {
+ return &Client{
+ server: url,
+ username: username,
+ password: password,
+ topicRoot: rootTopic,
+ channelHandlers: make(map[string][]HandlerFunc),
+ }
+}
+
+func (c *Client) TopicRoot() string {
+ return c.topicRoot
+}
+
+func (c *Client) Connect() error {
+ var alphabet = []rune("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789")
+ c.clientID = randomString(23, alphabet)
+
+ mqtt.DEBUG = log.StandardLog(log.StandardLogOptions{ForceLevel: log.DebugLevel})
+ mqtt.ERROR = log.StandardLog(log.StandardLogOptions{ForceLevel: log.ErrorLevel})
+ opts := mqtt.NewClientOptions().
+ AddBroker(c.server).
+ SetUsername(c.username).
+ SetOrderMatters(false).
+ SetPassword(c.password).
+ SetClientID(c.clientID).
+ SetCleanSession(false)
+ opts.SetKeepAlive(30 * time.Second)
+ opts.SetResumeSubs(true)
+ //opts.SetDefaultPublishHandler(f)
+ opts.SetPingTimeout(5 * time.Second)
+ opts.SetAutoReconnect(true)
+ opts.SetMaxReconnectInterval(1 * time.Minute)
+ opts.SetConnectionLostHandler(func(c mqtt.Client, err error) {
+ log.Error("mqtt connection lost", "err", err)
+ })
+ opts.SetReconnectingHandler(func(c mqtt.Client, options *mqtt.ClientOptions) {
+ log.Info("mqtt reconnecting")
+ })
+ opts.SetOnConnectHandler(func(client mqtt.Client) {
+ log.Info("connected to", "server", c.server)
+ })
+ c.client = mqtt.NewClient(opts)
+ if token := c.client.Connect(); token.Wait() && token.Error() != nil {
+ return token.Error()
+ }
+ return nil
+}
+
+// MQTT Message
+type Message struct {
+ Topic string
+ Payload []byte
+ Retained bool
+}
+
+// Publish a message to the broker
+func (c *Client) Publish(m *Message) error {
+ tok := c.client.Publish(m.Topic, 0, m.Retained, m.Payload)
+ if !tok.WaitTimeout(10 * time.Second) {
+ tok.Wait()
+ return errors.New("timeout on mqtt publish")
+ }
+ if tok.Error() != nil {
+ return tok.Error()
+ }
+ return nil
+}
+
+// Register a handler for messages on the specified channel
+func (c *Client) Handle(channel string, h HandlerFunc) {
+ c.Lock()
+ defer c.Unlock()
+ topic := c.GetFullTopicForChannel(channel)
+ c.channelHandlers[channel] = append(c.channelHandlers[channel], h)
+ c.client.Subscribe(topic+"/+", 0, c.handleBrokerMessage)
+}
+func (c *Client) GetFullTopicForChannel(channel string) string {
+ return c.topicRoot + "/c/" + channel
+}
+func (c *Client) GetChannelFromTopic(topic string) string {
+ trimmed := strings.TrimPrefix(topic, c.topicRoot+"/c/")
+ sepIndex := strings.Index(trimmed, "/")
+ if sepIndex > 0 {
+ return trimmed[:sepIndex]
+ }
+ return trimmed
+}
+func (c *Client) handleBrokerMessage(client mqtt.Client, message mqtt.Message) {
+ msg := Message{
+ Topic: message.Topic(),
+ Payload: message.Payload(),
+ Retained: message.Retained(),
+ }
+ c.RLock()
+ defer c.RUnlock()
+ channel := c.GetChannelFromTopic(msg.Topic)
+ chans := c.channelHandlers[channel]
+ if len(chans) == 0 {
+ log.Error("no handlers found", "topic", channel)
+ }
+ for _, ch := range chans {
+ go ch(msg)
+ }
+}
diff --git a/mqtt/node.go b/mqtt/node.go
new file mode 100644
index 0000000..3400307
--- /dev/null
+++ b/mqtt/node.go
@@ -0,0 +1,14 @@
+package mqtt
+
+import "buf.build/gen/go/meshtastic/protobufs/protocolbuffers/go/meshtastic"
+
+// Node implements a meshtastic node that connects only via MQTT
+type Node struct {
+ user *meshtastic.User
+}
+
+func NewNode(user *meshtastic.User) *Node {
+ return &Node{
+ user: user,
+ }
+}
diff --git a/mqtt/util.go b/mqtt/util.go
new file mode 100644
index 0000000..a934614
--- /dev/null
+++ b/mqtt/util.go
@@ -0,0 +1,23 @@
+package mqtt
+
+import (
+ "math/rand"
+ "strings"
+ "time"
+)
+
+// generates a random string for use as a client ID
+func randomString(n int, alphabet []rune) string {
+ var seededRand = rand.New(rand.NewSource(time.Now().UnixNano()))
+
+ alphabetSize := len(alphabet)
+ var sb strings.Builder
+
+ for i := 0; i < n; i++ {
+ ch := alphabet[seededRand.Intn(alphabetSize)]
+ sb.WriteRune(ch)
+ }
+
+ s := sb.String()
+ return s
+}
diff --git a/radio/aes.go b/radio/aes.go
new file mode 100644
index 0000000..7826ef8
--- /dev/null
+++ b/radio/aes.go
@@ -0,0 +1,72 @@
+package radio
+
+import (
+ "bytes"
+ "crypto/aes"
+ "crypto/cipher"
+ "encoding/binary"
+ "fmt"
+)
+
+// CreateNonce creates a 128-bit nonce.
+// It takes a uint32 packetId, converts it to a uint64, and a uint32 fromNode.
+// The nonce is concatenated as [64-bit packetId][32-bit fromNode][32-bit block counter].
+func CreateNonce(packetId uint32, fromNode uint32) ([]byte, error) {
+ // Expand packetId to 64 bits
+ packetId64 := uint64(packetId)
+
+ // Initialize block counter (32-bit, starts at zero)
+ blockCounter := uint32(0)
+
+ // Create a buffer for the nonce
+ buf := new(bytes.Buffer)
+
+ // Write packetId, fromNode, and block counter to the buffer
+ err := binary.Write(buf, binary.LittleEndian, packetId64)
+ if err != nil {
+ return nil, err
+ }
+ err = binary.Write(buf, binary.LittleEndian, fromNode)
+ if err != nil {
+ return nil, err
+ }
+ err = binary.Write(buf, binary.LittleEndian, blockCounter)
+ if err != nil {
+ return nil, err
+ }
+
+ return buf.Bytes(), nil
+}
+
+// XOR encrypts or decrypts text with the specified key. It requires the packetID and sending node ID for the AES IV
+func XOR(text []byte, key []byte, packetID, fromNode uint32) ([]byte, error) {
+ if len(key) != 16 && len(key) != 24 && len(key) != 32 {
+ return nil, fmt.Errorf("key length must be 16, 24, or 32 bytes")
+ }
+
+ block, err := aes.NewCipher(key)
+ if err != nil {
+ return nil, err
+ }
+
+ // The IV needs to be unique, but not secure. It's common to include it at
+ // the beginning of the text. In CTR mode, the IV size is equal to the block size.
+ //if len(text) < aes.BlockSize {
+ // return nil, fmt.Errorf("text too short")
+ //}
+ iv, err := CreateNonce(packetID, fromNode)
+ if err != nil {
+ return nil, err
+ }
+ //text = text[aes.BlockSize:]
+
+ // CTR mode is the same for both encryption and decryption, so we use
+ // the NewCTR function rather than NewCBCDecrypter.
+ stream := cipher.NewCTR(block, iv)
+
+ // XORKeyStream can work in-place if the two arguments are the same.
+ plaintext := make([]byte, len(text))
+ stream.XORKeyStream(plaintext, text)
+
+ return plaintext, nil
+}
diff --git a/radio/mqtt.go b/radio/mqtt.go
new file mode 100644
index 0000000..c93fbad
--- /dev/null
+++ b/radio/mqtt.go
@@ -0,0 +1,29 @@
+package radio
+
+import (
+ mqtt "github.com/eclipse/paho.mqtt.golang"
+ "time"
+)
+
+// some debugging junk that needs to be deleted
+type FakeRadio struct {
+ ID uint32
+}
+
+func NewFakeRadio() (*FakeRadio, error) {
+ opts := mqtt.NewClientOptions().AddBroker("tcp://mqtt.meshtastic.org:1883").SetClientID("poopypants").SetUsername("meshdev").SetPassword("large4cats")
+ opts.SetKeepAlive(2 * time.Second)
+ //opts.SetDefaultPublishHandler(f)
+ opts.SetPingTimeout(1 * time.Second)
+
+ c := mqtt.NewClient(opts)
+ if token := c.Connect(); token.Wait() && token.Error() != nil {
+ panic(token.Error())
+ }
+ ugh := c.Subscribe("msh/2/c/#", 0, func(client mqtt.Client, message mqtt.Message) {
+
+ })
+ _ = ugh
+
+ return nil, nil
+}
diff --git a/radio/radio.go b/radio/radio.go
new file mode 100644
index 0000000..f449a34
--- /dev/null
+++ b/radio/radio.go
@@ -0,0 +1,177 @@
+package radio
+
+import (
+ generated "buf.build/gen/go/meshtastic/protobufs/protocolbuffers/go/meshtastic"
+ b64 "encoding/base64"
+ "encoding/hex"
+ "errors"
+ "fmt"
+ "github.com/charmbracelet/log"
+ "github.com/kylelemons/godebug/pretty"
+
+ "google.golang.org/protobuf/proto"
+ "strings"
+)
+
+// not sure what i was getting at with this
+type comMode uint8
+
+const (
+ ComModeProtobuf comMode = iota + 1
+ ComModeSerialDebug
+)
+
+// Transport defines methods required for communicating with a radio via serial, ble, or tcp
+// Probably need to reevaluate this to just use the ToRadio and FromRadio protobufs
+type Transport interface {
+ Connect() error
+ SendPacket(data []byte) error
+ RequestConfig() error
+
+ // Listen(ch chan)
+ Close() error
+}
+
+// Something is something created to track keys for packet decrypting
+type Something struct {
+ keys map[string][]byte
+}
+
+// default encryption key, commonly referenced as AQ==
+// as base64: 1PG7OiApB1nwvP+rz05pAQ==
+var DefaultKey = []byte{0xd4, 0xf1, 0xbb, 0x3a, 0x20, 0x29, 0x07, 0x59, 0xf0, 0xbc, 0xff, 0xab, 0xcf, 0x4e, 0x69, 0x01}
+
+// clean up a base64 key that has been rendered safe for use in a URL
+func ParseKey(key string) []byte {
+ key = strings.ReplaceAll(key, "-", "+")
+ key = strings.ReplaceAll(key, "_", "/")
+ sDec, _ := b64.StdEncoding.DecodeString(key)
+ return sDec
+}
+
+func NewThing() *Something {
+ return &Something{keys: map[string][]byte{
+ "LongFast": DefaultKey,
+ "LongSlow": DefaultKey,
+ "VLongSlow": DefaultKey,
+ }}
+}
+
+// GenerateByteSlices creates a bunch of weak keys for use when interfacing on MQTT.
+// This creates 128, 192, and 256 bit AES keys with only a single byte specified
+func GenerateByteSlices() [][]byte {
+ // There are 256 possible values for a single byte
+ // We create 1536 slices: 512 with 16 bytes, 512 with 24 bytes, and 512 with 32 bytes
+ allSlices := make([][]byte, 256*3)
+
+ for i := 0; i < 256; i++ {
+ // Create a slice of 16 bytes for the first 256 slices
+ slice16 := make([]byte, 16)
+ // Set the last byte to the current iteration value.
+ slice16[15] = byte(i)
+ // Assign the slice to our slice of slices.
+ allSlices[i] = slice16
+
+ // Create a slice of 24 bytes (192 bits) for the next 256 slices
+ slice24 := make([]byte, 24)
+ // Set the last byte to the current iteration value.
+ slice24[23] = byte(i)
+ // Assign the slice to our slice of slices, offset by 256.
+ allSlices[i+256] = slice24
+
+ // Create a slice of 32 bytes for the last 256 slices
+ slice32 := make([]byte, 32)
+ // Set the last byte to the current iteration value.
+ slice32[31] = byte(i)
+ // Assign the slice to our slice of slices, offset by 512.
+ allSlices[i+512] = slice32
+ }
+
+ return allSlices
+}
+
+var ErrDecrypt = errors.New("unable to decrypt payload")
+var otherKeys = GenerateByteSlices()
+
+// xorHash computes a simple XOR hash of the provided byte slice.
+func xorHash(p []byte) uint8 {
+ var code uint8
+ for _, b := range p {
+ code ^= b
+ }
+ return code
+}
+
+// GenerateHash returns the hash for a given channel by XORing the channel name and PSK.
+func ChannelHash(channelName string, channelKey []byte) (uint32, error) {
+ if len(channelKey) == 0 {
+ return 0, fmt.Errorf("channel key cannot be empty")
+ }
+
+ h := xorHash([]byte(channelName))
+ h ^= xorHash(channelKey)
+
+ return uint32(h), nil
+}
+
+// Attempts to decrypt a packet with the specified key, or return the already decrypted data if present.
+func TryDecode(packet *generated.MeshPacket, key []byte) (*generated.Data, error) {
+ //packet := env.GetPacket()
+ switch packet.GetPayloadVariant().(type) {
+ case *generated.MeshPacket_Decoded:
+ //fmt.Println("decoded")
+ return packet.GetDecoded(), nil
+ case *generated.MeshPacket_Encrypted:
+ // fmt.Println("encrypted")
+ /*
+ key, exists := s.keys[env.ChannelId]
+ if !exists {
+ return nil, errors.New("no decryption key for channel")
+ }
+
+ */
+ decrypted, err := XOR(packet.GetEncrypted(), key, packet.Id, packet.From)
+ if err != nil {
+ //log.Error("failed decrypting packet", "error", err)
+ return nil, ErrDecrypt
+ }
+
+ var meshPacket generated.Data
+ err = proto.Unmarshal(decrypted, &meshPacket)
+ if err != nil {
+ // log.Info("failed with supplied key")
+ return nil, ErrDecrypt
+
+ // Below is some test code that tried every other known key, including the rainbow table of single byte keys.
+ for keyCount, k := range otherKeys {
+ decrypted, err := XOR(packet.GetEncrypted(), k, packet.Id, packet.From)
+ if err != nil {
+ continue
+ }
+ err = proto.Unmarshal(decrypted, &meshPacket)
+ if err != nil {
+ continue
+ }
+ log.Info("success", "key index", keyCount, "key", hex.EncodeToString(k), "from", packet.From)
+
+ pretty.Print(meshPacket)
+ return &meshPacket, nil
+ }
+ if strings.Contains(err.Error(), "cannot parse invalid wire-format data") {
+ return nil, ErrDecrypt
+ } else {
+ log.Error("wtf", "err", err)
+ }
+ return nil, err
+ }
+ //fmt.Println("supplied key success")
+ return &meshPacket, nil
+ default:
+ return nil, errors.New("unknown payload variant")
+ }
+}
+
+// decode a payload to a Data protobuf
+func (s *Something) TryDecode(packet *generated.MeshPacket, key []byte) (*generated.Data, error) {
+ return TryDecode(packet, key)
+}
diff --git a/readme.md b/readme.md
new file mode 100644
index 0000000..813f705
--- /dev/null
+++ b/readme.md
@@ -0,0 +1 @@
+Under heavy development. Consider these contracts written with a half-eaten crayon; they *will* change. \ No newline at end of file
diff --git a/transport/serial/serial.go b/transport/serial/serial.go
new file mode 100644
index 0000000..cea1dcc
--- /dev/null
+++ b/transport/serial/serial.go
@@ -0,0 +1,244 @@
+package serial
+
+import (
+ "buf.build/gen/go/meshtastic/protobufs/protocolbuffers/go/meshtastic"
+ "encoding/binary"
+ "fmt"
+ "github.com/charmbracelet/log"
+ "go.bug.st/serial"
+ "google.golang.org/protobuf/proto"
+ "io"
+ "math/rand"
+ "time"
+)
+
+const (
+ WAIT_AFTER_WAKE = 100 * time.Millisecond
+ START1 = 0x94
+ START2 = 0xc3
+ PACKET_MTU = 512
+ PORT_SPEED = 115200 //921600
+)
+
+// Serial connection to a node
+type Conn struct {
+ serialPort string
+ serialConn serial.Port
+}
+
+func NewConn(port string) *Conn {
+ var c = Conn{serialPort: port}
+ return &c
+}
+
+// You have to send this first to get the radio into protobuf mode and have it accept and send packets via serial
+func (c *Conn) sendGetConfig() {
+ r := rand.Uint32()
+
+ //log.Info("want config id", r)
+ msg := &meshtastic.ToRadio{
+ PayloadVariant: &meshtastic.ToRadio_WantConfigId{
+ WantConfigId: r,
+ },
+ }
+ c.sendToRadio(msg)
+}
+
+// TODO: refactor this to lose the channels
+func (c *Conn) Connect(ch chan *meshtastic.FromRadio, ch2 chan *meshtastic.ToRadio) error {
+ mode := &serial.Mode{
+ BaudRate: PORT_SPEED,
+ }
+ port, err := serial.Open(c.serialPort, mode)
+ if err != nil {
+ return err
+ }
+ c.serialConn = port
+
+ go c.decodeProtos(false, ch)
+ go func() {
+ for {
+ msg := <-ch2
+ c.sendToRadio(msg)
+ }
+ }()
+ c.sendGetConfig()
+ return nil
+}
+
+var txtCh = make(chan *meshtastic.MeshPacket)
+
+func (c *Conn) decodeProtos(printDebug bool, ch chan *meshtastic.FromRadio) {
+ for {
+ data, err := readUntilProtobuf(c.serialConn, printDebug)
+ if err != nil {
+ log.Info("error:", err)
+ continue
+ }
+ //log.Info("read from serial and got proto")
+ var msg2 meshtastic.FromRadio
+ err = proto.Unmarshal(data, &msg2)
+ if err != nil {
+ log.Fatal(err)
+ }
+ ch <- &msg2
+ continue
+
+ // i think everthing below was just crap for initial debugging and testing. will cleanup (TODO)
+ //fmt.Println("proto message parsed")
+
+ switch msg2.PayloadVariant.(type) {
+ case *meshtastic.FromRadio_Packet:
+ //log.Debug("packet recvd")
+ pkt := msg2.GetPacket()
+ payload := pkt.GetPayloadVariant()
+ switch payload.(type) {
+ case *meshtastic.MeshPacket_Decoded:
+ //log.Info("got decoded packet")
+ data := pkt.GetDecoded()
+ switch data.Portnum {
+ case meshtastic.PortNum_TEXT_MESSAGE_APP:
+ go func() {
+ txtCh <- pkt
+ }()
+ //fmt.Println("payload:", string(data.Payload))
+ //fmt.Println("to:", pkt.To)
+ case meshtastic.PortNum_ADMIN_APP:
+ var adminMsg meshtastic.AdminMessage
+ err := proto.Unmarshal(data.Payload, &adminMsg)
+ if err != nil {
+ log.Fatal(err)
+ }
+ // log.Println("admin msg", adminMsg)
+ switch adminMsg.GetPayloadVariant().(type) {
+ case *meshtastic.AdminMessage_GetDeviceMetadataResponse:
+ //log.Info("metadata:", adminMsg.GetGetDeviceMetadataResponse())
+ }
+ }
+ //log.Info("data", "packet", data)
+ case *meshtastic.MeshPacket_Encrypted:
+ log.Info("got encrypted packet")
+ }
+ //log.Info("got packet", pkt)
+ case *meshtastic.FromRadio_Metadata:
+ log.Info("got metadata", msg2.GetMetadata())
+
+ case *meshtastic.FromRadio_ConfigCompleteId:
+ log.Info("config complete", msg2.GetConfigCompleteId())
+ case *meshtastic.FromRadio_MyInfo: //*generated.deleteme.FromRadio_MyInfo:
+ //log.WithField("my_node", msg.GetMyInfo()).Debug("got my node info")
+ //fmt.Println(msg2.GetMyInfo().MyNodeNum)
+ /*
+ case *message.FromRadio_Radio:
+ log.WithField("radio", msg.GetRadio()).Debug("got radio config")
+ m.radioConfig = msg.GetRadio()
+ case *message.FromRadio_NodeInfo:
+ log.WithField("node", msg.GetNodeInfo()).Debug("got node info")
+ m.pub(TOPIC_NODE, msg.GetNodeInfo())
+ case *message.FromRadio_ConfigCompleteId:
+ // TODO: implement this
+ log.WithField("node", msg.GetConfigCompleteId()).Debug("got config complete")
+
+ */
+ case *meshtastic.FromRadio_MqttClientProxyMessage:
+ mqttMessage := msg2.GetMqttClientProxyMessage()
+
+ switch payload := mqttMessage.GetPayloadVariant().(type) {
+ case *meshtastic.MqttClientProxyMessage_Data:
+ //payload.Data
+ fmt.Println("got data payload")
+ case *meshtastic.MqttClientProxyMessage_Text:
+ fmt.Println("payload text", payload.Text)
+ }
+ log.Info("got mqtt client proxy")
+
+ default:
+ //log.Info("fromRadio", msg2.GetPayloadVariant())
+ }
+ }
+}
+func readUntilProtobuf(reader io.Reader, printDebug bool) ([]byte, error) {
+ buf := make([]byte, 4)
+ for {
+ // Read the first byte, looking for START1.
+ _, err := io.ReadFull(reader, buf[:1])
+ if err != nil {
+ return nil, err
+ }
+
+ // Check for START1.
+ if buf[0] != 0x94 {
+ if printDebug {
+ fmt.Print(string(buf[0]))
+ }
+ continue
+ }
+
+ // Read the second byte, looking for START2.
+ _, err = io.ReadFull(reader, buf[1:2])
+ if err != nil {
+ return nil, err
+ }
+
+ // Check for START2.
+ if buf[1] != 0xc3 {
+ continue
+ }
+
+ // The next two bytes should be the length of the protobuf message.
+ _, err = io.ReadFull(reader, buf[2:])
+ if err != nil {
+ return nil, err
+ }
+
+ length := int(binary.BigEndian.Uint16(buf[2:]))
+ if length > PACKET_MTU {
+ //packet corrupt, start over
+ continue
+ }
+ //fmt.Println("got packet from node with length", length)
+ data := make([]byte, length)
+
+ // Read the protobuf data.
+ _, err = io.ReadFull(reader, data)
+ if err != nil {
+ return nil, err
+ }
+
+ return data, nil
+ }
+}
+
+func (c *Conn) flushPort() error {
+ flush := make([]byte, 32, 32)
+ for j := 0; j < len(flush); j++ {
+ flush[j] = START2
+ }
+ _, err := c.serialConn.Write(flush)
+ if err != nil {
+ return err
+ }
+ return nil
+}
+func (c *Conn) sendToRadio(msg *meshtastic.ToRadio) error {
+ err := c.flushPort()
+ if err != nil {
+ return err
+ }
+ //fmt.Printf("Sent %v bytes\n", n)
+ data, err := proto.Marshal(msg)
+ if err != nil {
+ panic(err)
+ }
+ time.Sleep(WAIT_AFTER_WAKE)
+
+ datalen := len(data)
+ header := []byte{START1, START2, byte(datalen >> 8), byte(datalen)}
+ data = append(header, data...)
+ _, err = c.serialConn.Write(data)
+ if err != nil {
+ log.Fatal(err)
+ }
+ //fmt.Printf("Sent %v bytes\n", n)
+ return nil
+}
diff --git a/transport/serial/usb.go b/transport/serial/usb.go
new file mode 100644
index 0000000..ed15f99
--- /dev/null
+++ b/transport/serial/usb.go
@@ -0,0 +1,62 @@
+package serial
+
+import (
+ "fmt"
+ "github.com/charmbracelet/log"
+ "go.bug.st/serial/enumerator"
+)
+
+type usbDevice struct {
+ VID string
+ PID string
+}
+
+var knownDevices = []usbDevice{
+ {VID: "239A", PID: "8029"}, // rak4631_19003
+}
+
+func GetPorts() []string {
+ ports, err := enumerator.GetDetailedPortsList()
+ if err != nil {
+ log.Fatal(err)
+ }
+ var foundDevices []string
+ if len(ports) == 0 {
+ fmt.Println("No serial ports found!")
+ return nil
+ }
+ for _, port := range ports {
+ //fmt.Printf("Found port: %s\n", port.SettingName)
+ if port.IsUSB {
+ for _, device := range knownDevices {
+ if device.VID != port.VID {
+ continue
+ }
+ if device.PID != port.PID {
+ continue
+ }
+ foundDevices = append(foundDevices, port.Name)
+ }
+ }
+ }
+ return foundDevices
+}
+func getUSB() {
+ ports, err := enumerator.GetDetailedPortsList()
+ if err != nil {
+ log.Fatal(err)
+ }
+ if len(ports) == 0 {
+ fmt.Println("No serial ports found!")
+ return
+ }
+ for _, port := range ports {
+ fmt.Printf("Found port: %s\n", port.Name)
+ if port.IsUSB {
+ fmt.Printf(" Product %s\n", port.Product)
+
+ fmt.Printf(" USB ID %s:%s\n", port.VID, port.PID)
+ fmt.Printf(" USB serial %s\n", port.SerialNumber)
+ }
+ }
+}
diff --git a/util.go b/util.go
new file mode 100644
index 0000000..0de4297
--- /dev/null
+++ b/util.go
@@ -0,0 +1,46 @@
+package meshtastic
+
+import (
+ pbuf "buf.build/gen/go/meshtastic/protobufs/protocolbuffers/go/meshtastic"
+ "encoding/binary"
+ "fmt"
+)
+
+type NodeID uint32
+
+func (n NodeID) Uint32() uint32 {
+ return uint32(n)
+}
+
+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
+}
+
+type Node struct {
+ LongName string
+ ShortName string
+ ID uint32
+ HardwareModel pbuf.HardwareModel
+}
+
+// Not actually in use yet 😅
+func (n *Node) EncryptPacket(pkt *pbuf.MeshPacket, channelName string, key []byte) *pbuf.MeshPacket {
+ payload := pkt.GetPayloadVariant()
+ _ = payload
+ switch p := payload.(type) {
+ case *pbuf.MeshPacket_Decoded:
+ _ = p
+ encrypted := pbuf.MeshPacket_Encrypted{
+ Encrypted: nil,
+ }
+ _ = encrypted
+ }
+ return nil
+}