ESP32/php/mcs_fiwi.php

714 lines
32 KiB
PHP

<?php
/**
* Umber Fi-Wi: Deterministic Scaling (v34 - Mu-MIMO Logic)
* © 2026 Umber Networks, Inc.
*/
// Handle telemetry POST requests (for future measured data)
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
header('Content-Type: application/json');
$telemetryFile = __DIR__ . '/telemetry.json';
$data = json_decode(file_get_contents('php://input'), true);
if ($data && isset($data['device_id']) && isset($data['timestamp'])) {
$telemetry = [];
if (file_exists($telemetryFile)) {
$telemetry = json_decode(file_get_contents($telemetryFile), true) ?: [];
}
$deviceId = $data['device_id'];
if (!isset($telemetry[$deviceId])) {
$telemetry[$deviceId] = [];
}
$telemetry[$deviceId][] = array_merge($data, ['received_at' => time()]);
// Keep only last 1000 samples per device
if (count($telemetry[$deviceId]) > 1000) {
$telemetry[$deviceId] = array_slice($telemetry[$deviceId], -1000);
}
file_put_contents($telemetryFile, json_encode($telemetry, JSON_PRETTY_PRINT));
echo json_encode(['status' => 'success']);
exit;
}
echo json_encode(['status' => 'error', 'message' => 'Invalid data']);
exit;
}
?>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Umber Fi-Wi: Deterministic Scaling & Mu-MIMO</title>
<style>
:root {
--node-size: 14px;
--gap: 5px;
--fiwi-color: #00cc99; /* Green */
--auto-color: #ffaa00; /* Orange */
--mesh-color: #ff5500; /* Red-Orange */
--chaos-color: #ff3333; /* Red */
--wan-color: #cc66ff; /* Purple */
--mu-color: #0088ff; /* Blue for Mu-MIMO */
--bg-color: #0d0d0d;
--panel-bg: #151515;
--text-color: #eee;
}
body {
font-family: 'Segoe UI', monospace, sans-serif;
background-color: var(--bg-color);
color: var(--text-color);
margin: 0;
display: flex;
flex-direction: column;
align-items: center;
min-height: 100vh;
}
h1 { margin-top: 15px; font-size: 1.4rem; color: #fff; text-transform: uppercase; letter-spacing: 2px; border-bottom: 1px solid #333; padding-bottom: 5px; }
/* Global Stats */
.global-stats {
display: flex; gap: 20px; background: #000; padding: 15px 30px; border-radius: 8px; margin-bottom: 15px; border: 1px solid #333;
box-shadow: 0 4px 10px rgba(0,0,0,0.5); align-items: center; justify-content: center; width: 95%; max-width: 1000px;
}
.g-stat-item { display: flex; flex-direction: column; align-items: center; min-width: 120px; }
.g-label { font-size: 0.7rem; color: #888; text-transform: uppercase; letter-spacing: 1px; margin-bottom: 5px; }
.g-value { font-family: 'Consolas', monospace; font-size: 1.4rem; font-weight: bold; color: #fff; }
.g-sub { font-size: 0.65rem; margin-top: 2px; font-weight: bold; text-transform: uppercase; letter-spacing: 1px; }
.val-good { color: var(--fiwi-color); }
.val-mid { color: var(--auto-color); }
.val-bad { color: var(--chaos-color); }
.val-wan { color: var(--wan-color); text-shadow: 0 0 10px rgba(204,102,255,0.4); }
/* Scenarios Bar */
.scenario-bar { display: flex; gap: 10px; margin-bottom: 15px; }
.scene-btn {
background: #222; border: 1px solid #444; color: #aaa; padding: 5px 15px; border-radius: 4px; cursor: pointer; font-size: 0.75rem; text-transform: uppercase; letter-spacing: 1px; transition: all 0.2s;
}
.scene-btn:hover { background: #333; color: #fff; }
.scene-btn:active { background: #444; }
/* Controls Layout */
.controls-wrapper { display: flex; gap: 15px; align-items: stretch; margin-bottom: 15px; flex-wrap: wrap; justify-content: center; }
.ctrl-box {
background: #222; padding: 8px 12px; border-radius: 8px; border: 1px solid #444;
display: flex; flex-direction: column; align-items: center; justify-content: center;
}
/* Algo Toggle */
.switch-container { display: flex; align-items: center; gap: 10px; margin-bottom: 5px; }
.switch-label { font-weight: bold; font-size: 0.75rem; color: #666; }
.switch-label.active-chaos { color: var(--chaos-color); }
.switch-label.active-l4s { color: #00aaff; }
.toggle-checkbox {
position: relative; width: 40px; height: 20px; appearance: none; background: #000;
border-radius: 30px; cursor: pointer; border: 1px solid #444; outline: none;
}
.toggle-checkbox::after {
content: ''; position: absolute; top: 2px; left: 2px; width: 16px; height: 16px;
border-radius: 50%; background: #666; transition: all 0.2s;
}
.toggle-checkbox:checked::after { left: 22px; background: #00aaff; }
.toggle-checkbox:not(:checked)::after { background: var(--chaos-color); }
/* Sliders */
.slider-group { width: 120px; display: flex; flex-direction: column; gap: 2px; }
.slider-label { display: flex; justify-content: space-between; font-size: 0.65rem; color: #aaa; }
.slider-val { font-weight: bold; color: #fff; font-size: 0.8rem; }
input[type=range] { width: 100%; cursor: pointer; accent-color: #00cc99; height: 4px; }
/* Topo Buttons */
.topo-btns { display: flex; gap: 5px; }
.mode-btn {
background: transparent; border: 1px solid transparent; color: #666; font-weight: bold; padding: 4px 10px; cursor: pointer;
border-radius: 20px; transition: all 0.3s; font-size: 0.75rem;
}
.mode-btn:hover { color: #fff; border-color: #444; }
.mode-btn.active-fiwi { background: var(--fiwi-color); color: #000; box-shadow: 0 0 10px var(--fiwi-color); }
.mode-btn.active-auto { background: var(--auto-color); color: #000; box-shadow: 0 0 10px var(--auto-color); }
.mode-btn.active-mesh { background: var(--mesh-color); color: #000; box-shadow: 0 0 10px var(--mesh-color); }
/* Grid */
.quad-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 15px; max-width: 1200px; margin-bottom: 20px; }
.device-card {
background: var(--panel-bg); padding: 12px; border-radius: 8px; border: 1px solid #333;
display: flex; flex-direction: column; align-items: center; position: relative;
}
.hop-badge { position: absolute; top: 8px; right: 8px; background: #333; color: #aaa; font-size: 0.65rem; padding: 2px 6px; border-radius: 4px; display: none; }
.device-header { width: 100%; display: flex; justify-content: space-between; margin-bottom: 8px; border-bottom: 1px solid #333; padding-bottom: 4px; }
.dev-title { font-weight: bold; color: #888; font-size: 0.85rem; }
.dev-status { font-family: 'Consolas', monospace; font-size: 0.7rem; font-weight: bold; letter-spacing: -0.5px; }
.grid-wrapper { position: relative; }
.mumimo-label { position: absolute; left: -18px; top: 20%; transform: rotate(-90deg); font-size: 0.55rem; color: var(--mu-color); letter-spacing: 1px; opacity: 0.5; }
.mimo-label { position: absolute; left: -12px; bottom: 20%; transform: rotate(-90deg); font-size: 0.55rem; color: #666; letter-spacing: 1px; }
.mini-grid-row { display: flex; align-items: center; margin-bottom: var(--gap); }
.mini-row-label { width: 25px; text-align: right; padding-right: 6px; font-size: 0.55rem; color: #555; }
.mini-node {
width: var(--node-size); height: var(--node-size); border-radius: 50%; background-color: #2a2a2a; margin-right: var(--gap);
border: 1px solid #333; transition: background-color 0.1s;
}
.mini-node.active.fiwi { background-color: var(--fiwi-color); box-shadow: 0 0 6px var(--fiwi-color); border-color: #fff; transform: scale(1.1); }
.mini-node.active.auto { background-color: var(--auto-color); box-shadow: 0 0 6px var(--auto-color); border-color: #fff; transform: scale(1.1); }
.mini-node.active.mesh { background-color: var(--mesh-color); box-shadow: 0 0 6px var(--mesh-color); border-color: #fff; transform: scale(1.1); }
.mini-node.active.chaos { background-color: var(--chaos-color); box-shadow: 0 0 8px var(--chaos-color); border-color: #fff; transform: scale(1.2); }
.mini-node.active.mumimo { background-color: var(--mu-color); box-shadow: 0 0 8px var(--mu-color); border-color: #fff; transform: scale(1.1); }
@keyframes pulse-red { 0% { box-shadow: 0 0 0 0 rgba(255, 51, 51, 0.7); } 70% { box-shadow: 0 0 0 6px rgba(255, 51, 51, 0); } 100% { box-shadow: 0 0 0 0 rgba(255, 51, 51, 0); } }
.mini-node.collision { animation: pulse-red 1s infinite; border-color: #ff3333 !important; background-color: #500 !important; }
.mini-node.ghost { border: 1px dashed #666; background-color: rgba(255,255,255,0.05); }
.axis-label { width: 100%; text-align: right; font-size: 0.55rem; color: #555; margin-top: 2px; letter-spacing: 1px; font-family: monospace; }
.dev-telemetry { margin-top: 8px; width: 100%; display: flex; justify-content: space-between; align-items: flex-end; font-size: 0.7rem; color: #666; font-family: monospace; border-top: 1px solid #333; padding-top: 5px; }
.math-row { width: 100%; display: flex; justify-content: space-between; font-size: 0.65rem; color: #555; font-family: 'Courier New', monospace; margin-top: 2px; }
.math-val { color: #888; font-weight: bold; }
.math-bad { color: var(--chaos-color); } .math-ok { color: var(--fiwi-color); }
.t-bad { color: var(--chaos-color); } .t-warn { color: var(--mesh-color); } .t-good { color: var(--fiwi-color); } .t-mid { color: var(--auto-color); } .t-wan { color: var(--wan-color); }
.footer { margin-top: 10px; font-size: 0.75rem; color: #666; padding-bottom: 20px; }
.footer a { color: var(--fiwi-color); text-decoration: none; border-bottom: 1px dotted var(--fiwi-color); }
</style>
</head>
<body>
<h1>Umber Fi-Wi: Deterministic Scaling & Mu-MIMO</h1>
<div class="scenario-bar">
<button class="scene-btn" onclick="setScenario('light')">Light Load</button>
<button class="scene-btn" onclick="setScenario('typical')">Typical Office</button>
<button class="scene-btn" onclick="setScenario('dense')">Dense (12 Cli)</button>
<button class="scene-btn" onclick="setScenario('stress')">Stress Test</button>
</div>
<div class="global-stats">
<div class="g-stat-item">
<span class="g-label">Throughput</span>
<span class="g-value" id="glob-tput">--</span>
<span class="g-sub" id="glob-bottleneck">--</span>
</div>
<div class="g-stat-item">
<span class="g-label">P99 Latency</span>
<span class="g-value" id="glob-lat">--</span>
</div>
<div class="g-stat-item">
<span class="g-label">Packet Error Rate</span>
<span class="g-value" id="glob-per">--</span>
</div>
<div class="g-stat-item" style="border-left:1px solid #333; padding-left:20px;">
<span class="g-label">Collision Prob</span>
<span class="g-value" id="glob-bday">--</span>
</div>
</div>
<div class="controls-wrapper">
<div class="ctrl-box">
<div class="slider-group">
<div class="slider-label"><span>Active Rooms</span><span id="val-aps" class="slider-val">4</span></div>
<input type="range" id="aps-slider" min="1" max="4" step="1" value="4" oninput="updateSliders()">
</div>
</div>
<div class="ctrl-box">
<div class="slider-group">
<div class="slider-label"><span>Total Clients</span><span id="val-clients" class="slider-val">12</span></div>
<input type="range" id="clients-slider" min="1" max="50" step="1" value="12" oninput="updateSliders()">
</div>
</div>
<div class="ctrl-box">
<div class="slider-group">
<div class="slider-label"><span>Packet Pressure</span><span id="val-pps" class="slider-val">5000 PPS</span></div>
<input type="range" id="pps-slider" min="100" max="20000" step="100" value="5000" oninput="updateSliders()">
</div>
</div>
<div class="ctrl-box">
<div class="switch-container">
<span id="lbl-chaos" class="switch-label active-chaos">Greedy</span>
<input type="checkbox" id="algo-toggle" class="toggle-checkbox" onchange="updateSim()">
<span id="lbl-l4s" class="switch-label">L4S</span>
</div>
<div style="font-size:0.6rem; color:#666;">Rate Control</div>
</div>
<div class="ctrl-box">
<div class="slider-group">
<div class="slider-label"><span>Aggregation (MPDU)</span><span id="val-ampdu" class="slider-val">12</span></div>
<input type="range" id="ampdu-slider" min="1" max="64" step="1" value="12" oninput="updateSliders()">
</div>
</div>
<div class="ctrl-box">
<div class="topo-btns">
<button class="mode-btn active-fiwi" onclick="setTopo('fiwi')" id="btn-fiwi">Fi-Wi</button>
<button class="mode-btn" onclick="setTopo('auto')" id="btn-auto">Auton AP</button>
<button class="mode-btn" onclick="setTopo('mesh')" id="btn-mesh">Mesh</button>
</div>
</div>
</div>
<div style="margin-bottom:10px; font-size: 0.85rem; color: #aaa; height: 18px;">
<span id="status-text">...</span>
</div>
<div class="quad-grid" id="quad-container"></div>
<div class="footer">
Reference: <a href="https://mcsindex.com/" target="_blank">MCS Index Table (80 MHz)</a> | Shared WAN Cap: 1000 Mbps
</div>
<script>
const CONFIG = { mcs_max: 11, ss_max: 4 };
let topo = 'fiwi';
let algo = 'greedy';
let ampdu = 12;
let globalPPS = 5000;
let totalClients = 12;
let apCount = 4;
let latSamples = [];
// Physics Constants
const PHYSICS = {
TX_PROB_CSMA: 0.05,
RX_TURNAROUND: 0.04,
WAN_CAP: 1000
};
const devices = [
{ id: 0, name: "Room A", state: {mcs:6, ss:2}, ghost: {mcs:9, ss:4}, pps: 0, hops: 0, clients: 0 },
{ id: 1, name: "Room B", state: {mcs:5, ss:2}, ghost: {mcs:8, ss:3}, pps: 0, hops: 1, clients: 0 },
{ id: 2, name: "Room C", state: {mcs:4, ss:1}, ghost: {mcs:7, ss:2}, pps: 0, hops: 2, clients: 0 },
{ id: 3, name: "Room D", state: {mcs:7, ss:2}, ghost: {mcs:10,ss:3}, pps: 0, hops: 3, clients: 0 }
];
// 802.11ac VHT MCS rates for 80 MHz, 800ns GI, 1 Spatial Stream (from mcsindex.com)
// MCS 0-9: Standard VHT rates
// MCS 10-11: Extended rates (may vary by implementation)
const PHY_RATES_1SS = [
32.5, // MCS 0: BPSK 1/2
65, // MCS 1: QPSK 1/2
97.5, // MCS 2: QPSK 3/4
130, // MCS 3: 16-QAM 1/2
195, // MCS 4: 16-QAM 3/4
260, // MCS 5: 64-QAM 2/3
292.5, // MCS 6: 64-QAM 3/4
325, // MCS 7: 64-QAM 5/6
390, // MCS 8: 256-QAM 3/4
433.3, // MCS 9: 256-QAM 5/6
433.3, // MCS 10: 256-QAM 5/6 (same as MCS 9 in some implementations)
520 // MCS 11: 256-QAM 5/6 with 400ns GI (if supported, else falls back)
];
const PRESETS = {
'light': { aps: 2, clients: 4, pps: 1000, ampdu: 32 },
'typical': { aps: 4, clients: 8, pps: 3000, ampdu: 24 },
'dense': { aps: 4, clients: 12, pps: 5000, ampdu: 12 },
'stress': { aps: 4, clients: 40, pps: 15000, ampdu: 4 }
};
function init() {
const container = document.getElementById('quad-container');
container.innerHTML = '';
devices.forEach(dev => {
const card = document.createElement('div');
card.className = 'device-card';
card.id = `card-${dev.id}`;
card.innerHTML = `
<div class="hop-badge" id="hop-${dev.id}">Inactive</div>
<div class="device-header">
<span class="dev-title">${dev.name}</span>
<span class="dev-status" id="status-${dev.id}">INIT</span>
</div>
<div class="grid-wrapper">
<div class="mumimo-label">Mu-MIMO</div>
<div class="mimo-label">2x2 Client</div>
<div id="grid-${dev.id}"></div>
</div>
<div class="axis-label">MCS Index (0 - 11) &rarr;</div>
<div style="width:100%; border-top:1px solid #333; margin-top:5px; padding-top:2px;">
<div class="math-row">
<span>Eigenvalues:</span>
<span>&lambda;<sub>1</sub>:<span class="math-val" id="eig1-${dev.id}">1.0</span> &nbsp; &lambda;<sub>2</sub>:<span class="math-val" id="eig2-${dev.id}">0.5</span></span>
</div>
<div class="math-row">
<span>Condition (&kappa;):</span>
<span class="math-val" id="cond-${dev.id}">3.0 dB</span>
</div>
</div>
<div class="dev-telemetry">
<div style="display:flex; flex-direction:column;">
<span id="tput-${dev.id}" style="font-weight:bold;">0 Mbps</span>
<span id="state-${dev.id}" style="font-size:0.65rem; color:#666;">MCS -- / -- SS</span>
</div>
<span id="lat-${dev.id}">Lat: --</span>
</div>
`;
container.appendChild(card);
const gridBox = card.querySelector(`#grid-${dev.id}`);
// Render 4 Rows (4 SS down to 1 SS)
for (let ss = CONFIG.ss_max; ss >= 1; ss--) {
const row = document.createElement('div');
row.className = 'mini-grid-row';
const lbl = document.createElement('div');
lbl.className = 'mini-row-label'; lbl.innerText = `${ss}SS`;
row.appendChild(lbl);
for (let mcs = 0; mcs <= CONFIG.mcs_max; mcs++) {
const n = document.createElement('div');
n.className = 'mini-node'; n.id = `d${dev.id}-n-${ss}-${mcs}`;
row.appendChild(n);
}
gridBox.appendChild(row);
}
});
updateSliders();
updateSim();
setInterval(loop, 100);
}
function setScenario(name) {
const s = PRESETS[name];
if(!s) return;
document.getElementById('aps-slider').value = s.aps;
document.getElementById('clients-slider').value = s.clients;
document.getElementById('pps-slider').value = s.pps;
document.getElementById('ampdu-slider').value = s.ampdu;
updateSliders();
}
function updateSliders() {
ampdu = parseInt(document.getElementById('ampdu-slider').value);
document.getElementById('val-ampdu').innerText = ampdu;
globalPPS = parseInt(document.getElementById('pps-slider').value);
document.getElementById('val-pps').innerText = globalPPS + " PPS";
totalClients = parseInt(document.getElementById('clients-slider').value);
document.getElementById('val-clients').innerText = totalClients;
apCount = parseInt(document.getElementById('aps-slider').value);
document.getElementById('val-aps').innerText = apCount;
let base = Math.floor(totalClients / apCount);
let remainder = totalClients % apCount;
devices.forEach((dev, idx) => {
const card = document.getElementById(`card-${dev.id}`);
if (idx < apCount) {
card.style.opacity = '1';
card.style.filter = 'none';
dev.clients = base + (idx < remainder ? 1 : 0);
} else {
card.style.opacity = '0.3';
card.style.filter = 'grayscale(100%)';
dev.clients = 0;
}
});
}
function setTopo(t) { topo = t; updateSim(); }
function updateSim() {
const isL4S = document.getElementById('algo-toggle').checked;
algo = isL4S ? 'l4s' : 'greedy';
document.getElementById('lbl-chaos').className = !isL4S ? 'switch-label active-chaos' : 'switch-label';
document.getElementById('lbl-l4s').className = isL4S ? 'switch-label active-l4s' : 'switch-label';
['fiwi', 'auto', 'mesh'].forEach(m => {
const btn = document.getElementById(`btn-${m}`);
if (m === topo) btn.classList.add(`active-${m}`);
else btn.classList.remove(`active-fiwi`, `active-auto`, `active-mesh`);
});
devices.forEach((d, idx) => {
const badge = document.getElementById(`hop-${d.id}`);
if (idx >= apCount) { badge.style.display = 'none'; return; }
badge.style.display = 'block';
let role = (topo === 'fiwi') ? "Radio Head" : ((topo === 'auto') ? "AP" : "Node");
if(topo === 'mesh' && d.hops === 0) role = "Gateway";
badge.innerText = `${role} (${d.clients} Cli)`;
});
latSamples = [];
const txt = document.getElementById('status-text');
if (topo === 'fiwi') {
txt.innerText = (algo==='l4s')
? "Fi-Wi L4S: Deterministic scheduling enables stable Mu-MIMO (3-4 SS)."
: "Fi-Wi Greedy: Centralized control, but queues fill.";
} else if (topo === 'auto') {
txt.innerText = (algo==='l4s')
? "Autonomous L4S: Drift hampers Mu-MIMO coordination."
: "Autonomous Greedy: CSMA CHAOS. Collisions kill Mu-MIMO gains.";
} else if (topo === 'mesh') {
txt.innerText = "Mesh: Wireless Backhaul + Multi-Client = Latency Wall.";
}
}
function loop() {
let globalFailures = 0;
let globalPotentialTput = 0;
let bottleneck = "AIRTIME";
let globalBirthdayProb = 0;
let activeClientTotal = 0;
devices.forEach((d, idx) => {
if (idx < apCount && d.clients > 0) activeClientTotal += d.clients;
});
devices.forEach((d, idx) => {
if (idx < apCount && d.clients > 0) {
d.pps = (globalPPS * (d.clients / activeClientTotal));
} else { d.pps = 0; }
});
const results = devices.map((dev, idx) => {
if (idx >= apCount) return null;
return calculateDevicePhysics(dev);
});
results.forEach(res => { if(res) globalPotentialTput += res.airTput; });
let wanFactor = 1.0;
if (globalPotentialTput > PHYSICS.WAN_CAP) {
wanFactor = PHYSICS.WAN_CAP / globalPotentialTput;
bottleneck = "WAN LINK";
}
results.forEach((res, index) => {
if (!res) return;
const dev = devices[index];
globalFailures += res.per * dev.pps;
if (dev.clients > 0) {
latSamples.push(res.lat);
latSamples.push(res.lat);
}
if(res.bottleneck !== "AIRTIME") bottleneck = res.bottleneck;
if(res.birthdayProb > globalBirthdayProb) globalBirthdayProb = res.birthdayProb;
const finalTput = res.airTput * wanFactor;
updateDeviceUI(dev, res, finalTput, wanFactor < 1.0);
});
if (latSamples.length > 200) latSamples = latSamples.slice(-200);
const totalPPS = devices.reduce((sum, d) => sum + d.pps, 0);
const avgPer = (totalPPS > 0) ? (globalFailures / totalPPS) * 100 : 0;
const elPer = document.getElementById('glob-per');
elPer.innerText = avgPer.toFixed(2) + "%";
elPer.className = avgPer < 0.2 ? "g-value val-good" : (avgPer < 4 ? "g-value val-mid" : "g-value val-bad");
latSamples.sort((a,b) => a - b);
const p99Index = Math.floor(latSamples.length * 0.99);
const p99 = latSamples.length > 0 ? latSamples[p99Index] : 0;
const elLat = document.getElementById('glob-lat');
elLat.innerText = Math.round(p99) + "ms";
elLat.className = p99 < 8 ? "g-value val-good" : (p99 < 60 ? "g-value val-mid" : "g-value val-bad");
const elTput = document.getElementById('glob-tput');
const totalActualTput = Math.min(globalPotentialTput, PHYSICS.WAN_CAP);
if (totalActualTput > 1000) elTput.innerText = (totalActualTput/1000).toFixed(1) + " Gbps";
else elTput.innerText = Math.round(totalActualTput) + " Mbps";
const elBN = document.getElementById('glob-bottleneck');
elBN.innerText = "LIMIT: " + bottleneck;
if(bottleneck === "WAN LINK") { elTput.className = "g-value val-wan"; elBN.style.color = "#cc66ff"; }
else if(bottleneck === "AIRTIME") { elTput.className = "g-value val-mid"; elBN.style.color = "#ffaa00"; }
else { elTput.className = "g-value val-bad"; elBN.style.color = "#ff3333"; }
const elBday = document.getElementById('glob-bday');
elBday.innerText = (globalBirthdayProb * 100).toFixed(1) + "%";
elBday.className = globalBirthdayProb < 0.05 ? "g-value val-good" : (globalBirthdayProb < 0.3 ? "g-value val-mid" : "g-value val-bad");
}
function calculateDevicePhysics(dev) {
if (dev.clients === 0) {
return {
airTput: 0, per: 0, lat: 0, birthdayProb: 0,
visualClass: '', statusText: 'IDLE', statusClass: 'dev-status', bottleneck: 'NONE',
e1: 0, e2: 0, kappa: 0, ss: 0, mcs: 0
};
}
// Client Hardware Cap
let clientCap = (dev.clients > 1) ? 4 : 2; // MuMIMO requires >1 client
if (topo === 'mesh' && dev.hops > 0) clientCap = 2; // Mesh backhaul limit
let birthdayProb = 0;
let collisionPenalty = 0;
let latencyAdd = 0;
// --- PHYSICS: The Birthday Paradox ---
if (topo === 'fiwi') {
birthdayProb = 0.001 * dev.clients;
latencyAdd = 0.5 * dev.clients;
} else {
let n = dev.clients;
if (n < 2) birthdayProb = 0.01;
else birthdayProb = 1 - Math.pow(1 - PHYSICS.TX_PROB_CSMA, n * (n - 1));
if (apCount > 1) birthdayProb += 0.1 * (apCount - 1);
if(birthdayProb > 0.98) birthdayProb = 0.98;
collisionPenalty = birthdayProb;
latencyAdd = 5 * Math.pow(n, 1.5);
}
// Matrix Physics
let e1 = 1.0, e2 = 0.5, kappa = 6.0;
if (topo === 'fiwi') {
e1 = 0.95 + Math.random()*0.05; e2 = 0.6 + Math.random()*0.1;
kappa = 20 * Math.log10(e1/e2);
} else {
e1 = 0.9 + Math.random()*0.1;
let collapse = Math.max(0.001, 1.0 - birthdayProb);
e2 = 0.5 * collapse + (Math.random()*0.02);
kappa = 20 * Math.log10(e1/e2);
}
// Ghost Physics (Active Variation)
// Ghost can float up to Client Cap
if(Math.random() > 0.9) dev.ghost.ss = Math.min(clientCap, dev.ghost.ss + 1);
else if(Math.random() < 0.1) dev.ghost.ss = Math.max(1, dev.ghost.ss - 1);
if(Math.random() > 0.8) dev.ghost.mcs = Math.min(11, Math.max(4, dev.ghost.mcs + (Math.random()>0.5?1:-1)));
let per = 0;
let lat = 0;
let visualClass = '';
let statusText = '';
let statusClass = '';
let bottleneck = "AIRTIME";
const load = dev.pps / 10000;
// --- ALGORITHM LOGIC ---
if (algo === 'l4s') {
// L4S: Conservative SS
const safeSS = Math.min(clientCap, dev.ghost.ss); // Respect hardware cap
if (dev.state.ss > safeSS) dev.state.ss--;
else if (dev.state.ss < safeSS) dev.state.ss++;
const targetMcs = Math.max(0, dev.ghost.mcs - 2);
if (dev.state.mcs < targetMcs) dev.state.mcs++;
else if (dev.state.mcs > targetMcs) dev.state.mcs--;
if (topo === 'fiwi') {
per = 0.001; lat = 2 + latencyAdd;
visualClass = 'active fiwi';
if (dev.state.ss > 2) visualClass += ' mumimo';
statusText = `SCHEDULED (${dev.clients})`; statusClass = "dev-status t-good";
} else {
per = 0.01 + birthdayProb * 0.2; lat = 10 + latencyAdd;
visualClass = (topo==='mesh') ? 'active mesh' : 'active auto';
statusText = (topo==='mesh') ? "RELAYING" : `DRIFTING (${(birthdayProb*100).toFixed(0)}%)`;
statusClass = "dev-status t-mid";
}
} else {
// GREEDY: Aggressive SS
let targetSS = Math.min(clientCap, dev.ghost.ss); // Even Greedy respects physics
if (dev.state.ss < targetSS) dev.state.ss++;
else if (dev.state.ss > targetSS) dev.state.ss--;
if (dev.state.mcs < dev.ghost.mcs) {
if (Math.random() < 0.6) dev.state.mcs++;
} else if (dev.state.mcs > dev.ghost.mcs) {
dev.state.mcs = dev.ghost.mcs - 1; per = 0.5;
} else per = 0.1;
if (topo === 'fiwi') {
per = Math.min(per, 0.05); lat = 20 + latencyAdd;
visualClass = 'active fiwi';
if (dev.state.ss > 2) visualClass += ' mumimo';
statusText = "BUFFERBLOAT"; statusClass = "dev-status t-warn";
} else {
per = Math.max(per, 0.20 + birthdayProb); lat = 30 + latencyAdd * 3;
visualClass = 'active chaos';
if(birthdayProb > 0.3) visualClass += ' collision';
statusText = `COLLISION (${(birthdayProb*100).toFixed(0)}%)`;
statusClass = "dev-status t-bad";
}
}
let rawPhyRate = PHY_RATES_1SS[dev.state.mcs] * dev.state.ss;
let aggEff = Math.min(0.95, 0.3 + (0.65 * Math.log(ampdu + 1) / Math.log(65)));
let meshPenalty = (topo === 'mesh' && dev.hops > 0) ? Math.pow(0.5, dev.hops) : 1.0;
let safeCollisionPenalty = Math.min(0.99, Math.max(0, collisionPenalty));
let airTput = rawPhyRate * aggEff * (1 - per) * meshPenalty * (1 - safeCollisionPenalty);
if (airTput < 0) airTput = 0;
if (birthdayProb > 0.5) bottleneck = "BDAY PARADOX";
else if (per > 0.1) bottleneck = "RE-TX / NOISE";
return {
airTput: airTput, per: per, lat: lat, birthdayProb: birthdayProb,
visualClass: visualClass, statusText: statusText, statusClass: statusClass, bottleneck: bottleneck,
ss: dev.state.ss, mcs: dev.state.mcs, e1: e1, e2: e2, kappa: kappa
};
}
function updateDeviceUI(dev, res, finalTput, isWanLimited) {
document.getElementById(`eig1-${dev.id}`).innerText = res ? res.e1.toFixed(2) : "0.00";
document.getElementById(`eig2-${dev.id}`).innerText = res ? res.e2.toFixed(3) : "0.00";
const kEl = document.getElementById(`cond-${dev.id}`);
if(res) {
kEl.innerText = res.kappa.toFixed(1) + " dB";
if (res.kappa > 12) kEl.className = "math-val math-bad"; else kEl.className = "math-val math-ok";
} else { kEl.innerText = "--"; }
if (dev.clients === 0) {
document.getElementById(`status-${dev.id}`).innerText = "IDLE";
document.getElementById(`status-${dev.id}`).className = "dev-status";
document.getElementById(`lat-${dev.id}`).innerText = "--";
document.getElementById(`tput-${dev.id}`).innerText = "0 Mbps";
document.getElementById(`state-${dev.id}`).innerText = "Inactive";
for(let ss=1; ss<=4; ss++){ for(let m=0; m<=11; m++){ const n = document.getElementById(`d${dev.id}-n-${ss}-${m}`); if(n) n.className = 'mini-node'; } }
return;
}
for(let ss=1; ss<=4; ss++){
for(let m=0; m<=11; m++){
const n = document.getElementById(`d${dev.id}-n-${ss}-${m}`);
if(n) n.className = 'mini-node';
}
}
const g = document.getElementById(`d${dev.id}-n-${dev.ghost.ss}-${dev.ghost.mcs}`);
if(g) g.classList.add('ghost');
const a = document.getElementById(`d${dev.id}-n-${dev.state.ss}-${dev.state.mcs}`);
if(a) a.className = `mini-node ${res.visualClass}`;
const statEl = document.getElementById(`status-${dev.id}`);
const latEl = document.getElementById(`lat-${dev.id}`);
const tputEl = document.getElementById(`tput-${dev.id}`);
const stateEl = document.getElementById(`state-${dev.id}`);
let ssLabel = (res.ss > 2) ? `${res.ss} SS (Mu)` : `${res.ss} SS`;
stateEl.innerText = `MCS ${res.mcs} / ${ssLabel}`;
if (isWanLimited && res.per < 0.1) {
statEl.innerText = "WAN LIMITED"; statEl.className = "dev-status val-wan";
} else {
statEl.innerText = res.statusText; statEl.className = res.statusClass;
}
latEl.innerText = `Lat: ${Math.round(res.lat)}ms`;
if (res.lat < 10) latEl.className = "t-good"; else if (res.lat < 50) latEl.className = "t-mid"; else latEl.className = "t-bad";
tputEl.innerText = Math.round(finalTput) + " Mbps";
if (isWanLimited) tputEl.className = "t-wan"; else tputEl.className = (finalTput > 500) ? "t-good" : "t-mid";
}
init();
</script>
</body>
</html>