More protocols
This commit is contained in:
@@ -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() {
|
||||
SigningMethod = new(SigningMethodEd25519)
|
||||
jwt.RegisterSigningMethod(SigningMethod.Alg(), func() jwt.SigningMethod {
|
||||
return SigningMethod
|
||||
})
|
||||
@@ -17,10 +18,6 @@ func init() {
|
||||
|
||||
var SigningMethod jwt.SigningMethod
|
||||
|
||||
func init() {
|
||||
SigningMethod = new(SigningMethodEd25519)
|
||||
}
|
||||
|
||||
type SigningMethodEd25519 struct{}
|
||||
|
||||
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>"
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user