diff options
| author | Smoke <[email protected]> | 2024-01-19 10:51:52 -1000 |
|---|---|---|
| committer | Smoke <[email protected]> | 2024-01-19 10:51:52 -1000 |
| commit | 70bb2c77356d349165ba46ea98f8346284c2e44e (patch) | |
| tree | 7a1f858ca12386f7bd9478550e29bf3c1af109b5 | |
| parent | 320bb2e1e7dfe5092ea1f6b65a9c6e53e58ce387 (diff) | |
updates
| -rw-r--r-- | go.mod | 27 | ||||
| -rw-r--r-- | go.sum | 59 | ||||
| -rw-r--r-- | lora/helpers.go | 52 | ||||
| -rw-r--r-- | lora/stuff.go | 84 | ||||
| -rw-r--r-- | mqtt/client.go | 138 | ||||
| -rw-r--r-- | mqtt/node.go | 14 | ||||
| -rw-r--r-- | mqtt/util.go | 23 | ||||
| -rw-r--r-- | radio/aes.go | 72 | ||||
| -rw-r--r-- | radio/mqtt.go | 29 | ||||
| -rw-r--r-- | radio/radio.go | 177 | ||||
| -rw-r--r-- | readme.md | 1 | ||||
| -rw-r--r-- | transport/serial/serial.go | 244 | ||||
| -rw-r--r-- | transport/serial/usb.go | 62 | ||||
| -rw-r--r-- | util.go | 46 |
14 files changed, 1028 insertions, 0 deletions
@@ -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 +) @@ -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) + } + } +} @@ -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 +} |
