Updates
Some checks failed
Run tests / test (1.25) (push) Failing after 18s
Run tests / test (stable) (push) Failing after 19s

This commit is contained in:
2026-02-22 21:05:51 +01:00
parent db19ea81b0
commit e915c89710
5 changed files with 282 additions and 51 deletions

23
.gitea/workflows/dev.yaml Normal file
View File

@@ -0,0 +1,23 @@
name: Run tests
on:
push:
permissions:
contents: read
jobs:
test:
strategy:
matrix:
go: [stable, 1.25]
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- uses: actions/setup-go@v6
with:
go-version: ${{ matrix.go }}
- name: golangci-lint
uses: golangci/golangci-lint-action@v9
with:
go-version: ${{ matrix.go }}
version: v2.6

View File

@@ -0,0 +1,6 @@
package meshcore
import "github.com/sirupsen/logrus"
// Logger used by this package.
var Logger = logrus.New()

View File

@@ -1,11 +1,14 @@
package meshcore package meshcore
import ( import (
"bytes"
"encoding/binary" "encoding/binary"
"encoding/hex"
"errors"
"fmt" "fmt"
"io" "io"
"log" "log"
"math/rand/v2"
"slices"
"strings" "strings"
"sync" "sync"
"time" "time"
@@ -18,6 +21,7 @@ const (
maxCompanionFrameSize = 172 maxCompanionFrameSize = 172
) )
// Node can be any type of MeshCore node.
type Node struct { type Node struct {
OnPacket (*Packet) OnPacket (*Packet)
@@ -53,6 +57,13 @@ func (dev *Node) Info() *radio.Info {
return dev.driver.Info() return dev.driver.Info()
} }
func (dev *Node) Trace(path []byte) (snr []float64, err error) {
if tracer, ok := dev.driver.(nodeTracer); ok {
return tracer.Trace(path)
}
return nil, errors.New("meshcore: node doesn't support running traces")
}
type nodeDriver interface { type nodeDriver interface {
radio.Device radio.Device
protocol.PacketReceiver protocol.PacketReceiver
@@ -62,6 +73,10 @@ type nodeDriver interface {
Packets() <-chan *Packet Packets() <-chan *Packet
} }
type nodeTracer interface {
Trace(path []byte) (snr []float64, err error)
}
type CompanionError struct { type CompanionError struct {
Code byte Code byte
} }
@@ -90,7 +105,58 @@ type companionDriver struct {
mu sync.Mutex mu sync.Mutex
packets chan *Packet packets chan *Packet
rawPackets chan *protocol.Packet rawPackets chan *protocol.Packet
waiting chan *companionDriverWaiting
info companionInfo info companionInfo
traceTag uint32
traceAuthCode uint32
}
type companionDriverWaiting struct {
expect []byte
response chan []byte
err chan error
}
func newCompanionDriverWaiting(expect []byte) *companionDriverWaiting {
return &companionDriverWaiting{
expect: expect,
response: make(chan []byte),
err: make(chan error),
}
}
func (wait *companionDriverWaiting) Respond(response []byte) {
select {
case wait.response <- response:
default:
Logger.Warnf("meshcore: waiting for %02x discard response: %02x", wait.expect, response)
}
}
func (wait *companionDriverWaiting) Error(err error) {
select {
case wait.err <- err:
default:
Logger.Warnf("meshcore: waiting for %02x discard error: %v", wait.expect, err)
}
}
func (wait *companionDriverWaiting) Close() {
Logger.Tracef("meshcore: waiting for %02x closing", wait.expect)
close(wait.response)
close(wait.err)
}
func (wait *companionDriverWaiting) Wait() ([]byte, error) {
Logger.Tracef("meshcore: waiting for %02x", wait.expect)
select {
case err := <-wait.err:
Logger.Tracef("meshcore: waiting for %02x received error: %v", wait.expect, err)
return nil, err
case response := <-wait.response:
Logger.Tracef("meshcore: waiting for %02x received response: %d", wait.expect, len(response))
return response, nil
}
} }
type companionInfo struct { type companionInfo struct {
@@ -124,6 +190,9 @@ type companionInfo struct {
func newCompanionDriver(conn io.ReadWriteCloser) *companionDriver { func newCompanionDriver(conn io.ReadWriteCloser) *companionDriver {
return &companionDriver{ return &companionDriver{
conn: conn, conn: conn,
waiting: make(chan *companionDriverWaiting, 16),
traceTag: rand.Uint32(),
//traceAuthCode: rand.Uint32(),
} }
} }
@@ -132,13 +201,13 @@ func (drv *companionDriver) Close() error {
} }
func (drv *companionDriver) Setup() (err error) { func (drv *companionDriver) Setup() (err error) {
go drv.poll()
if err = drv.sendAppStart(); err != nil { if err = drv.sendAppStart(); err != nil {
return return
} }
if err = drv.sendDeviceInfo(); err != nil { if err = drv.sendDeviceInfo(); err != nil {
return return
} }
go drv.poll()
return return
} }
@@ -166,11 +235,29 @@ func (drv *companionDriver) Info() *radio.Info {
} }
var firmwareDate time.Time var firmwareDate time.Time
firmwareDate, _ = time.Parse("02-01-2006", drv.info.FirmwareBuildDate) for _, layout := range []string{
"02 Jan 2006",
"02-01-2006",
} {
var terr error
if firmwareDate, terr = time.Parse(layout, drv.info.FirmwareBuildDate); terr == nil {
break
}
}
var (
manufacturerPart = strings.SplitN(drv.info.Manufacturer, " ", 2)
manufacturer = manufacturerPart[0]
device string
)
if len(manufacturerPart) > 1 {
device = manufacturerPart[1]
}
return &radio.Info{ return &radio.Info{
Name: drv.info.Name, Name: drv.info.Name,
Manufacturer: drv.info.Manufacturer, Manufacturer: manufacturer,
Device: device,
FirmwareDate: firmwareDate, FirmwareDate: firmwareDate,
FirmwareVersion: drv.info.FirmwareVersion, FirmwareVersion: drv.info.FirmwareVersion,
Modulation: protocol.LoRa, Modulation: protocol.LoRa,
@@ -183,6 +270,25 @@ func (drv *companionDriver) Info() *radio.Info {
} }
} }
func (drv *companionDriver) Trace(path []byte) (snr []float64, err error) {
var (
args = make([]byte, 4+4+1+len(path))
data []byte
)
binary.LittleEndian.PutUint32(args[0:], drv.traceTag) // tag
binary.LittleEndian.PutUint32(args[4:], drv.traceAuthCode) // authcode
args[8] = 0 // flags
copy(args[9:], path) // path
Logger.Debugf("meshcore: trace %02x tag %08x authcode %08x", path, drv.traceTag, drv.traceAuthCode)
if data, err = drv.writeCommand(companionSendTracePath, args, companionResponseSent); err != nil {
return
}
log.Printf("trace response:\n%s", hex.Dump(data))
return
}
func (drv *companionDriver) readFrame() ([]byte, error) { func (drv *companionDriver) readFrame() ([]byte, error) {
var frame [3 + maxCompanionFrameSize]byte var frame [3 + maxCompanionFrameSize]byte
for { for {
@@ -212,7 +318,7 @@ func (drv *companionDriver) readFrame() ([]byte, error) {
o += n o += n
} }
//log.Printf("read %d:\n%s", size, hex.Dump(frame[:3+size])) Logger.Tracef("read %d:\n%s", size, hex.Dump(frame[:3+size]))
return frame[3 : 3+size], nil return frame[3 : 3+size], nil
} }
} }
@@ -227,7 +333,7 @@ func (drv *companionDriver) writeFrame(b []byte) (err error) {
binary.LittleEndian.PutUint16(frame[1:], uint16(len(b))) binary.LittleEndian.PutUint16(frame[1:], uint16(len(b)))
n := copy(frame[3:], b) n := copy(frame[3:], b)
//log.Printf("send %d:\n%s", n, hex.Dump(frame[:3+n])) //Logger.Tracef("send %d:\n%s", n, hex.Dump(frame[:3+n]))
_, err = drv.conn.Write(frame[:3+n]) _, err = drv.conn.Write(frame[:3+n])
return return
} }
@@ -243,38 +349,34 @@ func (drv *companionDriver) writeCommand(cmd byte, args []byte, wait ...byte) ([
return drv.wait(wait...) return drv.wait(wait...)
} }
func (drv *companionDriver) wait(wait ...byte) ([]byte, error) { func (drv *companionDriver) wait(expect ...byte) ([]byte, error) {
for { wait := newCompanionDriverWaiting(expect)
b, err := drv.readFrame() defer wait.Close()
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.waiting <- wait
drv.handlePushFrame(b) return wait.Wait()
continue }
case bytes.Contains(wait, b[:1]): func bytesContains(b byte, slice []byte) bool {
return b, nil for _, v := range slice {
if v == b {
case wait == nil: return true
return b, nil
} }
} }
return false
} }
func (drv *companionDriver) handlePushFrame(b []byte) { func (drv *companionDriver) handlePushFrame(b []byte) {
if len(b) < 1 {
return // illegal
}
switch b[0] { switch b[0] {
case companionPushAdvert: case companionPushAdvert:
case companionPushMessageWaiting: case companionPushMessageWaiting:
case companionPushLogRXData: case companionPushLogRXData:
drv.handleRXData(b[1:]) drv.handleRXData(b[1:])
default:
log.Printf("meshcore: unhandled push %02x:\n%s", b[0], hex.Dump(b[1:]))
} }
} }
@@ -324,8 +426,11 @@ func (drv *companionDriver) handleRXData(b []byte) {
} }
func (drv *companionDriver) sendAppStart() (err error) { func (drv *companionDriver) sendAppStart() (err error) {
var b []byte var (
if b, err = drv.writeCommand(companionAppStart, append(make([]byte, 8), []byte("git.maze.io/go/ham")...), companionResponseSelfInfo); err != nil { b []byte
args = append(make([]byte, 8), []byte("git.maze.io/go/ham")...)
)
if b, err = drv.writeCommand(companionAppStart, args, companionResponseSelfInfo); err != nil {
return fmt.Errorf("meshcore: can't send application start: %v", err) return fmt.Errorf("meshcore: can't send application start: %v", err)
} }
//log.Printf("companion app start response:\n%s", hex.Dump(b)) //log.Printf("companion app start response:\n%s", hex.Dump(b))
@@ -358,35 +463,77 @@ func (drv *companionDriver) sendAppStart() (err error) {
} }
func (drv *companionDriver) sendDeviceInfo() (err error) { func (drv *companionDriver) sendDeviceInfo() (err error) {
var b []byte var (
if b, err = drv.writeCommand(companionDeviceQuery, []byte{0x03}, companionResponseDeviceInfo); err != nil { args = []byte{0x03}
data []byte
)
if data, err = drv.writeCommand(companionDeviceQuery, args, companionResponseDeviceInfo); err != nil {
return return
} }
const expect = 4 + 4 + 12 + 40 + 20 const expect = 4 + 4 + 12 + 40 + 20
if len(b) < expect { if len(data) < expect {
return fmt.Errorf("companion: expected %d bytes of self info, got %d", expect, len(b)) return fmt.Errorf("companion: expected %d bytes of self info, got %d", expect, len(data))
} }
if b[0] != companionResponseDeviceInfo { if data[0] != companionResponseDeviceInfo {
return fmt.Errorf("companion: expected device info response, got %#02x", b[0]) return fmt.Errorf("companion: expected device info response, got %#02x", data[0])
} }
b = b[1:] data = data[1:]
drv.info.FirmwareVersionCode = b[0] drv.info.FirmwareVersionCode = data[0]
drv.info.MaxContacts = int(b[1]) * 2 drv.info.MaxContacts = int(data[1]) * 2
drv.info.MaxGroupChannels = int(b[2]) drv.info.MaxGroupChannels = int(data[2])
drv.info.FirmwareBuildDate = decodeCString(b[7:19]) drv.info.FirmwareBuildDate = decodeCString(data[7:19])
drv.info.Manufacturer = decodeCString(b[19:59]) drv.info.Manufacturer = decodeCString(data[19:59])
drv.info.FirmwareVersion = decodeCString(b[59:79]) drv.info.FirmwareVersion = decodeCString(data[59:79])
log.Printf("self info: %#+v", drv.info)
return return
} }
func (drv *companionDriver) poll() { func (drv *companionDriver) poll() {
for { for {
if _, err := drv.wait(); err != nil { frame, err := drv.readFrame()
log.Printf("meshcore: companion %s fatal error: %v", drv.info.Name, err) if err != nil {
Logger.Errorf("meshcore: unrecoverable error: %v", err)
return return
} else if len(frame) < 1 {
continue
}
response := frame[0]
Logger.Debugf("meshcore: handle %s (%02x, %d bytes)", companionResponseName(response), response, len(frame[1:]))
switch {
case response == companionResponseError:
err := CompanionError{Code: frame[1]}
select {
case waiting := <-drv.waiting:
Logger.Debugf("meshcore: sending error to waiting: %v", err)
waiting.Error(err)
default:
Logger.Debugf("meshcore: unexpected error: %v", err)
}
case response >= 0x80:
drv.handlePushFrame(frame)
default:
select {
case waiting := <-drv.waiting:
if len(waiting.expect) == 0 {
//Logger.Debugf("meshcore: respond %02x verbatim", response)
waiting.Respond(frame)
} else if slices.Contains(waiting.expect, response) {
//Logger.Debugf("meshcore: respond %02x to expected %02x", response, waiting.expect)
waiting.Respond(frame)
} else {
//Logger.Debugf("meshcore: unexpected %02x response (want %02x)", response, waiting.expect)
waiting.Error(fmt.Errorf("unexpected %02x response", response))
}
default:
Logger.Warnf("meshcore: unhandled %02x response", response)
}
} }
} }
} }

View File

@@ -1,5 +1,7 @@
package meshcore package meshcore
import "fmt"
const ( const (
companionErrCodeUnsupported byte = 1 + iota companionErrCodeUnsupported byte = 1 + iota
companionErrCodeNotFound companionErrCodeNotFound
@@ -122,3 +124,56 @@ const (
companionPushContactDeleted companionPushContactDeleted
companionPushContactsFull companionPushContactsFull
) )
var companionResponseNames = map[byte]string{
companionResponseOK: "ok",
companionResponseError: "error",
companionResponseContactsStart: "contact start",
companionResponseContact: "contact",
companionResponseEndOfContacts: "end of contacts",
companionResponseSelfInfo: "self info",
companionResponseSent: "sent",
companionResponseContactMessageReceived: "contact message received",
companionResponseChannelMessageReceived: "channel message received",
companionResponseCurrentTime: "current time",
companionResponseNoMoreMessages: "no more messages",
companionResponseExportContact: "export contact",
companionResponseBatteryAndStorage: "battery and storage",
companionResponseDeviceInfo: "device info",
companionResponsePrivateKey: "private key",
companionResponseDisabled: "disabled",
companionResponseContactMessageReceivedV3: "contact message received V3",
companionResponseChannelMessageReceivedV3: "channel message received V3",
companionResponseChannelInfo: "channel info",
companionResponseSignatureStart: "signature start",
companionResponseSignature: "signature",
companionResponseCustomVars: "custom vars",
companionResponseAdvertPath: "advert path",
companionResponseTuningParams: "tuning params",
companionResponseStats: "stats",
companionResponseAutoAddConfig: "auto add config",
companionPushAdvert: "push advert",
companionPushPathUpdated: "push path updated",
companionPushSendConfirmed: "push send confirmed",
companionPushMessageWaiting: "push message waiting",
companionPushRawData: "push raw data",
companionPushLoginSuccess: "push login success",
companionPushLoginFailure: "push login failure",
companionPushStatusResponse: "push status response",
companionPushLogRXData: "push log rx data",
companionPushTraceData: "push trace data",
companionPushNewAdvert: "push new advert",
companionPushTelemetryResponse: "push telemetry response",
companionPushBinaryResponse: "push binary response",
companionPushPathDiscoveryResponse: "push path discovery response",
companionPushControlData: "push control data",
companionPushContactDeleted: "push contact deleted",
companionPushContactsFull: "push contacts full",
}
func companionResponseName(response byte) string {
if s, ok := companionResponseNames[response]; ok {
return s
}
return fmt.Sprintf("unknown %02x", response)
}

View File

@@ -12,8 +12,8 @@ import (
) )
type Position struct { type Position struct {
Latitude float64 Latitude float64 `json:"latitude"`
Longitude float64 Longitude float64 `json:"longitude"`
} }
func (pos *Position) String() string { func (pos *Position) String() string {