WiFi Signal Mapper este un proiect care permite măsurarea și vizualizarea intensității semnalului WiFi în diverse puncte dintr-o încăpere sau spațiu exterior. Dispozitivul realizează scanări, stochează rezultatele și le afișează într-o interfață web.
Semnalul WiFi poate varia semnificativ în funcție de poziționarea routerului, obstrucții (pereți, mobilă) și sursele de interferență. Acest proiect ajută la identificarea zonelor cu acoperire slabă („zone moarte”) sau foarte bune, pentru optimizarea amplasării echipamentelor și îmbunătățirea experienței utilizatorului.
În această secțiune veți găsi codul sursă complet al proiectului. Utilizăm biblioteca WiFi.h pentru scanări, SPIFFS pentru stocarea locală a datelor și ArduinoJson pentru serializarea rezultatelor în format JSON.
#include <WiFi.h> #include <WebServer.h> #include <SPIFFS.h> #include <ArduinoJson.h> const char* ap_ssid = "WFSM"; const char* ap_password = ""; WebServer server(80); void handleRoot() { server.send(200, "text/html", R"rawliteral( <!DOCTYPE html> <html> <head> <meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>WiFi Signal Mapper</title> <style> *{margin:0;padding:0;box-sizing:border-box} body{font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif; background:linear-gradient(135deg,#667eea 0%,#764ba2 100%); min-height:100vh;padding:20px} .container{max-width:100%;margin:0 auto;background:white; border-radius:20px;box-shadow:0 20px 40px rgba(0,0,0,0.1); overflow:hidden} .header{background:linear-gradient(45deg,#ff6b6b,#ff8e53); color:white;padding:30px 20px;text-align:center} .header h1{font-size:28px;margin-bottom:10px} .content{padding:30px 20px} .input-group{margin-bottom:25px} label{display:block;margin-bottom:8px;font-weight:600;color:#333} input[type=text]{width:100%;padding:15px;border:2px solid #e1e8ed; border-radius:12px;font-size:16px;transition:border-color .3s} input[type=text]:focus{outline:none;border-color:#667eea} .button-group{display:flex;gap:15px;margin-bottom:30px;flex-wrap:wrap} button{flex:1;min-width:120px;padding:15px 25px;border:none; border-radius:12px;font-size:16px;font-weight:600; cursor:pointer;transition:all .3s;touch-action:manipulation} .btn-primary{background:linear-gradient(45deg,#667eea,#764ba2);color:white} .btn-primary:hover{transform:translateY(-2px); box-shadow:0 10px 20px rgba(102,126,234,0.3)} .btn-danger{background:linear-gradient(45deg,#ff6b6b,#ff8e53);color:white} .btn-danger:hover{transform:translateY(-2px); box-shadow:0 10px 20px rgba(255,107,107,0.3)} .data-section{margin-top:30px} .scan-group{margin-bottom:30px;border:1px solid #e1e8ed; border-radius:15px;overflow:hidden} .scan-header{background:#f8f9fa;padding:15px 20px; border-bottom:1px solid #e1e8ed} .scan-name{font-size:18px;font-weight:600;color:#333} .scan-time{font-size:12px;color:#666;margin-top:5px} .wifi-list{padding:20px} .wifi-item{display:flex;justify-content:space-between; align-items:center;padding:12px 0;border-bottom:1px solid #f0f0f0} .wifi-item:last-child{border-bottom:none} .wifi-name{font-weight:600;color:#333;flex:1;margin-right:15px} .wifi-strength{display:flex;align-items:center;gap:8px} .signal-strength{padding:4px 8px;border-radius:8px; font-size:12px;font-weight:600;color:white; min-width:70px;text-align:center; box-shadow:0 2px 4px rgba(0,0,0,0.1)} .signal-perfect{background:linear-gradient(45deg,#00C851,#007E33); box-shadow:0 0 10px rgba(0,200,81,0.3)} .signal-excellent{background:linear-gradient(45deg,#4CAF50,#45a049)} .signal-very-good{background:linear-gradient(45deg,#8BC34A,#7CB342)} .signal-good{background:linear-gradient(45deg,#CDDC39,#C0CA33)} .signal-fair{background:linear-gradient(45deg,#FFC107,#FF8F00)} .signal-weak{background:linear-gradient(45deg,#FF9800,#F57C00)} .signal-poor{background:linear-gradient(45deg,#FF5722,#E64A19)} .signal-very-poor{background:linear-gradient(45deg,#F44336,#D32F2F)} .signal-text{font-size:14px;color:#666;min-width:50px;font-weight:600} .spinner{border:3px solid #f3f3f3;border-top:3px solid #667eea; border-radius:50%;width:30px;height:30px; animation:spin 1s linear infinite;margin:0 auto 15px} @keyframes spin{0%{transform:rotate(0deg)}100%{transform:rotate(360deg)}} .loading{text-align:center;padding:20px;color:#666} @media (max-width:480px){ .button-group{flex-direction:column} button{flex:none} .wifi-item{flex-direction:column;align-items:flex-start;gap:8px} .wifi-strength{align-self:flex-end} } </style> </head> <body> <div class="container"> <div class="header"> <h1>WiFi Signal Mapper</h1> <p>Scan and map WiFi signal strengths</p> </div> <div class="content"> <div class="input-group"> <label for="scanName">Scan Location Name:</label> <input type="text" id="scanName" placeholder="e.g., Living Room, Office, Bedroom A"> </div> <div class="button-group"> <button class="btn-primary" onclick="startScan()">Start Scan</button> <button class="btn-danger" onclick="resetData()">Reset Data</button> </div> <div class="data-section"> <div id="scanData"></div> </div> </div> </div> <script> function startScan() { const name = document.getElementById('scanName').value.trim(); if (!name) { alert('Please enter a location name'); return; } document.getElementById('scanData').innerHTML = '<div class="loading"><div class="spinner"></div>Scanning WiFi networks...</div>'; fetch('/scan', { method: 'POST', headers: {'Content-Type':'application/x-www-form-urlencoded'}, body: 'name='+encodeURIComponent(name) }).then(()=>{ document.getElementById('scanName').value=''; setTimeout(loadData,3000); }); } function resetData() { if (!confirm('Are you sure you want to reset all scan data?')) return; fetch('/reset', {method:'POST'}).then(loadData); } function loadData() { document.getElementById('scanData').innerHTML = '<div class="loading">Loading scan data...</div>'; fetch('/data') .then(r=>r.json()) .then(scans => { if (!scans.length) { document.getElementById('scanData').innerHTML = '<div class="loading">No scan data available. Click "Start Scan" to begin.</div>'; return; } let html = ''; scans.slice().reverse().forEach(scan => { const nets = (scan.networks||[]).sort((a,b)=>b.rssi-a.rssi); html += `<div class="scan-group"> <div class="scan-header"> <div class="scan-name">${scan.name}</div> <div class="scan-time">Networks found: ${nets.length}</div> </div> <div class="wifi-list">`; if (!nets.length) { html += '<div class="loading">No networks found in this scan</div>'; } else { nets.forEach(net => { let cls,text; const r = net.rssi; if (r>=-30){cls='signal-perfect';text='Perfect';} else if(r>=-40){cls='signal-excellent';text='Excellent';} else if(r>=-50){cls='signal-very-good';text='Very Good';} else if(r>=-60){cls='signal-good';text='Good';} else if(r>=-70){cls='signal-fair';text='Fair';} else if(r>=-80){cls='signal-weak';text='Weak';} else if(r>=-90){cls='signal-poor';text='Poor';} else {cls='signal-very-poor';text='Very Poor';} const ss = net.ssid||'Hidden Network'; html += `<div class="wifi-item"> <div class="wifi-name">${ss}</div> <div class="wifi-strength"> <div class="signal-strength ${cls}">${text}</div> <div class="signal-text">${r} dBm</div> </div> </div>`; }); } html += '</div></div>'; }); document.getElementById('scanData').innerHTML = html; }) .catch(_=>{ document.getElementById('scanData').innerHTML = '<div class="loading">Error loading data</div>'; }); } // initial & periodic load loadData(); setInterval(loadData, 10000); </script> </body> </html> )rawliteral"); } void handleScan() { if (!server.hasArg("name")) { server.send(400, "text/plain", "Missing scan name"); return; } String name = server.arg("name"); int n = WiFi.scanNetworks(); // build this scan DynamicJsonDocument scanDoc(1024); scanDoc["name"] = name; auto arr = scanDoc.createNestedArray("networks"); for (int i = 0; i < n; ++i) { JsonObject obj = arr.createNestedObject(); obj["ssid"] = WiFi.SSID(i); obj["rssi"] = WiFi.RSSI(i); } WiFi.scanDelete(); // load existing or start new DynamicJsonDocument all(8192); JsonArray scans; if (SPIFFS.exists("/scans.json")) { File f = SPIFFS.open("/scans.json", "r"); if (f) { if (deserializeJson(all, f) == DeserializationError::Ok && all.is<JsonArray>()) { scans = all.as<JsonArray>(); } f.close(); } } if (!scans) scans = all.to<JsonArray>(); // append & save scans.add(scanDoc); File out = SPIFFS.open("/scans.json", "w"); if (out) { serializeJson(all, out); out.close(); } server.send(200, "text/plain", "Scan completed"); } void handleReset() { SPIFFS.remove("/scans.json"); server.send(200, "text/plain", "Data reset"); } void handleGetData() { if (SPIFFS.exists("/scans.json")) { File f = SPIFFS.open("/scans.json", "r"); String data = f.readString(); f.close(); server.send(200, "application/json", data); } else { server.send(200, "application/json", "[]"); } } void setup() { Serial.begin(115200); SPIFFS.begin(true); WiFi.softAP(ap_ssid, ap_password); server.on("/", HTTP_GET, handleRoot); server.on("/scan", HTTP_POST, handleScan); server.on("/reset", HTTP_POST, handleReset); server.on("/data", HTTP_GET, handleGetData); server.begin(); } void loop() { server.handleClient(); }
ESP32 servește o pagină web responsive care afișează rezultatele scanărilor: numele locației, lista rețelelor detectate și puterea semnalului, colorată de la “Perfect” la “Very Poor”.