Reticulum-Go/pkg/interfaces/announce_queue.go

166 lines
4.5 KiB
Go

// SPDX-License-Identifier: Apache-2.0
// Copyright (c) 2024-2026 Quad4.io
package interfaces
import (
"time"
"quad4/reticulum-go/pkg/debug"
)
// OutgoingAnnounceQueue is the optional per-interface delayed TX list used
// when announce_cap would be exceeded. Matches Python Interface.announce_queue.
type OutgoingAnnounceQueue interface {
ShouldQueueAnnounce() bool
QueueOutgoingAnnounce(raw []byte, dest []byte, hops byte, emitted uint32)
NoteAnnounceSent(nbytes int)
AnnounceQueueLen() int
DropAnnounceQueue() int
NextOutgoingAnnounce() ([]byte, bool)
}
type queuedAnnounce struct {
dest [16]byte
queued time.Time
hops byte
emitted uint32
raw []byte
}
// SetAnnounceCap stores announce_cap from config as a fraction of bitrate.
// percent 0 keeps the 2 percent default. Values are config percents (1 to 100).
func (i *BaseInterface) SetAnnounceCap(percent float64) {
i.Mutex.Lock()
defer i.Mutex.Unlock()
if percent <= 0 {
i.announceCap = 0
return
}
i.announceCap = percent / 100.0
}
func (i *BaseInterface) announceCapFraction() float64 {
if i.announceCap > 0 {
return i.announceCap
}
return DefaultAnnounceCapFraction
}
// ShouldQueueAnnounce reports whether a forwarded announce must wait.
// Local origin (hops 0) is decided by the caller, matching Python Transport.outbound.
func (i *BaseInterface) ShouldQueueAnnounce() bool {
i.Mutex.RLock()
defer i.Mutex.RUnlock()
if len(i.announceQueue) > 0 {
return true
}
if i.Bitrate <= 0 {
return true
}
return time.Now().Before(i.announceAllowedAt)
}
// QueueOutgoingAnnounce stores a forwarded announce for later TX.
// Same-destination entries keep the first unless emitted is strictly newer.
// Drops when the queue is already at MaxQueuedAnnounces.
func (i *BaseInterface) QueueOutgoingAnnounce(raw []byte, dest []byte, hops byte, emitted uint32) {
if i == nil || len(raw) == 0 {
return
}
entry := queuedAnnounce{
queued: time.Now(),
hops: hops,
emitted: emitted,
raw: append([]byte(nil), raw...),
}
copy(entry.dest[:], dest)
i.Mutex.Lock()
defer i.Mutex.Unlock()
if len(i.announceQueue) >= MaxQueuedAnnounces {
return
}
for idx := range i.announceQueue {
if i.announceQueue[idx].dest == entry.dest {
if emitted > i.announceQueue[idx].emitted {
i.announceQueue[idx] = entry
}
return
}
}
i.announceQueue = append(i.announceQueue, entry)
if debug.Enabled(debug.DebugVerbose) {
debug.Log(debug.DebugVerbose, "Queued announce for later transmission",
"name", i.Name, "queue", len(i.announceQueue), "hops", hops)
}
}
// NoteAnnounceSent records announce_cap airtime after an immediate send.
func (i *BaseInterface) NoteAnnounceSent(nbytes int) {
i.Mutex.Lock()
defer i.Mutex.Unlock()
i.noteAnnounceSentLocked(nbytes)
}
func (i *BaseInterface) noteAnnounceSentLocked(nbytes int) {
capFrac := i.announceCapFraction()
if i.Bitrate <= 0 || nbytes <= 0 || capFrac <= 0 {
return
}
txTime := (float64(nbytes) * 8) / float64(i.Bitrate)
wait := txTime / capFrac
i.announceAllowedAt = time.Now().Add(time.Duration(wait * float64(time.Second)))
}
// AnnounceQueueLen is the number of announces waiting for announce_cap.
func (i *BaseInterface) AnnounceQueueLen() int {
i.Mutex.RLock()
defer i.Mutex.RUnlock()
return len(i.announceQueue)
}
// DropAnnounceQueue discards queued outgoing announces and returns how many were dropped.
func (i *BaseInterface) DropAnnounceQueue() int {
i.Mutex.Lock()
defer i.Mutex.Unlock()
n := len(i.announceQueue)
i.announceQueue = nil
return n
}
// NextOutgoingAnnounce pops the next announce that may be sent now.
// Prefer fewer hops, then older queue time. Returns false when empty, stale-only, or still capped.
func (i *BaseInterface) NextOutgoingAnnounce() ([]byte, bool) {
i.Mutex.Lock()
defer i.Mutex.Unlock()
now := time.Now()
kept := i.announceQueue[:0]
for _, e := range i.announceQueue {
if now.Sub(e.queued) > QueuedAnnounceLife {
continue
}
kept = append(kept, e)
}
i.announceQueue = kept
if len(i.announceQueue) == 0 {
return nil, false
}
if now.Before(i.announceAllowedAt) || i.Bitrate <= 0 {
return nil, false
}
sel := 0
for idx := 1; idx < len(i.announceQueue); idx++ {
a := i.announceQueue[idx]
b := i.announceQueue[sel]
if a.hops < b.hops || (a.hops == b.hops && a.queued.Before(b.queued)) {
sel = idx
}
}
chosen := i.announceQueue[sel]
i.announceQueue = append(i.announceQueue[:sel], i.announceQueue[sel+1:]...)
i.noteAnnounceSentLocked(len(chosen.raw))
return append([]byte(nil), chosen.raw...), true
}