Differences

This shows you the differences between two versions of the page.

Link to this comparison view

iothings:laboratoare:2025:lab1 [2025/09/25 22:10]
dan.tudose [First Steps]
iothings:laboratoare:2025:lab1 [2025/09/25 22:37] (current)
dan.tudose
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>​
iothings/laboratoare/2025/lab1.1758827450.txt.gz · Last modified: 2025/09/25 22:10 by dan.tudose
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