Transport classes & FEC opt-in · default off
On a wide link, fairness is free. On a 64 kbit/s bearer shared by a command channel and a video stream, something decides who starves — and if you didn't decide, the queue did. The transport-class path makes that decision explicit: classify traffic, give every class a policy, and let the narrow moments degrade the right things first. With [qos] enabled = false (the default) none of this machinery is even constructed; the data path is byte-identical to the legacy build.
Six classes, one honest queue
Traffic is sorted into Control, Voice, Position, Telemetry, Video and Bulk — highest priority first — by a classifier that needs no application changes: DSCP marks win when present (CS6/CS7 → Control, EF → Voice, AF41 → Video, CS1 → Bulk), well-known ports fill the gaps (MAVLink's UDP 14550/14551 → Control, RTP ranges → Video), and everything else defaults sanely (TCP → Bulk). Class and reliability mode ride in the existing inner-header priority byte, so QoS-enabled and legacy nodes stay wire-compatible.
The queue is strict-priority with two safeties: per-class reserved-rate floors so even Bulk trickles rather than starving outright, and a byte-budget AQM (backlog_bytes, default 4 MB) that sheds from the lowest class when the backlog fills. A 4 Mbps video stream can never crowd out a 2 kbit/s command channel — that sentence is the feature.
Deadline budgets: late data is a lie
Real-time classes carry a per-packet deadline derived from the bond itself — deadline_k (default 3.5) × the worst alive link's RTT, clamped between deadline_floor_ms (80) and deadline_ceil_ms (1000). A packet that exceeds its budget while queued is dropped at dequeue: a video frame that would arrive 2 seconds late is worthless, and transmitting it anyway would delay the frames that still matter. Each class's reliability mode says what lateness means for it — reliable-bounded, deadline-bounded, latest-value-only, best-effort, or store-carry-forward.
Per-class multipath
Each class chooses how it uses the bond, overriding the global scheduler for its own traffic:
| Policy | Send plan | Meaning |
|---|---|---|
SingleBest | Mirror(1) | The lowest-cost alive link, and only it. |
Duplicate{n} | Mirror(n) | A copy on the n best links; the receiver dedups. Loss protection at n× cost. |
Aggregate | Stripe | Each packet on exactly one link, weighted by effective goodput (capacity × (1−loss)) — the bond's capacities add. |
Fec{overhead_pct} | Stripe (per shard) | Aggregate, plus parity shards that reconstruct losses with zero retransmission latency. |
Links that miss a class's latency budget are excluded from its stripe set before weights are computed; an emptied set falls back to the single best link, and an all-dead bond falls back to transmitting on everything configured. The default policy table ships sensible tactics — Control duplicated ×2 with an 80 ms budget and a reserved floor, Video striped with a 200 ms budget, Bulk stored when the peer is away — and everything is editable live.
Forward error correction
FEC batches K data packets (fec_block_packets, default 8), computes M parity shards with a systematic Reed-Solomon code, and stripes all K+M across the aggregate link set. Any K surviving shards reconstruct the block — loss on any link, recovered at the receiver, with no retransmission round trip. An idle flush (fec_flush_ms, 60) bounds latency when traffic is sparse, and incomplete blocks are abandoned after fec_block_timeout_ms (200), delivering whatever data shards arrived. It is doubly gated: the global fec_enabled switch and a per-class Fec policy must both be set. Worked example: 1-in-8 packet loss becomes invisible to the application for 12.5% overhead.
Duplicate, which is the simpler tool at that price. FEC's fractional-overhead benefit is real from three links up. The daemon computes, applies and reports this per class (effective_overhead_pct, parity_floor_applied) rather than letting a nominal 25% silently under-protect.Store-carry-forward
Bulk traffic addressed to a peer with no alive links is not dropped — it is stored (memory, or a file-backed write-ahead log via bundle_backend = "file") and flushed priority-then-FIFO the moment any link to that peer returns. Bounded by count, bytes and lifetime (bundle_max_count 10 000, bundle_max_bytes 16 MB, bundle_lifetime_ms 10 minutes). A vehicle that drives out of coverage delivers its backlog when it drives back in.
Adaptive per-class escalation
Optionally, individual classes escalate their own protection as conditions degrade — Duplicate adds copies, Aggregate steps into FEC and thickens it — with hysteresis and dwell on both directions, and two global guards that keep self-protection from becoming self-harm: an escalation bandwidth budget (default 30% of the peer's alive capacity) and a queue-depth gate so congestion loss can never trigger load amplification. Every decision is visible in the per-interface QoS stats document.
Turning it on
[qos]
enabled = true # master switch — off means none of this exists
fec_enabled = true # second gate for FEC; classes still need a Fec policy
rx_release_buffer = true # optional Video jitter/reorder buffer (default off)
bundle_backend = "file" # durable store-carry-forward across restarts
bundle_path = "/var/lib/atlas/nexus0.bundles.wal"
Class policies, per-class escalation and the guards live in a JSON policy document beside the daemon — re-read every reload_secs (default 2) and editable through GET/PUT /api/qos with merge semantics — so policy changes land mid-session with no restart and no packet loss. The configuration reference documents every key and default.