Differences

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

Link to this comparison view

iothings:laboratoare:2025:lab1 [2025/09/25 14:05]
dan.tudose
iothings:laboratoare:2025:lab1 [2025/09/25 22:37] (current)
dan.tudose
Line 24: Line 24:
  
 After downloading and installing the PlatformIO extension, create a new project using any ESP32-C6 board. After project creation, you will need to edit the platformio.ini file and replace it with the following: After downloading and installing the PlatformIO extension, create a new project using any ESP32-C6 board. After project creation, you will need to edit the platformio.ini file and replace it with the following:
 +
 <code bash platformio.ini>​ <code bash platformio.ini>​
 ; PlatformIO Project Configuration File ; PlatformIO Project Configuration File
Line 34: Line 35:
 ; Please visit documentation for the other options and examples ; Please visit documentation for the other options and examples
 ; https://​docs.platformio.org/​page/​projectconf.html ; https://​docs.platformio.org/​page/​projectconf.html
 + 
 [env:​esp32-c6-sparrow] [env:​esp32-c6-sparrow]
-platform = https://​github.com/​FarhadGUL06/​platform-espressif32.git +platform = https://​github.com/​pioarduino/​platform-espressif32/​releases/​download/​54.03.20/​platform-espressif32.zip 
-board = esp32-c6-sparrow+board = esp32-c6-devkitm-1
 framework = arduino framework = arduino
 +; use SPIFFS for on-board files
 +board_build.filesystem = spiffs
 +
 +build_flags =
 +  -D ARDUINO_USB_MODE=1
 +  -D ARDUINO_USB_CDC_ON_BOOT=1
 +  -D ESP32_C6_env
  
-; Serial monitor options 
 monitor_speed = 115200 monitor_speed = 115200
  
 lib_deps = lib_deps =
-    ​adafruit/​Adafruit NeoPixel@^1.11.0 +  ​adafruit/​Adafruit NeoPixel@^1.11.0 
-    adafruit/​Adafruit GFX Library@^1.11.9 +  adafruit/​Adafruit GFX Library@^1.11.9 
-    adafruit/​Adafruit SSD1306@^2.5.10 +  adafruit/​Adafruit SSD1306@^2.5.10 
-    dantudose/​LTR308 library@^1.0 +  ​adafruit/​Adafruit BME680 Library 
-    https://​github.com/​sparkfun/​SparkFun_MAX1704x_Fuel_Gauge_Arduino_Library +  ​dantudose/​LTR308 library@^1.0 
-    ​stm32duino/STM32duino LSM6DSL@^2.0.0+  https://​github.com/​sparkfun/​SparkFun_MAX1704x_Fuel_Gauge_Arduino_Library 
 +  ​h2zero/NimBLE-Arduino@^2.1.0  
 + 
 </​code>​ </​code>​
  
Line 102: Line 112:
 </​code>​ </​code>​
  
 +=== 3. Bringing up the sensors ===
  
-=== 3. Scan and display local WiFi networks ===+Now, let's do a quick sensor bring-up for the BME680 (temperature,​ humidity, pressure ​and gas). The sensor is connected on the I2C bus. Use the code below to read values over the serial terminal.
  
-Load this WiFi scanner code:+<code C main.cpp>​ 
 +#include <​Arduino.h>​ 
 +#include <​Wire.h>​ 
 +#include <​Adafruit_BME680.h>​ 
 + 
 +// Change this if you want altitude computed for your location 
 +#define SEALEVEL_HPA (1013.25f) 
 + 
 +// Try both common I2C addresses 
 +Adafruit_BME680 bme; // use the default constructor 
 + 
 +bool beginBME680() { 
 +  // SDA on pin 21 and SCL on pin 22 
 +  Wire.begin(21,​ 22); 
 + 
 +  // Try 0x76 first 
 +  if (bme.begin(0x76,​ &Wire)) { 
 +    Serial.println("​[BME680] Found at 0x76"​);​ 
 +    return true; 
 +  } 
 +  // Then 0x77 
 +  if (bme.begin(0x77,​ &Wire)) { 
 +    Serial.println("​[BME680] Found at 0x77"​);​ 
 +    return true; 
 +  } 
 + 
 +  Serial.println("​[BME680] Sensor not found at 0x76 or 0x77. Check wiring/​power."​);​ 
 +  return false; 
 +
 + 
 +void setupBME680() { 
 +  // Oversampling & filter settings tuned for ~1 Hz updates 
 +  bme.setTemperatureOversampling(BME680_OS_8X);​ 
 +  bme.setHumidityOversampling(BME680_OS_2X);​ 
 +  bme.setPressureOversampling(BME680_OS_4X);​ 
 +  bme.setIIRFilterSize(BME680_FILTER_SIZE_3);​ 
 + 
 +  // Enable gas heater: 320°C for 150 ms (typical example) 
 +  bme.setGasHeater(320,​ 150); 
 +
 + 
 +void setup() { 
 +  Serial.begin(115200);​ 
 +  while (!Serial) { delay(100); } 
 + 
 +  Serial.println("​\n[BOOT] BME680 serial demo (1 Hz)"​);​ 
 + 
 +  if (!beginBME680()) { 
 +    // Stay here so you can read the error 
 +    while (true) { delay(1000);​ } 
 +  } 
 + 
 +  setupBME680();​ 
 +
 + 
 +void loop() { 
 +  // Trigger a reading and wait for completion 
 +  if (!bme.performReading()) { 
 +    Serial.println("​[BME680] Failed to perform reading!"​);​ 
 +    delay(1000);​ 
 +    return; 
 +  } 
 + 
 +  // Values from the Adafruit_BME680 library: 
 +  float temperatureC = bme.temperature; ​              // °C 
 +  float pressureHpa ​ = bme.pressure / 100.0f; ​        // Pa -> hPa 
 +  float humidityPct ​ = bme.humidity; ​                 // % 
 +  float gasOhms ​     = bme.gas_resistance; ​           // Ω 
 +  float altitudeM ​   = bme.readAltitude(SEALEVEL_HPA);//​ meters (approx.) 
 + 
 +  // Print nicely 
 +  Serial.print("​T:​ "​); ​ Serial.print(temperatureC,​ 2); Serial.print("​ °C  | "); 
 +  Serial.print("​RH:​ "); Serial.print(humidityPct,​ 1);  Serial.print("​ %  | "); 
 +  Serial.print("​P:​ "​); ​ Serial.print(pressureHpa,​ 2);  Serial.print("​ hPa  | "); 
 +  Serial.print("​Gas:​ "​);​Serial.print(gasOhms,​ 0);      Serial.print("​ Ω  | "); 
 +  Serial.print("​Alt:​ "​);​Serial.print(altitudeM,​ 1);    Serial.println("​ m"); 
 + 
 +  // 1 Hz update 
 +  delay(1000);​ 
 +
 + 
 +</​code>​ 
 + 
 +=== 4. Scan and display local WiFi networks === 
 + 
 +Now let's make sure we can connect to WiFi. Load this WiFi scanner code:
  
 <code C main.cpp>​ <code C main.cpp>​
Line 174: Line 270:
  
  
-=== 3Advertise on BLE  ​===+=== 6Web Server ​===
  
-Load the "SimpleBleDevice" example. Install on your phone an app that scans nearby Bluetooth devices, such as this [[https://​play.google.com/​store/​apps/​details?​id=com.macdom.ble.blescanner&​hl=en&​gl=US|BLE Scanner]]. Check if your device is in the list.+Now let's bring everything together and configure ​the board to connect to our local WiFi, act as a web server and display a dynamic html page in which it can plot the sensor readings. 
 + 
 +Use this code to build the firmware image: 
 + 
 +<code C main.cpp>​ 
 +#include <​Arduino.h>​ 
 +#include <​Wire.h>​ 
 +#include <​WiFi.h>​ 
 +#include <​WebServer.h>​ 
 +#include <​SPIFFS.h>​ 
 +#include <​ESPmDNS.h>​ 
 +#include <​Adafruit_BME680.h>​ 
 + 
 +#define SEALEVEL_HPA (1013.25f) ​  // adjust for better altitude accuracy 
 + 
 +// ===== WiFi ===== 
 +const char* WIFI_SSID = "YOUR_SSID"
 +const char* WIFI_PASS = "​YOUR_PASSWORD";​ 
 +const char* MDNS_NAME = "​sparrow"; ​       // -> http://​sparrow.local/​ 
 + 
 +// ===== Web ===== 
 +WebServer server(80);​ 
 + 
 +// ===== BME680 ===== 
 +Adafruit_BME680 bme; 
 +bool haveBME = false; 
 + 
 +// ===== History buffer (5 minutes @ 1 Hz) ===== 
 +static const size_t HISTORY_MAX = 300; 
 + 
 +struct Sample { 
 +  uint32_t t_ms; 
 +  float tC; 
 +  float hPct; 
 +  float pHpa; 
 +  float altM; 
 +}; 
 + 
 +Sample hist[HISTORY_MAX];​ 
 +size_t histHead = 0; 
 +size_t histCount = 0; 
 + 
 +void pushSample(const Sample& s) { 
 +  hist[histHead] = s; 
 +  histHead = (histHead + 1) % HISTORY_MAX;​ 
 +  if (histCount < HISTORY_MAX) histCount++;​ 
 +
 + 
 +Sample getSampleByAge(size_t iOldToNew) { 
 +  size_t idx = (histHead + HISTORY_MAX - histCount + iOldToNew) % HISTORY_MAX;​ 
 +  return hist[idx];​ 
 +
 + 
 +// ===== Sensor init and reading ===== 
 +bool beginBME680() { 
 +  Wire.begin(21,​ 22); // set custom SDA/SCL here if needed: Wire.begin(SDA,​ SCL); 
 + 
 +  if (bme.begin(0x76,​ &Wire)) { Serial.println("​[BME680] Found at 0x76"​);​ return true; } 
 +  if (bme.begin(0x77,​ &Wire)) { Serial.println("​[BME680] Found at 0x77"​);​ return true; } 
 +  Serial.println("​[BME680] Not found at 0x76/​0x77"​);​ 
 +  return false; 
 +
 + 
 +void setupBME680() { 
 +  bme.setTemperatureOversampling(BME680_OS_8X);​ 
 +  bme.setHumidityOversampling(BME680_OS_2X);​ 
 +  bme.setPressureOversampling(BME680_OS_4X);​ 
 +  bme.setIIRFilterSize(BME680_FILTER_SIZE_3);​ 
 +  bme.setGasHeater(320,​ 150); // optional 
 +
 + 
 +bool takeAndStoreSample() { 
 +  if (!haveBME) return false; 
 +  if (!bme.performReading()) return false; 
 + 
 +  Sample s; 
 +  s.t_ms = millis(); 
 +  s.tC   = bme.temperature;​ 
 +  s.hPct = bme.humidity;​ 
 +  s.pHpa = bme.pressure / 100.0f; 
 +  s.altM = bme.readAltitude(SEALEVEL_HPA);​ 
 +  pushSample(s);​ 
 +  return true; 
 +
 + 
 +// ===== HTTP handlers ===== 
 +void handleRoot() { 
 +  File f = SPIFFS.open("/​index.html",​ "​r"​);​ 
 +  if (!f) { server.send(500,​ "​text/​plain",​ "​index.html missing"​);​ return; } 
 +  server.streamFile(f,​ "​text/​html"​);​ 
 +  f.close();​ 
 +
 + 
 +void handleApiSensor() { 
 +  if (histCount == 0) takeAndStoreSample();​ 
 +  Sample s = (histCount == 0) ? Sample{millis(),​ NAN, NAN, NAN, NAN} 
 +                              : getSampleByAge(histCount - 1); 
 + 
 +  String json = "​{";​ 
 +  json += "​\"​ok\":"​ + String(haveBME ? "​true"​ : "​false"​);​ 
 +  json += ",​\"​temperature_c\":"​ + String(isnan(s.tC)?​0:​s.tC,​ 2); 
 +  json += ",​\"​humidity_pct\":"​ + String(isnan(s.hPct)?​0:​s.hPct,​ 1); 
 +  json += ",​\"​pressure_hpa\":"​ + String(isnan(s.pHpa)?​0:​s.pHpa,​ 2); 
 +  json += ",​\"​altitude_m\":"​ + String(isnan(s.altM)?​0:​s.altM,​ 1); 
 +  json += "​}";​ 
 +  server.send(200,​ "​application/​json",​ json); 
 +
 + 
 +void handleApiHistory() { 
 +  uint32_t nowms = millis(); 
 +  size_t n = histCount;​ 
 +  if (n == 0) { server.send(200,​ "​application/​json",​ "​{\"​ok\":​true,​\"​n\":​0}"​);​ return; } 
 + 
 +  String json; 
 +  json.reserve(n * 64 + 128); 
 +  json += "​{\"​ok\":​true,​\"​n\":"​ + String(n);​ 
 + 
 +  // seconds ago array 
 +  json += ",​\"​s\":​[";​ 
 +  for (size_t i = 0; i < n; ++i) { 
 +    Sample s = getSampleByAge(i);​ 
 +    uint32_t secAgo = (nowms - s.t_ms) / 1000; 
 +    json += String(secAgo);​ 
 +    if (i + 1 < n) json += ",";​ 
 +  } 
 +  json += "​]";​ 
 + 
 +  auto appendSeries = [&​](const char* key, float Sample::​*field,​ int digits) { 
 +    json += ",​\"";​ json += key; json += "​\":​[";​ 
 +    for (size_t i = 0; i < n; ++i) { 
 +      Sample s = getSampleByAge(i);​ 
 +      json += String(s.*field,​ digits); 
 +      if (i + 1 < n) json += ",";​ 
 +    } 
 +    json += "​]";​ 
 +  }; 
 + 
 +  appendSeries("​tc", ​ &​Sample::​tC, ​  2); 
 +  appendSeries("​rh", ​ &​Sample::​hPct,​ 1); 
 +  appendSeries("​p", ​  &​Sample::​pHpa,​ 2); 
 +  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 += "​}";​ 
 +  server.send(200,​ "​application/​json",​ json); 
 +
 + 
 +void setup() { 
 +  Serial.begin(115200);​ 
 +  while (!Serial) { delay(10); } 
 +  Serial.println("​\n[BOOT] ESP32-C6 BME680 Web Graphs + Status + mDNS"​);​ 
 + 
 +  if (!SPIFFS.begin(true)) Serial.println("​[SPIFFS] Mount failed"​);​ 
 +  else Serial.println("​[SPIFFS] Mounted"​);​ 
 + 
 +  // WiFi + mDNS 
 +  WiFi.mode(WIFI_STA);​ 
 +  WiFi.setSleep(false);​ 
 +  WiFi.setHostname(MDNS_NAME);​ 
 +  WiFi.begin(WIFI_SSID,​ WIFI_PASS);​ 
 +  Serial.printf("​[WiFi] Connecting to %s", WIFI_SSID);​ 
 +  unsigned long t0 = millis(); 
 +  while (WiFi.status() != WL_CONNECTED && millis() - t0 < 20000) { 
 +    Serial.print("​."​);​ 
 +    delay(500);​ 
 +  } 
 +  Serial.println();​ 
 +  if (WiFi.status() == WL_CONNECTED) { 
 +    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 { 
 +    Serial.println("​[WiFi] Failed to connect (continuing)"​);​ 
 +  } 
 + 
 +  // Sensor 
 +  haveBME = beginBME680();​ 
 +  if (haveBME) setupBME680();​ 
 + 
 +  // Prime buffer 
 +  takeAndStoreSample();​ 
 + 
 +  // Routes 
 +  server.on("/", ​          ​HTTP_GET,​ handleRoot);​ 
 +  server.on("/​api/​sensor",​ HTTP_GET, handleApiSensor);​ 
 +  server.on("/​api/​history",​HTTP_GET,​ handleApiHistory);​ 
 +  server.on("/​api/​status",​ HTTP_GET, handleApiStatus);​ 
 + 
 +  server.onNotFound([]() { 
 +    String path = server.uri();​ 
 +    if (path == "/"​) { handleRoot();​ return; } 
 +    if (!SPIFFS.exists(path)) { server.send(404,​ "​text/​plain",​ "Not found"​);​ return; } 
 +    String ct = "​text/​plain";​ 
 +    if      (path.endsWith("​.html"​)) ct = "​text/​html";​ 
 +    else if (path.endsWith("​.css"​)) ​ ct = "​text/​css";​ 
 +    else if (path.endsWith("​.js"​)) ​  ct = "​application/​javascript";​ 
 +    File f = SPIFFS.open(path,​ "​r"​);​ server.streamFile(f,​ ct); f.close();​ 
 +  }); 
 + 
 +  server.begin();​ 
 +  Serial.println("​[HTTP] Server started"​);​ 
 +  if (WiFi.status() == WL_CONNECTED) 
 +    Serial.println("​[HTTP] Open: http://"​ + WiFi.localIP().toString() + "/ ​ or  http://​sparrow.local/"​);​ 
 +
 + 
 +void loop() { 
 +  server.handleClient();​ 
 + 
 +  // 1 Hz sampling 
 +  static uint32_t lastSample = 0; 
 +  uint32_t now = millis(); 
 +  if (now - lastSample >= 1000) { 
 +    lastSample = now; 
 +    takeAndStoreSample();​ 
 +  } 
 +  delay(2); 
 +
 + 
 + 
 +</​code>​ 
 + 
 +The web page that the node will serve needs to be stored in ESP32'​s internal memory. It has a limited amount of internal Flash (8MB) which is enough to store a tiny web page. Create a new folder in the root of your project and mane it "​data"​. In it, create a file called index.html and paste the contents below: 
 + 
 +<code html index.html>​ 
 +<​!doctype html> 
 +<html lang="​en"​ data-theme="​light">​ 
 +<​head>​ 
 +  <meta charset="​utf-8"/>​ 
 +  <​title>​BME680 Dashboard</​title>​ 
 +  <meta name="​viewport"​ content="​width=device-width,​ initial-scale=1"/>​ 
 +  <​style>​ 
 +    :root { 
 +      --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; } 
 +    .row { display:​flex;​ gap:16px; flex-wrap:​wrap;​ align-items:​baseline;​ } 
 +    .metric { font-size: 1.4rem; } 
 +    .label { color: var(--muted);​ font-size:​.9rem;​ } 
 +    .grid { display:​grid;​ grid-template-columns:​ repeat(2, minmax(280px,​1fr));​ gap:16px; } 
 +    .card { background: var(--card);​ border-radius:​ 12px; padding: 12px; box-shadow: 0 1px 3px rgba(0,​0,​0,​.06);​ } 
 +    .title { color: var(--muted);​ margin-bottom:​ 6px; } 
 +    canvas { width:100%; height:​200px;​ display:​block;​ } 
 +    .ts { color: var(--muted);​ margin-top: 8px; font-size:​.9rem;​ } 
 +    @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>​ 
 +</​head>​ 
 +<​body>​ 
 +  <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><​span class="​label">​Temperature</​span>​ <span id="​t"​ class="​metric">​--</​span></​div>​ 
 +    <​div><​span class="​label">​Humidity</​span>​ <span id="​h"​ class="​metric">​--</​span></​div>​ 
 +    <​div><​span class="​label">​Pressure</​span>​ <span id="​p"​ class="​metric">​--</​span></​div>​ 
 +    <​div><​span class="​label">​Altitude</​span>​ <span id="​a"​ class="​metric">​--</​span></​div>​ 
 +  </​div>​ 
 + 
 +  <div class="​grid"​ style="​margin-top:​12px;">​ 
 +    <div class="​card"><​div class="​title">​Temperature (°C)</​div><​canvas id="​ct"></​canvas></​div>​ 
 +    <div class="​card"><​div class="​title">​Humidity (%)</​div><​canvas id="​ch"></​canvas></​div>​ 
 +    <div class="​card"><​div class="​title">​Pressure (hPa)</​div><​canvas id="​cp"></​canvas></​div>​ 
 +    <div class="​card"><​div class="​title">​Altitude (m)</​div><​canvas id="​ca"></​canvas></​div>​ 
 +  </​div>​ 
 + 
 +  <div class="​ts"​ id="​ts">​Waiting for data…</​div>​ 
 + 
 +<​script>​ 
 +const secsWindow = 300; // 5 minutes 
 +function $(id){ return document.getElementById(id);​ } 
 + 
 +// ---- 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){ 
 +  if (!isFinite(range) || range <= 0) return 1; 
 +  if (range < 2) return 2; 
 +  if (range < 20) return 1; 
 +  return 0; 
 +
 +function drawSeries(canvas,​ secondsAgo, values, color) { 
 +  const dpr = window.devicePixelRatio || 1; 
 +  const padL = 48 * dpr, padR = 8 * dpr, padT = 8 * dpr, padB = 18 * dpr; 
 + 
 +  const ctx = canvas.getContext('​2d'​);​ 
 +  const w = canvas.width ​ = Math.max(10,​ canvas.clientWidth ​ * dpr); 
 +  const h = canvas.height = Math.max(10,​ canvas.clientHeight * dpr); 
 + 
 +  ctx.clearRect(0,​0,​w,​h);​ 
 +  if (!values || values.length < 2) return; 
 + 
 +  const clean = values.filter(v => isFinite(v));​ 
 +  if (clean.length < 2) return; 
 + 
 +  let vMin = Math.min(...clean);​ 
 +  let vMax = Math.max(...clean);​ 
 +  if (vMin === vMax) { vMin -= 0.5; vMax += 0.5; } 
 +  const pad = (vMax - vMin) * 0.08; 
 +  const yMin = vMin - pad; 
 +  const yMax = vMax + pad; 
 +  const yRange = yMax - yMin; 
 +  const yDec = decimalsForRange(yRange);​ 
 + 
 +  const plotW = w - padL - padR; 
 +  const plotH = h - padT - padB; 
 + 
 +  // Axes + grid 
 +  ctx.strokeStyle = getComputedStyle(document.documentElement).getPropertyValue('​--axis'​).trim();​ 
 +  ctx.lineWidth = 1 * dpr; 
 +  ctx.beginPath();​ ctx.moveTo(padL,​ padT); ctx.lineTo(padL,​ h - padB); ctx.stroke();​ 
 +  ctx.beginPath();​ ctx.moveTo(padL,​ h - padB); ctx.lineTo(w - padR, h - padB); ctx.stroke();​ 
 + 
 +  ctx.strokeStyle = getComputedStyle(document.documentElement).getPropertyValue('​--grid'​).trim();​ 
 +  ctx.fillStyle = getComputedStyle(document.documentElement).getPropertyValue('​--axis'​).trim();​ 
 +  ctx.textAlign = '​right';​ ctx.textBaseline = '​middle';​ 
 +  ctx.font = `${12*dpr}px system-ui,​-apple-system,​Segoe UI,​Roboto,​sans-serif`;​ 
 +  for (let i = 0; i <= 5; i++) { 
 +    const frac = i / 5; 
 +    const yVal = yMax - yRange * frac; 
 +    const y = padT + plotH * frac; 
 +    ctx.beginPath();​ ctx.moveTo(padL,​ y); ctx.lineTo(w - padR, y); ctx.stroke();​ 
 +    ctx.fillText(yVal.toFixed(yDec),​ padL - 6*dpr, y); 
 +  } 
 + 
 +  const xForSec = s => padL + plotW * (1 - Math.min(s, secsWindow) / secsWindow);​ 
 +  const yForVal = v => padT + plotH * ((yMax - v) / yRange); 
 + 
 +  ctx.strokeStyle = color || getComputedStyle(document.documentElement).getPropertyValue('​--line'​).trim();​ 
 +  ctx.lineWidth = Math.max(1, 1.5 * dpr); 
 +  ctx.beginPath();​ 
 +  for (let i = 0; i < values.length;​ i++) { 
 +    const x = xForSec(secondsAgo[i]);​ 
 +    const y = yForVal(values[i]);​ 
 +    if (i === 0) ctx.moveTo(x,​ y); else ctx.lineTo(x,​ y); 
 +  } 
 +  ctx.stroke();​ 
 + 
 +  // X labels 
 +  ctx.fillStyle = getComputedStyle(document.documentElement).getPropertyValue('​--axis'​).trim();​ 
 +  ctx.textAlign = '​center';​ ctx.textBaseline = '​top';​ 
 +  const xLabels = [{s:300, txt:'​-5m'​},​ {s:150, txt:'​-2.5m'​},​ {s:5, txt:'​now'​}];​ 
 +  xLabels.forEach(l=>​{ 
 +    const x = xForSec(l.s);​ 
 +    ctx.fillText(l.txt,​ x, h - padB + 4*dpr); 
 +  }); 
 +
 + 
 +// ---- Data refresh ---- 
 +async function refreshHistory() { 
 +  try { 
 +    const r = await fetch('/​api/​history',​ {cache:'​no-store'​});​ 
 +    const j = await r.json(); 
 +    if (!j.ok) { $('​ts'​).textContent = 'No data'; return; } 
 + 
 +    const n = j.n || 0; 
 +    if (n>0) { 
 +      $('​t'​).textContent = j.tc[n-1].toFixed(2)+'​ °C';​ 
 +      $('​h'​).textContent = j.rh[n-1].toFixed(1)+'​ %'; 
 +      $('​p'​).textContent = j.p[n-1].toFixed(2)+'​ hPa';​ 
 +      $('​a'​).textContent = j.alt[n-1].toFixed(1)+'​ m'; 
 +    } 
 + 
 +    drawSeries($('​ct'​),​ j.s, j.tc, '#​e4572e'​);​ 
 +    drawSeries($('​ch'​),​ j.s, j.rh, '#​17bebb'​);​ 
 +    drawSeries($('​cp'​),​ j.s, j.p,  '#​4a7'​);​ 
 +    drawSeries($('​ca'​),​ j.s, j.alt,'#​999'​);​ 
 + 
 +    $('​ts'​).textContent = '​Updated:​ ' + new Date().toLocaleTimeString();​ 
 +  } catch { 
 +    $('​ts'​).textContent = 'Fetch error';​ 
 +  } 
 +
 + 
 +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>​ 
 +</​body>​ 
 +</​html>​ 
 + 
 + 
 +</​code>​ 
 + 
 +<note warning>​In order to upload the contents of the /data folder, you will need to run "​Upload Filesystem Image"​. 
 +</​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=1nRF 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|}} {{:​iothings:​laboratoare:​lab1-ble-scanner.jpg?​300|}}
iothings/laboratoare/2025/lab1.1758798332.txt.gz · Last modified: 2025/09/25 14:05 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