WiFi Signal Mapper

Autor: Sorescu Teodor

Email: teodor.sorescu@stud.etti.upb.ro

Master: SAS2

Introducere

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.

Context

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.

Hardware

  • ESP32-D0WD-V3 (revizia v3.1) – microcontroller cu WiFi încorporat
  • Baterie externă portabilă – pentru alimentare
  • Cablu USB-A → USB-C – pentru alimentarea ESP32 de la bateria portabilă

Imagine cu ansamblul hardware

Software

Î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();
}

Frontend (Web Server)

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”.

Interfața web de monitorizare a semnalului Interfața web de monitorizare a semnalului Interfața web de monitorizare a semnalului

Referințe

iothings/proiecte/2025sric/wfsm.txt · Last modified: 2025/05/29 11:35 by teodor.sorescu
CC Attribution-Share Alike 3.0 Unported
www.chimeric.de Valid CSS Driven by DokuWiki do yourself a favour and use a real browser - get firefox!! Recent changes RSS feed Valid XHTML 1.0