From cbf2eaea78a9b2963970c49347e3068ab9a5bd9a Mon Sep 17 00:00:00 2001 From: Sudo-Ivan Date: Thu, 1 Jan 2026 00:59:39 -0600 Subject: [PATCH] feat: implement RNodeInterface for Reticulum with command handling and device initialization --- pkg/interfaces/rnode.go | 262 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 262 insertions(+) create mode 100644 pkg/interfaces/rnode.go diff --git a/pkg/interfaces/rnode.go b/pkg/interfaces/rnode.go new file mode 100644 index 0000000..2096455 --- /dev/null +++ b/pkg/interfaces/rnode.go @@ -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) +}