More protocols
This commit is contained in:
@@ -1,6 +1,7 @@
|
|||||||
package aprs
|
package aprs
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"strings"
|
"strings"
|
||||||
@@ -46,6 +47,25 @@ func (a Address) Secret() int16 {
|
|||||||
return h & 0x7fff
|
return h & 0x7fff
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (a Address) MarshalJSON() ([]byte, error) {
|
||||||
|
return json.Marshal(a.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *Address) UnmarshalJSON(b []byte) error {
|
||||||
|
var s string
|
||||||
|
if err := json.Unmarshal(b, &s); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
p, err := ParseAddress(s)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
a.Call = p.Call
|
||||||
|
a.SSID = p.SSID
|
||||||
|
a.IsRepeated = p.IsRepeated
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func ParseAddress(s string) (Address, error) {
|
func ParseAddress(s string) (Address, error) {
|
||||||
r := strings.HasSuffix(s, "*")
|
r := strings.HasSuffix(s, "*")
|
||||||
if r {
|
if r {
|
||||||
|
|||||||
188
protocol/aprs/aprsis/proxy.go
Normal file
188
protocol/aprs/aprsis/proxy.go
Normal file
@@ -0,0 +1,188 @@
|
|||||||
|
package aprsis
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bufio"
|
||||||
|
"bytes"
|
||||||
|
"fmt"
|
||||||
|
"net"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
|
||||||
|
"git.maze.io/go/ham/protocol"
|
||||||
|
"github.com/sirupsen/logrus"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
DefaultListenAddr = ":14580"
|
||||||
|
DefaultServerAddr = "rotate.aprs2.net:14580"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Proxy struct {
|
||||||
|
Logger *logrus.Logger
|
||||||
|
Filter string
|
||||||
|
server string
|
||||||
|
listen net.Listener
|
||||||
|
packets chan *protocol.Packet
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewProxy(listen, server string) (*Proxy, error) {
|
||||||
|
if _, err := net.ResolveTCPAddr("tcp", server); err != nil {
|
||||||
|
return nil, fmt.Errorf("aprsis: error resolving %q: %v", server, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
listenAddr, err := net.ResolveTCPAddr("tcp", listen)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("aprsis: error listening on %s: %v", listen, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
listener, err := net.ListenTCP("tcp", listenAddr)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("aprsis: error listening on %s: %v", listen, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
proxy := &Proxy{
|
||||||
|
Logger: logrus.New(),
|
||||||
|
server: server,
|
||||||
|
listen: listener,
|
||||||
|
}
|
||||||
|
go proxy.accept()
|
||||||
|
|
||||||
|
return proxy, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (proxy *Proxy) Close() error {
|
||||||
|
if proxy.packets != nil {
|
||||||
|
close(proxy.packets)
|
||||||
|
proxy.packets = nil
|
||||||
|
}
|
||||||
|
return proxy.listen.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (proxy *Proxy) RawPackets() <-chan *protocol.Packet {
|
||||||
|
if proxy.packets == nil {
|
||||||
|
proxy.packets = make(chan *protocol.Packet, 16)
|
||||||
|
}
|
||||||
|
return proxy.packets
|
||||||
|
}
|
||||||
|
|
||||||
|
func (proxy *Proxy) accept() {
|
||||||
|
for {
|
||||||
|
client, err := proxy.listen.Accept()
|
||||||
|
if err != nil {
|
||||||
|
proxy.Logger.Errorf("aprs-is proxy: error accepting client: %v", err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
go proxy.handle(client)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (proxy *Proxy) handle(client net.Conn) {
|
||||||
|
defer func() { _ = client.Close() }()
|
||||||
|
|
||||||
|
host, _, _ := net.SplitHostPort(client.RemoteAddr().String())
|
||||||
|
proxy.Logger.Infof("aprs-is proxy[%s]: new connection", host)
|
||||||
|
|
||||||
|
server, err := net.Dial("tcp", proxy.server)
|
||||||
|
if err != nil {
|
||||||
|
proxy.Logger.Warnf("aprs-is proxy[%s]: can't connecto to APRS-IS server %s: %v", host, proxy.server, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer func() { _ = server.Close() }()
|
||||||
|
|
||||||
|
var (
|
||||||
|
wait sync.WaitGroup
|
||||||
|
call string
|
||||||
|
)
|
||||||
|
wait.Go(func() { proxy.proxy(client, server, host, "->", &call) })
|
||||||
|
wait.Go(func() { proxy.proxy(server, client, host, "<-", nil) })
|
||||||
|
wait.Wait()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (proxy *Proxy) proxy(dst, src net.Conn, host, dir string, call *string) {
|
||||||
|
defer func() {
|
||||||
|
if tcp, ok := dst.(*net.TCPConn); ok {
|
||||||
|
_ = tcp.CloseWrite()
|
||||||
|
} else {
|
||||||
|
_ = dst.Close()
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
reader := bufio.NewReader(src)
|
||||||
|
for {
|
||||||
|
line, err := reader.ReadBytes('\n')
|
||||||
|
if err != nil {
|
||||||
|
proxy.Logger.Warnf("aprs-is proxy[%s]: %s read error: %v", host, src.RemoteAddr(), err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// proxy to remote unaltered
|
||||||
|
if len(line) > 0 {
|
||||||
|
if _, err = dst.Write(line); err != nil {
|
||||||
|
proxy.Logger.Warnf("aprs-is proxy[%s]: %s write error: %v", host, dst.RemoteAddr(), err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// parse line
|
||||||
|
line = bytes.TrimRight(line, "\r\n")
|
||||||
|
if len(line) > 0 {
|
||||||
|
proxy.Logger.Tracef("aprs-is proxy[%s]: %s %s", host, dir, string(line))
|
||||||
|
|
||||||
|
if call != nil && strings.HasPrefix(string(line), "# logresp ") {
|
||||||
|
// server responds to client login
|
||||||
|
part := strings.SplitN(string(line), " ", 5)
|
||||||
|
if len(part) > 4 && part[3] == "verified," {
|
||||||
|
*call = part[2]
|
||||||
|
proxy.Logger.Infof("aprs-is proxy[%s]: logged in as %s", host, *call)
|
||||||
|
|
||||||
|
if proxy.Filter != "" {
|
||||||
|
proxy.Logger.Tracef("aprs-is proxy[%s]: %s filter %s", host, dir, proxy.Filter)
|
||||||
|
if _, err = fmt.Fprintf(src, "filter %s\r\n", proxy.Filter); err != nil {
|
||||||
|
proxy.Logger.Warnf("aprs-is proxy[%s]: %s write error: %v", host, src.RemoteAddr(), err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !isCommand(line) {
|
||||||
|
proxy.handleRawPacket(line)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (proxy *Proxy) handleRawPacket(data []byte) {
|
||||||
|
if proxy.packets == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
select {
|
||||||
|
case proxy.packets <- &protocol.Packet{
|
||||||
|
Protocol: "aprs",
|
||||||
|
Raw: data,
|
||||||
|
}:
|
||||||
|
default:
|
||||||
|
proxy.Logger.Warn("aprs-is proxy: raw packet channel full, dropping packet")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func isCommand(line []byte) bool {
|
||||||
|
if len(line) == 0 {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
if line[0] == '#' {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
if i := bytes.IndexByte(line, ' '); i > -1 {
|
||||||
|
switch strings.ToLower(string(line[:i])) {
|
||||||
|
case "user", "filter":
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
@@ -140,7 +140,7 @@ func (o OmniDFStrength) Directivity() float64 {
|
|||||||
// Packet contains an APRS packet.
|
// Packet contains an APRS packet.
|
||||||
type Packet struct {
|
type Packet struct {
|
||||||
// Raw packet (as captured from the air or APRS-IS).
|
// Raw packet (as captured from the air or APRS-IS).
|
||||||
Raw string `json:"raw"`
|
Raw string `json:"-"`
|
||||||
|
|
||||||
// Src is the source address.
|
// Src is the source address.
|
||||||
Src Address `json:"src"`
|
Src Address `json:"src"`
|
||||||
|
|||||||
@@ -1,12 +0,0 @@
|
|||||||
module git.maze.io/go/ham/protocol/meshcore/crypto/jwt
|
|
||||||
|
|
||||||
go 1.25.6
|
|
||||||
|
|
||||||
replace git.maze.io/go/ham => ../../../..
|
|
||||||
|
|
||||||
require (
|
|
||||||
git.maze.io/go/ham v0.0.0-20260214171233-1d56998dd300
|
|
||||||
github.com/golang-jwt/jwt/v5 v5.3.1
|
|
||||||
)
|
|
||||||
|
|
||||||
require filippo.io/edwards25519 v1.1.0 // indirect
|
|
||||||
@@ -1,4 +0,0 @@
|
|||||||
filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
|
|
||||||
filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
|
|
||||||
github.com/golang-jwt/jwt/v5 v5.3.1 h1:kYf81DTWFe7t+1VvL7eS+jKFVWaUnK9cB1qbwn63YCY=
|
|
||||||
github.com/golang-jwt/jwt/v5 v5.3.1/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE=
|
|
||||||
@@ -10,6 +10,7 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
|
SigningMethod = new(SigningMethodEd25519)
|
||||||
jwt.RegisterSigningMethod(SigningMethod.Alg(), func() jwt.SigningMethod {
|
jwt.RegisterSigningMethod(SigningMethod.Alg(), func() jwt.SigningMethod {
|
||||||
return SigningMethod
|
return SigningMethod
|
||||||
})
|
})
|
||||||
@@ -17,10 +18,6 @@ func init() {
|
|||||||
|
|
||||||
var SigningMethod jwt.SigningMethod
|
var SigningMethod jwt.SigningMethod
|
||||||
|
|
||||||
func init() {
|
|
||||||
SigningMethod = new(SigningMethodEd25519)
|
|
||||||
}
|
|
||||||
|
|
||||||
type SigningMethodEd25519 struct{}
|
type SigningMethodEd25519 struct{}
|
||||||
|
|
||||||
func (m *SigningMethodEd25519) Alg() string {
|
func (m *SigningMethodEd25519) Alg() string {
|
||||||
|
|||||||
141
protocol/meshcore/crypto/x25519.go
Normal file
141
protocol/meshcore/crypto/x25519.go
Normal file
@@ -0,0 +1,141 @@
|
|||||||
|
package crypto
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/aes"
|
||||||
|
"crypto/hmac"
|
||||||
|
"crypto/sha256"
|
||||||
|
"encoding/binary"
|
||||||
|
"encoding/hex"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"golang.org/x/crypto/curve25519"
|
||||||
|
)
|
||||||
|
|
||||||
|
type SharedSecret struct {
|
||||||
|
aBytes [32]byte
|
||||||
|
}
|
||||||
|
|
||||||
|
func MakeSharedSecret(key []byte) SharedSecret {
|
||||||
|
var secret SharedSecret
|
||||||
|
copy(secret.aBytes[:], key)
|
||||||
|
return secret
|
||||||
|
}
|
||||||
|
|
||||||
|
func MakeSharedSecretFromGroupSecret(group []byte) SharedSecret {
|
||||||
|
var secret SharedSecret
|
||||||
|
copy(secret.aBytes[:16], group)
|
||||||
|
return secret
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ss *SharedSecret) HMAC(message []byte) uint16 {
|
||||||
|
h := hmac.New(sha256.New, ss.aBytes[:])
|
||||||
|
h.Write(message)
|
||||||
|
r := h.Sum(nil)
|
||||||
|
return binary.BigEndian.Uint16(r[:2])
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ss *SharedSecret) key() []byte {
|
||||||
|
return ss.aBytes[:aes.BlockSize]
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ss *SharedSecret) Decrypt(text []byte) ([]byte, error) {
|
||||||
|
block, err := aes.NewCipher(ss.key())
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
length := len(text)
|
||||||
|
message := make([]byte, length)
|
||||||
|
copy(message, text)
|
||||||
|
|
||||||
|
const chunkSize = aes.BlockSize
|
||||||
|
remain := length % chunkSize
|
||||||
|
|
||||||
|
if remain > 0 {
|
||||||
|
padding := chunkSize - remain
|
||||||
|
message = append(message, make([]byte, padding)...)
|
||||||
|
}
|
||||||
|
|
||||||
|
for i, l := 0, len(message); i < l; i += chunkSize {
|
||||||
|
block.Decrypt(message[i:i+chunkSize], message[i:i+chunkSize])
|
||||||
|
}
|
||||||
|
|
||||||
|
return message[:length], nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ss *SharedSecret) MACThenDecrypt(text []byte, mac uint16) ([]byte, error) {
|
||||||
|
if our := ss.HMAC(text); our != mac {
|
||||||
|
return nil, fmt.Errorf("expected MAC %04X, got %04X", mac, our)
|
||||||
|
}
|
||||||
|
return ss.Decrypt(text)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ss *SharedSecret) Encrypt(message []byte) ([]byte, error) {
|
||||||
|
block, err := aes.NewCipher(ss.key())
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
text := make([]byte, len(message))
|
||||||
|
copy(text, message)
|
||||||
|
|
||||||
|
const chunkSize = aes.BlockSize
|
||||||
|
remain := len(text) % chunkSize
|
||||||
|
|
||||||
|
if remain > 0 {
|
||||||
|
padding := chunkSize - remain
|
||||||
|
text = append(text, make([]byte, padding)...)
|
||||||
|
}
|
||||||
|
|
||||||
|
for i, l := 0, len(text); i < l; i += chunkSize {
|
||||||
|
block.Encrypt(text[i:i+chunkSize], text[i:i+chunkSize])
|
||||||
|
}
|
||||||
|
|
||||||
|
return text, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ss *SharedSecret) EncryptThenMAC(message []byte) (uint16, []byte, error) {
|
||||||
|
text, err := ss.Encrypt(message)
|
||||||
|
if err != nil {
|
||||||
|
return 0, nil, err
|
||||||
|
}
|
||||||
|
return ss.HMAC(text), text, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type StaticSecret [32]byte
|
||||||
|
|
||||||
|
func (ss StaticSecret) PublicKey() (*PublicKey, error) {
|
||||||
|
pub, err := curve25519.X25519(ss[:], curve25519.Basepoint)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return NewPublicKey(pub)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ss StaticSecret) DiffieHellman(other *PublicKey) (SharedSecret, error) {
|
||||||
|
shared, err := curve25519.X25519(ss[:], other.Bytes())
|
||||||
|
if err != nil {
|
||||||
|
return SharedSecret{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var montgomery SharedSecret
|
||||||
|
copy(montgomery.aBytes[:], shared)
|
||||||
|
return montgomery, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type Signature [64]byte
|
||||||
|
|
||||||
|
func (sig Signature) MarshalJSON() ([]byte, error) {
|
||||||
|
s := hex.EncodeToString(sig[:])
|
||||||
|
return json.Marshal(s)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (sig *Signature) UnmarshalJSON(b []byte) error {
|
||||||
|
var s string
|
||||||
|
if err := json.Unmarshal(b, &s); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
copy((*sig)[:], []byte(s))
|
||||||
|
return nil
|
||||||
|
}
|
||||||
75
protocol/meshcore/identity.go
Normal file
75
protocol/meshcore/identity.go
Normal file
@@ -0,0 +1,75 @@
|
|||||||
|
package meshcore
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/sha256"
|
||||||
|
"encoding/hex"
|
||||||
|
"encoding/json"
|
||||||
|
|
||||||
|
"git.maze.io/go/ham/protocol/meshcore/crypto"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Identity struct {
|
||||||
|
Name string
|
||||||
|
PrivateKey *crypto.PrivateKey
|
||||||
|
}
|
||||||
|
|
||||||
|
func (id *Identity) Hash() uint8 {
|
||||||
|
return id.PrivateKey.PublicKey()[0]
|
||||||
|
}
|
||||||
|
|
||||||
|
type Contact struct {
|
||||||
|
Name string
|
||||||
|
PublicKey *crypto.PublicKey
|
||||||
|
}
|
||||||
|
|
||||||
|
func (contact *Contact) Hash() uint8 {
|
||||||
|
return contact.PublicKey.Bytes()[0]
|
||||||
|
}
|
||||||
|
|
||||||
|
type Group struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
Hash [32]byte `json:"hash"`
|
||||||
|
Secret crypto.SharedSecret `json:"-"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (group Group) ChannelHash() uint8 {
|
||||||
|
return group.Hash[0]
|
||||||
|
}
|
||||||
|
|
||||||
|
func (group *Group) MarshalJSON() ([]byte, error) {
|
||||||
|
return json.Marshal(map[string]string{
|
||||||
|
"name": group.Name,
|
||||||
|
"hash": hex.EncodeToString(group.Hash[:]),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (group *Group) UnmarshalJSON(b []byte) error {
|
||||||
|
var kv = make(map[string]string)
|
||||||
|
if err := json.Unmarshal(b, &kv); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
group.Name = kv["name"]
|
||||||
|
h, err := hex.DecodeString(kv["hash"])
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
copy(group.Hash[:], h)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func PublicGroup(name string) *Group {
|
||||||
|
h := sha256.Sum256([]byte(name))
|
||||||
|
return &Group{
|
||||||
|
Name: name,
|
||||||
|
Hash: sha256.Sum256(h[:16]),
|
||||||
|
Secret: crypto.MakeSharedSecretFromGroupSecret(h[:16]),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func SecretGroup(name string, key []byte) *Group {
|
||||||
|
return &Group{
|
||||||
|
Name: name,
|
||||||
|
Hash: sha256.Sum256(key),
|
||||||
|
Secret: crypto.MakeSharedSecret(key),
|
||||||
|
}
|
||||||
|
}
|
||||||
341
protocol/meshcore/node.go
Normal file
341
protocol/meshcore/node.go
Normal file
@@ -0,0 +1,341 @@
|
|||||||
|
package meshcore
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/binary"
|
||||||
|
"encoding/hex"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"log"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
|
||||||
|
"git.maze.io/go/ham/protocol"
|
||||||
|
"git.maze.io/go/ham/protocol/meshcore/crypto"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
maxCompanionFrameSize = 172
|
||||||
|
)
|
||||||
|
|
||||||
|
type Node struct {
|
||||||
|
OnPacket (*Packet)
|
||||||
|
|
||||||
|
driver nodeDriver
|
||||||
|
}
|
||||||
|
|
||||||
|
type NodeInfo struct {
|
||||||
|
Manufacturer string `json:"manufacturer"`
|
||||||
|
FirmwareVersion string `json:"firmware_version"`
|
||||||
|
Type NodeType `json:"node_type"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
Power uint8 `json:"power"`
|
||||||
|
MaxPower uint8 `json:"max_power"`
|
||||||
|
PublicKey *crypto.PublicKey `json:"public_key"`
|
||||||
|
Position *Position `json:"position"`
|
||||||
|
Frequency float64 `json:"frequency"` // in MHz
|
||||||
|
Bandwidth float64 `json:"bandwidth"` // in kHz
|
||||||
|
SpreadingFactor uint8 `json:"sf"`
|
||||||
|
CodingRate uint8 `json:"cr"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewCompanion connects to a companion device type (over serial, TCP or BLE).
|
||||||
|
func NewCompanion(conn io.ReadWriteCloser) (*Node, error) {
|
||||||
|
driver := newCompanionDriver(conn)
|
||||||
|
|
||||||
|
if err := driver.Setup(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &Node{
|
||||||
|
driver: driver,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (dev *Node) Close() error {
|
||||||
|
return dev.driver.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (dev *Node) Packets() <-chan *Packet {
|
||||||
|
return dev.driver.Packets()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (dev *Node) RawPackets() <-chan *protocol.Packet {
|
||||||
|
return dev.driver.RawPackets()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (dev *Node) Info() *NodeInfo {
|
||||||
|
return dev.driver.Info()
|
||||||
|
}
|
||||||
|
|
||||||
|
type nodeDriver interface {
|
||||||
|
Setup() error
|
||||||
|
|
||||||
|
Close() error
|
||||||
|
|
||||||
|
Packets() <-chan *Packet
|
||||||
|
RawPackets() <-chan *protocol.Packet
|
||||||
|
|
||||||
|
Info() *NodeInfo
|
||||||
|
}
|
||||||
|
|
||||||
|
type CompanionError struct {
|
||||||
|
Code byte
|
||||||
|
}
|
||||||
|
|
||||||
|
func (err CompanionError) Error() string {
|
||||||
|
switch err.Code {
|
||||||
|
case companionErrCodeUnsupported:
|
||||||
|
return "meshcore: companion: unsupported"
|
||||||
|
case companionErrCodeNotFound:
|
||||||
|
return "meshcore: companion: not found"
|
||||||
|
case companionErrCodeTableFull:
|
||||||
|
return "meshcore: companion: table full"
|
||||||
|
case companionErrCodeBadState:
|
||||||
|
return "meshcore: companion: bad state"
|
||||||
|
case companionErrCodeFileIOError:
|
||||||
|
return "meshcore: companion: file input/output error"
|
||||||
|
case companionErrCodeIllegalArgument:
|
||||||
|
return "meshcore: companion: illegal argument"
|
||||||
|
default:
|
||||||
|
return fmt.Sprintf("meshcore: companion: unknown error code %#02x", err.Code)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type companionDriver struct {
|
||||||
|
conn io.ReadWriteCloser
|
||||||
|
mu sync.Mutex
|
||||||
|
packets chan *Packet
|
||||||
|
rawPackets chan *protocol.Packet
|
||||||
|
info NodeInfo
|
||||||
|
}
|
||||||
|
|
||||||
|
func newCompanionDriver(conn io.ReadWriteCloser) *companionDriver {
|
||||||
|
return &companionDriver{
|
||||||
|
conn: conn,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (drv *companionDriver) Close() error {
|
||||||
|
return drv.conn.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (drv *companionDriver) Setup() (err error) {
|
||||||
|
if err = drv.sendAppStart(); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err = drv.sendDeviceInfo(); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
go drv.poll()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (drv *companionDriver) Packets() <-chan *Packet {
|
||||||
|
if drv.packets == nil {
|
||||||
|
drv.packets = make(chan *Packet, 16)
|
||||||
|
}
|
||||||
|
return drv.packets
|
||||||
|
}
|
||||||
|
|
||||||
|
func (drv *companionDriver) RawPackets() <-chan *protocol.Packet {
|
||||||
|
if drv.rawPackets == nil {
|
||||||
|
drv.rawPackets = make(chan *protocol.Packet, 16)
|
||||||
|
}
|
||||||
|
return drv.rawPackets
|
||||||
|
}
|
||||||
|
|
||||||
|
func (drv *companionDriver) Info() *NodeInfo {
|
||||||
|
return &drv.info
|
||||||
|
}
|
||||||
|
|
||||||
|
func (drv *companionDriver) readFrame() ([]byte, error) {
|
||||||
|
var frame [3 + maxCompanionFrameSize]byte
|
||||||
|
for {
|
||||||
|
n, err := drv.conn.Read(frame[:])
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
} else if n < 3 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if frame[0] != '>' {
|
||||||
|
// not a companion frame
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
size := int(binary.LittleEndian.Uint16(frame[1:]))
|
||||||
|
if size > maxCompanionFrameSize {
|
||||||
|
return nil, fmt.Errorf("meshcore: companion sent frame size of %d, which exceeds maximum of %d", size, maxCompanionFrameSize)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Make sure we have read all bytes
|
||||||
|
o := n
|
||||||
|
for (o - 3) < size {
|
||||||
|
if n, err = drv.conn.Read(frame[o:]); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
o += n
|
||||||
|
}
|
||||||
|
|
||||||
|
//log.Printf("read %d:\n%s", size, hex.Dump(frame[:3+size]))
|
||||||
|
return frame[3 : 3+size], nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (drv *companionDriver) writeFrame(b []byte) (err error) {
|
||||||
|
if len(b) > maxCompanionFrameSize {
|
||||||
|
return fmt.Errorf("meshcore: companion: frame size %d exceed maximum of %d", len(b), maxCompanionFrameSize)
|
||||||
|
}
|
||||||
|
|
||||||
|
var frame [3 + maxCompanionFrameSize]byte
|
||||||
|
frame[0] = '<'
|
||||||
|
binary.LittleEndian.PutUint16(frame[1:], uint16(len(b)))
|
||||||
|
n := copy(frame[3:], b)
|
||||||
|
|
||||||
|
//log.Printf("send %d:\n%s", n, hex.Dump(frame[:3+n]))
|
||||||
|
_, err = drv.conn.Write(frame[:3+n])
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (drv *companionDriver) writeCommand(cmd byte, args []byte, wait ...byte) ([]byte, error) {
|
||||||
|
drv.mu.Lock()
|
||||||
|
defer drv.mu.Unlock()
|
||||||
|
|
||||||
|
if err := drv.writeFrame(append([]byte{cmd}, args...)); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return drv.wait(wait...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (drv *companionDriver) wait(wait ...byte) ([]byte, error) {
|
||||||
|
for {
|
||||||
|
b, err := drv.readFrame()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if len(b) < 1 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
switch {
|
||||||
|
case b[0] == companionResponseError:
|
||||||
|
return nil, CompanionError{Code: b[1]}
|
||||||
|
|
||||||
|
case b[0] >= 0x80:
|
||||||
|
drv.handlePushFrame(b)
|
||||||
|
continue
|
||||||
|
|
||||||
|
case bytes.Contains(wait, b[:1]):
|
||||||
|
return b, nil
|
||||||
|
|
||||||
|
case wait == nil:
|
||||||
|
return b, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (drv *companionDriver) handlePushFrame(b []byte) {
|
||||||
|
switch b[0] {
|
||||||
|
case companionPushAdvert:
|
||||||
|
case companionPushMessageWaiting:
|
||||||
|
case companionPushLogRXData:
|
||||||
|
drv.handleRXData(b[1:])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (drv *companionDriver) handleRXData(b []byte) {
|
||||||
|
if len(b) < 2+minPacketSize {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if drv.packets == nil {
|
||||||
|
return // not listening for packets, discard
|
||||||
|
}
|
||||||
|
|
||||||
|
packet := new(Packet)
|
||||||
|
if err := packet.UnmarshalBytes(b[2:]); err == nil {
|
||||||
|
packet.SNR = float64(b[0]) / 4
|
||||||
|
packet.RSSI = int8(b[1])
|
||||||
|
select {
|
||||||
|
case drv.packets <- packet:
|
||||||
|
default:
|
||||||
|
log.Printf("meshcore: packet channel full, dropping packet")
|
||||||
|
}
|
||||||
|
if drv.rawPackets != nil {
|
||||||
|
select {
|
||||||
|
case drv.rawPackets <- &protocol.Packet{
|
||||||
|
Protocol: "meshcore",
|
||||||
|
SNR: packet.SNR,
|
||||||
|
RSSI: packet.RSSI,
|
||||||
|
Raw: packet.Raw,
|
||||||
|
}:
|
||||||
|
default:
|
||||||
|
log.Printf("meshcore: raw packet channel full, dropping packet")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (drv *companionDriver) sendAppStart() (err error) {
|
||||||
|
var b []byte
|
||||||
|
if b, err = drv.writeCommand(companionAppStart, append(make([]byte, 8), []byte("git.maze.io/go/ham")...), companionResponseSelfInfo); err != nil {
|
||||||
|
return fmt.Errorf("meshcore: can't send application start: %v", err)
|
||||||
|
}
|
||||||
|
log.Printf("companion app start response:\n%s", hex.Dump(b))
|
||||||
|
|
||||||
|
const expect = 1 + 1 + 1 + 1 + 32 + 4 + 4 + 1 + 1 + 1 + 1 + 4 + 4 + 1 + 1
|
||||||
|
if len(b) < expect {
|
||||||
|
return fmt.Errorf("companion: expected %d bytes of self info, got %d", expect, len(b))
|
||||||
|
}
|
||||||
|
if b[0] != companionResponseSelfInfo {
|
||||||
|
return fmt.Errorf("companion: expected self info response, got %#02x", b[0])
|
||||||
|
}
|
||||||
|
b = b[1:]
|
||||||
|
|
||||||
|
drv.info.Type = NodeType(b[0])
|
||||||
|
drv.info.Power = b[1]
|
||||||
|
drv.info.MaxPower = b[2]
|
||||||
|
drv.info.PublicKey, _ = crypto.NewPublicKey(b[3 : 3+crypto.PublicKeySize])
|
||||||
|
drv.info.Position = new(Position)
|
||||||
|
drv.info.Position.Unmarshal(b[35:])
|
||||||
|
//drv.info.HasMultiACKs = b[43] != 0
|
||||||
|
//drv.info.AdvertLocationPolicy = b[44]
|
||||||
|
//drv.info.TelemetryFlags = b[45]
|
||||||
|
//drv.info.ManualAddContacts = b[46]
|
||||||
|
drv.info.Frequency = decodeFrequency(b[47:])
|
||||||
|
drv.info.Bandwidth = decodeFrequency(b[51:])
|
||||||
|
drv.info.SpreadingFactor = b[55]
|
||||||
|
drv.info.CodingRate = b[56]
|
||||||
|
drv.info.Name = strings.TrimRight(string(b[57:]), "\x00")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (drv *companionDriver) sendDeviceInfo() (err error) {
|
||||||
|
var b []byte
|
||||||
|
if b, err = drv.writeCommand(companionDeviceQuery, []byte{0x03}, companionResponseDeviceInfo); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const expect = 4 + 4 + 12 + 40 + 20
|
||||||
|
if len(b) < expect {
|
||||||
|
return fmt.Errorf("companion: expected %d bytes of self info, got %d", expect, len(b))
|
||||||
|
}
|
||||||
|
if b[0] != companionResponseDeviceInfo {
|
||||||
|
return fmt.Errorf("companion: expected device info response, got %#02x", b[0])
|
||||||
|
}
|
||||||
|
b = b[1:]
|
||||||
|
|
||||||
|
drv.info.Manufacturer = decodeCString(b[19:59])
|
||||||
|
drv.info.FirmwareVersion = decodeCString(b[59:79])
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (drv *companionDriver) poll() {
|
||||||
|
for {
|
||||||
|
if _, err := drv.wait(); err != nil {
|
||||||
|
log.Printf("meshcore: companion %s fatal error: %v", drv.info.Name, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
124
protocol/meshcore/node_const.go
Normal file
124
protocol/meshcore/node_const.go
Normal file
@@ -0,0 +1,124 @@
|
|||||||
|
package meshcore
|
||||||
|
|
||||||
|
const (
|
||||||
|
companionErrCodeUnsupported byte = 1 + iota
|
||||||
|
companionErrCodeNotFound
|
||||||
|
companionErrCodeTableFull
|
||||||
|
companionErrCodeBadState
|
||||||
|
companionErrCodeFileIOError
|
||||||
|
companionErrCodeIllegalArgument
|
||||||
|
)
|
||||||
|
|
||||||
|
// companion command bytes
|
||||||
|
const (
|
||||||
|
companionAppStart byte = 1 + iota //
|
||||||
|
companionSendTextMessage //
|
||||||
|
companionSendChannelTextMessage //
|
||||||
|
companionGetContacts // with optional 'since' (for efficient sync)
|
||||||
|
companionGetDeviceTime //
|
||||||
|
companionSetDeviceTime //
|
||||||
|
companionSendSelfAdvert //
|
||||||
|
companionSetAdvertName //
|
||||||
|
companionAddUpdateContact //
|
||||||
|
companionSyncMessages //
|
||||||
|
companionSetRadioParams //
|
||||||
|
companionSetRadioTXPower //
|
||||||
|
companionResetPath //
|
||||||
|
companionSetAdvertLatLon //
|
||||||
|
companionRemoveContact //
|
||||||
|
companionShareContact //
|
||||||
|
companionExportContact //
|
||||||
|
companionImportContact //
|
||||||
|
companionReboot //
|
||||||
|
companionGetBatteryAndStorage // was CMD_GetBATTERY_VOLTAGE
|
||||||
|
companionSetTuningParams //
|
||||||
|
companionDeviceQuery //
|
||||||
|
companionExportPrivateKey //
|
||||||
|
companionImportPrivateKey //
|
||||||
|
companionSendRawData //
|
||||||
|
companionSendLogin //
|
||||||
|
companionSendStatusRequest //
|
||||||
|
companionHasConnection //
|
||||||
|
companionLogout // 'Disconnect'
|
||||||
|
companionGetContactByKey //
|
||||||
|
companionGetChannel //
|
||||||
|
companionSetChannel //
|
||||||
|
companionSignStart //
|
||||||
|
companionSignData //
|
||||||
|
companionSignFinish //
|
||||||
|
companionSendTracePath //
|
||||||
|
companionSetDevicePIN //
|
||||||
|
companionSetOtherParams //
|
||||||
|
companionSendTelemetryRequest //
|
||||||
|
companionGetCustomVars //
|
||||||
|
companionSetCustomVar //
|
||||||
|
companionGetAdvertPath //
|
||||||
|
companionGetTuningParams //
|
||||||
|
_ // parked
|
||||||
|
_ // parked
|
||||||
|
_ // parked
|
||||||
|
_ // parked
|
||||||
|
_ // parked
|
||||||
|
_ // parked
|
||||||
|
companionSendBinaryRequest //
|
||||||
|
companionFactoryReset //
|
||||||
|
companionSendPathDiscoveryRequest //
|
||||||
|
_ // parked
|
||||||
|
companionSetFloodScope // v8+
|
||||||
|
companionSendControlData // v8+
|
||||||
|
companionGetStats // v8+, second byte is stats type
|
||||||
|
companionSendAnonymousRequest //
|
||||||
|
companionSetAutoAddConfig //
|
||||||
|
companionGetAutoAddConfig //
|
||||||
|
)
|
||||||
|
|
||||||
|
// companion response bytes
|
||||||
|
const (
|
||||||
|
companionResponseOK byte = iota
|
||||||
|
companionResponseError
|
||||||
|
companionResponseContactsStart // first reply to CMD_GetCONTACTS
|
||||||
|
companionResponseContact // multiple of these (after CMD_GetCONTACTS)
|
||||||
|
companionResponseEndOfContacts // last reply to CMD_GetCONTACTS
|
||||||
|
companionResponseSelfInfo // reply to CMD_APP_START
|
||||||
|
companionResponseSent // reply to CMD_SEND_TXT_MSG
|
||||||
|
companionResponseContactMessageReceived // a reply to CMD_SYNC_NEXT_MESSAGE (ver < 3)
|
||||||
|
companionResponseChannelMessageReceived // a reply to CMD_SYNC_NEXT_MESSAGE (ver < 3)
|
||||||
|
companionResponseCurrentTime // a reply to CMD_GetDEVICE_TIME
|
||||||
|
companionResponseNoMoreMessages // a reply to CMD_SYNC_NEXT_MESSAGE
|
||||||
|
companionResponseExportContact //
|
||||||
|
companionResponseBatteryAndStorage // a reply to a CMD_GetBATT_AND_STORAGE
|
||||||
|
companionResponseDeviceInfo // a reply to CMD_DEVICE_QEURY
|
||||||
|
companionResponsePrivateKey // a reply to CMD_EXPORT_PRIVATE_KEY
|
||||||
|
companionResponseDisabled //
|
||||||
|
companionResponseContactMessageReceivedV3 // a reply to CMD_SYNC_NEXT_MESSAGE (ver >= 3)
|
||||||
|
companionResponseChannelMessageReceivedV3 // a reply to CMD_SYNC_NEXT_MESSAGE (ver >= 3)
|
||||||
|
companionResponseChannelInfo // a reply to CMD_GetCHANNEL
|
||||||
|
companionResponseSignatureStart //
|
||||||
|
companionResponseSignature //
|
||||||
|
companionResponseCustomVars //
|
||||||
|
companionResponseAdvertPath //
|
||||||
|
companionResponseTuningParams //
|
||||||
|
companionResponseStats // v8+, second byte is stats type
|
||||||
|
companionResponseAutoAddConfig //
|
||||||
|
)
|
||||||
|
|
||||||
|
// companion push code bytes
|
||||||
|
const (
|
||||||
|
companionPushAdvert byte = 0x80 + iota
|
||||||
|
companionPushPathUpdated
|
||||||
|
companionPushSendConfirmed
|
||||||
|
companionPushMessageWaiting
|
||||||
|
companionPushRawData
|
||||||
|
companionPushLoginSuccess
|
||||||
|
companionPushLoginFailure
|
||||||
|
companionPushStatusResponse
|
||||||
|
companionPushLogRXData
|
||||||
|
companionPushTraceData
|
||||||
|
companionPushNewAdvert
|
||||||
|
companionPushTelemetryResponse
|
||||||
|
companionPushBinaryResponse
|
||||||
|
companionPushPathDiscoveryResponse
|
||||||
|
companionPushControlData
|
||||||
|
companionPushContactDeleted
|
||||||
|
companionPushContactsFull
|
||||||
|
)
|
||||||
302
protocol/meshcore/packet.go
Normal file
302
protocol/meshcore/packet.go
Normal file
@@ -0,0 +1,302 @@
|
|||||||
|
package meshcore
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/sha256"
|
||||||
|
"encoding/binary"
|
||||||
|
"encoding/hex"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
minPacketSize = 2
|
||||||
|
maxPathSize = 64
|
||||||
|
maxPayloadSize = 184
|
||||||
|
)
|
||||||
|
|
||||||
|
type Packet struct {
|
||||||
|
// SNR is the signal-to-noise ratio.
|
||||||
|
SNR float64 `json:"snr"`
|
||||||
|
|
||||||
|
// RSSI is the received signal strength indicator (in dBm).
|
||||||
|
RSSI int8 `json:"rssi"`
|
||||||
|
|
||||||
|
// Raw bytes (optional).
|
||||||
|
Raw []byte `json:"raw,omitempty"`
|
||||||
|
|
||||||
|
// RouteType is the type of route for this packet.
|
||||||
|
RouteType RouteType `json:"route_type"`
|
||||||
|
|
||||||
|
// PayloadType is the type of payload for this packet.
|
||||||
|
PayloadType PayloadType `json:"payload_type"`
|
||||||
|
|
||||||
|
// TransportCodes are set by transport route types.
|
||||||
|
TransportCodes []uint16 `json:"transport_codes,omitempty"`
|
||||||
|
|
||||||
|
// Path are repeater hashes.
|
||||||
|
Path []byte `json:"path"`
|
||||||
|
|
||||||
|
// Payload is the raw (encoded) payload.
|
||||||
|
Payload []byte `json:"payload,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func Decode(data []byte) (Payload, error) {
|
||||||
|
packet := new(Packet)
|
||||||
|
if err := packet.UnmarshalBytes(data); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return packet.Decode()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (packet *Packet) Decode() (Payload, error) {
|
||||||
|
var payload Payload
|
||||||
|
switch packet.PayloadType {
|
||||||
|
case TypeRequest:
|
||||||
|
payload = &Request{Raw: packet}
|
||||||
|
case TypeResponse:
|
||||||
|
payload = &Response{Raw: packet}
|
||||||
|
case TypeText:
|
||||||
|
payload = &Text{Raw: packet}
|
||||||
|
case TypeAck:
|
||||||
|
payload = &Acknowledgement{Raw: packet}
|
||||||
|
case TypeAdvert:
|
||||||
|
payload = &Advert{Raw: packet}
|
||||||
|
case TypeGroupText:
|
||||||
|
payload = &GroupText{Raw: packet}
|
||||||
|
case TypeGroupData:
|
||||||
|
payload = &GroupData{Raw: packet}
|
||||||
|
case TypeAnonRequest:
|
||||||
|
payload = &AnonymousRequest{Raw: packet}
|
||||||
|
case TypePath:
|
||||||
|
payload = &Path{Raw: packet}
|
||||||
|
case TypeTrace:
|
||||||
|
payload = &Trace{Raw: packet}
|
||||||
|
case TypeMultipart:
|
||||||
|
payload = &Multipart{Raw: packet}
|
||||||
|
case TypeControl:
|
||||||
|
payload = &Control{Raw: packet}
|
||||||
|
case TypeRawCustom:
|
||||||
|
payload = &RawCustom{Raw: packet}
|
||||||
|
default:
|
||||||
|
return nil, fmt.Errorf("meshcore: invalid payload type %#02x", packet.PayloadType)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := payload.Unmarshal(packet.Payload); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return payload, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (packet *Packet) String() string {
|
||||||
|
s := []string{
|
||||||
|
packet.RouteType.String(),
|
||||||
|
packet.PayloadType.String(),
|
||||||
|
}
|
||||||
|
if len(packet.TransportCodes) == 2 {
|
||||||
|
s = append(s, fmt.Sprintf("%02X%02X", packet.TransportCodes[0], packet.TransportCodes[1]))
|
||||||
|
}
|
||||||
|
if len(packet.Path) > 0 {
|
||||||
|
s = append(s, formatPath(packet.Path))
|
||||||
|
} else {
|
||||||
|
s = append(s, "direct")
|
||||||
|
}
|
||||||
|
return strings.Join(s, " ")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (packet *Packet) MarshalBytes() []byte {
|
||||||
|
var (
|
||||||
|
data [1 + 4 + 1 + maxPathSize + maxPayloadSize]byte
|
||||||
|
offset int
|
||||||
|
)
|
||||||
|
data[offset] = byte(packet.RouteType&0x03) | byte((packet.PayloadType&0x0f)<<2)
|
||||||
|
offset += 1
|
||||||
|
|
||||||
|
if packet.RouteType.HasTransportCodes() {
|
||||||
|
binary.LittleEndian.PutUint16(data[offset:], packet.TransportCodes[0])
|
||||||
|
offset += 2
|
||||||
|
binary.LittleEndian.PutUint16(data[offset:], packet.TransportCodes[1])
|
||||||
|
offset += 2
|
||||||
|
}
|
||||||
|
|
||||||
|
data[offset] = byte(len(packet.Path))
|
||||||
|
offset += 1
|
||||||
|
offset += copy(data[offset:], packet.Path)
|
||||||
|
offset += copy(data[offset:], packet.Payload)
|
||||||
|
return data[:offset]
|
||||||
|
}
|
||||||
|
|
||||||
|
func (packet *Packet) UnmarshalBytes(data []byte) error {
|
||||||
|
if len(data) < minPacketSize {
|
||||||
|
return io.ErrUnexpectedEOF
|
||||||
|
}
|
||||||
|
|
||||||
|
packet.Raw = make([]byte, len(data))
|
||||||
|
copy(packet.Raw, data)
|
||||||
|
|
||||||
|
packet.RouteType = RouteType(data[0] & 0x03)
|
||||||
|
packet.PayloadType = PayloadType((data[0] >> 2) & 0x0f)
|
||||||
|
offset := 1
|
||||||
|
|
||||||
|
if packet.RouteType.HasTransportCodes() {
|
||||||
|
if len(data) < minPacketSize+4 {
|
||||||
|
return io.ErrUnexpectedEOF
|
||||||
|
}
|
||||||
|
|
||||||
|
packet.TransportCodes = []uint16{
|
||||||
|
binary.LittleEndian.Uint16(data[offset+0:]),
|
||||||
|
binary.LittleEndian.Uint16(data[offset+2:]),
|
||||||
|
}
|
||||||
|
offset += 4
|
||||||
|
} else {
|
||||||
|
packet.TransportCodes = nil
|
||||||
|
}
|
||||||
|
|
||||||
|
pathLength := int(data[offset])
|
||||||
|
offset += 1
|
||||||
|
if pathLength > maxPathSize {
|
||||||
|
return fmt.Errorf("meshcore: path length %d exceeds maximum of %d", pathLength, maxPathSize)
|
||||||
|
} else if pathLength > len(data[offset:]) {
|
||||||
|
return io.ErrUnexpectedEOF
|
||||||
|
}
|
||||||
|
packet.Path = make([]byte, pathLength)
|
||||||
|
offset += copy(packet.Path, data[offset:])
|
||||||
|
|
||||||
|
payloadLength := len(data[offset:])
|
||||||
|
if payloadLength > maxPayloadSize {
|
||||||
|
return fmt.Errorf("meshcore: payload length %d exceeds maximum of %d", payloadLength, maxPayloadSize)
|
||||||
|
}
|
||||||
|
packet.Payload = make([]byte, payloadLength)
|
||||||
|
copy(packet.Payload, data[offset:])
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (packet *Packet) Hash() []byte {
|
||||||
|
h := sha256.New()
|
||||||
|
h.Write([]byte{byte(packet.PayloadType)})
|
||||||
|
if packet.PayloadType == TypeTrace {
|
||||||
|
h.Write(packet.Path)
|
||||||
|
}
|
||||||
|
h.Write(packet.Payload)
|
||||||
|
return h.Sum(nil)[:8]
|
||||||
|
}
|
||||||
|
|
||||||
|
func (packet *Packet) MarshalJSON() ([]byte, error) {
|
||||||
|
return json.Marshal(struct {
|
||||||
|
SNR float64 `json:"snr"`
|
||||||
|
RSSI int8 `json:"rssi"`
|
||||||
|
RouteType RouteType `json:"route_type"`
|
||||||
|
PayloadType PayloadType `json:"payload_type"`
|
||||||
|
TransportCodes []uint16 `json:"transport_codes,omitempty"`
|
||||||
|
Path string `json:"path"`
|
||||||
|
Payload string `json:"payload,omitempty"`
|
||||||
|
Hash string `json:"hash"`
|
||||||
|
}{
|
||||||
|
packet.SNR,
|
||||||
|
packet.RSSI,
|
||||||
|
packet.RouteType,
|
||||||
|
packet.PayloadType,
|
||||||
|
packet.TransportCodes,
|
||||||
|
hex.EncodeToString(packet.Path),
|
||||||
|
hex.EncodeToString(packet.Payload),
|
||||||
|
hex.EncodeToString(packet.Hash()),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
type RouteType byte
|
||||||
|
|
||||||
|
// Route types.
|
||||||
|
const (
|
||||||
|
TransportFlood RouteType = iota
|
||||||
|
Flood
|
||||||
|
Direct
|
||||||
|
TransportDirect
|
||||||
|
)
|
||||||
|
|
||||||
|
// HasTransportCodes indicates if this route type has transport codes in the packet.
|
||||||
|
func (rt RouteType) HasTransportCodes() bool {
|
||||||
|
return rt == TransportFlood || rt == TransportDirect
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsDirect is a direct routing type.
|
||||||
|
func (rt RouteType) IsDirect() bool {
|
||||||
|
return rt == Direct || rt == TransportDirect
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsFlood is a flood routing type.
|
||||||
|
func (rt RouteType) IsFlood() bool {
|
||||||
|
return rt == Flood || rt == TransportFlood
|
||||||
|
}
|
||||||
|
|
||||||
|
func (rt RouteType) String() string {
|
||||||
|
switch rt {
|
||||||
|
case TransportFlood:
|
||||||
|
return "F[T]"
|
||||||
|
case Flood:
|
||||||
|
return "F"
|
||||||
|
case Direct:
|
||||||
|
return "D"
|
||||||
|
case TransportDirect:
|
||||||
|
return "D[T]"
|
||||||
|
default:
|
||||||
|
return "?"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type PayloadType byte
|
||||||
|
|
||||||
|
// Payload types.
|
||||||
|
const (
|
||||||
|
TypeRequest PayloadType = iota
|
||||||
|
TypeResponse
|
||||||
|
TypeText
|
||||||
|
TypeAck
|
||||||
|
TypeAdvert
|
||||||
|
TypeGroupText
|
||||||
|
TypeGroupData
|
||||||
|
TypeAnonRequest
|
||||||
|
TypePath
|
||||||
|
TypeTrace
|
||||||
|
TypeMultipart
|
||||||
|
TypeControl
|
||||||
|
_ // reserved
|
||||||
|
_ // reserved
|
||||||
|
_ // reserved
|
||||||
|
TypeRawCustom
|
||||||
|
)
|
||||||
|
|
||||||
|
func (pt PayloadType) String() string {
|
||||||
|
switch pt {
|
||||||
|
case TypeRequest:
|
||||||
|
return "request"
|
||||||
|
case TypeResponse:
|
||||||
|
return "response"
|
||||||
|
case TypeText:
|
||||||
|
return "text"
|
||||||
|
case TypeAck:
|
||||||
|
return "ack"
|
||||||
|
case TypeAdvert:
|
||||||
|
return "advert"
|
||||||
|
case TypeGroupText:
|
||||||
|
return "group text"
|
||||||
|
case TypeGroupData:
|
||||||
|
return "group data"
|
||||||
|
case TypeAnonRequest:
|
||||||
|
return "anon request"
|
||||||
|
case TypePath:
|
||||||
|
return "path"
|
||||||
|
case TypeTrace:
|
||||||
|
return "trace"
|
||||||
|
case TypeMultipart:
|
||||||
|
return "multipart"
|
||||||
|
case TypeControl:
|
||||||
|
return "control"
|
||||||
|
case TypeRawCustom:
|
||||||
|
return "raw custom"
|
||||||
|
default:
|
||||||
|
return fmt.Sprintf("invalid %02x", byte(pt))
|
||||||
|
}
|
||||||
|
}
|
||||||
107
protocol/meshcore/packet_test.go
Normal file
107
protocol/meshcore/packet_test.go
Normal file
@@ -0,0 +1,107 @@
|
|||||||
|
package meshcore
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/hex"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestDecode(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
Name string
|
||||||
|
Data []byte
|
||||||
|
Payload Payload
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
"request/direct",
|
||||||
|
mustHexDecode("02000F6950F496ACE4B62F77C1CB31FF24E2FF27AAE9"),
|
||||||
|
&Request{},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"request/flood",
|
||||||
|
mustHexDecode("01152E4A72D025293EA6C0F2897D45FECC0162331DBFC5B903A2DE5FF12F012505AA12BA64258639F20EC3"),
|
||||||
|
&Request{},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"response",
|
||||||
|
mustHexDecode("05064CE383CC2BFEB1ADC8757CAB3B1F05AC4019AA77CF860278F7FD"),
|
||||||
|
&Response{},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"text",
|
||||||
|
mustHexDecode("092C1D9FF5EAE2A69AAAEBCE27F5317E1FB64FEC3E985EDFD27ACC905A5B48BADAFECCCC6AB8662CFAD9D6724E26667F5FC2B7B4D7DC7BAE8F08DE13AD9C42188249"),
|
||||||
|
&Text{},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ack",
|
||||||
|
mustHexDecode("0D0152ED9C3667"),
|
||||||
|
&Acknowledgement{},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"advert/room",
|
||||||
|
mustHexDecode("110178DE95A0BB204F245F70BD08DEF93BAA4AEB9C4AD3DE3D0A81ECF888BDF830857D9BC96266A1CDDBA8F0EFF4DE1D59390C17BCBA0ABF4B452A0E24568E8BA18606A771553B095B1AA1F249E9CA22ECE6A1FB9494D00ABB31E0566D6E96A762D8F285D3C40393000000000000000030373120526F6F6D"),
|
||||||
|
&Advert{},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"advert/repeater",
|
||||||
|
mustHexDecode("11003E4E39B98F8923DCC1E1A4C65908C9C5002CF2A8CC837F4F8B1F5A62BAE9759C20BE4466E081F73083C86AF7F7F7BBA227A9176DB7B80C45DC9454F61A799C90F9B06251F6D79F07C26D5A34A2669577CE6B93CC2D02FF20A04D99AEE5E2AD89C2CF690D929EED0F03933F5E004E4C2D564E4C2D434E54522D52503031"),
|
||||||
|
&Advert{},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"grouptext",
|
||||||
|
mustHexDecode("15011C113EF72DDDBF0FAB26DF1AB902D8062FD94AF9CF337ACB8634E55105B9D77BE8E87BB3AB67CFA4E01044B7AC5EB8B510BD1B6CA395FC991CAFB6D338CDE8599DB45360"),
|
||||||
|
&GroupText{},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"groupdata",
|
||||||
|
mustHexDecode("1940EE145428BE90740D2B92838D171164ED40286C45EB40E1390323628D0422A98FA752108D5E7361A4D8B55D98E6EF7B7D6BB6289745C3288D92E09326274911544D9B376EDCF9FCE418B34E38806DB9F402649A683BA4633AA2602E13A455408E9E00CCAD3A2284BA2B7CEAF7EDDBB76FDE5C63D34A5D14C72C8F6ED6BAB2ADAB9BA033D83B"),
|
||||||
|
&GroupData{},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"anonrequest",
|
||||||
|
mustHexDecode("1D094379CC88D0E9451C457325C3AC56D53937E22FA358D0AF64562BC038ABF99ABC2911595A13A3748573DCC0B799BEE196939DDDAE210D4D979C665FF2"),
|
||||||
|
&AnonymousRequest{},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path",
|
||||||
|
mustHexDecode("210720CCBED9FE20431125CC1EEA3480E9B06064A5F3B21E890BF91653"),
|
||||||
|
&Path{},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"trace",
|
||||||
|
mustHexDecode("E50DE8626B887827CCE9FE43CCCDFEB3EA419F807FD2F7838AA6D3B98A8F7490BA37F6"),
|
||||||
|
&Trace{},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"control",
|
||||||
|
mustHexDecode("AD17348E1E5B0FB88C22E948BE3A835161548C561DFBB1C91AC382C06746F6569B2D3911A749A57ACC"),
|
||||||
|
&Control{},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"multipart",
|
||||||
|
mustHexDecode("2A04A0BC07E0B2157ECCBA9E2A04A0BCC35A61DE61F43B18E1283721A28871ABD4B00AAEC753B8BEB4141DF364F71E0073E80F5F94B61FA046DA3B35EDF37DFC43F0A0E7"),
|
||||||
|
&Multipart{},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"raw",
|
||||||
|
mustHexDecode("7D0A83F2A5A7441F88D10E3F7CEA3B62D383C7C5DA58F5613057EA12730F543CC38E9B3CA5EAB84621714B0E56F8B3A7D51B83108B9E942F3C7C0A6A73DC2699363F9AE7D2D35558"),
|
||||||
|
&RawCustom{},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, test := range tests {
|
||||||
|
t.Run(test.Name, func(t *testing.T) {
|
||||||
|
payload, err := Decode(test.Data)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
t.Logf("%s %s", payload.Packet(), payload)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func mustHexDecode(s string) []byte {
|
||||||
|
b, err := hex.DecodeString(s)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
return b
|
||||||
|
}
|
||||||
854
protocol/meshcore/payload.go
Normal file
854
protocol/meshcore/payload.go
Normal file
@@ -0,0 +1,854 @@
|
|||||||
|
package meshcore
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/binary"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"log"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"git.maze.io/go/ham/protocol/meshcore/crypto"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
zeroTime time.Time
|
||||||
|
zeroPositionBytes = []byte{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}
|
||||||
|
)
|
||||||
|
|
||||||
|
type Payload interface {
|
||||||
|
fmt.Stringer
|
||||||
|
|
||||||
|
// Packet returns the underlying raw packet (if available, can be nil).
|
||||||
|
Packet() *Packet
|
||||||
|
|
||||||
|
// Marhal encodes the payload to bytes.
|
||||||
|
Marshal() []byte
|
||||||
|
|
||||||
|
// Unmarshal decodes the payload from bytes.
|
||||||
|
Unmarshal([]byte) error
|
||||||
|
}
|
||||||
|
|
||||||
|
type Acknowledgement struct {
|
||||||
|
// Raw packet (optional).
|
||||||
|
Raw *Packet `json:"-"`
|
||||||
|
|
||||||
|
// Checksum of message timestamp, text and sender public key.
|
||||||
|
Checksum uint32 `json:"checksum"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ack *Acknowledgement) String() string {
|
||||||
|
return fmt.Sprintf("checksum=%08X", ack.Checksum)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ack *Acknowledgement) Packet() *Packet {
|
||||||
|
return ack.Raw
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ack *Acknowledgement) Marshal() []byte {
|
||||||
|
var b [4]byte
|
||||||
|
binary.BigEndian.PutUint32(b[:], ack.Checksum)
|
||||||
|
return b[:]
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ack *Acknowledgement) Unmarshal(b []byte) error {
|
||||||
|
if len(b) < 4 {
|
||||||
|
return io.ErrUnexpectedEOF
|
||||||
|
}
|
||||||
|
ack.Checksum = binary.BigEndian.Uint32(b)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type RequestType byte
|
||||||
|
|
||||||
|
const (
|
||||||
|
GetStats RequestType = iota + 1
|
||||||
|
KeepAlive
|
||||||
|
GetTelemetryData
|
||||||
|
GetMinMaxAvgData
|
||||||
|
GetAccessList
|
||||||
|
GetNeighbors
|
||||||
|
GetOwnerInfo
|
||||||
|
)
|
||||||
|
|
||||||
|
func (rt RequestType) String() string {
|
||||||
|
switch rt {
|
||||||
|
case GetStats:
|
||||||
|
return "get stats"
|
||||||
|
case KeepAlive:
|
||||||
|
return "keep alive"
|
||||||
|
case GetTelemetryData:
|
||||||
|
return "get telemetry data"
|
||||||
|
case GetMinMaxAvgData:
|
||||||
|
return "get min/max/avg data"
|
||||||
|
case GetAccessList:
|
||||||
|
return "get access list"
|
||||||
|
case GetNeighbors:
|
||||||
|
return "get neighbors"
|
||||||
|
case GetOwnerInfo:
|
||||||
|
return "get owner info"
|
||||||
|
default:
|
||||||
|
return fmt.Sprintf("invalid %#02x", byte(rt))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type Request struct {
|
||||||
|
// Raw packet (optional).
|
||||||
|
Raw *Packet `json:"-"`
|
||||||
|
|
||||||
|
EncryptedData
|
||||||
|
|
||||||
|
// Only available after successful decryption:
|
||||||
|
Content *RequestContent `json:"content,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type RequestContent struct {
|
||||||
|
// Time of sending the request.
|
||||||
|
Time time.Time `json:"time"`
|
||||||
|
|
||||||
|
// Type of request.
|
||||||
|
Type RequestType `json:"type"`
|
||||||
|
|
||||||
|
// Data is the request payload.
|
||||||
|
Data []byte `json:"data"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (req *RequestContent) String() string {
|
||||||
|
return fmt.Sprintf("time=%s, type=%s, data=(%d bytes)", req.Time, req.Type.String(), len(req.Data))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (req *Request) String() string {
|
||||||
|
if req.Content == nil {
|
||||||
|
return req.EncryptedData.String()
|
||||||
|
}
|
||||||
|
return req.Content.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (req *Request) Packet() *Packet {
|
||||||
|
return req.Raw
|
||||||
|
}
|
||||||
|
|
||||||
|
type Response struct {
|
||||||
|
// Raw packet (optional).
|
||||||
|
Raw *Packet `json:"-"`
|
||||||
|
|
||||||
|
EncryptedData
|
||||||
|
|
||||||
|
// Only available after successful decryption:
|
||||||
|
Content *ResponseContent `json:"content,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ResponseContent struct {
|
||||||
|
// Tag.
|
||||||
|
Tag uint32 `json:"tag"`
|
||||||
|
|
||||||
|
// Content of the response.
|
||||||
|
Content []byte `json:"content"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (res *ResponseContent) String() string {
|
||||||
|
return fmt.Sprintf("tag=%08X content=%q", res.Tag, res.Content)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (res *Response) String() string {
|
||||||
|
if res.Content == nil {
|
||||||
|
return res.EncryptedData.String()
|
||||||
|
}
|
||||||
|
return res.Content.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (res *Response) Packet() *Packet {
|
||||||
|
return res.Raw
|
||||||
|
}
|
||||||
|
|
||||||
|
type TextType byte
|
||||||
|
|
||||||
|
const (
|
||||||
|
PlainText TextType = iota
|
||||||
|
CLICommand
|
||||||
|
SignedPlainText
|
||||||
|
)
|
||||||
|
|
||||||
|
type Text struct {
|
||||||
|
// Raw packet (optional).
|
||||||
|
Raw *Packet `json:"-"`
|
||||||
|
|
||||||
|
EncryptedData
|
||||||
|
|
||||||
|
// Only available after successful decryption:
|
||||||
|
Content *TextContent `json:"content,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type TextContent struct {
|
||||||
|
// Time of sending the message.
|
||||||
|
Time time.Time `json:"time"`
|
||||||
|
|
||||||
|
// Type of text.
|
||||||
|
Type TextType `json:"type"`
|
||||||
|
|
||||||
|
// Attempt for thie packet.
|
||||||
|
Attempt uint8 `json:"attempt"`
|
||||||
|
|
||||||
|
// Message contains the text message.
|
||||||
|
Message []byte `json:"message"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (text *Text) Packet() *Packet {
|
||||||
|
return text.Raw
|
||||||
|
}
|
||||||
|
|
||||||
|
type EncryptedData struct {
|
||||||
|
// Destination hash is the first byte of the recipient public key.
|
||||||
|
Destination byte `json:"dst"`
|
||||||
|
|
||||||
|
// Source hash is the first byte of the sender public key.
|
||||||
|
Source byte `json:"src"`
|
||||||
|
|
||||||
|
// CipherMAC is the message authenticator.
|
||||||
|
CipherMAC uint16 `json:"cipher_mac"`
|
||||||
|
|
||||||
|
// CipherText is the encrypted message.
|
||||||
|
CipherText []byte `json:"cipher_text"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (enc *EncryptedData) String() string {
|
||||||
|
return fmt.Sprintf("dst=%02X src=%02X mac=%04X text=<encrypted %d bytes>",
|
||||||
|
enc.Destination,
|
||||||
|
enc.Source,
|
||||||
|
enc.CipherMAC,
|
||||||
|
len(enc.CipherText))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (enc *EncryptedData) Marshal() []byte {
|
||||||
|
b := make([]byte, 4)
|
||||||
|
b[0] = enc.Destination
|
||||||
|
b[1] = enc.Source
|
||||||
|
binary.BigEndian.PutUint16(b[2:], enc.CipherMAC)
|
||||||
|
return append(b, enc.CipherText...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (enc *EncryptedData) Unmarshal(b []byte) error {
|
||||||
|
if len(b) < 4 {
|
||||||
|
return io.ErrUnexpectedEOF
|
||||||
|
}
|
||||||
|
|
||||||
|
enc.Destination = b[0]
|
||||||
|
enc.Source = b[1]
|
||||||
|
enc.CipherMAC = binary.BigEndian.Uint16(b[2:])
|
||||||
|
enc.CipherText = make([]byte, len(b)-4)
|
||||||
|
copy(enc.CipherText, b[4:])
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type GroupText struct {
|
||||||
|
// Raw packet (optional).
|
||||||
|
Raw *Packet `json:"-"`
|
||||||
|
|
||||||
|
EncryptedGroupData
|
||||||
|
|
||||||
|
// Only available after successful decryption:
|
||||||
|
Content *GroupTextContent `json:"content,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type GroupTextContent struct {
|
||||||
|
// Group this was sent on (not part of the packet).
|
||||||
|
Group *Group `json:"group"`
|
||||||
|
|
||||||
|
// Time of sending.
|
||||||
|
Time time.Time `json:"time"`
|
||||||
|
|
||||||
|
// Flags is generally 0x00 indicating a plain text message.
|
||||||
|
Type TextType `json:"type"`
|
||||||
|
|
||||||
|
// Attempt is the number of retries.
|
||||||
|
Attempt uint8 `json:"attempt"`
|
||||||
|
|
||||||
|
// Text sent to the group. Typically contains a "<name>: " prefix.
|
||||||
|
Text string `json:"text"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (text *GroupTextContent) String() string {
|
||||||
|
return fmt.Sprintf("time=%s type=%02X attempt=%d text=%q",
|
||||||
|
text.Time,
|
||||||
|
text.Type,
|
||||||
|
text.Attempt,
|
||||||
|
text.Text)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (text *GroupText) String() string {
|
||||||
|
if text.Content == nil {
|
||||||
|
return text.EncryptedGroupData.String()
|
||||||
|
}
|
||||||
|
return text.Content.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (text *GroupText) Packet() *Packet {
|
||||||
|
return text.Raw
|
||||||
|
}
|
||||||
|
|
||||||
|
func (text *GroupText) Decrypt(group *Group) error {
|
||||||
|
b, err := group.Secret.MACThenDecrypt(text.CipherText, text.CipherMAC)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if len(b) < 5 {
|
||||||
|
return io.ErrUnexpectedEOF
|
||||||
|
}
|
||||||
|
|
||||||
|
text.Content = &GroupTextContent{
|
||||||
|
Group: group,
|
||||||
|
Time: decodeTime(b),
|
||||||
|
Type: TextType(b[4] >> 2),
|
||||||
|
Attempt: b[4] & 0x03,
|
||||||
|
Text: decodeCString(b[5:]),
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type GroupData struct {
|
||||||
|
// Raw packet (optional).
|
||||||
|
Raw *Packet `json:"-"`
|
||||||
|
|
||||||
|
EncryptedGroupData
|
||||||
|
|
||||||
|
// Content contains the group datagram.
|
||||||
|
Content []byte `json:"content,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (data *GroupData) String() string {
|
||||||
|
if data.Content == nil {
|
||||||
|
return data.EncryptedGroupData.String()
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("data=%q", data.Content)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (data *GroupData) Packet() *Packet {
|
||||||
|
return data.Raw
|
||||||
|
}
|
||||||
|
|
||||||
|
func (data *GroupData) Decrypt(group *Group) error {
|
||||||
|
b, err := group.Secret.MACThenDecrypt(data.CipherText, data.CipherMAC)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
data.Content = b
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type EncryptedGroupData struct {
|
||||||
|
// ChannelHash is the first byte of the channel public key.
|
||||||
|
ChannelHash byte `json:"channel_hash"`
|
||||||
|
|
||||||
|
// CipherMAC is the message authenticator.
|
||||||
|
CipherMAC uint16 `json:"cipher_mac"`
|
||||||
|
|
||||||
|
// CipherText is the encrypted message.
|
||||||
|
CipherText []byte `json:"cipher_text"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (enc *EncryptedGroupData) String() string {
|
||||||
|
return fmt.Sprintf("channel=%02X mac=%04X text=<encrypted %d bytes>",
|
||||||
|
enc.ChannelHash,
|
||||||
|
enc.CipherMAC,
|
||||||
|
len(enc.CipherText))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (enc *EncryptedGroupData) Marshal() []byte {
|
||||||
|
b := make([]byte, 3)
|
||||||
|
b[0] = enc.ChannelHash
|
||||||
|
binary.BigEndian.PutUint16(b[1:], enc.CipherMAC)
|
||||||
|
return append(b, enc.CipherText...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (enc *EncryptedGroupData) Unmarshal(b []byte) error {
|
||||||
|
if len(b) < 3 {
|
||||||
|
return io.ErrUnexpectedEOF
|
||||||
|
}
|
||||||
|
enc.ChannelHash = b[0]
|
||||||
|
enc.CipherMAC = binary.BigEndian.Uint16(b[1:])
|
||||||
|
enc.CipherText = make([]byte, len(b)-3)
|
||||||
|
copy(enc.CipherText, b[3:])
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type NodeType byte
|
||||||
|
|
||||||
|
const (
|
||||||
|
Chat NodeType = iota + 1
|
||||||
|
Repeater
|
||||||
|
Room
|
||||||
|
Sensor
|
||||||
|
)
|
||||||
|
|
||||||
|
func (nt NodeType) String() string {
|
||||||
|
switch nt {
|
||||||
|
case Chat:
|
||||||
|
return "chat"
|
||||||
|
case Repeater:
|
||||||
|
return "repeater"
|
||||||
|
case Room:
|
||||||
|
return "room"
|
||||||
|
case Sensor:
|
||||||
|
return "sensor"
|
||||||
|
default:
|
||||||
|
return fmt.Sprintf("<unknown %02X>", byte(nt))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const (
|
||||||
|
advertHasPosition = 0x10
|
||||||
|
advertHasFeature1 = 0x20
|
||||||
|
advertHasFeature2 = 0x40
|
||||||
|
advertHasName = 0x80
|
||||||
|
)
|
||||||
|
|
||||||
|
type Advert struct {
|
||||||
|
// Raw packet (optional).
|
||||||
|
Raw *Packet `json:"-"`
|
||||||
|
|
||||||
|
PublicKey *crypto.PublicKey `json:"public_key"`
|
||||||
|
Time time.Time `json:"time"`
|
||||||
|
Signature crypto.Signature `json:"signature"`
|
||||||
|
Type NodeType `json:"node_type"`
|
||||||
|
Position *Position `json:"position,omitempty"`
|
||||||
|
Feature1 *uint16 `json:"feature1,omitempty"`
|
||||||
|
Feature2 *uint16 `json:"feature2,omitempty"`
|
||||||
|
Name string `json:"name,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (adv *Advert) String() string {
|
||||||
|
return fmt.Sprintf("type=%s position=%s name=%q key=%s",
|
||||||
|
adv.Type,
|
||||||
|
adv.Position,
|
||||||
|
adv.Name,
|
||||||
|
formatPublicKey(adv.PublicKey.Bytes()))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (adv *Advert) Packet() *Packet {
|
||||||
|
return adv.Raw
|
||||||
|
}
|
||||||
|
|
||||||
|
func (adv *Advert) Marshal() []byte {
|
||||||
|
b := make([]byte, crypto.PublicKeySize+4+64+1)
|
||||||
|
copy(b, adv.PublicKey.Bytes())
|
||||||
|
encodeTime(b[crypto.PublicKeySize:], adv.Time)
|
||||||
|
copy(b[crypto.PublicKeySize+4:], adv.Signature[:])
|
||||||
|
const flagsOffset = crypto.PublicKeySize + 4 + 64
|
||||||
|
b[flagsOffset] = byte(adv.Type) & 0x7
|
||||||
|
if adv.Position != nil {
|
||||||
|
b[flagsOffset] |= advertHasPosition
|
||||||
|
b = append(b, adv.Position.Marshal()...)
|
||||||
|
}
|
||||||
|
if adv.Feature1 != nil {
|
||||||
|
b[flagsOffset] |= advertHasFeature1
|
||||||
|
b = append(b, byte(*adv.Feature1))
|
||||||
|
b = append(b, byte(*adv.Feature1>>8))
|
||||||
|
}
|
||||||
|
if adv.Feature2 != nil {
|
||||||
|
b[flagsOffset] |= advertHasFeature2
|
||||||
|
b = append(b, byte(*adv.Feature2))
|
||||||
|
b = append(b, byte(*adv.Feature2>>8))
|
||||||
|
}
|
||||||
|
if len(adv.Name) > 0 {
|
||||||
|
b[flagsOffset] |= advertHasName
|
||||||
|
b = append(b, []byte(adv.Name)...)
|
||||||
|
}
|
||||||
|
return b
|
||||||
|
}
|
||||||
|
|
||||||
|
func (adv *Advert) Unmarshal(b []byte) error {
|
||||||
|
if len(b) < crypto.PublicKeySize+4+64+1 {
|
||||||
|
return io.ErrUnexpectedEOF
|
||||||
|
}
|
||||||
|
var (
|
||||||
|
n int
|
||||||
|
err error
|
||||||
|
)
|
||||||
|
|
||||||
|
// parse public key
|
||||||
|
if adv.PublicKey, err = crypto.NewPublicKey(b[:crypto.PublicKeySize]); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
n += crypto.PublicKeySize
|
||||||
|
|
||||||
|
// parse time
|
||||||
|
adv.Time = decodeTime(b[n:])
|
||||||
|
log.Printf("time: %s", adv.Time)
|
||||||
|
n += 4
|
||||||
|
|
||||||
|
// parse signature
|
||||||
|
n += copy(adv.Signature[:], b[n:])
|
||||||
|
|
||||||
|
// parse flags
|
||||||
|
flags, rest := b[n], b[n+1:]
|
||||||
|
adv.Type = NodeType(flags & 0x07)
|
||||||
|
|
||||||
|
if flags&advertHasPosition != 0 {
|
||||||
|
if len(rest) < 8 {
|
||||||
|
return io.ErrUnexpectedEOF
|
||||||
|
}
|
||||||
|
// nb: some repeaters have no location and send 0,0; we're going to ignore these
|
||||||
|
if !bytes.Equal(rest[:8], zeroPositionBytes) {
|
||||||
|
adv.Position = new(Position)
|
||||||
|
if err = adv.Position.Unmarshal(rest); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
rest = rest[8:]
|
||||||
|
}
|
||||||
|
|
||||||
|
if flags&advertHasFeature1 != 0 {
|
||||||
|
if len(rest) < 2 {
|
||||||
|
return io.ErrUnexpectedEOF
|
||||||
|
}
|
||||||
|
adv.Feature1 = new(uint16)
|
||||||
|
*adv.Feature1, rest = binary.LittleEndian.Uint16(rest), rest[2:]
|
||||||
|
}
|
||||||
|
|
||||||
|
if flags&advertHasFeature2 != 0 {
|
||||||
|
if len(rest) < 2 {
|
||||||
|
return io.ErrUnexpectedEOF
|
||||||
|
}
|
||||||
|
adv.Feature2 = new(uint16)
|
||||||
|
*adv.Feature2, rest = binary.LittleEndian.Uint16(rest), rest[2:]
|
||||||
|
}
|
||||||
|
|
||||||
|
if flags&advertHasName != 0 {
|
||||||
|
adv.Name = string(rest)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type AnonymousRequest struct {
|
||||||
|
// Raw packet (optional).
|
||||||
|
Raw *Packet `json:"-"`
|
||||||
|
|
||||||
|
// Destination hash is the first byte of the recipient public key.
|
||||||
|
Destination byte `json:"dst"`
|
||||||
|
|
||||||
|
// PublicKey of the sender.
|
||||||
|
PublicKey *crypto.PublicKey `json:"public_key"`
|
||||||
|
|
||||||
|
// CipherMAC is the message authenticator.
|
||||||
|
CipherMAC uint16 `json:"cipher_mac"`
|
||||||
|
|
||||||
|
// CipherText is the encrypted message.
|
||||||
|
CipherText []byte `json:"cipher_text"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (req *AnonymousRequest) String() string {
|
||||||
|
return fmt.Sprintf("dst=%02X key=%s mac=%02X text=<encrypted %d bytes>",
|
||||||
|
req.Destination,
|
||||||
|
formatPublicKey(req.PublicKey.Bytes()),
|
||||||
|
req.CipherMAC,
|
||||||
|
len(req.CipherText))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (req *AnonymousRequest) Packet() *Packet {
|
||||||
|
return req.Raw
|
||||||
|
}
|
||||||
|
|
||||||
|
func (req *AnonymousRequest) Marshal() []byte {
|
||||||
|
b := make([]byte, 1+crypto.PublicKeySize+2)
|
||||||
|
b[0] = req.Destination
|
||||||
|
n := 1 + copy(b[1:], req.PublicKey.Bytes())
|
||||||
|
binary.BigEndian.PutUint16(b[n:], req.CipherMAC)
|
||||||
|
return append(b, req.CipherText...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (req *AnonymousRequest) Unmarshal(b []byte) error {
|
||||||
|
if len(b) < 1+crypto.PublicKeySize+2 {
|
||||||
|
return io.ErrUnexpectedEOF
|
||||||
|
}
|
||||||
|
var err error
|
||||||
|
req.Destination = b[0]
|
||||||
|
if req.PublicKey, err = crypto.NewPublicKey(b[1 : 1+crypto.PublicKeySize]); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
req.CipherMAC = binary.BigEndian.Uint16(b[1+crypto.PublicKeySize:])
|
||||||
|
req.CipherText = make([]byte, len(b)-1-crypto.PublicKeySize-2)
|
||||||
|
copy(req.CipherText, b[1+crypto.PublicKeySize+2:])
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type Path struct {
|
||||||
|
// Raw packet (optional).
|
||||||
|
Raw *Packet `json:"-"`
|
||||||
|
|
||||||
|
EncryptedData
|
||||||
|
}
|
||||||
|
|
||||||
|
func (path *Path) Packet() *Packet {
|
||||||
|
return path.Raw
|
||||||
|
}
|
||||||
|
|
||||||
|
type Trace struct {
|
||||||
|
// Raw packet (optional).
|
||||||
|
Raw *Packet `json:"-"`
|
||||||
|
|
||||||
|
Tag uint32 `json:"tag"`
|
||||||
|
AuthCode uint32 `json:"authcode"`
|
||||||
|
Flags byte `json:"flags"`
|
||||||
|
Path []byte `json:"path"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (trace *Trace) String() string {
|
||||||
|
return fmt.Sprintf("tag=%08X authcode=%08X flags=%02X path=%s",
|
||||||
|
trace.Tag,
|
||||||
|
trace.AuthCode,
|
||||||
|
trace.Flags,
|
||||||
|
formatPath(trace.Path))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (trace *Trace) Packet() *Packet {
|
||||||
|
return trace.Raw
|
||||||
|
}
|
||||||
|
|
||||||
|
func (trace *Trace) Marshal() []byte {
|
||||||
|
b := make([]byte, 4+4+1)
|
||||||
|
binary.LittleEndian.PutUint32(b[0:], trace.Tag)
|
||||||
|
binary.LittleEndian.PutUint32(b[4:], trace.AuthCode)
|
||||||
|
b[8] = trace.Flags
|
||||||
|
return append(b, trace.Path...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (trace *Trace) Unmarshal(b []byte) error {
|
||||||
|
if len(b) < 9 {
|
||||||
|
return io.ErrUnexpectedEOF
|
||||||
|
}
|
||||||
|
trace.Tag = binary.LittleEndian.Uint32(b[0:])
|
||||||
|
trace.AuthCode = binary.LittleEndian.Uint32(b[4:])
|
||||||
|
trace.Flags = b[8]
|
||||||
|
trace.Path = make([]byte, len(b)-9)
|
||||||
|
copy(trace.Path, b[9:])
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type Multipart struct {
|
||||||
|
// Raw packet (optional).
|
||||||
|
Raw *Packet `json:"-"`
|
||||||
|
|
||||||
|
Remaining uint8 `json:"remaining"`
|
||||||
|
Type PayloadType `json:"type"`
|
||||||
|
Data []byte `json:"data"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (multi *Multipart) String() string {
|
||||||
|
return fmt.Sprintf("remaining=%d type=%s data=%q",
|
||||||
|
multi.Remaining,
|
||||||
|
multi.Type.String(),
|
||||||
|
multi.Data)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (multi *Multipart) Packet() *Packet {
|
||||||
|
return multi.Raw
|
||||||
|
}
|
||||||
|
|
||||||
|
func (multi *Multipart) Marshal() []byte {
|
||||||
|
return append([]byte{
|
||||||
|
multi.Remaining<<4 | byte(multi.Type&0x0F),
|
||||||
|
}, multi.Data...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (multi *Multipart) Unmarshal(b []byte) error {
|
||||||
|
if len(b) < 1 {
|
||||||
|
return io.ErrUnexpectedEOF
|
||||||
|
}
|
||||||
|
multi.Remaining = b[0] >> 4
|
||||||
|
multi.Type = PayloadType(b[0] & 0x0F)
|
||||||
|
multi.Data = make([]byte, len(b)-1)
|
||||||
|
copy(multi.Data, b[1:])
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type ControlType byte
|
||||||
|
|
||||||
|
const (
|
||||||
|
DiscoverRequest ControlType = 0x80
|
||||||
|
DiscoverResponse ControlType = 0x90
|
||||||
|
)
|
||||||
|
|
||||||
|
type Control struct {
|
||||||
|
// Raw packet (optional).
|
||||||
|
Raw *Packet `json:"-"`
|
||||||
|
|
||||||
|
// Type of control packet.
|
||||||
|
Type ControlType `json:"type"`
|
||||||
|
|
||||||
|
// Request for discovery.
|
||||||
|
Request *ControlDiscoverRequest `json:"request,omitempty"`
|
||||||
|
|
||||||
|
// Response for discovery.
|
||||||
|
Response *ControlDiscoverResponse `json:"response,omitempty"`
|
||||||
|
|
||||||
|
// Data contains the data bytes for unknown/unparsed control types.
|
||||||
|
Data []byte `json:"data"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ControlDiscoverRequest struct {
|
||||||
|
Flags byte `json:"flags"` // upper 4 bits
|
||||||
|
PrefixOnly bool `json:"prefix_only"` // lower 1 bit
|
||||||
|
TypeFilter byte `json:"type_filter"`
|
||||||
|
Tag uint32 `json:"tag"`
|
||||||
|
Since *time.Time `json:"since,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (req ControlDiscoverRequest) String() string {
|
||||||
|
var since string
|
||||||
|
if req.Since != nil {
|
||||||
|
since = " " + req.Since.String()
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("flags=%X prefixonly=%t filter=%08b tag=%08X"+since,
|
||||||
|
req.Flags,
|
||||||
|
req.PrefixOnly,
|
||||||
|
req.TypeFilter,
|
||||||
|
req.Tag)
|
||||||
|
}
|
||||||
|
|
||||||
|
type ControlDiscoverResponse struct {
|
||||||
|
Flags byte `json:"flags"` // upper 4 bits
|
||||||
|
NodeType NodeType `json:"node_type"` // lower 4 bits
|
||||||
|
SNR float64 `json:"snr"`
|
||||||
|
Tag uint32 `json:"tag"`
|
||||||
|
PublicKey []byte `json:"public_key"` // 8 or 32 bytes
|
||||||
|
}
|
||||||
|
|
||||||
|
func (res ControlDiscoverResponse) String() string {
|
||||||
|
return fmt.Sprintf("flags=%X node=%s snr=%g tag=%08X key=%s",
|
||||||
|
res.Flags,
|
||||||
|
res.NodeType.String(),
|
||||||
|
res.SNR,
|
||||||
|
res.Tag,
|
||||||
|
formatPublicKey(res.PublicKey))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (control *Control) String() string {
|
||||||
|
switch {
|
||||||
|
case control.Request != nil:
|
||||||
|
return "request " + control.Request.String()
|
||||||
|
case control.Response != nil:
|
||||||
|
return "response " + control.Response.String()
|
||||||
|
default:
|
||||||
|
return fmt.Sprintf("type=%02X data=%q", control.Type, control.Data)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (control *Control) Packet() *Packet {
|
||||||
|
return control.Raw
|
||||||
|
}
|
||||||
|
|
||||||
|
func (control *Control) Marshal() []byte {
|
||||||
|
b := []byte{byte(control.Type)}
|
||||||
|
switch control.Type {
|
||||||
|
case DiscoverRequest:
|
||||||
|
if control.Request != nil {
|
||||||
|
b = append(b,
|
||||||
|
control.Request.Flags<<4, // 1
|
||||||
|
control.Request.TypeFilter, // 2
|
||||||
|
0x00, 0x00, 0x00, 0x00, // 3-6: tag
|
||||||
|
0x00, 0x00, 0x00, 0x00, // 7-10: since
|
||||||
|
)
|
||||||
|
if control.Request.PrefixOnly {
|
||||||
|
b[1] |= 0x01
|
||||||
|
}
|
||||||
|
binary.LittleEndian.PutUint32(b[3:], control.Request.Tag)
|
||||||
|
if control.Request.Since != nil {
|
||||||
|
encodeTime(b[7:], *control.Request.Since)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
case DiscoverResponse:
|
||||||
|
if control.Response != nil {
|
||||||
|
b = append(b,
|
||||||
|
control.Response.Flags<<4|byte(control.Response.NodeType&0x0F), // 1
|
||||||
|
byte(control.Response.SNR*4), // 2
|
||||||
|
0x00, 0x00, 0x00, 0x00, // 3-6: tag
|
||||||
|
)
|
||||||
|
binary.LittleEndian.PutUint32(b[3:], control.Response.Tag)
|
||||||
|
}
|
||||||
|
b = append(b, control.Response.PublicKey...)
|
||||||
|
|
||||||
|
default:
|
||||||
|
b = append(b, control.Data...)
|
||||||
|
}
|
||||||
|
return b
|
||||||
|
}
|
||||||
|
|
||||||
|
func (control *Control) Unmarshal(b []byte) error {
|
||||||
|
if len(b) < 1 {
|
||||||
|
return io.ErrUnexpectedEOF
|
||||||
|
}
|
||||||
|
|
||||||
|
control.Type = ControlType(b[0] >> 4)
|
||||||
|
switch control.Type {
|
||||||
|
case DiscoverRequest:
|
||||||
|
if len(b) < 7 {
|
||||||
|
return io.ErrUnexpectedEOF
|
||||||
|
}
|
||||||
|
control.Request = &ControlDiscoverRequest{
|
||||||
|
Flags: b[1] >> 4,
|
||||||
|
PrefixOnly: b[1]&0x01 != 0,
|
||||||
|
TypeFilter: b[2],
|
||||||
|
Tag: binary.LittleEndian.Uint32(b[3:]),
|
||||||
|
}
|
||||||
|
if len(b) >= 11 {
|
||||||
|
since := decodeTime(b[7:])
|
||||||
|
control.Request.Since = &since
|
||||||
|
}
|
||||||
|
|
||||||
|
case DiscoverResponse:
|
||||||
|
if len(b) < 15 {
|
||||||
|
return io.ErrUnexpectedEOF
|
||||||
|
}
|
||||||
|
control.Response = &ControlDiscoverResponse{
|
||||||
|
Flags: b[1] >> 4,
|
||||||
|
NodeType: NodeType(b[1] & 0x0F),
|
||||||
|
SNR: float64(int8(b[2])) / 4,
|
||||||
|
Tag: binary.LittleEndian.Uint32(b[3:]),
|
||||||
|
}
|
||||||
|
control.Response.PublicKey = make([]byte, len(b)-6)
|
||||||
|
copy(control.Response.PublicKey, b[6:])
|
||||||
|
|
||||||
|
default:
|
||||||
|
control.Data = make([]byte, len(b)-1)
|
||||||
|
copy(control.Data, b[1:])
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type RawCustom struct {
|
||||||
|
// Raw packet (optional).
|
||||||
|
Raw *Packet `json:"-"`
|
||||||
|
|
||||||
|
// Data in the payload.
|
||||||
|
Data []byte `json:"data"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (raw *RawCustom) String() string {
|
||||||
|
return fmt.Sprintf("data=%q", raw.Data)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (raw *RawCustom) Packet() *Packet {
|
||||||
|
return raw.Raw
|
||||||
|
}
|
||||||
|
|
||||||
|
func (raw *RawCustom) Marshal() []byte {
|
||||||
|
return raw.Data
|
||||||
|
}
|
||||||
|
|
||||||
|
func (raw *RawCustom) Unmarshal(b []byte) error {
|
||||||
|
raw.Data = make([]byte, len(b))
|
||||||
|
copy(raw.Data, b)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
_ Payload = (*Acknowledgement)(nil)
|
||||||
|
_ Payload = (*Request)(nil)
|
||||||
|
_ Payload = (*Response)(nil)
|
||||||
|
_ Payload = (*Text)(nil)
|
||||||
|
_ Payload = (*GroupText)(nil)
|
||||||
|
_ Payload = (*GroupData)(nil)
|
||||||
|
)
|
||||||
84
protocol/meshcore/util.go
Normal file
84
protocol/meshcore/util.go
Normal file
@@ -0,0 +1,84 @@
|
|||||||
|
package meshcore
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/binary"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"git.maze.io/go/ham/protocol/meshcore/crypto"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Position struct {
|
||||||
|
Latitude float64
|
||||||
|
Longitude float64
|
||||||
|
}
|
||||||
|
|
||||||
|
func (pos *Position) String() string {
|
||||||
|
if pos == nil {
|
||||||
|
return "<unknown>"
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("%f,%f", pos.Latitude, pos.Longitude)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (pos *Position) Marshal() []byte {
|
||||||
|
var buf [8]byte
|
||||||
|
binary.LittleEndian.PutUint32(buf[0:], uint32(pos.Latitude*1e6))
|
||||||
|
binary.LittleEndian.PutUint32(buf[4:], uint32(pos.Longitude*1e6))
|
||||||
|
return buf[:]
|
||||||
|
}
|
||||||
|
|
||||||
|
func (pos *Position) Unmarshal(b []byte) error {
|
||||||
|
if len(b) < 8 {
|
||||||
|
return io.ErrUnexpectedEOF
|
||||||
|
}
|
||||||
|
pos.Latitude = float64(binary.LittleEndian.Uint32(b[0:])) / 1e6
|
||||||
|
pos.Longitude = float64(binary.LittleEndian.Uint32(b[4:])) / 1e6
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func encodeTime(b []byte, t time.Time) {
|
||||||
|
binary.LittleEndian.PutUint32(b, uint32(t.Unix()))
|
||||||
|
}
|
||||||
|
|
||||||
|
func decodeTime(b []byte) time.Time {
|
||||||
|
return time.Unix(int64(binary.LittleEndian.Uint32(b)), 0).UTC()
|
||||||
|
}
|
||||||
|
|
||||||
|
func encodeFrequency(b []byte, f float64) {
|
||||||
|
binary.LittleEndian.PutUint32(b, uint32(f*1e3))
|
||||||
|
}
|
||||||
|
|
||||||
|
func decodeFrequency(b []byte) float64 {
|
||||||
|
return float64(int64(binary.LittleEndian.Uint32(b))) / 1e3
|
||||||
|
}
|
||||||
|
|
||||||
|
func decodeCString(b []byte) string {
|
||||||
|
if i := bytes.IndexByte(b, 0x00); i > -1 {
|
||||||
|
return string(b[:i])
|
||||||
|
}
|
||||||
|
return string(b)
|
||||||
|
}
|
||||||
|
|
||||||
|
func formatPath(path []byte) string {
|
||||||
|
p := make([]string, len(path))
|
||||||
|
for i, node := range path {
|
||||||
|
p[i] = fmt.Sprintf("%02X", node)
|
||||||
|
}
|
||||||
|
return strings.Join(p, ">")
|
||||||
|
}
|
||||||
|
|
||||||
|
func formatPublicKey(b []byte) string {
|
||||||
|
switch len(b) {
|
||||||
|
case 8:
|
||||||
|
return fmt.Sprintf("<%016x>", b)
|
||||||
|
case crypto.PublicKeySize:
|
||||||
|
return fmt.Sprintf("<%06x…%06x>", b[:3], b[29:])
|
||||||
|
case crypto.PrivateKeySize:
|
||||||
|
return formatPublicKey(b[32:])
|
||||||
|
default:
|
||||||
|
return "<unknown>"
|
||||||
|
}
|
||||||
|
}
|
||||||
30
protocol/packet.go
Normal file
30
protocol/packet.go
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
package protocol
|
||||||
|
|
||||||
|
// Packet represents a raw packet.
|
||||||
|
type Packet struct {
|
||||||
|
Protocol string `json:"protocol"` // Protocol name
|
||||||
|
SNR float64 `json:"snr"` // Signal-to-noise Ratio
|
||||||
|
RSSI int8 `json:"rssi"` // Received Signal Strength Indicator
|
||||||
|
Raw []byte `json:"raw"` // Raw packet
|
||||||
|
}
|
||||||
|
|
||||||
|
type Device interface {
|
||||||
|
// Close the device.
|
||||||
|
Close() error
|
||||||
|
}
|
||||||
|
|
||||||
|
// Receiver of packets.
|
||||||
|
type Receiver interface {
|
||||||
|
Device
|
||||||
|
|
||||||
|
// RawPackets starts receiving raw packets.
|
||||||
|
RawPackets() <-chan *Packet
|
||||||
|
}
|
||||||
|
|
||||||
|
// Transmitter of packets.
|
||||||
|
type Transmitter interface {
|
||||||
|
Device
|
||||||
|
|
||||||
|
// SendRawPacket sends a raw (encoded) packet.
|
||||||
|
SendRawPacket(*Packet) error
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user