feat: implement RNodeInterface for Reticulum with command handling and device initialization
This commit is contained in:
262
pkg/interfaces/rnode.go
Normal file
262
pkg/interfaces/rnode.go
Normal file
@@ -0,0 +1,262 @@
|
||||
// SPDX-License-Identifier: 0BSD
|
||||
|
||||
package interfaces
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"git.quad4.io/Networks/Reticulum-Go/pkg/common"
|
||||
"git.quad4.io/Networks/Reticulum-Go/pkg/debug"
|
||||
)
|
||||
|
||||
const (
|
||||
RNODE_CMD_DATA = 0x00
|
||||
RNODE_CMD_FREQUENCY = 0x01
|
||||
RNODE_CMD_BANDWIDTH = 0x02
|
||||
RNODE_CMD_TXPOWER = 0x03
|
||||
RNODE_CMD_SF = 0x04
|
||||
RNODE_CMD_CR = 0x05
|
||||
RNODE_CMD_RADIO_STATE = 0x06
|
||||
RNODE_CMD_RADIO_LOCK = 0x07
|
||||
RNODE_CMD_DETECT = 0x08
|
||||
RNODE_CMD_LEAVE = 0x0A
|
||||
RNODE_CMD_ST_ALOCK = 0x0B
|
||||
RNODE_CMD_LT_ALOCK = 0x0C
|
||||
RNODE_CMD_READY = 0x0F
|
||||
RNODE_CMD_STAT_RX = 0x21
|
||||
RNODE_CMD_STAT_TX = 0x22
|
||||
RNODE_CMD_STAT_RSSI = 0x23
|
||||
RNODE_CMD_STAT_SNR = 0x24
|
||||
RNODE_CMD_FW_VERSION = 0x50
|
||||
RNODE_CMD_PLATFORM = 0x48
|
||||
RNODE_CMD_MCU = 0x49
|
||||
|
||||
RNODE_DETECT_REQ = 0x73
|
||||
RNODE_DETECT_RESP = 0x46
|
||||
|
||||
RNODE_RSSI_OFFSET = 157
|
||||
)
|
||||
|
||||
// RNodeInterface represents a Reticulum node interface.
|
||||
type RNodeInterface struct {
|
||||
Interface
|
||||
frequency uint32
|
||||
bandwidth uint32
|
||||
sf uint8
|
||||
cr uint8
|
||||
txPower uint8
|
||||
callback common.PacketCallback
|
||||
|
||||
rFrequency uint32
|
||||
rBandwidth uint32
|
||||
rTXPower uint8
|
||||
rSF uint8
|
||||
rCR uint8
|
||||
rState uint8
|
||||
rDetected bool
|
||||
rMajVer uint8
|
||||
rMinVer uint8
|
||||
|
||||
interfaceReady bool
|
||||
packetQueue [][]byte
|
||||
}
|
||||
|
||||
// NewRNodeInterface creates a new RNodeInterface.
|
||||
func NewRNodeInterface(name string, underlying Interface, freq uint32, bw uint32, sf uint8, cr uint8, txPower uint8) (*RNodeInterface, error) {
|
||||
ri := &RNodeInterface{
|
||||
Interface: underlying,
|
||||
frequency: freq,
|
||||
bandwidth: bw,
|
||||
sf: sf,
|
||||
cr: cr,
|
||||
txPower: txPower,
|
||||
}
|
||||
|
||||
underlying.SetPacketCallback(ri.handleIncoming)
|
||||
return ri, nil
|
||||
}
|
||||
|
||||
// SetPacketCallback sets the packet callback for the RNodeInterface.
|
||||
func (ri *RNodeInterface) SetPacketCallback(cb common.PacketCallback) {
|
||||
ri.callback = cb
|
||||
}
|
||||
|
||||
func (ri *RNodeInterface) handleIncoming(data []byte, ni common.NetworkInterface) {
|
||||
if len(data) < 1 {
|
||||
return
|
||||
}
|
||||
|
||||
cmd := data[0]
|
||||
payload := data[1:]
|
||||
|
||||
switch cmd {
|
||||
case RNODE_CMD_DATA:
|
||||
if ri.callback != nil {
|
||||
ri.callback(payload, ri)
|
||||
}
|
||||
case RNODE_CMD_READY:
|
||||
ri.processQueue()
|
||||
case RNODE_CMD_DETECT:
|
||||
if len(payload) >= 1 && payload[0] == RNODE_DETECT_RESP {
|
||||
ri.rDetected = true
|
||||
}
|
||||
case RNODE_CMD_FW_VERSION:
|
||||
if len(payload) >= 2 {
|
||||
ri.rMajVer = payload[0]
|
||||
ri.rMinVer = payload[1]
|
||||
debug.Log(debug.DEBUG_INFO, "RNode firmware version", "name", ri.GetName(), "version", fmt.Sprintf("%d.%d", ri.rMajVer, ri.rMinVer))
|
||||
}
|
||||
case RNODE_CMD_FREQUENCY:
|
||||
if len(payload) >= 4 {
|
||||
ri.rFrequency = binary.BigEndian.Uint32(payload)
|
||||
}
|
||||
case RNODE_CMD_BANDWIDTH:
|
||||
if len(payload) >= 4 {
|
||||
ri.rBandwidth = binary.BigEndian.Uint32(payload)
|
||||
}
|
||||
case RNODE_CMD_TXPOWER:
|
||||
if len(payload) >= 1 {
|
||||
ri.rTXPower = payload[0]
|
||||
}
|
||||
case RNODE_CMD_SF:
|
||||
if len(payload) >= 1 {
|
||||
ri.rSF = payload[0]
|
||||
}
|
||||
case RNODE_CMD_CR:
|
||||
if len(payload) >= 1 {
|
||||
ri.rCR = payload[0]
|
||||
}
|
||||
case RNODE_CMD_RADIO_STATE:
|
||||
if len(payload) >= 1 {
|
||||
ri.rState = payload[0]
|
||||
}
|
||||
case RNODE_CMD_STAT_RSSI:
|
||||
if len(payload) >= 1 {
|
||||
rssi := int(payload[0]) - RNODE_RSSI_OFFSET
|
||||
debug.Log(debug.DEBUG_VERBOSE, "RNode RSSI", "name", ri.GetName(), "rssi", rssi)
|
||||
}
|
||||
case RNODE_CMD_STAT_SNR:
|
||||
if len(payload) >= 1 {
|
||||
snr := float32(int8(payload[0])) * 0.25
|
||||
debug.Log(debug.DEBUG_VERBOSE, "RNode SNR", "name", ri.GetName(), "snr", snr)
|
||||
}
|
||||
default:
|
||||
debug.Log(debug.DEBUG_ALL, "RNode received command", "cmd", fmt.Sprintf("0x%02x", cmd), "len", len(payload))
|
||||
}
|
||||
}
|
||||
|
||||
func (ri *RNodeInterface) processQueue() {
|
||||
ri.interfaceReady = true
|
||||
if len(ri.packetQueue) > 0 {
|
||||
packet := ri.packetQueue[0]
|
||||
ri.packetQueue = ri.packetQueue[1:]
|
||||
_ = ri.Send(packet, "")
|
||||
}
|
||||
}
|
||||
|
||||
// Start initializes the RNodeInterface and configures device parameters.
|
||||
func (ri *RNodeInterface) Start() error {
|
||||
err := ri.Interface.Start()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
time.Sleep(2 * time.Second)
|
||||
|
||||
if err := ri.detect(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
debug.Log(debug.DEBUG_INFO, "Initializing RNode...", "name", ri.GetName())
|
||||
|
||||
if ri.frequency != 0 {
|
||||
freqBytes := make([]byte, 4)
|
||||
binary.BigEndian.PutUint32(freqBytes, ri.frequency)
|
||||
if err := ri.sendRNodeCommand(RNODE_CMD_FREQUENCY, freqBytes); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if ri.bandwidth != 0 {
|
||||
bwBytes := make([]byte, 4)
|
||||
binary.BigEndian.PutUint32(bwBytes, ri.bandwidth)
|
||||
if err := ri.sendRNodeCommand(RNODE_CMD_BANDWIDTH, bwBytes); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if ri.sf != 0 {
|
||||
if err := ri.sendRNodeCommand(RNODE_CMD_SF, []byte{ri.sf}); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if ri.cr != 0 {
|
||||
if err := ri.sendRNodeCommand(RNODE_CMD_CR, []byte{ri.cr}); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if ri.txPower != 0 {
|
||||
if err := ri.sendRNodeCommand(RNODE_CMD_TXPOWER, []byte{ri.txPower}); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if err := ri.sendRNodeCommand(RNODE_CMD_RADIO_STATE, []byte{0x01}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
ri.interfaceReady = true
|
||||
|
||||
debug.Log(debug.DEBUG_INFO, "RNode initialized", "name", ri.GetName())
|
||||
return nil
|
||||
}
|
||||
|
||||
// detect attempts to detect the RNode device and obtain firmware version.
|
||||
func (ri *RNodeInterface) detect() error {
|
||||
detectCmd := []byte{RNODE_DETECT_REQ}
|
||||
if err := ri.sendRNodeCommand(RNODE_CMD_DETECT, detectCmd); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
start := time.Now()
|
||||
for !ri.rDetected {
|
||||
if time.Since(start) > 2*time.Second {
|
||||
debug.Log(debug.DEBUG_ERROR, "RNode detection timed out", "name", ri.GetName())
|
||||
break
|
||||
}
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
}
|
||||
|
||||
if err := ri.sendRNodeCommand(RNODE_CMD_FW_VERSION, []byte{0x00}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// sendRNodeCommand sends a command to the RNode device.
|
||||
func (ri *RNodeInterface) sendRNodeCommand(cmd byte, data []byte) error {
|
||||
if kissInterface, ok := ri.Interface.(interface {
|
||||
SendKISS(byte, []byte) error
|
||||
}); ok {
|
||||
return kissInterface.SendKISS(cmd, data)
|
||||
}
|
||||
|
||||
frame := make([]byte, 0, len(data)+1)
|
||||
frame = append(frame, cmd)
|
||||
frame = append(frame, data...)
|
||||
return ri.Interface.Send(frame, "")
|
||||
}
|
||||
|
||||
// Send transmits data using the underlying interface.
|
||||
func (ri *RNodeInterface) Send(data []byte, addr string) error {
|
||||
if !ri.interfaceReady {
|
||||
ri.packetQueue = append(ri.packetQueue, data)
|
||||
return nil
|
||||
}
|
||||
return ri.Interface.Send(data, addr)
|
||||
}
|
||||
Reference in New Issue
Block a user