Differences

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

Link to this comparison view

iothings:laboratoare:2025:lab1 [2025/09/25 13:41]
dan.tudose [Necessary gear]
iothings:laboratoare:2025:lab1 [2025/09/25 22:37] (current)
dan.tudose
Line 11: Line 11:
  
  
-===== ESP32 Sparrow specs =====+===== ESP32-C6 Sparrow specs =====
  
 This is the pinout diagram of the development board: This is the pinout diagram of the development board:
Line 20: Line 20:
 ===== First Steps ===== ===== First Steps =====
  
-=== 1. Blink the on-board LEDs ===+=== 1. Configuring PlatformIO ​===
  
-Open the "​Blink"​ example in the Arduino IDE 
  
-{{:​iothings:​laboratoare:​lab1-blink.jpg?600|}}+After downloading and installing the PlatformIO extension, create a new project using any ESP32-C6 boardAfter project creation, you will need to edit the platformio.ini file and replace it with the following:
  
-Define LED_BUILTIN to correspond to one of the on-board ​LEDs+<code bash platformio.ini>​ 
 +; PlatformIO Project Configuration File 
 +
 +;   Build options: build flags, source filter 
 +;   ​Upload options: custom upload port, speed and extra flags 
 +;   ​Library options: dependencies,​ extra library storages 
 +;   ​Advanced options: extra scripting 
 +
 +; Please visit documentation for the other options and examples 
 +; https://​docs.platformio.org/​page/​projectconf.html 
 +  
 +[env:​esp32-c6-sparrow] 
 +platform = https://​github.com/​pioarduino/​platform-espressif32/​releases/​download/​54.03.20/​platform-espressif32.zip 
 +board = esp32-c6-devkitm-1 
 +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 
 + 
 +monitor_speed = 115200 
 + 
 +lib_deps = 
 +  adafruit/​Adafruit NeoPixel@^1.11.0 
 +  adafruit/​Adafruit GFX Library@^1.11.9 
 +  adafruit/​Adafruit SSD1306@^2.5.10 
 +  adafruit/​Adafruit BME680 Library 
 +  dantudose/​LTR308 library@^1.0 
 +  https://​github.com/​sparkfun/​SparkFun_MAX1704x_Fuel_Gauge_Arduino_Library 
 +  h2zero/​NimBLE-Arduino@^2.1.
  
-For the <color green>​GREEN boards </​color>​ (rev. 1) the LED has the following pinout 
-  * Red - GPIO25, Green - GPIO26, Blue - GPIO27 
-And for the  <color blue>​BLUE boards </​color>​ (rev. 2), the LED has the following pinout: 
-  * Red - GPIO14, Green - GPIO13, Blue - GPIO15 
  
-<​code>​ 
-#define LED_BUILTIN ... 
 </​code>​ </​code>​
  
-=== 2. Scan and display local WiFi networks === 
  
-Load the "​WiFiScan"​ example from Arduino IDE.+=== 2Simple LED blink ===
  
-{{:​iothings:​laboratoare:​lab1-wifi-scanner.jpg?600|}}+The board has a Neopixel attached on GPIO3 of the ESP32-C6 processorLet's test if the board is working properly by turning on the LED. Use the code below and paste it in your main.c project file (you will have to rename it to main.cpp): ​
  
-Open the Serial Monitor to view the result of the scan+<code C main.cpp>​ 
 +#include <​Arduino.h>​ 
 +#include <​Adafruit_NeoPixel.h>​
  
-{{:​iothings:​laboratoare:​lab1-serial-monitor.jpg?600|}}+// --- config --- 
 +#define LED_PIN ​      ​3 ​       // your NeoPixel data pin 
 +#define NUM_PIXELS ​   1        // change if you have more 
 +#define BRIGHTNESS ​   30       // 0..255 (keep modest if powered from USB)
  
-=== 3. Advertise on BLE  ===+// Most WS2812/​NeoPixel strips are GRB @ 800 kHz: 
 +#define PIXEL_TYPE ​   (NEO_GRB + NEO_KHZ800)
  
-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.+Adafruit_NeoPixel strip(NUM_PIXELS,​ LED_PIN, PIXEL_TYPE);​ 
 + 
 +static void solid(uint32_t c, uint16_t ms) { 
 +  for (uint16_t i = 0; i < NUM_PIXELS; i++) strip.setPixelColor(i,​ c); 
 +  strip.show();​ 
 +  delay(ms);​ 
 +
 + 
 +void setup() { 
 +  strip.begin();​ 
 +  strip.setBrightness(BRIGHTNESS);​ 
 +  strip.show();​ // all off 
 + 
 +  // Quick RGB sanity check (each color ~300 ms) 
 +  solid(strip.Color(255, ​  ​0, ​  0), 300);  // Red 
 +  solid(strip.Color( ​ 0, 255,   0), 300);  // Green 
 +  solid(strip.Color( ​ 0,   0, 255), 300);  // Blue 
 +  solid(strip.Color( ​ 0,   ​0, ​  0), 200);  // Off 
 +
 + 
 +void loop() { 
 +  // Smooth rainbow using HSV -> RGB with gamma correction 
 +  static uint16_t hue = 0;              // 0..65535 
 +  uint32_t c = strip.gamma32(strip.ColorHSV(hue));​ 
 +  for (uint16_t i = 0; i < NUM_PIXELS; i++) strip.setPixelColor(i,​ c); 
 +  strip.show();​ 
 + 
 +  hue += 256;                           // step size (smaller = slower) 
 +  delay(20); ​                           // frame rate (~50 FPS) 
 +
 + 
 +</​code>​ 
 + 
 +=== 3. Bringing up the sensors === 
 + 
 +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. 
 + 
 +<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>​ 
 +#include <​Arduino.h>​ 
 +#include <​WiFi.h> ​ // Arduino-ESP32 WiFi (works on ESP32-C6 with Arduino 3.x) 
 + 
 +static const char* authModeToStr(wifi_auth_mode_t m) { 
 +  switch (m) { 
 +    case WIFI_AUTH_OPEN: ​          ​return "​OPEN";​ 
 +    case WIFI_AUTH_WEP: ​           return "​WEP";​ 
 +    case WIFI_AUTH_WPA_PSK: ​       return "​WPA_PSK";​ 
 +    case WIFI_AUTH_WPA2_PSK: ​      ​return "​WPA2_PSK";​ 
 +    case WIFI_AUTH_WPA_WPA2_PSK: ​  ​return "​WPA/​WPA2_PSK";​ 
 +    case WIFI_AUTH_WPA2_ENTERPRISE:​return "​WPA2_ENT";​ 
 +    case WIFI_AUTH_WPA3_PSK: ​      ​return "​WPA3_PSK";​ 
 +    case WIFI_AUTH_WPA2_WPA3_PSK: ​ return "​WPA2/​WPA3";​ 
 +    case WIFI_AUTH_WAPI_PSK: ​      ​return "​WAPI_PSK";​ 
 +    case WIFI_AUTH_OWE: ​           return "​OWE";​ 
 +    default: ​                      ​return "​UNKNOWN";​ 
 +  } 
 +
 + 
 +void setup() { 
 +  Serial.begin(115200);​ 
 +  delay(300);​ 
 + 
 +  // Scan as a station, disconnected 
 +  WiFi.mode(WIFI_STA);​ 
 +  WiFi.persistent(false);​ 
 +  WiFi.disconnect(true,​ true); 
 +  WiFi.setSleep(false);​ 
 + 
 +  Serial.println("​\nWiFi scanner ready."​);​ 
 +
 + 
 +void loop() { 
 +  Serial.println("​\n--- Scanning... ---"​);​ 
 + 
 +  // Synchronous scan; set 2nd arg to true to include hidden SSIDs 
 +  int n = WiFi.scanNetworks(/​*async=*/​false,​ /​*show_hidden=*/​true);​ 
 +  if (n <= 0) { 
 +    Serial.println("​No networks found."​);​ 
 +  } else { 
 +    Serial.printf("​Found %d network(s):​\n",​ n); 
 +    for (int i = 0; i < n; ++i) { 
 +      String ssid   = WiFi.SSID(i);​ 
 +      String bssid  = WiFi.BSSIDstr(i);​ 
 +      int32_t rssi  = WiFi.RSSI(i);​ 
 +      int32_t ch    = WiFi.channel(i);​ 
 +      auto  auth    = (wifi_auth_mode_t)WiFi.encryptionType(i);​ 
 + 
 +      bool hidden = (ssid.length() == 0);   // no API needed 
 +      if (hidden) ssid = "​(hidden)";​ 
 + 
 +      Serial.printf("​%2d) %-32s  BSSID:​%s ​ CH:​%2ld ​ RSSI:%4ld dBm  AUTH:​%s\n",​ 
 +        i + 1, ssid.c_str(),​ bssid.c_str(),​ (long)ch, (long)rssi, authModeToStr(auth) 
 +      ); 
 +    } 
 +  } 
 + 
 +  WiFi.scanDelete(); ​    // free RAM 
 +  delay(5000);​ 
 +
 + 
 +</​code>​ 
 + 
 +Build, Upload and Monitor ​the results. You should be able to see a periodic scan of the available WiFi networks in your proximity. 
 + 
 + 
 +=== 6. Web Server === 
 + 
 +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.1758796870.txt.gz · Last modified: 2025/09/25 13:41 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