MangoWifi
Reference · Wi-Fi 6/7/8 test suite · Tauri 2 + Rust

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.

Part 1 — Source

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 shelllib.rsTauri setup; picks role from cfg; registers every #[tauri::command]
Configconfig.rsJSON at $MANGOWIFI_DATA_DIR/config.json — role, ports, pair token, target SSID, LAN receiver
Rolerole.rsAgent / Console / unset (drives first-launch role picker)
Wirewire.rsJSON WireFrame / Request / Response — the WS protocol shape
Agent serveragent_server.rsTCP 7745 WS listener + token gate + UDP 7746 echo responder for the probe
Console clientconsole_client.rsWS dialer + pending-request map + remote_* proxies + pub(crate) call_with_timeout primitive
Catalogtests.rsEnums (WifiGen, TestSection, BaselineCategory, CriteriaCategory, Wifi8Category, WfaProgram) + the builtin_catalog() producer
Baseline runnerbaseline.rs~30 pre-flight checks (radio, RF path, station setup, traffic tooling, AP reach, environment) — Agent-side
Criteria runnercriteria.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 probeprobe.rsConsole-side runners: probe_basic_latency, wifi8_rel_p95_latency_run, wifi8_rel_stress_endurance_run, wifi8_rel_p999_latency_run
Probe enginelatency_probe.rsHDR-histogram UDP probe: run() (single window) + run_bucketed() (per-bucket for drift analysis)
STA lifecyclewifi_sta.rsLinux virtual STA primitives (wpa_supplicant, iw, dhcp) — unused on Windows
STA batchingstations.rsMulti-STA orchestration (Linux)
Auxradios.rs, traffic.rs, measure.rs, monitor.rs, report.rsRadio 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
BaselinePre-flight rig readiness — "is the AP even reachable? does iperf3 exist?"baseline.rs
CriteriaVeriwave/Spirent-parity per-metric tests — basic / TCP / UDP / RFC / range / capacity / roaming / application / backwards-compatcriteria.rs
WfaWFA-cert-aligned rows (Wi-Fi 6/6E/7, Vantage, Voice-Ent, WPA3, etc.)(planned)
Wifi8Draft 802.11bn (D2.0, July 2026) — MAPC / Enhanced MLO / Seamless Roaming / Reliability / Power3/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.

6 unit tests, all passing: probe roundtrips on loopback, rejects bad params, reports full loss when target dead, bucketed splits and aggregates. Windows tokio timer floor (~15 ms) is budgeted in the asserts.

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)

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/ #

Wi-Fi 8 progress #

Runners live
3 / 18
all in Reliability category
Rows inventory
15
runner pending chip in UI
Spec basis
D2.0
IEEE 802.11bn · July 2026
Cert launch
2027
WFA CERTIFIED 8 target
live real runner, hits real code paths inventory catalog only, needs instrumentation
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 #


Part 2 — Hardware

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.
The key insight for Wi-Fi 8: Console→Agent UDP probe travels over the AP under test in the typical layout (Console wired to AP LAN side, Agent Wi-Fi STA on the other side). The round-trip RTT is real air-link latency.

Setup mode field on each test

Every test in the catalog carries a SetupCompat:

No wired-RF driver has shipped yet — WiredOnly tests skip until an attenuator driver lands.


Part 3 — Future

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

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)  │
                                                                └──────────┘

C.3 — MLO-capable STA driver (Enhanced MLO — 4 tests) #

C.4 — RF chamber (optional) #

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

LAN receiver

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 #

Ready for a paying buyer with a lab bench: Wi-Fi 6/7 Baseline + Criteria + first three Wi-Fi 8 Reliability KPIs (p95, endurance with drift, p99.9 tail).
Needs hardware investment before demo: MAPC cluster tests (multiple APs), Enhanced MLO tests (real Wi-Fi 8 STA silicon), Seamless Roaming tests (attenuator, ~$1–2k).
Not yet in the box: certification-grade PHY measurements (chamber gear, VSA), a co-located UDP echo responder on the LAN receiver (so Agent-side tests can also capture p95).

History

The 12-commit arc #

Everything shipped in this session lives on MangoWifi’s main branch.

  1. c2e844aWi-Fi 8 D2.0 draft catalog — 18 tests across 5 categories
  2. f2ab24fHDR-histogram UDP probe module (latency_probe.rs, 4 unit tests)
  3. d32ad24basic_probe_latency wired end-to-end (Agent UDP echo, Console-side probe)
  4. da3a8aewifi8_rel_p95_latency Phase 1 — first UHR runner (unloaded)
  5. 3a8b236Same runner Phase 2 — probe under concurrent Agent iperf3 load
  6. 969a5c2basic_ping_rtt: real p50/p95 + Windows time<1ms parse fix
  7. 5a14486wifi8_rel_stress_endurance — 30-min UHR runner
  8. a967a26Endurance Phase 2 — LoadPolicy::Cycle, load runs the full 30 min
  9. 2f97baeEndurance Phase 3 — bucketed HDR probe + drift analysis + drifted → Warn
  10. 626bbe9Endurance sparkline — per-bucket p95 SVG bar chart in the row
  11. 0b7b3f1wifi8_rel_p999_latency — third UHR runner, tail-focused (PercentileTarget)