Documentation
¶
Overview ¶
Package txgovernor is graywolf's centralized transmit governor. All transmit sources — KISS, AGW, beacons, digipeater, iGate IS→RF — funnel through a single Governor before frames reach the Rust modem. It enforces:
- Per-channel rate limits (packets/min and packets/5min, sliding window)
- Deduplication keyed on (dest + source + info) within a configurable window (default 30s) across all input sources
- Priority queue (beacons < digipeated < KISS/AGW < iGate message) so higher-priority traffic preempts lower-priority traffic
- DCD-aware timing: before sending, wait until the radio channel is clear (DCD low) and run a p-persistence / slot-time CSMA decision
Index ¶
- Constants
- Variables
- type ChannelTiming
- type Config
- type Governor
- func (g *Governor) AddTxHook(h TxHook) (id uint64, unregister func())
- func (g *Governor) QueueLen() int
- func (g *Governor) Run(ctx context.Context) error
- func (g *Governor) SetChannelTiming(channel uint32, t ChannelTiming)
- func (g *Governor) SetSkipCSMA(fn func(channel uint32) bool)
- func (g *Governor) Stats() Stats
- func (g *Governor) Submit(ctx context.Context, channel uint32, frame *ax25.Frame, src SubmitSource) error
- type Sender
- type Stats
- type SubmitSource
- type TxHook
- type TxHookRegistry
- type TxSink
Constants ¶
const ( PriorityBeacon = ax25.PriorityBeacon PriorityDigipeated = ax25.PriorityDigipeated PriorityClient = ax25.PriorityClient // KISS/AGW client-originated PriorityAX25Conn = ax25.PriorityAX25Conn PriorityIGateMsg = ax25.PriorityIGateMsg )
Priority levels. Higher value = sent sooner. These re-export the canonical constants defined in pkg/ax25 so existing call sites keep working; the ax25 package owns the truth so pkg/kiss, pkg/agw, and pkg/aprs can reference the same values without importing txgovernor.
Variables ¶
var ( // ErrQueueFull is returned when Submit cannot enqueue because the // governor's pending queue has reached QueueCapacity. Back-pressure // — retry later with a fresher context. ErrQueueFull = errors.New("txgovernor: queue full") // ErrStopped is returned when Submit is called after the governor's // Run loop has exited (context cancelled). Terminal — do not retry. ErrStopped = errors.New("txgovernor: closed") // ErrNilFrame is returned when Submit is called with a nil *Frame. // Caller bug; should never happen at runtime. ErrNilFrame = errors.New("txgovernor: nil frame") )
Sentinel errors returned by Submit. Callers can errors.Is these to classify drops without substring-matching error text. Keep this list closed: new error kinds get their own exported sentinel so callers can always distinguish "queue full" (back-pressure, retry) from "stopped" (terminal, abandon) from "nil frame" (caller bug).
Functions ¶
This section is empty.
Types ¶
type ChannelTiming ¶
type ChannelTiming struct {
SlotTime time.Duration // defaults to 100 ms
Persist uint8 // 0..255, default 63
FullDup bool // skip CSMA entirely
}
ChannelTiming holds the CSMA parameters for one radio channel. Values mirror the tx_timing SQLite row (ms units). TX delay and tail live in ConfigurePtt (hot-reloaded by the bridge) and are not duplicated here.
type Config ¶
type Config struct {
// Sender is the downstream TransmitFrame consumer. Required.
Sender Sender
// DcdEvents is an optional channel of per-channel DCD state changes
// from modembridge. If nil, CSMA falls back to "always clear".
DcdEvents <-chan *pb.DcdChange
// Rate1MinLimit and Rate5MinLimit cap the number of frames transmitted
// per channel in the last 1 and 5 minutes. Zero = unlimited.
Rate1MinLimit int
Rate5MinLimit int
// DedupWindow is the suppression window for identical frames. Default
// 30s if zero.
DedupWindow time.Duration
// QueueCapacity caps the total pending queue. Default 256.
QueueCapacity int
// Channels maps channel number to timing parameters. Missing channels
// use defaults.
Channels map[uint32]ChannelTiming
// Logger is optional.
Logger *slog.Logger
// RandSource allows tests to inject a deterministic random source for
// p-persist decisions. Defaults to time-seeded rand.
RandSource *rand.Rand
}
Config is the Governor's static configuration.
type Governor ¶
type Governor struct {
// contains filtered or unexported fields
}
Governor is the centralized TX scheduler.
func (*Governor) AddTxHook ¶
AddTxHook registers h to be invoked after every successful frame submission. It returns the assigned id (for diagnostics) and an unregister closure that removes h when called. unregister is idempotent: calling it twice is a no-op.
Hooks run inline in the governor's worker goroutine, in registration order. See TxHook for the non-blocking contract — any slow work (disk I/O, network, blocking channel sends) must be pushed to a separate goroutine by the hook itself.
AddTxHook is safe to call at any time, including while the worker loop is invoking hooks. A hook registered concurrently with a send may or may not observe that send; subsequent sends will see it.
func (*Governor) Run ¶
Run executes the worker loop until ctx is cancelled. It consumes DCD events from cfg.DcdEvents, drains the queue, and calls Sender. Blocks.
func (*Governor) SetChannelTiming ¶
func (g *Governor) SetChannelTiming(channel uint32, t ChannelTiming)
SetChannelTiming installs or replaces the timing parameters for one channel under the governor's lock. Safe to call from startup and live-reconfig paths.
func (*Governor) SetSkipCSMA ¶
SetSkipCSMA installs a predicate consulted per frame to decide whether the p-persistence / slot-time / DCD wait should be bypassed for a channel. Wired by the txbackend dispatcher so KISS-only channels short-circuit CSMA (no carrier on TCP). Safe to call from startup; not expected to change after wiring.
Passing nil restores the default "never skip" behaviour.
func (*Governor) Submit ¶
func (g *Governor) Submit(ctx context.Context, channel uint32, frame *ax25.Frame, src SubmitSource) error
Submit enqueues a frame. Deduplicates, rate-checks are deferred to the worker loop so Submit never blocks on the channel. Returns an error if the queue is full, the governor is closed, or the caller's context has already been cancelled. Submit honors ctx so that a caller-imposed deadline (e.g. the iGate's IS->RF timeout) propagates into the governor's accept path and so that a cancelled session cannot be charged a newly enqueued frame.
type Sender ¶
type Sender func(*pb.TransmitFrame) error
Sender transmits one frame to the Rust modem. In production this is modembridge.Bridge.SendTransmitFrame; in tests, a fake.
type Stats ¶
type Stats struct {
Enqueued uint64
Sent uint64
Deduped uint64
RateLimited uint64
QueueDropped uint64
}
Stats exposes counters for metrics.
type SubmitSource ¶
type SubmitSource struct {
Kind string // "kiss" | "agw" | "beacon" | "digipeater" | "igate"
Detail string
Priority int
SkipDedup bool // bypass dedup window (e.g. operator-triggered send-now)
}
SubmitSource describes the origin of a TX request for logging, dedup scoping, and metrics.
type TxHook ¶
type TxHook func(channel uint32, frame *ax25.Frame, source SubmitSource)
TxHook is invoked after a frame has been successfully submitted to the downstream Sender. It runs inline in the governor's worker goroutine, so implementations MUST be fast and non-blocking (packetlog record, counter bumps, channel send with default, etc). A blocking hook stalls the governor's send loop for every registered consumer.
Multiple hooks may be registered concurrently via AddTxHook; they are invoked in registration order on each successful send.
type TxHookRegistry ¶
TxHookRegistry is the narrow hook-registration interface. Callers that only need to register/unregister a TxHook should depend on this interface rather than the concrete *Governor so tests can inject a fake. The returned unregister closure is idempotent.
type TxSink ¶
type TxSink interface {
Submit(ctx context.Context, channel uint32, frame *ax25.Frame, src SubmitSource) error
}
TxSink is implemented by anything that accepts AX.25 frames for transmit scheduling. The canonical implementation is *Governor; callers that need only to submit frames should depend on this interface rather than the concrete type so tests can inject a fake without reaching for a package-local interface copy.
Every production call site funnels through txgovernor.Governor. Previously each caller (kiss, agw, beacon, igate) declared its own TxSink + SubmitSource types duplicating this signature, which meant a change to the governor's Submit contract could silently drift away from the duplicates until a compile error finally surfaced in the adapter layer. Depending on this type eliminates that risk.