Some checks failed
Bearer / scan (push) Successful in 9s
Go Build Multi-Platform / build (amd64, linux) (push) Successful in 42s
Go Build Multi-Platform / build (amd64, darwin) (push) Successful in 44s
Go Build Multi-Platform / build (arm, freebsd) (push) Successful in 41s
Go Build Multi-Platform / build (arm, windows) (push) Successful in 39s
Go Build Multi-Platform / build (arm64, windows) (push) Successful in 1m8s
Go Build Multi-Platform / build (wasm, js) (push) Successful in 1m6s
TinyGo Build / tinygo-build (tinygo-wasm, tinygo-wasm, reticulum-go.wasm, wasm) (pull_request) Failing after 1m2s
TinyGo Build / tinygo-build (tinygo-build, tinygo-default, reticulum-go-tinygo, ) (pull_request) Failing after 1m4s
Go Revive Lint / lint (push) Successful in 1m4s
Go Test Multi-Platform / Test (ubuntu-latest, arm64) (push) Successful in 1m24s
Run Gosec / tests (push) Successful in 1m29s
Go Test Multi-Platform / Test (ubuntu-latest, amd64) (push) Successful in 2m31s
Go Build Multi-Platform / build (amd64, freebsd) (push) Successful in 9m28s
Go Build Multi-Platform / build (arm, linux) (push) Successful in 9m28s
Go Build Multi-Platform / build (amd64, windows) (push) Successful in 9m30s
Go Build Multi-Platform / build (arm64, darwin) (push) Successful in 9m27s
Go Build Multi-Platform / build (arm64, linux) (push) Successful in 9m26s
Go Build Multi-Platform / build (arm64, freebsd) (push) Successful in 9m29s
Go Build Multi-Platform / Create Release (push) Has been skipped
305 lines
6.7 KiB
Go
305 lines
6.7 KiB
Go
// SPDX-License-Identifier: 0BSD
|
|
// Copyright (c) 2024-2026 Sudo-Ivan / Quad4.io
|
|
package channel
|
|
|
|
import (
|
|
"errors"
|
|
"math"
|
|
"sync"
|
|
"time"
|
|
|
|
"git.quad4.io/Networks/Reticulum-Go/pkg/common"
|
|
"git.quad4.io/Networks/Reticulum-Go/pkg/debug"
|
|
"git.quad4.io/Networks/Reticulum-Go/pkg/transport"
|
|
)
|
|
|
|
const (
|
|
// Window sizes and thresholds
|
|
WindowInitial = 2
|
|
WindowMin = 2
|
|
WindowMinSlow = 2
|
|
WindowMinMedium = 5
|
|
WindowMinFast = 16
|
|
WindowMaxSlow = 5
|
|
WindowMaxMedium = 12
|
|
WindowMaxFast = 48
|
|
WindowMax = WindowMaxFast
|
|
WindowFlexibility = 4
|
|
|
|
// RTT thresholds
|
|
RTTFast = 0.18
|
|
RTTMedium = 0.75
|
|
RTTSlow = 1.45
|
|
|
|
// Sequence numbers
|
|
SeqMax uint16 = 0xFFFF
|
|
SeqModulus uint16 = SeqMax
|
|
|
|
FastRateThreshold = 10
|
|
|
|
// Timeout calculation constants
|
|
RTTMinThreshold = 0.025
|
|
TimeoutBaseMultiplier = 1.5
|
|
TimeoutRingMultiplier = 2.5
|
|
TimeoutRingOffset = 2
|
|
|
|
// Packet header constants
|
|
ChannelHeaderSize = 6
|
|
ChannelHeaderBits = 8
|
|
|
|
// Default retry count
|
|
DefaultMaxTries = 3
|
|
)
|
|
|
|
// MessageState represents the state of a message
|
|
type MessageState int
|
|
|
|
const (
|
|
MsgStateNew MessageState = iota
|
|
MsgStateSent
|
|
MsgStateDelivered
|
|
MsgStateFailed
|
|
)
|
|
|
|
// MessageBase defines the interface for messages that can be sent over a channel
|
|
type MessageBase interface {
|
|
Pack() ([]byte, error)
|
|
Unpack([]byte) error
|
|
GetType() uint16
|
|
}
|
|
|
|
// Channel manages reliable message delivery over a transport link
|
|
type Channel struct {
|
|
link transport.LinkInterface
|
|
mutex sync.RWMutex
|
|
txRing []*Envelope
|
|
rxRing []*Envelope
|
|
window int
|
|
windowMax int
|
|
windowMin int
|
|
windowFlex int
|
|
nextSequence uint16
|
|
nextRxSequence uint16
|
|
maxTries int
|
|
fastRateRounds int
|
|
medRateRounds int
|
|
messageHandlers []messageHandlerEntry
|
|
nextHandlerID int
|
|
}
|
|
|
|
type messageHandlerEntry struct {
|
|
id int
|
|
handler func(MessageBase) bool
|
|
}
|
|
|
|
// Envelope wraps a message with metadata for transmission
|
|
type Envelope struct {
|
|
Sequence uint16
|
|
Message MessageBase
|
|
Raw []byte
|
|
Packet interface{}
|
|
Tries int
|
|
Timestamp time.Time
|
|
}
|
|
|
|
// NewChannel creates a new Channel instance
|
|
func NewChannel(link transport.LinkInterface) *Channel {
|
|
return &Channel{
|
|
link: link,
|
|
messageHandlers: make([]messageHandlerEntry, 0),
|
|
mutex: sync.RWMutex{},
|
|
windowMax: WindowMaxSlow,
|
|
windowMin: WindowMinSlow,
|
|
window: WindowInitial,
|
|
maxTries: DefaultMaxTries,
|
|
}
|
|
}
|
|
|
|
// Send transmits a message over the channel
|
|
func (c *Channel) Send(msg MessageBase) error {
|
|
if c.link.GetStatus() != transport.STATUS_ACTIVE {
|
|
return errors.New("link not ready")
|
|
}
|
|
|
|
env := &Envelope{
|
|
Sequence: c.nextSequence,
|
|
Message: msg,
|
|
Timestamp: time.Now(),
|
|
}
|
|
|
|
c.mutex.Lock()
|
|
c.nextSequence = (c.nextSequence + common.ONE) % SeqModulus
|
|
c.txRing = append(c.txRing, env)
|
|
c.mutex.Unlock()
|
|
|
|
data, err := msg.Pack()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
env.Raw = data
|
|
packet := c.link.Send(data)
|
|
env.Packet = packet
|
|
env.Tries++
|
|
|
|
timeout := c.getPacketTimeout(env.Tries)
|
|
c.link.SetPacketTimeout(packet, c.handleTimeout, timeout)
|
|
c.link.SetPacketDelivered(packet, c.handleDelivered)
|
|
|
|
return nil
|
|
}
|
|
|
|
// handleTimeout handles packet timeout events
|
|
func (c *Channel) handleTimeout(packet interface{}) {
|
|
c.mutex.Lock()
|
|
defer c.mutex.Unlock()
|
|
|
|
for _, env := range c.txRing {
|
|
if env.Packet == packet {
|
|
if env.Tries >= c.maxTries {
|
|
// Remove from ring and notify failure
|
|
return
|
|
}
|
|
env.Tries++
|
|
if err := c.link.Resend(packet); err != nil { // #nosec G104
|
|
// Handle resend error, e.g., log it or mark envelope as failed
|
|
debug.Log(debug.DEBUG_INFO, "Failed to resend packet", "error", err)
|
|
// Optionally, mark the envelope as failed or remove it from txRing
|
|
// env.State = MsgStateFailed
|
|
// c.txRing = append(c.txRing[:i], c.txRing[i+1:]...)
|
|
return
|
|
}
|
|
timeout := c.getPacketTimeout(env.Tries)
|
|
c.link.SetPacketTimeout(packet, c.handleTimeout, timeout)
|
|
break
|
|
}
|
|
}
|
|
}
|
|
|
|
// handleDelivered handles packet delivery confirmations
|
|
func (c *Channel) handleDelivered(packet interface{}) {
|
|
c.mutex.Lock()
|
|
defer c.mutex.Unlock()
|
|
|
|
for i, env := range c.txRing {
|
|
if env.Packet == packet {
|
|
c.txRing = append(c.txRing[:i], c.txRing[i+1:]...)
|
|
break
|
|
}
|
|
}
|
|
}
|
|
|
|
func (c *Channel) getPacketTimeout(tries int) time.Duration {
|
|
rtt := c.link.GetRTT()
|
|
if rtt < RTTMinThreshold {
|
|
rtt = RTTMinThreshold
|
|
}
|
|
|
|
timeout := math.Pow(TimeoutBaseMultiplier, float64(tries-common.ONE)) * rtt * TimeoutRingMultiplier * float64(len(c.txRing)+TimeoutRingOffset)
|
|
return time.Duration(timeout * float64(time.Second))
|
|
}
|
|
|
|
func (c *Channel) AddMessageHandler(handler func(MessageBase) bool) int {
|
|
c.mutex.Lock()
|
|
defer c.mutex.Unlock()
|
|
id := c.nextHandlerID
|
|
c.nextHandlerID++
|
|
c.messageHandlers = append(c.messageHandlers, messageHandlerEntry{id: id, handler: handler})
|
|
return id
|
|
}
|
|
|
|
func (c *Channel) RemoveMessageHandler(id int) {
|
|
c.mutex.Lock()
|
|
defer c.mutex.Unlock()
|
|
for i, entry := range c.messageHandlers {
|
|
if entry.id == id {
|
|
c.messageHandlers = append(c.messageHandlers[:i], c.messageHandlers[i+1:]...)
|
|
break
|
|
}
|
|
}
|
|
}
|
|
|
|
func (c *Channel) updateRateThresholds() {
|
|
rtt := c.link.RTT()
|
|
|
|
if rtt > RTTFast {
|
|
c.fastRateRounds = common.ZERO
|
|
|
|
if rtt > RTTMedium {
|
|
c.medRateRounds = common.ZERO
|
|
} else {
|
|
c.medRateRounds++
|
|
if c.windowMax < WindowMaxMedium && c.medRateRounds == FastRateThreshold {
|
|
c.windowMax = WindowMaxMedium
|
|
c.windowMin = WindowMinMedium
|
|
}
|
|
}
|
|
} else {
|
|
c.fastRateRounds++
|
|
if c.windowMax < WindowMaxFast && c.fastRateRounds == FastRateThreshold {
|
|
c.windowMax = WindowMaxFast
|
|
c.windowMin = WindowMinFast
|
|
}
|
|
}
|
|
}
|
|
|
|
func (c *Channel) HandleInbound(data []byte) error {
|
|
if len(data) < ChannelHeaderSize {
|
|
return errors.New("channel packet too short")
|
|
}
|
|
|
|
msgType := uint16(data[0])<<ChannelHeaderBits | uint16(data[1])
|
|
sequence := uint16(data[2])<<ChannelHeaderBits | uint16(data[3])
|
|
length := uint16(data[4])<<ChannelHeaderBits | uint16(data[5])
|
|
|
|
if len(data) < ChannelHeaderSize+int(length) {
|
|
return errors.New("channel packet incomplete")
|
|
}
|
|
|
|
msgData := data[ChannelHeaderSize : ChannelHeaderSize+length]
|
|
|
|
c.mutex.Lock()
|
|
defer c.mutex.Unlock()
|
|
|
|
for _, entry := range c.messageHandlers {
|
|
if entry.handler != nil {
|
|
msg := &GenericMessage{
|
|
Type: msgType,
|
|
Data: msgData,
|
|
Seq: sequence,
|
|
}
|
|
if entry.handler(msg) {
|
|
break
|
|
}
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
type GenericMessage struct {
|
|
Type uint16
|
|
Data []byte
|
|
Seq uint16
|
|
}
|
|
|
|
func (g *GenericMessage) Pack() ([]byte, error) {
|
|
return g.Data, nil
|
|
}
|
|
|
|
func (g *GenericMessage) Unpack(data []byte) error {
|
|
g.Data = data
|
|
return nil
|
|
}
|
|
|
|
func (g *GenericMessage) GetType() uint16 {
|
|
return g.Type
|
|
}
|
|
|
|
func (c *Channel) Close() error {
|
|
c.mutex.Lock()
|
|
defer c.mutex.Unlock()
|
|
// Cleanup resources
|
|
return nil
|
|
}
|