A native macOS Wi-Fi analyzer for nearby network inspection, channel ranking, ASUS Merlin heuristics, and exportable reports — shipped as a scriptable CLI and a lightweight SwiftUI app.
- Why
- Highlights
- Architecture
- Installation
- Usage
- Backend selection
- Permissions & degraded mode
- Project structure
- Verification
- Troubleshooting
- Roadmap & non-goals
- License
Dedicated RF hardware and enterprise spectrum tools are overkill when you just want to pick a cleaner 5 GHz channel or sanity-check an ASUS Merlin deployment. WIFI_MAC stays honest about what macOS actually exposes: it ranks channels, surfaces Merlin hints, and tells you — in plain language — when macOS redacted the data instead of inventing it.
| Area | What you get |
|---|---|
| Dual backends | Prefers Apple's private airport, falls back to CoreWLAN, fails closed if neither works |
| Channel ranking | Scored recommendations with confidence grades and stability scoring |
| Merlin heuristics | Detects ASUS Merlin routers, country-mode mismatches, and AiMesh / guest / IoT topology hints |
| Degraded-mode honesty | Missing SSID/BSSID/noise/SNR stay nil; warnings explain why instead of guessing |
| Export | Deterministic JSON and text reports with full provenance, warnings, and snapshot data |
| Two fronts | Scriptable WiFiCLI for terminals and automation; WiFiGUIApp SwiftUI host for live use |
graph TD
Domain["WiFiDomain<br/>contracts · provenance · warnings"]
Capture["WiFiCapture<br/>airport · CoreWLAN · capabilities"]
Analysis["WiFiAnalysis<br/>ranking · Merlin · China policy"]
Reports["WiFiReports<br/>JSON · text renderer"]
CLI["WiFiCLI<br/>executable"]
GUI["WiFiGUI<br/>SwiftUI root view"]
GUIApp["WiFiGUIApp<br/>macOS app host"]
Domain --> Capture
Domain --> Analysis
Domain --> Reports
Analysis --> Reports
Capture --> CLI
Reports --> CLI
Analysis --> CLI
Reports --> GUI
Analysis --> GUI
Capture --> GUIApp
GUI --> GUIApp
Each layer depends only on WiFiDomain (and WiFiAnalysis for reporting), so capture churn never leaks into analysis or UI. The boundary is enforced by ArchitectureBoundariesTests.
| Requirement | Minimum version | How to check |
|---|---|---|
| macOS | 13 (Ventura) | sw_vers |
| Swift toolchain | 6.0 | swift --version |
| Xcode command-line tools | Xcode 16 or Swift 6 toolchain | xcode-select -p |
| Wi-Fi hardware | Built-in Apple Silicon or Intel Mac Wi-Fi | System Information > Network > Wi-Fi |
git clone https://github.com/leedale30/WIFI_MAC.git
cd WIFI_MACswift buildThis compiles all seven modules and produces two executables:
.build/<arch>-apple-macosx/debug/WiFiCLI— the command-line tool.build/<arch>-apple-macosx/debug/WiFiGUIApp— the SwiftUI app
Sandbox note: If
swift buildfails withsandbox-exec: Operation not permitted, add--disable-sandbox:swift build --disable-sandbox
swift test # 53 tests across 8 suites
swift run WiFiCLI capabilitiesYou should see capability output listing airport, CoreWLAN, and system_profiler backends.
swift build -c releaseRelease binaries land in .build/<arch>-apple-macosx/release/.
The WiFiGUIApp binary runs fine from the terminal, but macOS will not foreground its window reliably without a bundle wrapper. To create one:
APP_DIR="$HOME/Applications/WiFiAnalyzer.app/Contents"
mkdir -p "$APP_DIR/MacOS"
cp .build/release/WiFiGUIApp "$APP_DIR/MacOS/WiFiGUIApp"
cat > "$APP_DIR/Info.plist" << 'PLIST'
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" \
"http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleName</key> <string>WiFi Analyzer</string>
<key>CFBundleDisplayName</key> <string>WiFi Analyzer</string>
<key>CFBundleIdentifier</key> <string>com.leedale30.wifianalyzer</string>
<key>CFBundleVersion</key> <string>1.0</string>
<key>CFBundleExecutable</key> <string>WiFiGUIApp</string>
<key>CFBundlePackageType</key> <string>APPL</string>
<key>NSHighResolutionCapable</key> <true/>
</dict>
</plist>
PLIST
open "$HOME/Applications/WiFiAnalyzer.app"sudo cp .build/release/WiFiCLI /usr/local/bin/wifi-analyzer
wifi-analyzer scanThe CLI is the primary interface for scripting and automation. All commands exit 0 on success and non-zero on failure.
| Command | Purpose |
|---|---|
scan |
Run a Wi-Fi scan and print a human-readable report |
scan --json |
Same scan, machine-readable JSON |
capabilities |
Show detected backend capability state |
diagnostics |
Capability state + scope limits in one place (best for support handoff) |
export |
Write a report file to disk |
analyze |
Alias of scan (same report path) |
channel-analysis |
Alias of scan (same report path) |
| Flag | Applies to | Effect |
|---|---|---|
--json |
scan, diagnostics, export |
Emit JSON instead of text |
--interface <name> |
scan, diagnostics |
Force CoreWLAN to target a specific interface (e.g. en0) |
--same-lan |
scan |
Enable same-LAN fingerprint probing (extension hook — stock builds ship no active probe) |
--format <json|text> |
export |
Output format for the exported file |
--output <path> |
export |
Destination file path |
Basic scan (text output):
swift run WiFiCLI scanOutput (abbreviated):
Wi-Fi Analyzer Report
Captured: 2026-08-11T10:02:15Z
Interface: en1
Observed networks: 8
Source backends: coreWLAN
Strongest RSSI: -59 dBm
Stability: 35%
Confidence: 45% (low)
Top Findings:
- Prefer 5GHz channel 161 (80MHz) [score: 71.4, confidence: 45%]
- 4 warning(s) require review before making a final channel change.
Warnings:
- [WARNING] Some CoreWLAN fields were missing or redacted.
- [WARNING] airport integration is unavailable on this host.
- [WARNING] Overlapping 2.4 GHz channels are increasing adjacent-channel contention.
Ranked Recommendations:
1. Prefer 5GHz channel 161 (80MHz) | band: 5GHz | channel: 161 | width: 80MHz | score: 71.4 | confidence: 45%
Observed Networks:
- corewlan-0 | band: 2.4GHz | channel: 6 | RSSI: -61 dBm | SNR: 29 dB | source: coreWLAN
- corewlan-1 | band: 5GHz | channel: 149 | RSSI: -64 dBm | SNR: 26 dB | source: coreWLAN
...
JSON scan (for piping into jq or other tools):
swift run WiFiCLI scan --json | jq '.summary'{
"capturedAt" : "2026-08-11T10:02:15Z",
"interfaceName" : "en1",
"networkCount" : 8,
"strongestRSSI" : -59,
"stabilityScore" : 0.35,
"confidence" : 0.45,
"confidenceLevel" : "low",
"topRecommendation" : {
"title" : "Prefer 5GHz channel 161 (80MHz)",
"band" : "5GHz",
"channel" : 161,
"channelWidth" : "80MHz",
"score" : 71.4,
"confidence" : 0.45
}
}Target a specific interface:
swift run WiFiCLI scan --interface en0Check backend capabilities:
swift run WiFiCLI capabilitiesRecommended backend: airport
Backend capabilities:
- airport: available [preferred]
path: /System/Library/PrivateFrameworks/Apple80211.framework/Versions/Current/Resources/airport
- CoreWLAN: available
- system_profiler: available
path: /usr/sbin/system_profiler
Run diagnostics (best for support handoff):
swift run WiFiCLI diagnosticsDiagnostics combines capability state, scan status, and scope limits in a single report. Use this when filing issues or asking for help.
Launch the SwiftUI app:
swift run WiFiGUIAppOr if you created the .app bundle:
open ~/Applications/WiFiAnalyzer.app| Panel | Content |
|---|---|
| Left sidebar | Navigation: Dashboard, Networks, Channel Analysis, History & Comparison |
| Center area | Summary cards (network count, strongest RSSI, confidence, stability, region, top channel), top findings, warnings, scope limits |
| Right inspector | Selected network details: band, channel, width, security, RSSI, noise, SNR, overlap pressure, composite score, missing fields |
| Toolbar | Run Scan, Export JSON, Export Text, Hide/Show Inspector, Disassociate |
- Launch the app — the dashboard shows an empty state explaining why no data is present yet.
- Click Run Scan (or Run First Scan on first launch).
- Summary cards populate with live network data.
- Click any network in the list to inspect it in the right panel.
- Use Export JSON or Export Text to save a report to disk.
- Switch to Channel Analysis in the sidebar for the ranked recommendation view.
Both CLI and GUI produce the same deterministic report format. Reports include:
- Snapshot — raw scan data (interface, observations, capabilities, warnings)
- Analysis — ranked recommendations, stability score, confidence, region resolution
- Summary — captured-at, network count, strongest RSSI, top recommendation
- Provenance — recommended backend, observed backends, per-source observation counts
- Warnings — severity, category, summary, detail
- Region notes — country-code evidence, mismatch signals
- Scope limitations — what macOS did not expose and why
- Merlin evidence — confirmed detections and topology hints
Export to a file (CLI):
swift run WiFiCLI export --format json --output ./wifi-report.json
swift run WiFiCLI export --format text --output ./wifi-report.txtExport to a file (GUI):
Click Export JSON or Export Text in the toolbar and choose a save location.
Pick a cleaner 5 GHz channel:
swift run WiFiCLI scan
# Read the "Ranked Recommendations" section
# Cross-check with the "Observed Networks" list for overlapAudit an ASUS Merlin deployment:
swift run WiFiCLI scan --json | jq '.merlinEvidence'Automate periodic scans (cron):
# Save a JSON report every hour
0 * * * * cd /path/to/WIFI_MAC && swift run WiFiCLI scan --json > /tmp/wifi-$(date +\%Y\%m\%d-\%H\%M).jsonPipe into jq for quick channel overview:
swift run WiFiCLI scan --json | jq '.analysis.recommendations[] | {channel, band, score, confidence}'flowchart TD
Start([Scan requested]) --> A{airport runnable?}
A -- yes --> B[Try airport]
B -- ok --> D[Emit report]
B -- fail --> C{CoreWLAN available?}
A -- no --> C
C -- yes --> E[Use CoreWLAN<br/>+ degraded-mode warning]
C -- no --> F[Fail closed<br/>no fabricated result]
E --> D
WIFI_MAC prefers the private airport backend because it exposes more fields (BSSID, noise, country code, channel width). When airport is unavailable or fails, it falls back to CoreWLAN and attaches a degraded-mode warning. If neither backend works, the scan fails closed — no fabricated results.
macOS may redact or omit Wi-Fi metadata even when a scan succeeds. WIFI_MAC never fills gaps with guesses.
- Missing SSID, BSSID, noise, SNR, country code, or channel width remain
nil/ empty. - Reports and GUI states surface warnings instead of inventing measurements.
- The GUI first-launch state explains why the dashboard is empty before the first scan.
Typical causes of reduced fidelity: ungranted location/Wi-Fi permissions, a present-but-blocked airport, CoreWLAN exposing fewer fields than airport, or hardware/OS limits.
.
├── Examples/Reports/ Example JSON and text outputs for verification
├── Sources/
│ ├── WiFiDomain/ Shared contracts, provenance, warnings, export models
│ ├── WiFiCapture/ Capability detection + airport/CoreWLAN acquisition
│ ├── WiFiAnalysis/ Metrics, channel ranking, Merlin heuristics, China policy
│ ├── WiFiReports/ Text and JSON report rendering
│ ├── WiFiCLI/ Executable CLI entry point
│ ├── WiFiGUI/ Reusable SwiftUI root view and GUI state helpers
│ └── WiFiGUIApp/ Minimal standalone macOS SwiftUI app host
├── Tests/ Unit, fixture, CLI, report, and GUI state coverage
└── Package.swift SwiftPM manifest
swift test # 53 tests across 8 suites
swift build
swift build --product WiFiGUIApp
swift run WiFiCLI diagnostics| Symptom | Cause | Fix |
|---|---|---|
sandbox-exec: Operation not permitted |
SwiftPM sandbox blocks file system access | Run with --disable-sandbox |
airport backend reported unavailable |
macOS removed or restricted the private framework | Expected on some macOS versions; CoreWLAN fallback covers basic scans |
| GUI window does not appear | Bare SwiftPM binary is not foregrounded by macOS | Create an .app bundle wrapper (see Installation Step 5) |
Strongest RSSI: n/a in report |
macOS redacted Wi-Fi metadata (permissions or OS limits) | Grant Wi-Fi/location permissions if prompted; otherwise the nil is intentional |
--interface en0 has no effect |
The airport backend cannot target an interface |
Only CoreWLAN honors --interface; if airport is active, the flag is ignored |
swift run WiFiGUIApp exits immediately |
Missing entitlements or sandbox restrictions on the terminal | Run from a properly sandboxed terminal or use the .app bundle |
Intentionally narrower than a dedicated spectrum analyzer or enterprise RF suite.
Not in scope:
- Legality determination for China or any other regulatory domain
- Dedicated spectrum, waterfall, or packet-capture tooling
- Active throughput, roaming, retry-rate, or airtime testing
- Stock same-LAN fingerprint probe in the default build
- Ranked 6 GHz planning (6 GHz observations are surfaced when macOS exposes them)
- Claiming parity with WiFi Explorer, NetSpot, or hardware-backed analyzers field-for-field
Ready for extension:
- Channel widths beyond 80 MHz (160 MHz, 320 MHz)
- Multi-Link Operation (MLO) domain models for Wi-Fi 7
- BE-series Merlin naming, firmware tokens, and new ASUS topology conventions
- PSC / wider-channel / regulatory-context 6 GHz planning once capture paths expose them reliably
MIT — © 2026 leedale30
