Architecture & rig topology
What the codebase looks like, and what plugs into what — from dev loopback on a single Windows box, to a real lab with an AP under test, to the still-unbuilt multi-agent MAPC cluster. Written for coming-back-to, not for someone reading it top-to-bottom the first time.
What the codebase looks like #
Single mangowifi.exe binary that runs in one of two roles (Agent or Console). Both roles are the same binary; role is picked by the config file it reads on startup. Config location is chosen by the MANGOWIFI_DATA_DIR environment variable — no CLI flag. That's the trap that ate an hour of this session's verification claims and is now saved as memory.
Topology #
┌─────────────────────┐ Wi-Fi ┌─────────────────────┐ wired LAN │ Console │◀──── over the AP ───▶│ Agent │◀───────────────────▶ LAN receiver │ (dev box, wired) │ under test │ (STA-side box) │ (iperf3 -s, ...) │ │ │ │ │ · UI (Tauri WebView)│ TCP 7745 │ · WS server │ │ · orchestrates │───── WS control ────▶│ · UDP 7746 echo │ │ · runs probe │◀──── UDP echoes ─────│ · runs iperf3 │ │ (Console-side) │ port 7746 │ (Agent-side) │ └─────────────────────┘ └─────────────────────┘
Source layout — src-tauri/src/ #
| Layer | File | Purpose |
|---|---|---|
| App shell | lib.rs | Tauri setup; picks role from cfg; registers every #[tauri::command] |
| Config | config.rs | JSON at $MANGOWIFI_DATA_DIR/config.json — role, ports, pair token, target SSID, LAN receiver |
| Role | role.rs | Agent / Console / unset (drives first-launch role picker) |
| Wire | wire.rs | JSON WireFrame / Request / Response — the WS protocol shape |
| Agent server | agent_server.rs | TCP 7745 WS listener + token gate + UDP 7746 echo responder for the probe |
| Console client | console_client.rs | WS dialer + pending-request map + remote_* proxies + pub(crate) call_with_timeout primitive |
| Catalog | tests.rs | Enums (WifiGen, TestSection, BaselineCategory, CriteriaCategory, Wifi8Category, WfaProgram) + the builtin_catalog() producer |
| Baseline runner | baseline.rs | ~30 pre-flight checks (radio, RF path, station setup, traffic tooling, AP reach, environment) — Agent-side |
| Criteria runner | criteria.rs | ~35 Veriwave/IxChariot-parity tests (basic, TCP/UDP, RFC-2544, range, capacity, roaming, application, backwards-compat) — Agent-side, drives iperf3 / ping |
| Wi-Fi 8 probe | probe.rs | Console-side runners: probe_basic_latency, wifi8_rel_p95_latency_run, wifi8_rel_stress_endurance_run, wifi8_rel_p999_latency_run |
| Probe engine | latency_probe.rs | HDR-histogram UDP probe: run() (single window) + run_bucketed() (per-bucket for drift analysis) |
| STA lifecycle | wifi_sta.rs | Linux virtual STA primitives (wpa_supplicant, iw, dhcp) — unused on Windows |
| STA batching | stations.rs | Multi-STA orchestration (Linux) |
| Aux | radios.rs, traffic.rs, measure.rs, monitor.rs, report.rs | Radio enumeration, traffic driver, per-station live metrics, test-run history |
The catalog model #
TestSection splits the whole suite into four buckets that the sidebar mirrors 1:1.
| Section | Purpose | Runner |
|---|---|---|
| Baseline | Pre-flight rig readiness — "is the AP even reachable? does iperf3 exist?" | baseline.rs |
| Criteria | Veriwave/Spirent-parity per-metric tests — basic / TCP / UDP / RFC / range / capacity / roaming / application / backwards-compat | criteria.rs |
| Wfa | WFA-cert-aligned rows (Wi-Fi 6/6E/7, Vantage, Voice-Ent, WPA3, etc.) | (planned) |
| Wifi8 | Draft 802.11bn (D2.0, July 2026) — MAPC / Enhanced MLO / Seamless Roaming / Reliability / Power | 3/18 live, rest inventory |
Every row is a BuiltinTest struct — the important fields:
BuiltinTest {
id, name, description, section,
wfa_program, wfa_ref, coverage, // WFA section
criteria_category, criteria_ref, // Criteria section
baseline_category, baseline_required, // Baseline section
wifi8_category, spec_ref, // Wifi8 section — spec_ref points at "802.11bn D2.0 §35.14" etc.
setup, min_gen, duration_secs, tags,
}
The Wi-Fi 8 probe stack #
This is the piece we built from scratch in this arc.
latency_probe.rs — the instrument
pub struct ProbeSummary {
p50_us, p95_us, p99_us, p99_9_us,
mean_us, min_us, max_us,
sent, received, lost, loss_ratio, count,
duration_ms, rate_pps,
}
pub struct BucketedSummary {
overall: ProbeSummary,
bucket_ms,
buckets: Vec<ProbeSummary>,
}
pub async fn run(target, rate_pps, duration)
-> Result<ProbeSummary, String>
pub async fn run_bucketed(target, rate_pps, duration, bucket_dur)
-> Result<BucketedSummary, String>
16-byte UDP probe packet: [u64 LE seq | u64 LE send_time_ns]. Sender loop and receiver task run concurrently; RTT recorded into an HdrHistogram (1 µs .. 60 s, 3 sig figs). run_bucketed uses a Vec of per-bucket histograms indexed by elapsed time.
probe.rs — the shared core
enum PercentileTarget { P95, P999 }
enum LoadPolicy { None, Once(&'static str), Cycle(&'static str) }
struct ProbeThresholds {
tail_pass_ms, tail_warn_ms,
loss_pass_pct, loss_warn_pct,
target, label,
}
async fn run_probe_core(
state, test_id, defaults, thresholds,
load_policy, bucket_ms, params,
) -> CriteriaResult
One implementation body, four Tauri commands lit up by different constants:
| Runner | Duration | Threshold | Load | Bucketing |
|---|---|---|---|---|
probe_basic_latency |
5 s | p95 ≤ 20 ms Pass | None | None |
wifi8_rel_p95_latency_run |
30 s | p95 ≤ 5 ms Pass, ≤ 10 Warn | Once("udp_high_load") | None |
wifi8_rel_stress_endurance_run |
30 min | p95 ≤ 5 ms + drift ≤ 1.5× | Cycle("udp_high_load") | 5-min buckets |
wifi8_rel_p999_latency_run |
30 min | p99.9 ≤ 10 ms Pass, ≤ 20 Warn | Cycle("udp_high_load") | None |
How concurrent load is scored — LoadOutcome
enum LoadOutcome {
NotRequested,
Completed { status: String, cycles: u32 }, // "load[6× pass]" for cycled runs
Timeout,
Error(String),
}
Load task spawns before the probe (iperf3 gets a beat to spool up). For Cycle, the load task loops until 5 s before the probe deadline; stops early on skip/not_configured/fail so we don't hammer a broken rig. Reap grace: 30 s (Once) / 60 s (Cycle).
Drift analysis (endurance-only)
- Baseline = first populated bucket's p95 (skips warm-up)
- Drift ratio =
max_bucket_p95 / baseline - Drifted = ratio ≥ 1.5
- A
Passaggregate that hides a climbing tail is downgraded to Warn — the whole point of endurance testing
Full BucketedSummary serialised into CriteriaResult.evidence as JSON, so the JS renders a sparkline without a second round-trip.
Wire flow — what happens when you click Run #
Path for wifi8_rel_p95_latency — the shortest of the three UHR runners.
Console UI: user clicks Run
├─ app.js runWifi8One('wifi8_rel_p95_latency')
│ └─ invoke('wifi8_rel_p95_latency_run') ← Tauri IPC
│
└─ probe.rs::wifi8_rel_p95_latency_run(state, params)
└─ run_probe_core(P95 thresholds, LoadPolicy::Once("udp_high_load"), bucket_ms=None)
│
├─ tokio::spawn(load_task):
│ └─ console_client::call_with_timeout('criteria_run_one',
│ {test_id:"udp_high_load"}, 120)
│ └─ ── WS Request ──▶ Agent
│ └─ agent_server: dispatch "criteria_run_one"
│ └─ criteria.rs::run_one("udp_high_load")
│ └─ iperf3 -c <LAN receiver> -u -b 500M ...
│ ◀── WS Response ── CriteriaResult{"status":"skip"|"pass"|...}
│ → returns LoadOutcome
│
├─ latency_probe::run(<agent-ip>:7746, 200 pps, 30 s) ← Console-side probe
│ ├─ sender loop: 16-byte timestamped UDP probes ──▶ Agent UDP 7746
│ └─ receiver task: ◀── echoes ── record RTT into HdrHistogram
│ → ProbeSummary
│
└─ marshal → CriteriaResult { metrics{p50/p95/p99/p99.9/loss/load_status_code},
evidence(JSON), status(Pass/Warn/Fail) }
└─ store in criteria.results (server-side)
└─ return to UI
└─ refreshWifi8() re-renders the row:
· status chip
· p50/p95/p99/p99.9 row
· loss chip
· load chip ("load[pass]" / "load[skip]" / ...)
· drift chip + sparkline (endurance only)
The frontend — src/ #
index.html— sidebar + view containers;data-view="wifi8"for the Wi-Fi 8 tab (electric-blue glyph)app.js—callRig(method, args)router: Agent role → directinvoke, Console role →invoke('remote_' + method)WIFI8_RUNNERSmap (test_id → Tauri command) — grows as runners landcriteriaHeadline(res)— per-test metric row renderer (percentiles, load chip, drift chip)renderEnduranceSparkline(res)— SVG bar chart from evidence JSONrefreshWifi8()— fetches catalog + criteria_results, groups by category, renders one card per category with Run buttons for runnable tests
Wi-Fi 8 progress #
| Category | Live | Inventory (blocker) |
|---|---|---|
| Multi-AP Coordination (Co-BF / Co-SR / Co-TDMA / NPCA) | 0 / 4 | Needs a multi-Agent cluster harness |
| Enhanced MLO (link add/remove/failure/cross-AP) | 0 / 4 | Needs MLO-capable STA driver |
| Seamless Roaming (state carryover, MPDU-loss, re-auth, sticky-client) | 0 / 4 | Needs programmable attenuator |
| Reliability KPIs (p95, endurance/drift, p99.9) | 3 / 4 | throughput_at_range — needs attenuator |
| Power (enhanced TWT, low-latency doze) | 0 / 2 | Real Wi-Fi 8 STA silicon + power meter |
Verified traps saved as memory #
MANGOWIFI_DATA_DIRis the env var, not a--config-dirCLI flag — silently ignored otherwise, two instances clobber each other's config- Windows tokio sleep floor is ~15 ms — sub-15 ms cadence loops silently run at ~66 Hz on Windows; budget test asserts accordingly
- Pre-existing: Tauri IPC blocks browser-preview verification (use Agent+Console launch, not the Browser pane)
What plugs into what #
Three topologies escalating from dev-loopback to production-lab. Each row of the sidebar (Baseline / Criteria / Wi-Fi 8) has different hardware appetite; here's what actually plugs in.
Topology A — dev loopback (one box, no AP) #
What we've been verifying in this session.
┌──────────────────────────────────────────────────┐ │ Windows dev PC (WINDEV-PC1) │ │ │ │ ┌────────────────┐ ┌────────────────┐ │ │ │ mangowifi.exe │ │ mangowifi.exe │ │ │ │ (Agent) │ loop- │ (Console) │ │ │ │ MANGOWIFI_ │ back │ MANGOWIFI_ │ │ │ │ DATA_DIR= │◀───────▶│ DATA_DIR= │ │ │ │ agent-data/ │ │ console-data/ │ │ │ └────────────────┘ └────────────────┘ │ │ TCP 7745, UDP 7746 → 127.0.0.1:7745 │ └──────────────────────────────────────────────────┘
What works: probe wire, WS pair, echo responder, catalog rendering, JS UI. Every commit in this arc was verified in this topology.
What doesn't: no radio, no AP, no Wi-Fi at all — Criteria/Baseline runners that call iw / iperf3 -c <LAN> all skip. basic_probe_latency measures sub-ms loopback RTT (not useful, but proves the wire). Agent's udp_high_load returns NotConfigured → the Wi-Fi 8 runners' load-status chip shows load[skip], probe runs unloaded.
Topology B — real lab (2 boxes + AP under test) — the target #
┌──────────────────────┐
│ AP under test │
│ (Wi-Fi 6/6E/7/…) │
│ │
│ ┌───┐ ┌───┐ │
│ │RF │ │LAN│ │
│ └─┬─┘ └─┬─┘ │
└────│─────────────│────┘
│ │
over the air ────┘ └──── copper/fibre
│ │
▼ ▼
┌───────────────────────────┐ ┌──────────────────────────────┐
│ Agent box (STA-side) │ │ LAN receiver │
│ Linux preferred │ │ Linux/Windows box on the │
│ │ │ AP's LAN side │
│ · mangowifi.exe (Agent) │ │ │
│ · Wi-Fi NIC (real radio) │ │ · iperf3 -s -p 5201 │
│ · wpa_supplicant + iw │ │ · (future: echo responder │
│ · iperf3 client │ │ on 7749 for over-air │
│ · TCP 7745, UDP 7746 │ │ latency probe) │
│ │ │ │
│ Optional 2nd Wi-Fi NIC: │ │ │
│ · monitor / sniffer │ │ │
└────────┬───────────────────┘ └──────────────────────────────┘
│
│ Ethernet (mgmt) to same LAN as the Console
│
▼
┌───────────────────────────┐
│ Console box (dev PC) │
│ · mangowifi.exe (Console) │
│ · WebView UI │
│ · dials TCP 7745 → Agent │
│ · UDP probe → Agent:7746 │
└────────────────────────────┘
What each box runs
| Box | OS | MangoWifi role | Extra software | RF/wire |
|---|---|---|---|---|
| Console | Any (Windows tested) | mangowifi.exe --role=Console |
none | Ethernet to Agent's mgmt LAN |
| Agent (STA) | Linux preferred — Windows works for control, but wifi_sta primitives (iw, wpa_supplicant) are Linux-only |
mangowifi.exe --role=Agent |
iw, wpa_supplicant ≥ 2.10, iperf3 ≥ 3.9, ping |
Wi-Fi to AP + Ethernet mgmt |
| AP under test | vendor firmware | — | — | Wi-Fi + LAN |
| LAN receiver | Linux/Windows | — | iperf3 -s running, any port |
Wired to AP LAN |
How each test category exercises the rig
| Section | What actually happens over the wire |
|---|---|
| Baseline | Runs Agent-side: probes for iw, wpa_supplicant, iperf3, radio caps; scans for the target SSID; pings the LAN receiver. Zero AP-side action. |
| Criteria (TCP/UDP) | Agent-side iperf3 -c <LAN-receiver-IP> — traffic actually crosses Wi-Fi (STA→AP) → wire (AP→LAN receiver). This is what measures throughput. |
Criteria (basic_ping_rtt) |
Agent-side ping <traffic_receiver> — 10 pings on Windows (1 s each), 30 pings on Linux (100 ms each). RTT parsed into p50/p95 percentiles. |
| Wi-Fi 8 UHR runners | Console-side UDP probes to Agent:7746 → echo crosses the Wi-Fi under test in both directions. Concurrent iperf3 load fired from Console → Agent → LAN receiver via WS. |
Setup mode field on each test
Every test in the catalog carries a SetupCompat:
OtaOnly— needs the AP on the air (real radio, real Wi-Fi)WiredOnly— needs coax/programmable-attenuator into an RF chamber (for repeatable range/roaming testing)Both— works either way
No wired-RF driver has shipped yet — WiredOnly tests skip until an attenuator driver lands.
Topology C — not yet built #
Three hardware additions unlock the still-inventory Wi-Fi 8 categories.
C.1 — Programmable attenuator (Seamless Roaming) #
STA antenna port ── coax ── [Vaunix/Mini-Circuits USB attenuator] ── coax ── AP antenna port
▲
│ USB
Console orchestrates
RSSI decay schedule
- Vendor: Vaunix LDA-602Q or Mini-Circuits RCDAT-6000-90 (~$1–2k each)
- Enables
wifi8_roam_boundary_mpdu_loss,wifi8_roam_sticky_prevention,wifi8_rel_throughput_at_range, and the pre-existingrange_*Criteria tests - Software: new
attenuator.rsmodule wrapping the vendor's USB CDC protocol; a boot-time detection like the existing radio probing
C.2 — Multi-Agent cluster (MAPC — 4 tests) #
┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐
│ Console │──WS──▶│ Agent 1 │──Wi-Fi▶│ AP A │◀Wi-Fi──│ Agent 2 │
│ │──WS──▶│ (STA A) │ │ │ │ (STA B) │
└──────────┘ └──────────┘ │◀ coord ─│ └──────────┘
│ cable/ │
│ wireless ┌──────────┐
└──▶ AP B ◀────── │ Agent 3 │
│ (STA C) │
└──────────┘
- 2+ coordinated APs sharing a channel — MAPC is the whole point of Wi-Fi 8, and it requires multiple APs to test
- MangoWifi's Agent/Console architecture already handles this shape: Console pairs to N Agents, orchestrates their probes in a MAPC cluster test
- Software gap: a
MapcClusterorchestrator on Console-side that fans probe+load calls out to multiple Agents simultaneously; result-aggregation across Agents
C.3 — MLO-capable STA driver (Enhanced MLO — 4 tests) #
- Needs a real Wi-Fi 7/8 client radio with MLO support (Qualcomm FastConnect 7800/8800, MediaTek Filogic 880/8000, Broadcom BCM6725/4918)
- Software gap:
wifi_sta.rsextensions foriwMLO commands —iw dev <sta> link mlo add,mlo remove, etc. - Needs
iw≥ 6.9 and Linux kernel ≥ 6.10 for the nl80211 attributes
C.4 — RF chamber (optional) #
- Faraday-cage isolation eliminates ambient interference — required for cert-grade / regulatory measurements
- Not strictly needed for MangoWifi's target buyer (OEM regression / interop) but standard equipment at authorised test labs
- Vendors: Octobox, ETS-Lindgren
Software dependencies by platform #
Agent box (Linux — the intended production path)
$ ip link # link mgmt $ iw dev # radio + STA operations (>= 6.9 for full Wi-Fi 8) $ wpa_supplicant -v # >= 2.10 (SAE / MLO) $ iperf3 -v # >= 3.9 $ ping -c ... # iputils standard $ dhclient / dhcpcd # for real STA DHCP
Linux kernel: ≥ 6.10 for Wi-Fi 8 nl80211 attributes. 6.6 LTS is fine for Wi-Fi 6/7.
Agent box (Windows — dev / partial)
The Windows Agent works for control-plane testing (WS server, UDP echo, catalog rendering) but can't run a virtual STA — the wifi_sta.rs primitives are Linux-only. Windows can still do: Baseline environment checks, ICMP ping tests (via ping.exe), iperf3 against a LAN receiver (using the built-in Windows adapter). For the Wi-Fi 8 UHR runners, Windows is fine as an Agent because the probe just needs the UDP echo responder and the concurrent load can be iperf3 on the AP-facing adapter.
Console box
- Windows / macOS / Linux — the app is Tauri, so anywhere a WebView2/WebKit runs
- No radio requirements; just Ethernet to Agent
LAN receiver
- Any Linux/Windows box with
iperf3 -srunning - Optional (future): a small
mangowifi-echobinary on port 7749 for Agent→LAN UDP echoes, so criteria tests can capture their own p95 without pairing back to Console
Wire-level summary #
| Wire | Direction | Protocol | Port | Purpose |
|---|---|---|---|---|
| Ethernet | Console → Agent | TCP | 7745 | WS control (JSON-RPC) |
| Ethernet | Agent → Console | TCP | 7745 (reply) | WS responses |
| Wi-Fi | Console → Agent (via AP) | UDP | 7746 | HDR-histogram probe out |
| Wi-Fi | Agent → Console (via AP) | UDP | 7746 (echo) | Probe echoes back |
| Wi-Fi + wired | Agent → LAN receiver | TCP/UDP | 5201 | iperf3 goodput / UDP load |
| USB (future) | Console → attenuator | CDC serial | vendor | RSSI decay schedule |
Honest scope statement — what's shippable today #
The 12-commit arc #
Everything shipped in this session lives on MangoWifi’s main branch.
- c2e844aWi-Fi 8 D2.0 draft catalog — 18 tests across 5 categories
- f2ab24fHDR-histogram UDP probe module (
latency_probe.rs, 4 unit tests) - d32ad24
basic_probe_latencywired end-to-end (Agent UDP echo, Console-side probe) - da3a8ae
wifi8_rel_p95_latencyPhase 1 — first UHR runner (unloaded) - 3a8b236Same runner Phase 2 — probe under concurrent Agent
iperf3load - 969a5c2
basic_ping_rtt: real p50/p95 + Windowstime<1msparse fix - 5a14486
wifi8_rel_stress_endurance— 30-min UHR runner - a967a26Endurance Phase 2 —
LoadPolicy::Cycle, load runs the full 30 min - 2f97baeEndurance Phase 3 — bucketed HDR probe + drift analysis +
drifted → Warn - 626bbe9Endurance sparkline — per-bucket p95 SVG bar chart in the row
- 0b7b3f1
wifi8_rel_p999_latency— third UHR runner, tail-focused (PercentileTarget)