Added MeshCore repeater protocol
This commit is contained in:
420
protocol/meshcore/node_repeater.go
Normal file
420
protocol/meshcore/node_repeater.go
Normal file
@@ -0,0 +1,420 @@
|
||||
package meshcore
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"io"
|
||||
"math"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"git.maze.io/go/ham/protocol"
|
||||
"git.maze.io/go/ham/radio"
|
||||
)
|
||||
|
||||
type repeaterDriver struct {
|
||||
conn io.ReadWriteCloser
|
||||
packets chan *Packet
|
||||
rawPackets chan *protocol.Packet
|
||||
waiting chan *repeaterDriverWaiting
|
||||
hasSNR bool
|
||||
lastFrame []byte
|
||||
lastFrameAt time.Time
|
||||
info repeaterInfo
|
||||
err error
|
||||
}
|
||||
|
||||
type repeaterDriverWaiting struct {
|
||||
response chan string
|
||||
err chan error
|
||||
}
|
||||
|
||||
func newRepeaterDriverWaiting() *repeaterDriverWaiting {
|
||||
return &repeaterDriverWaiting{
|
||||
response: make(chan string),
|
||||
err: make(chan error),
|
||||
}
|
||||
}
|
||||
|
||||
func (wait *repeaterDriverWaiting) Respond(response string) {
|
||||
select {
|
||||
case wait.response <- response:
|
||||
default:
|
||||
Logger.Warnf("meshcore: waiting discard response: %q", response)
|
||||
}
|
||||
}
|
||||
|
||||
func (wait *repeaterDriverWaiting) Error(err error) {
|
||||
select {
|
||||
case wait.err <- err:
|
||||
default:
|
||||
Logger.Warnf("meshcore: waiting discard error: %q", err)
|
||||
}
|
||||
}
|
||||
|
||||
func (wait *repeaterDriverWaiting) Close() {
|
||||
Logger.Trace("meshcore: waiting closing")
|
||||
close(wait.response)
|
||||
close(wait.err)
|
||||
}
|
||||
|
||||
func (wait *repeaterDriverWaiting) Wait() (string, error) {
|
||||
Logger.Trace("meshcore: waiting for response")
|
||||
select {
|
||||
case err := <-wait.err:
|
||||
Logger.Tracef("meshcore: waiting received error: %v", err)
|
||||
return "", err
|
||||
case response := <-wait.response:
|
||||
Logger.Tracef("meshcore: waiting received response: %d", len(response))
|
||||
return response, nil
|
||||
}
|
||||
}
|
||||
|
||||
type repeaterInfo struct {
|
||||
// Fields returns by CMD_APP_START.
|
||||
Type NodeType
|
||||
Power byte // in dBm
|
||||
MaxPower byte // in dBm
|
||||
PublicKey [32]byte
|
||||
Latitude float64
|
||||
Longitude float64
|
||||
HasMultiACKs bool
|
||||
AdvertLocationPolicy byte
|
||||
TelemetryFlags byte
|
||||
ManualAddContacts byte
|
||||
Frequency float64 // in MHz
|
||||
Bandwidth float64 // in kHz
|
||||
SpreadingFactor byte
|
||||
CodingRate byte
|
||||
Name string
|
||||
|
||||
// Fields returns by CMD_DEVICE_QUERY.
|
||||
FirmwareVersion string
|
||||
FirmwareVersionCode byte
|
||||
FirmwareBuildDate string
|
||||
Manufacturer string
|
||||
}
|
||||
|
||||
func newRepeaterDriver(conn io.ReadWriteCloser, hasSNR bool) *repeaterDriver {
|
||||
return &repeaterDriver{
|
||||
conn: conn,
|
||||
waiting: make(chan *repeaterDriverWaiting, 16),
|
||||
hasSNR: hasSNR,
|
||||
}
|
||||
}
|
||||
|
||||
func (drv *repeaterDriver) Close() error {
|
||||
return drv.conn.Close()
|
||||
}
|
||||
|
||||
func (drv *repeaterDriver) Setup() (err error) {
|
||||
go drv.poll()
|
||||
if err = drv.queryDeviceInfo(); err != nil {
|
||||
return err
|
||||
}
|
||||
return drv.err
|
||||
}
|
||||
|
||||
func (drv *repeaterDriver) Info() *radio.Info {
|
||||
var (
|
||||
firmwareVersion = drv.info.FirmwareVersion
|
||||
firmwareDate time.Time
|
||||
)
|
||||
if strings.Contains(firmwareVersion, "(Build: ") {
|
||||
// "v1.13.0 (Build: 15 Feb 2026)"
|
||||
part := strings.SplitN(firmwareVersion, "(Build: ", 2)
|
||||
firmwareVersion = strings.TrimSpace(part[0])
|
||||
firmwareDate, _ = time.Parse("2 Jan 2006", strings.TrimRight(part[1], ")"))
|
||||
}
|
||||
|
||||
var (
|
||||
manufacturerPart = strings.SplitN(drv.info.Manufacturer, " ", 2)
|
||||
manufacturer = manufacturerPart[0]
|
||||
device string
|
||||
)
|
||||
if len(manufacturerPart) > 1 {
|
||||
device = manufacturerPart[1]
|
||||
}
|
||||
|
||||
var pos *radio.Position
|
||||
if drv.info.Latitude != 0 && drv.info.Longitude != 0 {
|
||||
pos = &radio.Position{
|
||||
Latitude: drv.info.Latitude,
|
||||
Longitude: drv.info.Longitude,
|
||||
}
|
||||
}
|
||||
|
||||
return &radio.Info{
|
||||
Name: drv.info.Name,
|
||||
Manufacturer: manufacturer,
|
||||
Device: device,
|
||||
FirmwareVersion: firmwareVersion,
|
||||
FirmwareDate: firmwareDate,
|
||||
Modulation: protocol.LoRa,
|
||||
Position: pos,
|
||||
Frequency: math.Ceil(drv.info.Frequency*1000) / 1000,
|
||||
Bandwidth: drv.info.Bandwidth,
|
||||
Power: float64(drv.info.Power),
|
||||
LoRaSF: drv.info.SpreadingFactor,
|
||||
LoRaCR: drv.info.CodingRate,
|
||||
}
|
||||
}
|
||||
|
||||
func (drv *repeaterDriver) Packets() <-chan *Packet {
|
||||
if drv.packets == nil {
|
||||
drv.packets = make(chan *Packet, 16)
|
||||
}
|
||||
return drv.packets
|
||||
}
|
||||
|
||||
func (drv *repeaterDriver) RawPackets() <-chan *protocol.Packet {
|
||||
if drv.rawPackets == nil {
|
||||
drv.rawPackets = make(chan *protocol.Packet, 16)
|
||||
}
|
||||
return drv.rawPackets
|
||||
}
|
||||
|
||||
func (drv *repeaterDriver) queryDeviceInfo() (err error) {
|
||||
if drv.info.FirmwareVersion, err = drv.writeCommand("ver"); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// Fetch name
|
||||
if drv.info.Name, err = drv.writeCommand("get", "name"); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
var line string
|
||||
|
||||
// Fetch frequency, bandwidth and LoRa settings
|
||||
if line, err = drv.writeCommand("get", "radio"); err != nil {
|
||||
return
|
||||
}
|
||||
if _, err = fmt.Sscanf(line, "%f,%f,%d,%d", &drv.info.Frequency, &drv.info.Bandwidth, &drv.info.SpreadingFactor, &drv.info.CodingRate); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Fetch tx power
|
||||
if line, err = drv.writeCommand("get", "tx"); err != nil {
|
||||
return
|
||||
}
|
||||
if _, err = fmt.Sscanf(line, "%d", &drv.info.Power); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// Fetch location
|
||||
if line, err = drv.writeCommand("get", "lat"); err != nil {
|
||||
return
|
||||
}
|
||||
if line != "0.0" {
|
||||
drv.info.Latitude, _ = strconv.ParseFloat(line, 64)
|
||||
if line, err = drv.writeCommand("get", "lon"); err != nil {
|
||||
return
|
||||
}
|
||||
drv.info.Longitude, _ = strconv.ParseFloat(line, 64)
|
||||
}
|
||||
|
||||
// Fetch node type
|
||||
if line, err = drv.writeCommand("get", "role"); err != nil {
|
||||
return err
|
||||
}
|
||||
switch line {
|
||||
case "repeater":
|
||||
drv.info.Type = Repeater
|
||||
case "room_server":
|
||||
drv.info.Type = Room
|
||||
case "sensor":
|
||||
drv.info.Type = Sensor
|
||||
}
|
||||
|
||||
// Fetch board type
|
||||
if drv.info.Manufacturer, err = drv.writeCommand("board"); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
func (drv *repeaterDriver) writeCommand(command string, args ...string) (response string, err error) {
|
||||
full := append([]string{command}, args...)
|
||||
if _, err = fmt.Fprintf(drv.conn, "%s\r\n", strings.Join(full, " ")); err != nil {
|
||||
return
|
||||
}
|
||||
return drv.wait()
|
||||
}
|
||||
|
||||
func (drv *repeaterDriver) wait() (string, error) {
|
||||
wait := newRepeaterDriverWaiting()
|
||||
defer wait.Close()
|
||||
|
||||
drv.waiting <- wait
|
||||
return wait.Wait()
|
||||
}
|
||||
|
||||
func (drv *repeaterDriver) handleRXData(line string) {
|
||||
part := strings.SplitN(line, " RAW: ", 2)
|
||||
if len(part) == 1 || len(part[0]) < 21 {
|
||||
return
|
||||
}
|
||||
|
||||
b, err := hex.DecodeString(part[1])
|
||||
if err != nil {
|
||||
Logger.Warnf("meshcore: corrupt raw packet: %v", err)
|
||||
return
|
||||
} else if len(b) < 2 {
|
||||
return // nothing to do!
|
||||
}
|
||||
|
||||
when, err := time.Parse("15:04:05 - 02/3/2006", strings.TrimSpace(line[:21]))
|
||||
if err != nil {
|
||||
Logger.Warnf("meshcore: corrupt raw packet: %v", err)
|
||||
}
|
||||
|
||||
var (
|
||||
snr float64
|
||||
rssi int8
|
||||
data []byte
|
||||
)
|
||||
if drv.hasSNR {
|
||||
snr = float64(b[0]) / 4
|
||||
rssi = int8(b[1])
|
||||
data = b[2:]
|
||||
if drv.rawPackets != nil {
|
||||
select {
|
||||
case drv.rawPackets <- &protocol.Packet{
|
||||
Time: when,
|
||||
Protocol: protocol.MeshCore,
|
||||
SNR: snr,
|
||||
RSSI: rssi,
|
||||
Raw: data,
|
||||
}:
|
||||
default:
|
||||
Logger.Warn("meshcore: raw packet channel full, dropping packet")
|
||||
}
|
||||
}
|
||||
|
||||
// Decode payload
|
||||
if drv.packets != nil {
|
||||
packet := new(Packet)
|
||||
if err := packet.UnmarshalBytes(data); err == nil {
|
||||
packet.SNR = snr
|
||||
packet.RSSI = rssi
|
||||
select {
|
||||
case drv.packets <- packet:
|
||||
default:
|
||||
Logger.Warn("meshcore: packet channel full, dropping packet")
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Record last frame
|
||||
drv.lastFrame = b
|
||||
drv.lastFrameAt = when
|
||||
}
|
||||
}
|
||||
|
||||
func (drv *repeaterDriver) handleRX(line string) {
|
||||
if drv.hasSNR || len(drv.lastFrame) == 0 {
|
||||
return // nothing to do here
|
||||
}
|
||||
|
||||
var snr, rssi int
|
||||
// "11:08:55 - 15/5/2024 U: RX, len=74 (type=5, route=F, payload_len=67) SNR=12 RSSI=-94 score=1000 time=738 hash=C46CD3BB7D0D99E1"
|
||||
for _, part := range strings.Split(line, " ") {
|
||||
switch {
|
||||
case strings.HasPrefix(part, "SNR="):
|
||||
snr, _ = strconv.Atoi(part[3:])
|
||||
case strings.HasPrefix(part, "RSSI="):
|
||||
rssi, _ = strconv.Atoi(part[4:])
|
||||
}
|
||||
}
|
||||
|
||||
if drv.rawPackets != nil {
|
||||
select {
|
||||
case drv.rawPackets <- &protocol.Packet{
|
||||
Time: drv.lastFrameAt,
|
||||
Protocol: protocol.MeshCore,
|
||||
SNR: float64(snr),
|
||||
RSSI: int8(rssi),
|
||||
Raw: drv.lastFrame,
|
||||
}:
|
||||
default:
|
||||
Logger.Warn("meshcore: raw packet channel full, dropping packet")
|
||||
}
|
||||
}
|
||||
|
||||
// Decode payload
|
||||
if drv.packets != nil {
|
||||
packet := new(Packet)
|
||||
if err := packet.UnmarshalBytes(drv.lastFrame); err == nil {
|
||||
packet.SNR = float64(snr)
|
||||
packet.RSSI = int8(rssi)
|
||||
select {
|
||||
case drv.packets <- packet:
|
||||
default:
|
||||
Logger.Warn("meshcore: packet channel full, dropping packet")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Clear state
|
||||
drv.lastFrame = nil
|
||||
}
|
||||
|
||||
func (drv *repeaterDriver) poll() {
|
||||
r := bufio.NewReader(drv.conn)
|
||||
for {
|
||||
line, err := r.ReadString('\n')
|
||||
if err != nil {
|
||||
Logger.Errorf("meshcore: unrecoverable error: %v", err)
|
||||
drv.err = err
|
||||
return
|
||||
}
|
||||
|
||||
line = strings.TrimSpace(line)
|
||||
|
||||
Logger.Tracef("meshcore: handle %q", line)
|
||||
switch {
|
||||
case strings.HasPrefix(line, "-> "):
|
||||
select {
|
||||
case waiting := <-drv.waiting:
|
||||
waiting.Respond(strings.TrimPrefix(line[3:], "> "))
|
||||
default:
|
||||
Logger.Warnf("meshcore: unhandled response %q", line[3:])
|
||||
}
|
||||
|
||||
case strings.Contains(line, " RAW: "):
|
||||
// "10:53:04 - 15/5/2024 U RAW: 0917081DE1B03B130A270E01233EA0FB261218CCABBAA02278F51E97585B9B3285B95EFEEC83BE91E3D1E4F79D88B2C9484AA6882EB217C992B5C3C99C"
|
||||
drv.handleRXData(line)
|
||||
|
||||
case strings.Contains(line, ": RX,"):
|
||||
drv.handleRX(line)
|
||||
|
||||
case strings.Contains(line, ": TX,"):
|
||||
// ignore (for now)
|
||||
|
||||
default:
|
||||
Logger.Tracef("meshcore: repeater sent gibberish %q", line)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (drv *repeaterDriver) parsePacket(line string) (time.Time, string, error) {
|
||||
// "11:08:38 - 15/5/2024 U: TX, len=124 (type=4, route=D, payload_len=122)"
|
||||
if len(line) < 22 {
|
||||
return time.Time{}, "", io.EOF
|
||||
}
|
||||
|
||||
i := strings.IndexByte(line, ':') - 2
|
||||
if i < 0 {
|
||||
return time.Time{}, "", io.EOF
|
||||
}
|
||||
|
||||
t, err := time.Parse("15:04:05 - 02/3/2006", line[:i])
|
||||
if err != nil {
|
||||
return time.Time{}, "", err
|
||||
}
|
||||
return t, line[i:], nil
|
||||
}
|
||||
Reference in New Issue
Block a user