From e915c89710fe188395fdeeed6a1beb875b50fca6 Mon Sep 17 00:00:00 2001 From: maze Date: Sun, 22 Feb 2026 21:05:51 +0100 Subject: [PATCH] Updates --- .gitea/workflows/dev.yaml | 23 +++ protocol/meshcore/logger.go | 6 + protocol/meshcore/node.go | 245 +++++++++++++++++++++++++------- protocol/meshcore/node_const.go | 55 +++++++ protocol/meshcore/util.go | 4 +- 5 files changed, 282 insertions(+), 51 deletions(-) create mode 100644 .gitea/workflows/dev.yaml create mode 100644 protocol/meshcore/logger.go diff --git a/.gitea/workflows/dev.yaml b/.gitea/workflows/dev.yaml new file mode 100644 index 0000000..2be040a --- /dev/null +++ b/.gitea/workflows/dev.yaml @@ -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 diff --git a/protocol/meshcore/logger.go b/protocol/meshcore/logger.go new file mode 100644 index 0000000..184bcf5 --- /dev/null +++ b/protocol/meshcore/logger.go @@ -0,0 +1,6 @@ +package meshcore + +import "github.com/sirupsen/logrus" + +// Logger used by this package. +var Logger = logrus.New() diff --git a/protocol/meshcore/node.go b/protocol/meshcore/node.go index 3f98f58..ef9defd 100644 --- a/protocol/meshcore/node.go +++ b/protocol/meshcore/node.go @@ -1,11 +1,14 @@ package meshcore import ( - "bytes" "encoding/binary" + "encoding/hex" + "errors" "fmt" "io" "log" + "math/rand/v2" + "slices" "strings" "sync" "time" @@ -18,6 +21,7 @@ const ( maxCompanionFrameSize = 172 ) +// Node can be any type of MeshCore node. type Node struct { OnPacket (*Packet) @@ -53,6 +57,13 @@ func (dev *Node) Info() *radio.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 { radio.Device protocol.PacketReceiver @@ -62,6 +73,10 @@ type nodeDriver interface { Packets() <-chan *Packet } +type nodeTracer interface { + Trace(path []byte) (snr []float64, err error) +} + type CompanionError struct { Code byte } @@ -86,11 +101,62 @@ func (err CompanionError) Error() string { } type companionDriver struct { - conn io.ReadWriteCloser - mu sync.Mutex - packets chan *Packet - rawPackets chan *protocol.Packet - info companionInfo + conn io.ReadWriteCloser + mu sync.Mutex + packets chan *Packet + rawPackets chan *protocol.Packet + waiting chan *companionDriverWaiting + 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 { @@ -123,7 +189,10 @@ type companionInfo struct { func newCompanionDriver(conn io.ReadWriteCloser) *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) { + go drv.poll() if err = drv.sendAppStart(); err != nil { return } if err = drv.sendDeviceInfo(); err != nil { return } - go drv.poll() return } @@ -166,11 +235,29 @@ func (drv *companionDriver) Info() *radio.Info { } 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{ Name: drv.info.Name, - Manufacturer: drv.info.Manufacturer, + Manufacturer: manufacturer, + Device: device, FirmwareDate: firmwareDate, FirmwareVersion: drv.info.FirmwareVersion, 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) { var frame [3 + maxCompanionFrameSize]byte for { @@ -212,7 +318,7 @@ func (drv *companionDriver) readFrame() ([]byte, error) { 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 } } @@ -227,7 +333,7 @@ func (drv *companionDriver) writeFrame(b []byte) (err error) { 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])) + //Logger.Tracef("send %d:\n%s", n, hex.Dump(frame[:3+n])) _, err = drv.conn.Write(frame[:3+n]) return } @@ -243,38 +349,34 @@ func (drv *companionDriver) writeCommand(cmd byte, args []byte, wait ...byte) ([ 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]} +func (drv *companionDriver) wait(expect ...byte) ([]byte, error) { + wait := newCompanionDriverWaiting(expect) + defer wait.Close() - case b[0] >= 0x80: - drv.handlePushFrame(b) - continue + drv.waiting <- wait + return wait.Wait() +} - case bytes.Contains(wait, b[:1]): - return b, nil - - case wait == nil: - return b, nil +func bytesContains(b byte, slice []byte) bool { + for _, v := range slice { + if v == b { + return true } } + return false } func (drv *companionDriver) handlePushFrame(b []byte) { + if len(b) < 1 { + return // illegal + } switch b[0] { case companionPushAdvert: case companionPushMessageWaiting: case companionPushLogRXData: 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) { - var b []byte - if b, err = drv.writeCommand(companionAppStart, append(make([]byte, 8), []byte("git.maze.io/go/ham")...), companionResponseSelfInfo); err != nil { + var ( + 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) } //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) { - var b []byte - if b, err = drv.writeCommand(companionDeviceQuery, []byte{0x03}, companionResponseDeviceInfo); err != nil { + var ( + args = []byte{0x03} + data []byte + ) + if data, err = drv.writeCommand(companionDeviceQuery, args, 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 len(data) < expect { + return fmt.Errorf("companion: expected %d bytes of self info, got %d", expect, len(data)) } - if b[0] != companionResponseDeviceInfo { - return fmt.Errorf("companion: expected device info response, got %#02x", b[0]) + if data[0] != companionResponseDeviceInfo { + 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.MaxContacts = int(b[1]) * 2 - drv.info.MaxGroupChannels = int(b[2]) - drv.info.FirmwareBuildDate = decodeCString(b[7:19]) - drv.info.Manufacturer = decodeCString(b[19:59]) - drv.info.FirmwareVersion = decodeCString(b[59:79]) + drv.info.FirmwareVersionCode = data[0] + drv.info.MaxContacts = int(data[1]) * 2 + drv.info.MaxGroupChannels = int(data[2]) + drv.info.FirmwareBuildDate = decodeCString(data[7:19]) + drv.info.Manufacturer = decodeCString(data[19:59]) + drv.info.FirmwareVersion = decodeCString(data[59:79]) + + log.Printf("self info: %#+v", drv.info) return } func (drv *companionDriver) poll() { for { - if _, err := drv.wait(); err != nil { - log.Printf("meshcore: companion %s fatal error: %v", drv.info.Name, err) + frame, err := drv.readFrame() + if err != nil { + Logger.Errorf("meshcore: unrecoverable error: %v", err) 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) + } } } } diff --git a/protocol/meshcore/node_const.go b/protocol/meshcore/node_const.go index 3298d25..efe9eff 100644 --- a/protocol/meshcore/node_const.go +++ b/protocol/meshcore/node_const.go @@ -1,5 +1,7 @@ package meshcore +import "fmt" + const ( companionErrCodeUnsupported byte = 1 + iota companionErrCodeNotFound @@ -122,3 +124,56 @@ const ( companionPushContactDeleted 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) +} diff --git a/protocol/meshcore/util.go b/protocol/meshcore/util.go index 657ee5e..c9c6045 100644 --- a/protocol/meshcore/util.go +++ b/protocol/meshcore/util.go @@ -12,8 +12,8 @@ import ( ) type Position struct { - Latitude float64 - Longitude float64 + Latitude float64 `json:"latitude"` + Longitude float64 `json:"longitude"` } func (pos *Position) String() string {