This shows you the differences between two versions of the page.
|
iothings:laboratoare:2025:lab1 [2025/09/25 22:10] dan.tudose [First Steps] |
iothings:laboratoare:2025:lab1 [2025/10/24 21:12] (current) dan.tudose [Lab 1. WiFi Server] |
||
|---|---|---|---|
| Line 1: | Line 1: | ||
| - | ===== Lab 1: Getting Started ====== | + | ===== Lab 1. Getting Started. WiFi Basics ====== |
| ===== Necessary gear ===== | ===== Necessary gear ===== | ||
| Line 282: | Line 282: | ||
| #include <WebServer.h> | #include <WebServer.h> | ||
| #include <SPIFFS.h> | #include <SPIFFS.h> | ||
| + | #include <ESPmDNS.h> | ||
| #include <Adafruit_BME680.h> | #include <Adafruit_BME680.h> | ||
| - | #define SEALEVEL_HPA (1019.9f) // adjust for better altitude accuracy | + | #define SEALEVEL_HPA (1013.25f) // adjust for better altitude accuracy |
| // ===== WiFi ===== | // ===== WiFi ===== | ||
| - | const char* WIFI_SSID = "TP-Link_2A64"; | + | const char* WIFI_SSID = "YOUR_SSID"; |
| - | const char* WIFI_PASS = "99481100"; | + | const char* WIFI_PASS = "YOUR_PASSWORD"; |
| + | const char* MDNS_NAME = "sparrow"; // -> http://sparrow.local/ | ||
| // ===== Web ===== | // ===== Web ===== | ||
| Line 301: | Line 303: | ||
| struct Sample { | struct Sample { | ||
| - | uint32_t t_ms; // millis() at sample time | + | uint32_t t_ms; |
| - | float tC; // °C | + | float tC; |
| - | float hPct; // % | + | float hPct; |
| - | float pHpa; // hPa | + | float pHpa; |
| - | float altM; // meters | + | float altM; |
| }; | }; | ||
| Sample hist[HISTORY_MAX]; | Sample hist[HISTORY_MAX]; | ||
| - | size_t histHead = 0; // next write index | + | size_t histHead = 0; |
| - | size_t histCount = 0; // how many valid samples | + | size_t histCount = 0; |
| void pushSample(const Sample& s) { | void pushSample(const Sample& s) { | ||
| Line 319: | Line 321: | ||
| Sample getSampleByAge(size_t iOldToNew) { | Sample getSampleByAge(size_t iOldToNew) { | ||
| - | // iOldToNew: 0=oldest .. histCount-1=newest | ||
| size_t idx = (histHead + HISTORY_MAX - histCount + iOldToNew) % HISTORY_MAX; | size_t idx = (histHead + HISTORY_MAX - histCount + iOldToNew) % HISTORY_MAX; | ||
| return hist[idx]; | return hist[idx]; | ||
| Line 328: | Line 329: | ||
| Wire.begin(21, 22); // set custom SDA/SCL here if needed: Wire.begin(SDA, SCL); | Wire.begin(21, 22); // set custom SDA/SCL here if needed: Wire.begin(SDA, SCL); | ||
| - | if (bme.begin(0x76, &Wire)) { | + | if (bme.begin(0x76, &Wire)) { Serial.println("[BME680] Found at 0x76"); return true; } |
| - | Serial.println("[BME680] Found at 0x76"); | + | if (bme.begin(0x77, &Wire)) { Serial.println("[BME680] Found at 0x77"); return true; } |
| - | return true; | + | |
| - | } | + | |
| - | if (bme.begin(0x77, &Wire)) { | + | |
| - | Serial.println("[BME680] Found at 0x77"); | + | |
| - | return true; | + | |
| - | } | + | |
| Serial.println("[BME680] Not found at 0x76/0x77"); | Serial.println("[BME680] Not found at 0x76/0x77"); | ||
| return false; | return false; | ||
| Line 345: | Line 340: | ||
| bme.setPressureOversampling(BME680_OS_4X); | bme.setPressureOversampling(BME680_OS_4X); | ||
| bme.setIIRFilterSize(BME680_FILTER_SIZE_3); | bme.setIIRFilterSize(BME680_FILTER_SIZE_3); | ||
| - | bme.setGasHeater(320, 150); // gas heater on; harmless if gas not used | + | bme.setGasHeater(320, 150); // optional |
| } | } | ||
| Line 371: | Line 366: | ||
| void handleApiSensor() { | void handleApiSensor() { | ||
| - | // Return the latest reading (take one now if buffer is empty) | ||
| if (histCount == 0) takeAndStoreSample(); | if (histCount == 0) takeAndStoreSample(); | ||
| Sample s = (histCount == 0) ? Sample{millis(), NAN, NAN, NAN, NAN} | Sample s = (histCount == 0) ? Sample{millis(), NAN, NAN, NAN, NAN} | ||
| Line 391: | Line 385: | ||
| if (n == 0) { server.send(200, "application/json", "{\"ok\":true,\"n\":0}"); return; } | if (n == 0) { server.send(200, "application/json", "{\"ok\":true,\"n\":0}"); return; } | ||
| - | // Build JSON: seconds-ago array + each metric | ||
| String json; | String json; | ||
| json.reserve(n * 64 + 128); | json.reserve(n * 64 + 128); | ||
| json += "{\"ok\":true,\"n\":" + String(n); | json += "{\"ok\":true,\"n\":" + String(n); | ||
| - | // seconds ago | + | // seconds ago array |
| json += ",\"s\":["; | json += ",\"s\":["; | ||
| for (size_t i = 0; i < n; ++i) { | for (size_t i = 0; i < n; ++i) { | ||
| Line 407: | Line 400: | ||
| auto appendSeries = [&](const char* key, float Sample::*field, int digits) { | auto appendSeries = [&](const char* key, float Sample::*field, int digits) { | ||
| - | json += ",\""; | + | json += ",\""; json += key; json += "\":["; |
| - | json += key; | + | |
| - | json += "\":["; | + | |
| for (size_t i = 0; i < n; ++i) { | for (size_t i = 0; i < n; ++i) { | ||
| Sample s = getSampleByAge(i); | Sample s = getSampleByAge(i); | ||
| Line 423: | Line 414: | ||
| appendSeries("alt", &Sample::altM, 1); | appendSeries("alt", &Sample::altM, 1); | ||
| + | json += "}"; | ||
| + | server.send(200, "application/json", json); | ||
| + | } | ||
| + | |||
| + | void handleApiStatus() { | ||
| + | bool up = WiFi.status() == WL_CONNECTED; | ||
| + | long rssi = up ? WiFi.RSSI() : -127; // dBm | ||
| + | uint32_t uptimeS = millis() / 1000; | ||
| + | |||
| + | String json = "{"; | ||
| + | json += "\"ok\":true"; | ||
| + | json += ",\"rssi_dbm\":" + String(rssi); | ||
| + | json += ",\"uptime_s\":" + String(uptimeS); | ||
| + | json += ",\"hostname\":\"" + String(MDNS_NAME) + "\""; | ||
| + | json += ",\"ip\":\"" + (up ? WiFi.localIP().toString() : String("")) + "\""; | ||
| json += "}"; | json += "}"; | ||
| server.send(200, "application/json", json); | server.send(200, "application/json", json); | ||
| Line 430: | Line 436: | ||
| Serial.begin(115200); | Serial.begin(115200); | ||
| while (!Serial) { delay(10); } | while (!Serial) { delay(10); } | ||
| - | Serial.println("\n[BOOT] ESP32-C6 BME680 Web Graphs"); | + | Serial.println("\n[BOOT] ESP32-C6 BME680 Web Graphs + Status + mDNS"); |
| if (!SPIFFS.begin(true)) Serial.println("[SPIFFS] Mount failed"); | if (!SPIFFS.begin(true)) Serial.println("[SPIFFS] Mount failed"); | ||
| else Serial.println("[SPIFFS] Mounted"); | else Serial.println("[SPIFFS] Mounted"); | ||
| - | // WiFi | + | // WiFi + mDNS |
| WiFi.mode(WIFI_STA); | WiFi.mode(WIFI_STA); | ||
| WiFi.setSleep(false); | WiFi.setSleep(false); | ||
| + | WiFi.setHostname(MDNS_NAME); | ||
| WiFi.begin(WIFI_SSID, WIFI_PASS); | WiFi.begin(WIFI_SSID, WIFI_PASS); | ||
| Serial.printf("[WiFi] Connecting to %s", WIFI_SSID); | Serial.printf("[WiFi] Connecting to %s", WIFI_SSID); | ||
| Line 448: | Line 455: | ||
| if (WiFi.status() == WL_CONNECTED) { | if (WiFi.status() == WL_CONNECTED) { | ||
| Serial.printf("[WiFi] Connected. IP: %s\n", WiFi.localIP().toString().c_str()); | Serial.printf("[WiFi] Connected. IP: %s\n", WiFi.localIP().toString().c_str()); | ||
| + | if (MDNS.begin(MDNS_NAME)) { | ||
| + | MDNS.addService("http", "tcp", 80); | ||
| + | Serial.printf("[mDNS] http://%s.local/\n", MDNS_NAME); | ||
| + | } else { | ||
| + | Serial.println("[mDNS] start failed"); | ||
| + | } | ||
| } else { | } else { | ||
| Serial.println("[WiFi] Failed to connect (continuing)"); | Serial.println("[WiFi] Failed to connect (continuing)"); | ||
| Line 456: | Line 469: | ||
| if (haveBME) setupBME680(); | if (haveBME) setupBME680(); | ||
| - | // Prime buffer with one sample so the page has data immediately | + | // Prime buffer |
| takeAndStoreSample(); | takeAndStoreSample(); | ||
| // Routes | // Routes | ||
| - | server.on("/", HTTP_GET, handleRoot); | + | server.on("/", HTTP_GET, handleRoot); |
| - | server.on("/api/sensor", HTTP_GET, handleApiSensor); | + | server.on("/api/sensor", HTTP_GET, handleApiSensor); |
| - | server.on("/api/history", HTTP_GET, handleApiHistory); | + | server.on("/api/history",HTTP_GET, handleApiHistory); |
| + | server.on("/api/status", HTTP_GET, handleApiStatus); | ||
| server.onNotFound([]() { | server.onNotFound([]() { | ||
| Line 478: | Line 492: | ||
| Serial.println("[HTTP] Server started"); | Serial.println("[HTTP] Server started"); | ||
| if (WiFi.status() == WL_CONNECTED) | if (WiFi.status() == WL_CONNECTED) | ||
| - | Serial.println("[HTTP] Open: http://" + WiFi.localIP().toString() + "/"); | + | Serial.println("[HTTP] Open: http://" + WiFi.localIP().toString() + "/ or http://sparrow.local/"); |
| } | } | ||
| Line 484: | Line 498: | ||
| server.handleClient(); | server.handleClient(); | ||
| - | // Take a sample once per second | + | // 1 Hz sampling |
| static uint32_t lastSample = 0; | static uint32_t lastSample = 0; | ||
| uint32_t now = millis(); | uint32_t now = millis(); | ||
| Line 490: | Line 504: | ||
| lastSample = now; | lastSample = now; | ||
| takeAndStoreSample(); | takeAndStoreSample(); | ||
| - | |||
| - | // Drop samples older than 5 minutes (optional; ring buffer already caps size) | ||
| - | // (kept for clarity if you ever increase HISTORY_MAX) | ||
| } | } | ||
| - | |||
| delay(2); | delay(2); | ||
| } | } | ||
| + | |||
| </code> | </code> | ||
| Line 504: | Line 515: | ||
| <code html index.html> | <code html index.html> | ||
| <!doctype html> | <!doctype html> | ||
| - | <html lang="en"> | + | <html lang="en" data-theme="light"> |
| <head> | <head> | ||
| <meta charset="utf-8"/> | <meta charset="utf-8"/> | ||
| - | <title>ESP32-C6 Sparrow Dashboard</title> | + | <title>BME680 Dashboard</title> |
| <meta name="viewport" content="width=device-width, initial-scale=1"/> | <meta name="viewport" content="width=device-width, initial-scale=1"/> | ||
| <style> | <style> | ||
| - | :root { --fg:#222; --muted:#777; --card:#f7f7f7; } | + | :root { |
| - | body { font-family: system-ui,-apple-system,Segoe UI,Roboto,sans-serif; margin: 16px; color: var(--fg); } | + | --bg:#ffffff; --fg:#222; --muted:#777; --card:#f7f7f7; --axis:#00000066; --grid:#00000012; --line:#0b6; |
| + | } | ||
| + | [data-theme="dark"] { | ||
| + | --bg:#0f1115; --fg:#eaeef2; --muted:#9aa4b2; --card:#171a21; --axis:#ffffff66; --grid:#ffffff13; --line:#38bdf8; | ||
| + | } | ||
| + | html,body { height:100%; } | ||
| + | body { background: var(--bg); color: var(--fg); | ||
| + | font-family: system-ui,-apple-system,Segoe UI,Roboto,sans-serif; margin: 16px; } | ||
| h1 { margin: 0 0 8px 0; } | h1 { margin: 0 0 8px 0; } | ||
| .row { display:flex; gap:16px; flex-wrap:wrap; align-items:baseline; } | .row { display:flex; gap:16px; flex-wrap:wrap; align-items:baseline; } | ||
| Line 521: | Line 539: | ||
| canvas { width:100%; height:200px; display:block; } | canvas { width:100%; height:200px; display:block; } | ||
| .ts { color: var(--muted); margin-top: 8px; font-size:.9rem; } | .ts { color: var(--muted); margin-top: 8px; font-size:.9rem; } | ||
| - | @media (max-width: 700px) { .grid { grid-template-columns: 1fr; } } /* optional: stack on small screens */ | + | @media (max-width: 700px) { .grid { grid-template-columns: 1fr; } } |
| + | |||
| + | /* Toggle + status pill */ | ||
| + | .topbar { display:flex; justify-content:space-between; align-items:center; margin-bottom:8px; gap:12px; } | ||
| + | .toggle { cursor:pointer; padding:6px 10px; border-radius:10px; background:var(--card); border:1px solid #00000010; } | ||
| + | .pill { position: sticky; top: 8px; align-self: start; padding:6px 10px; border-radius:999px; | ||
| + | background:var(--card); border:1px solid #00000010; color:var(--fg); font-size:.9rem; } | ||
| + | .pill a { color: inherit; text-decoration: none; border-bottom:1px dotted var(--muted); } | ||
| </style> | </style> | ||
| </head> | </head> | ||
| <body> | <body> | ||
| - | <h1>ESP32-C6 Sparrow Dashboard</h1> | + | <div class="topbar"> |
| + | <h1>BME680 Dashboard (5 min)</h1> | ||
| + | <div class="toggle" id="themeToggle">🌙 Dark mode</div> | ||
| + | </div> | ||
| + | |||
| + | <div class="row" style="margin-bottom:8px;"> | ||
| + | <div class="pill" id="status">Wi-Fi: -- dBm · Uptime: --:--:-- · <a href="http://sparrow.local" target="_blank" rel="noreferrer">sparrow.local</a></div> | ||
| + | </div> | ||
| <div class="row"> | <div class="row"> | ||
| <div><span class="label">Temperature</span> <span id="t" class="metric">--</span></div> | <div><span class="label">Temperature</span> <span id="t" class="metric">--</span></div> | ||
| Line 544: | Line 577: | ||
| <script> | <script> | ||
| const secsWindow = 300; // 5 minutes | const secsWindow = 300; // 5 minutes | ||
| - | |||
| function $(id){ return document.getElementById(id); } | function $(id){ return document.getElementById(id); } | ||
| - | // Simple helper to choose decimals based on range | + | // ---- Theme toggle (persisted) ---- |
| + | (function initTheme(){ | ||
| + | const saved = localStorage.getItem('theme') || 'light'; | ||
| + | document.documentElement.setAttribute('data-theme', saved); | ||
| + | $('themeToggle').textContent = saved === 'dark' ? '☀️ Light mode' : '🌙 Dark mode'; | ||
| + | $('themeToggle').addEventListener('click', () => { | ||
| + | const cur = document.documentElement.getAttribute('data-theme') || 'light'; | ||
| + | const next = cur === 'dark' ? 'light' : 'dark'; | ||
| + | document.documentElement.setAttribute('data-theme', next); | ||
| + | localStorage.setItem('theme', next); | ||
| + | $('themeToggle').textContent = next === 'dark' ? '☀️ Light mode' : '🌙 Dark mode'; | ||
| + | }); | ||
| + | })(); | ||
| + | |||
| + | // ---- Chart helpers ---- | ||
| function decimalsForRange(range){ | function decimalsForRange(range){ | ||
| if (!isFinite(range) || range <= 0) return 1; | if (!isFinite(range) || range <= 0) return 1; | ||
| Line 554: | Line 600: | ||
| return 0; | return 0; | ||
| } | } | ||
| - | |||
| - | // Draw a line chart with a Y axis (left), ticks, labels | ||
| function drawSeries(canvas, secondsAgo, values, color) { | function drawSeries(canvas, secondsAgo, values, color) { | ||
| const dpr = window.devicePixelRatio || 1; | const dpr = window.devicePixelRatio || 1; | ||
| - | const padL = 48 * dpr, padR = 8 * dpr, padT = 8 * dpr, padB = 18 * dpr; // room for axis/labels | + | const padL = 48 * dpr, padR = 8 * dpr, padT = 8 * dpr, padB = 18 * dpr; |
| const ctx = canvas.getContext('2d'); | const ctx = canvas.getContext('2d'); | ||
| Line 582: | Line 626: | ||
| const plotH = h - padT - padB; | const plotH = h - padT - padB; | ||
| - | // Axes | + | // Axes + grid |
| - | ctx.strokeStyle = 'rgba(0,0,0,0.35)'; | + | ctx.strokeStyle = getComputedStyle(document.documentElement).getPropertyValue('--axis').trim(); |
| ctx.lineWidth = 1 * dpr; | ctx.lineWidth = 1 * dpr; | ||
| - | // Y axis line | + | ctx.beginPath(); ctx.moveTo(padL, padT); ctx.lineTo(padL, h - padB); ctx.stroke(); |
| - | ctx.beginPath(); | + | ctx.beginPath(); ctx.moveTo(padL, h - padB); ctx.lineTo(w - padR, h - padB); ctx.stroke(); |
| - | ctx.moveTo(padL, padT); | + | |
| - | ctx.lineTo(padL, h - padB); | + | |
| - | ctx.stroke(); | + | |
| - | // X axis baseline | + | |
| - | ctx.beginPath(); | + | |
| - | ctx.moveTo(padL, h - padB); | + | |
| - | ctx.lineTo(w - padR, h - padB); | + | |
| - | ctx.stroke(); | + | |
| - | // Horizontal grid + Y tick labels (6 ticks) | + | ctx.strokeStyle = getComputedStyle(document.documentElement).getPropertyValue('--grid').trim(); |
| - | ctx.strokeStyle = 'rgba(0,0,0,0.07)'; | + | ctx.fillStyle = getComputedStyle(document.documentElement).getPropertyValue('--axis').trim(); |
| - | ctx.fillStyle = 'rgba(0,0,0,0.6)'; | + | ctx.textAlign = 'right'; ctx.textBaseline = 'middle'; |
| - | ctx.textAlign = 'right'; | + | |
| - | ctx.textBaseline = 'middle'; | + | |
| ctx.font = `${12*dpr}px system-ui,-apple-system,Segoe UI,Roboto,sans-serif`; | ctx.font = `${12*dpr}px system-ui,-apple-system,Segoe UI,Roboto,sans-serif`; | ||
| for (let i = 0; i <= 5; i++) { | for (let i = 0; i <= 5; i++) { | ||
| Line 606: | Line 640: | ||
| const yVal = yMax - yRange * frac; | const yVal = yMax - yRange * frac; | ||
| const y = padT + plotH * frac; | const y = padT + plotH * frac; | ||
| - | // grid line | + | ctx.beginPath(); ctx.moveTo(padL, y); ctx.lineTo(w - padR, y); ctx.stroke(); |
| - | ctx.beginPath(); | + | ctx.fillText(yVal.toFixed(yDec), padL - 6*dpr, y); |
| - | ctx.moveTo(padL, y); | + | |
| - | ctx.lineTo(w - padR, y); | + | |
| - | ctx.stroke(); | + | |
| - | // label | + | |
| - | const label = yVal.toFixed(yDec); | + | |
| - | ctx.fillText(label, padL - 6*dpr, y); | + | |
| } | } | ||
| - | // Map data points to plot coords | ||
| const xForSec = s => padL + plotW * (1 - Math.min(s, secsWindow) / secsWindow); | const xForSec = s => padL + plotW * (1 - Math.min(s, secsWindow) / secsWindow); | ||
| const yForVal = v => padT + plotH * ((yMax - v) / yRange); | const yForVal = v => padT + plotH * ((yMax - v) / yRange); | ||
| - | // Data line | + | ctx.strokeStyle = color || getComputedStyle(document.documentElement).getPropertyValue('--line').trim(); |
| - | ctx.strokeStyle = color || '#0b6'; | + | |
| ctx.lineWidth = Math.max(1, 1.5 * dpr); | ctx.lineWidth = Math.max(1, 1.5 * dpr); | ||
| ctx.beginPath(); | ctx.beginPath(); | ||
| Line 631: | Line 657: | ||
| ctx.stroke(); | ctx.stroke(); | ||
| - | // X labels (left=-5m, mid=-2.5m, right=now) | + | // X labels |
| - | ctx.fillStyle = 'rgba(0,0,0,0.5)'; | + | ctx.fillStyle = getComputedStyle(document.documentElement).getPropertyValue('--axis').trim(); |
| - | ctx.textAlign = 'center'; | + | ctx.textAlign = 'center'; ctx.textBaseline = 'top'; |
| - | ctx.textBaseline = 'top'; | + | const xLabels = [{s:300, txt:'-5m'}, {s:150, txt:'-2.5m'}, {s:5, txt:'now'}]; |
| - | const xLabels = [{s:secsWindow, txt:'-5m'}, {s:secsWindow/2, txt:'-2.5m'}, {s:20, txt:'now'}]; | + | |
| xLabels.forEach(l=>{ | xLabels.forEach(l=>{ | ||
| const x = xForSec(l.s); | const x = xForSec(l.s); | ||
| Line 642: | Line 667: | ||
| } | } | ||
| - | async function refresh() { | + | // ---- Data refresh ---- |
| + | async function refreshHistory() { | ||
| try { | try { | ||
| const r = await fetch('/api/history', {cache:'no-store'}); | const r = await fetch('/api/history', {cache:'no-store'}); | ||
| const j = await r.json(); | const j = await r.json(); | ||
| - | |||
| if (!j.ok) { $('ts').textContent = 'No data'; return; } | if (!j.ok) { $('ts').textContent = 'No data'; return; } | ||
| Line 660: | Line 685: | ||
| drawSeries($('ch'), j.s, j.rh, '#17bebb'); | drawSeries($('ch'), j.s, j.rh, '#17bebb'); | ||
| drawSeries($('cp'), j.s, j.p, '#4a7'); | drawSeries($('cp'), j.s, j.p, '#4a7'); | ||
| - | drawSeries($('ca'), j.s, j.alt,'#555'); | + | drawSeries($('ca'), j.s, j.alt,'#999'); |
| $('ts').textContent = 'Updated: ' + new Date().toLocaleTimeString(); | $('ts').textContent = 'Updated: ' + new Date().toLocaleTimeString(); | ||
| - | } catch (e) { | + | } catch { |
| $('ts').textContent = 'Fetch error'; | $('ts').textContent = 'Fetch error'; | ||
| } | } | ||
| } | } | ||
| - | refresh(); | + | |
| - | setInterval(refresh, 2000); | + | function fmtUptime(sec){ |
| + | const h = Math.floor(sec/3600); | ||
| + | const m = Math.floor((sec%3600)/60); | ||
| + | const s = sec%60; | ||
| + | const pad = n => n.toString().padStart(2,'0'); | ||
| + | return `${pad(h)}:${pad(m)}:${pad(s)}`; | ||
| + | } | ||
| + | |||
| + | async function refreshStatus(){ | ||
| + | try { | ||
| + | const r = await fetch('/api/status', {cache:'no-store'}); | ||
| + | const j = await r.json(); | ||
| + | const rssi = (j && Number.isFinite(j.rssi_dbm)) ? `${j.rssi_dbm} dBm` : '-- dBm'; | ||
| + | const up = (j && Number.isFinite(j.uptime_s)) ? fmtUptime(j.uptime_s) : '--:--:--'; | ||
| + | $('status').innerHTML = `Wi-Fi: ${rssi} · Uptime: ${up} · <a href="http://${j.hostname||'sparrow'}.local" target="_blank" rel="noreferrer">${j.hostname||'sparrow'}.local</a>`; | ||
| + | } catch { | ||
| + | $('status').textContent = 'Wi-Fi: -- dBm · Uptime: --:--:-- · sparrow.local'; | ||
| + | } | ||
| + | } | ||
| + | |||
| + | refreshHistory(); | ||
| + | refreshStatus(); | ||
| + | setInterval(refreshHistory, 2000); | ||
| + | setInterval(refreshStatus, 5000); | ||
| </script> | </script> | ||
| </body> | </body> | ||
| </html> | </html> | ||
| + | |||
| </code> | </code> | ||
| Line 678: | Line 727: | ||
| </note> | </note> | ||
| - | === 6. Advertise on BLE === | ||
| - | |||
| - | Build the example below, which advertises the board on BLE. Install on your phone an app that scans nearby Bluetooth devices, such as [[https://play.google.com/store/apps/details?id=no.nordicsemi.android.mcp&hl=en&pli=1| nRF Connect]]. Check if your device is in the list. | ||
| - | |||
| - | <code C main.cpp> | ||
| - | #include <Arduino.h> | ||
| - | #include <NimBLEDevice.h> | ||
| - | |||
| - | static const char* DEVICE_NAME = "ESP32-C6 Demo"; | ||
| - | static NimBLEUUID SERVICE_UUID("6E400001-B5A3-F393-E0A9-E50E24DCCA9E"); | ||
| - | static NimBLEUUID CHAR_UUID ("6E400002-B5A3-F393-E0A9-E50E24DCCA9E"); | ||
| - | |||
| - | NimBLEServer* gServer = nullptr; | ||
| - | NimBLEService* gService = nullptr; | ||
| - | NimBLECharacteristic* gChar = nullptr; | ||
| - | |||
| - | void startBLE() { | ||
| - | NimBLEDevice::init(DEVICE_NAME); | ||
| - | |||
| - | gServer = NimBLEDevice::createServer(); | ||
| - | gService = gServer->createService(SERVICE_UUID); | ||
| - | |||
| - | gChar = gService->createCharacteristic( | ||
| - | CHAR_UUID, | ||
| - | NIMBLE_PROPERTY::READ | ||
| - | ); | ||
| - | gChar->setValue("Hello from ESP32-C6!"); | ||
| - | gService->start(); | ||
| - | |||
| - | NimBLEAdvertising* adv = NimBLEDevice::getAdvertising(); | ||
| - | |||
| - | // Advertise our service UUID | ||
| - | adv->addServiceUUID(SERVICE_UUID); | ||
| - | |||
| - | // (v2.x) Build advertising + scan-response payloads explicitly | ||
| - | NimBLEAdvertisementData advData; | ||
| - | advData.setFlags(0x06); // LE General Discoverable + BR/EDR Not Supported | ||
| - | |||
| - | NimBLEAdvertisementData scanData; | ||
| - | scanData.setName(DEVICE_NAME); // put the name in scan response | ||
| - | // you can also add manufacturer data here if you want: | ||
| - | // std::string mfg = "\x34\x12C6"; scanData.setManufacturerData(mfg); | ||
| - | |||
| - | adv->setAdvertisementData(advData); | ||
| - | adv->setScanResponseData(scanData); | ||
| - | |||
| - | // Appearance is still supported | ||
| - | adv->setAppearance(0x0200); // Generic Tag | ||
| - | |||
| - | NimBLEDevice::startAdvertising(); | ||
| - | } | ||
| - | |||
| - | void setup() { | ||
| - | Serial.begin(115200); | ||
| - | while (!Serial) { delay(10); } | ||
| - | startBLE(); | ||
| - | Serial.println("Advertising as ESP32-C6 Demo. Open nRF Connect -> Scan."); | ||
| - | } | ||
| - | |||
| - | void loop() { | ||
| - | delay(1000); | ||
| - | } | ||
| - | |||
| - | </code> | ||
| - | {{:iothings:laboratoare:lab1-ble-scanner.jpg?300|}} | ||
| ===== Resources ===== | ===== Resources ===== | ||