Differences

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

Link to this comparison view

iothings:laboratoare:2025:lab2 [2025/10/06 13:34]
dan.tudose [Primary Characteristic: Current Time]
iothings:laboratoare:2025:lab2 [2025/10/07 13:44] (current)
dan.tudose [BLE Security and Pairing]
Line 520: Line 520:
 ==== Operation Example ==== ==== Operation Example ====
  
-A client (e.g., BLE thermometer) connects to a server (e.g., smartphone). +  * A client (e.g., BLE thermometer) connects to a server (e.g., smartphone). 
-The client reads the Current Time characteristic to get the date/​time. +  ​* ​The client reads the Current Time characteristic to get the date/​time. 
-The client may subscribe to notifications,​ so when the time updates (e.g., due to DST), it receives an update automatically.+  ​* ​The client may subscribe to notifications,​ so when the time updates (e.g., due to DST), it receives an update automatically. 
 + 
 + 
 +==== Set Up nRF Connect to Broadcast Current Time Service (CTS) ==== 
 + 
 +<note warning>​These steps are for Android. nRF Connect on iOS can advertise as a peripheral, but emulating CTS server behavior there is limited compared to Android.</​note>​ 
 + 
 + 
 +=== Part 1 — Create a CTS GATT Server (Android) === 
 + 
 +1. **Open nRF Connect → Menu ☰ → “Configure GATT server”.** 
 +    * Tap **Add service**. From the list, pick **Current Time Service (0x1805)** (there’s a built-in preset). 
 + 
 +2. **Verify characteristics.** 
 +    * Inside the service, you should see: 
 +      * **Current Time (0x2A2B)** — properties typically **Read**, **Notify**, and **Write (with response)**. 
 +      * (Optional) **Local Time Information (0x2A0F)** and **Reference Time Information (0x2A14)**. 
 +    * If something’s missing, add it manually with the correct 16-bit UUIDs. 
 + 
 +3. **(Optional) Set an initial value manually** for testing: ​  
 +    The Current Time value is 10 bytes. Example for **2025-10-06 14:32:10**, **Monday**, fractions=0,​ **AdjustReason=External ref update**: 
 +    `E9 07 0A 06 0E 20 0A 01 00 02`   
 +    (Little-endian year 0x07E9, then month, day, hour, minute, second, day-of-week 1=Mon, fractions256,​ adjust-reason bitmask). 
 + 
 + 
 +=== Part 2 — Advertise it so Clients Can Find You === 
 + 
 +1. **Go to “Advertiser”** (tab at the bottom) and **create a new set**: 
 +    * Enable **Connectable** (so the client can connect). 
 +    * Add **Service UUID 0x1805** to the advertising data (helps clients filter). 
 +    * Start advertising. 
 + 
 +2. **Start the server** (if your app version separates server start from advertising). 
 +    * Make sure your created CTS config is the active one. 
 + 
 + 
 +=== Part 3 — Connect from the Client and Sync Time === 
 + 
 +1. **Connect from your BLE client**. 
 +    * Many CTS clients will **read 0x2A2B** on connect and may **subscribe to notifications**. 
 +    * Some stacks require **bonding** before allowing CTS writes/​reads;​ accept pairing if prompted. 
 + 
 +2. **Confirm it works.** 
 +    * On the client, you should see **Service 0x1805** with **Characteristic 0x2A2B**. 
 +    * A read should return your current time payload; enabling notifications will push updates when you modify it. 
 + 
 +==== CTS Service on NimBLE ==== 
 + 
 +Here is an example of a NimBLE client that discovers a peripheral advertising the Current Time Service, connects, reads characteristic 0x2A2B, and prints the decoded timestamp. Run it on your Sparrow node after setting up a CTS GATT Server. 
 + 
 +<code C main.cpp>​ 
 +#include <​Arduino.h>​ 
 +#include <​NimBLEDevice.h>​ 
 +#include <​Wire.h>​ 
 +#include <​Adafruit_SSD1306.h>​ 
 + 
 +namespace { 
 + 
 +const NimBLEUUID CTS_SERVICE_UUID((uint16_t)0x1805);​ 
 +const NimBLEUUID CTS_CHARACTERISTIC_UUID((uint16_t)0x2A2B);​ 
 +constexpr uint32_t CTS_READ_INTERVAL_MS = 10000; // 10 seconds 
 + 
 +const NimBLEAdvertisedDevice* g_targetDevice = nullptr; 
 +NimBLEClient* g_client = nullptr; 
 +NimBLERemoteCharacteristic* g_ctsChar = nullptr; 
 +bool g_doConnect = false; 
 +uint32_t g_lastReadMillis = 0; 
 + 
 +constexpr uint8_t OLED_WIDTH = 128; 
 +constexpr uint8_t OLED_HEIGHT = 64; 
 +constexpr uint8_t OLED_RESET_PIN = -1; 
 +constexpr uint8_t OLED_I2C_ADDR = 0x3C; 
 +constexpr uint8_t OLED_LINE_HEIGHT = 10; 
 +constexpr uint8_t OLED_TOP_MARGIN = 12; 
 + 
 +Adafruit_SSD1306 g_display(OLED_WIDTH,​ OLED_HEIGHT,​ &Wire, OLED_RESET_PIN);​ 
 + 
 +struct CurrentTime { 
 +  uint16_t year; 
 +  uint8_t month; 
 +  uint8_t day; 
 +  uint8_t hours; 
 +  uint8_t minutes; 
 +  uint8_t seconds; 
 +  uint8_t dayOfWeek;​ 
 +  uint8_t fractions256;​ 
 +  uint8_t adjustReason;​ 
 +}; 
 + 
 +void printCurrentTime(const CurrentTime&​ time) { 
 +  static const char* kDays[] = {"​Unknown",​ "​Mon",​ "​Tue",​ "​Wed",​ "​Thu",​ "​Fri",​ "​Sat",​ "​Sun"​};​ 
 +  const char* dow = (time.dayOfWeek >= 1 && time.dayOfWeek <= 7) ? kDays[time.dayOfWeek] : kDays[0]; 
 + 
 +  Serial.printf("​[CTS] %04u-%02u-%02u %02u:​%02u:​%02u (%s) adj=0x%02X frac=%u\n",​ 
 +                time.year,​ 
 +                time.month,​ 
 +                time.day, 
 +                time.hours,​ 
 +                time.minutes,​ 
 +                time.seconds,​ 
 +                dow, 
 +                time.adjustReason,​ 
 +                time.fractions256);​ 
 +
 + 
 +bool parseCurrentTime(const uint8_t* buffer, size_t length, CurrentTime&​ out) { 
 +  if (length < 10) { 
 +    Serial.printf("​[CTS] Expected 10 bytes, got %u\n", static_cast<​unsigned>​(length));​ 
 +    return false; 
 +  } 
 + 
 +  out.year = static_cast<​uint16_t>​(buffer[0] | (buffer[1] << 8)); 
 +  out.month = buffer[2];​ 
 +  out.day = buffer[3];​ 
 +  out.hours = buffer[4];​ 
 +  out.minutes = buffer[5];​ 
 +  out.seconds = buffer[6];​ 
 +  out.dayOfWeek = buffer[7];​ 
 +  out.fractions256 = buffer[8];​ 
 +  out.adjustReason = buffer[9];​ 
 +  return true; 
 +
 + 
 +void displayStatusMessage(const char* message) { 
 +  g_display.clearDisplay();​ 
 +  g_display.setCursor(0,​ OLED_TOP_MARGIN);​ 
 +  g_display.println(message);​ 
 +  g_display.display();​ 
 +
 + 
 +void displayCurrentTime(const CurrentTime&​ time) { 
 +  g_display.clearDisplay();​ 
 +  g_display.setCursor(0,​ OLED_TOP_MARGIN);​ 
 + 
 +  char line[24]; 
 +  snprintf(line,​ sizeof(line),​ "​%04u-%02u-%02u",​ time.year, time.month, time.day);​ 
 +  g_display.println(line);​ 
 + 
 +  snprintf(line,​ sizeof(line),​ "​%02u:​%02u:​%02u",​ time.hours, time.minutes,​ time.seconds);​ 
 +  g_display.println(line);​ 
 + 
 +  static const char* kDays[] = {"",​ "​Mon",​ "​Tue",​ "​Wed",​ "​Thu",​ "​Fri",​ "​Sat",​ "​Sun"​};​ 
 +  const char* dow = (time.dayOfWeek >= 1 && time.dayOfWeek <= 7) ? kDays[time.dayOfWeek] : "";​ 
 +  g_display.println(dow);​ 
 + 
 +  g_display.display();​ 
 +
 + 
 +class ClientCallbacks : public NimBLEClientCallbacks { 
 +  void onConnect(NimBLEClient* client) override { 
 +    Serial.printf("​[BLE] Connected to %s\n", client->​getPeerAddress().toString().c_str());​ 
 +    displayStatusMessage("​Connected"​);​ 
 +  } 
 + 
 +  void onDisconnect(NimBLEClient* client, int reason) override { 
 +    Serial.printf("​[BLE] Disconnected (reason %d), restarting scan\n",​ reason); 
 +    g_ctsChar = nullptr; 
 +    g_lastReadMillis = 0; 
 +    NimBLEDevice::​deleteClient(client);​ 
 +    g_client = nullptr; 
 +    displayStatusMessage("​Disconnected"​);​ 
 +    NimBLEDevice::​getScan()->​start(0,​ false, true); 
 +  } 
 +}; 
 + 
 +class ScanCallbacks : public NimBLEScanCallbacks { 
 +  void onResult(const NimBLEAdvertisedDevice* advertisedDevice) override { 
 +    if (!advertisedDevice->​isAdvertisingService(CTS_SERVICE_UUID)) { 
 +      return; 
 +    } 
 + 
 +    Serial.printf("​[BLE] Found CTS peripheral: %s\n", advertisedDevice->​toString().c_str());​ 
 +    NimBLEDevice::​getScan()->​stop();​ 
 + 
 +    g_targetDevice = advertisedDevice;​ 
 +    g_doConnect = true; 
 +  } 
 + 
 +  void onScanEnd(const NimBLEScanResults&​ results, int reason) override { 
 +    Serial.printf("​[BLE] Scan ended (reason %d, %d devices)\n",​ reason, results.getCount());​ 
 +    if (!g_doConnect && g_ctsChar == nullptr) { 
 +      NimBLEDevice::​getScan()->​start(0,​ false, true); 
 +    } 
 +  } 
 +}; 
 + 
 +ClientCallbacks g_clientCallbacks;​ 
 +ScanCallbacks g_scanCallbacks;​ 
 + 
 +void startScan() { 
 +  NimBLEScan* scan = NimBLEDevice::​getScan();​ 
 +  scan->​setScanCallbacks(&​g_scanCallbacks,​ false); 
 +  scan->​setActiveScan(true);​ 
 +  scan->​setInterval(160);​ 
 +  scan->​setWindow(120);​ 
 +  scan->​start(0,​ false, true); 
 +  Serial.println("​[BLE] Scanning for Current Time Service peripherals..."​);​ 
 +  displayStatusMessage("​Scanning..."​);​ 
 +
 + 
 +bool connectToCurrentTimeService() { 
 +  if (!g_doConnect || g_targetDevice == nullptr) { 
 +    return false; 
 +  } 
 + 
 +  g_doConnect = false; 
 + 
 +  if (g_client == nullptr) { 
 +    g_client = NimBLEDevice::​createClient();​ 
 +    g_client->​setClientCallbacks(&​g_clientCallbacks,​ false); 
 +    g_client->​setConnectionParams(12,​ 12, 0, 200); 
 +    g_client->​setConnectTimeout(5000);​ 
 +  } 
 + 
 +  Serial.println("​[BLE] Connecting to peripheral..."​);​ 
 +  if (!g_client->​connect(g_targetDevice)) { 
 +    Serial.println("​[BLE] Connection failed, restarting scan"​);​ 
 +    NimBLEDevice::​deleteClient(g_client);​ 
 +    g_client = nullptr; 
 +    g_ctsChar = nullptr; 
 +    g_targetDevice = nullptr; 
 +    NimBLEDevice::​getScan()->​start(0,​ false, true); 
 +    return false; 
 +  } 
 + 
 +  NimBLERemoteService* service = g_client->​getService(CTS_SERVICE_UUID);​ 
 +  if (service == nullptr) { 
 +    Serial.println("​[BLE] Current Time Service not found on peripheral"​);​ 
 +    g_client->​disconnect();​ 
 +    NimBLEDevice::​deleteClient(g_client);​ 
 +    g_client = nullptr; 
 +    g_ctsChar = nullptr; 
 +    g_targetDevice = nullptr; 
 +    NimBLEDevice::​getScan()->​start(0,​ false, true); 
 +    return false; 
 +  } 
 + 
 +  NimBLERemoteCharacteristic* characteristic = service->​getCharacteristic(CTS_CHARACTERISTIC_UUID);​ 
 +  if (characteristic == nullptr) { 
 +    Serial.println("​[BLE] Current Time characteristic missing"​);​ 
 +    g_client->​disconnect();​ 
 +    NimBLEDevice::​deleteClient(g_client);​ 
 +    g_client = nullptr; 
 +    g_ctsChar = nullptr; 
 +    g_targetDevice = nullptr; 
 +    NimBLEDevice::​getScan()->​start(0,​ false, true); 
 +    return false; 
 +  } 
 + 
 +  if (!characteristic->​canRead()) { 
 +    Serial.println("​[BLE] Current Time characteristic is not readable"​);​ 
 +    g_client->​disconnect();​ 
 +    NimBLEDevice::​deleteClient(g_client);​ 
 +    g_client = nullptr; 
 +    g_ctsChar = nullptr; 
 +    g_targetDevice = nullptr; 
 +    NimBLEDevice::​getScan()->​start(0,​ false, true); 
 +    return false; 
 +  } 
 + 
 +  g_ctsChar = characteristic;​ 
 +  g_lastReadMillis = 0; 
 +  g_targetDevice = nullptr; 
 + 
 +  Serial.println("​[BLE] Ready to read current time"​);​ 
 +  displayStatusMessage("​CTS ready"​);​ 
 +  return true; 
 +
 + 
 +void readAndPrintCurrentTime() { 
 +  if (g_ctsChar == nullptr || g_client == nullptr) { 
 +    return; 
 +  } 
 + 
 +  if (!g_client->​isConnected()) { 
 +    return; 
 +  } 
 + 
 +  const uint32_t now = millis(); 
 +  if (now - g_lastReadMillis < CTS_READ_INTERVAL_MS) { 
 +    return; 
 +  } 
 +  g_lastReadMillis = now; 
 + 
 +  Serial.println("​[CTS] Reading current time..."​);​ 
 +  NimBLEAttValue value = g_ctsChar->​readValue();​ 
 +  if (value.length() == 0) { 
 +    Serial.println("​[CTS] Read returned empty value"​);​ 
 +    return; 
 +  } 
 + 
 +  CurrentTime time{}; 
 +  if (!parseCurrentTime(value.data(),​ value.length(),​ time)) { 
 +    return; 
 +  } 
 + 
 +  printCurrentTime(time);​ 
 +  displayCurrentTime(time);​ 
 +
 + 
 +} // namespace 
 + 
 +void setup() { 
 +  Serial.begin(115200);​ 
 +  while (!Serial) { 
 +    delay(10);​ 
 +  } 
 + 
 +  Serial.println();​ 
 +  Serial.println("​[BOOT] BLE Current Time Service client"​);​ 
 + 
 +  NimBLEDevice::​init("​ESP32C6-CTS-Client"​);​ 
 +  NimBLEDevice::​setPower(ESP_PWR_LVL_P9);​ 
 +  NimBLEDevice::​setSecurityAuth(false,​ false, true); 
 + 
 +  Wire.begin(21,​ 22); 
 +  if (!g_display.begin(SSD1306_SWITCHCAPVCC,​ OLED_I2C_ADDR)) { 
 +    Serial.println("​[OLED] SSD1306 allocation failed"​);​ 
 +    while (true) { 
 +      delay(1000);​ 
 +    } 
 +  } 
 +  g_display.clearDisplay();​ 
 +  g_display.setTextColor(SSD1306_WHITE);​ 
 +  g_display.setTextSize(2);​ 
 +  g_display.setCursor(0,​ OLED_TOP_MARGIN);​ 
 +  g_display.println("​BLE CTS Client"​);​ 
 +  g_display.display();​ 
 + 
 +  startScan();​ 
 +
 + 
 +void loop() { 
 +  connectToCurrentTimeService();​ 
 +  readAndPrintCurrentTime();​ 
 +  delay(100);​ 
 +
 + 
 +</​code>​ 
 + 
 +==== BLE Security and Pairing ==== 
 +All BLE connections done so far in this lab have been without any authentication/​encryption. For applications where this is necessary, BLE offers multiple security modes and a bonding scheme which establishes and encrypted connection between devices. ​  
 + 
 +^ Mode | Level | Description ^ 
 +| **Mode 1, Level 1** | No security | No encryption or authentication | 
 +| **Mode 1, Level 2** | Unauthenticated pairing with encryption | "Just Works" — no MITM protection | 
 +| **Mode 1, Level 3** | Authenticated pairing with encryption | Uses Passkey/​Numeric Comparison | 
 +| **Mode 1, Level 4** | Authenticated LE Secure Connections | Uses modern ECDH key exchange | 
 + 
 + 
 +A typical pairing scenarion over BLE goes through these steps: 
 + 
 +1. **Pairing Request/​Response:​** Devices agree on IO capabilities (keyboard, display, none). 
 +2. **Key Exchange:** Exchange temporary keys (STK, LTK, IRK, etc.). 
 +3. **Encryption Setup:** Link is encrypted using generated keys. 
 +4. **Bonding (optional):​** Keys are stored persistently for future use. 
 + 
 +We can test pairing with the nRF Connect mobile app.  
 + 
 +  - Flash your ESP32-C6 firmware with the code below, which has security-enabled GATT characteristics. 
 +  - Open **nRF Connect → Scan → Connect** to your ESP32-C6. 
 +  - Try to **read** a protected characteristic — expect something like: <​code>​Error 0x05: Insufficient Authentication</​code>​ 
 +  - Tap **⋮ → Bond** to start pairing. 
 +  - Depending on setup: 
 +    * **Just Works:** automatic pairing. 
 +    * **Passkey Entry:** app prompts for a 6-digit PIN. 
 +    * **Numeric Comparison:​** both sides show a code for confirmation. 
 +  - After pairing, retry read — it should succeed.  
 + 
 +<code C main.cpp>​ 
 +#include <​Arduino.h>​ 
 +#include <​Wire.h>​ 
 +#include <​cmath>​ 
 +#include <​Adafruit_BME680.h>​ 
 +#include <​NimBLEDevice.h>​ 
 +#include <​NimBLEAdvertisementData.h>​ 
 + 
 + 
 + 
 +namespace { 
 + 
 +#define I2C_SDA_PIN 21 
 +#define I2C_SCL_PIN 22 
 +#define BME680_I2C_ADDR 0x77 
 + 
 +constexpr char kDeviceName[] = "​ESP32C6 ESS";​ 
 +const NimBLEUUID kEnvironmentalServiceUUID((uint16_t)0x181A);​ 
 +const NimBLEUUID kTemperatureCharUUID((uint16_t)0x2A6E);​ 
 +const NimBLEUUID kHumidityCharUUID((uint16_t)0x2A6F);​ 
 +const NimBLEUUID kPressureCharUUID((uint16_t)0x2A6D);​ 
 +constexpr uint32_t kSecurityPasskey = 654321; // 6-digit static passkey for pairing 
 +constexpr uint32_t kNotifyIntervalMs = 5000;  // send notify every 5 seconds 
 + 
 +Adafruit_BME680 g_bme; 
 +bool g_bmeReady = false; 
 +NimBLECharacteristic* g_temperatureCharacteristic = nullptr; 
 +NimBLECharacteristic* g_humidityCharacteristic = nullptr; 
 +NimBLECharacteristic* g_pressureCharacteristic = nullptr; 
 +volatile bool g_isConnected = false; 
 +uint32_t g_lastNotify = 0; 
 + 
 +class ServerCallbacks : public NimBLEServerCallbacks { 
 +  void onConnect(NimBLEServer* server, NimBLEConnInfo&​ connInfo) override { 
 +    g_isConnected = true; 
 +    Serial.printf("​[BLE] Central connected: %s\n", connInfo.getAddress().toString().c_str());​ 
 +  } 
 + 
 +  void onDisconnect(NimBLEServer* server, NimBLEConnInfo&​ connInfo, int reason) override { 
 +    g_isConnected = false; 
 +    Serial.printf("​[BLE] Central disconnected (reason %d)\n",​ reason); 
 +    if (!NimBLEDevice::​startAdvertising()) { 
 +      Serial.println("​[BLE] Failed to restart advertising"​);​ 
 +    } 
 +  } 
 + 
 +  uint32_t onPassKeyDisplay() override { 
 +    Serial.printf("​[BLE] Displaying passkey: %06u\n",​ kSecurityPasskey);​ 
 +    return kSecurityPasskey;​ 
 +  } 
 + 
 +  void onConfirmPassKey(NimBLEConnInfo&​ connInfo, uint32_t pin) override { 
 +    Serial.printf("​[BLE] Confirm passkey %06u for %s\n", pin, connInfo.getAddress().toString().c_str());​ 
 +    NimBLEDevice::​injectConfirmPasskey(connInfo,​ true); 
 +  } 
 + 
 +  void onAuthenticationComplete(NimBLEConnInfo&​ connInfo) override { 
 +    if (connInfo.isEncrypted()) { 
 +      Serial.printf("​[BLE] Link encrypted; bonded=%s, authenticated=%s\n",​ 
 +                    connInfo.isBonded() ? "​yes"​ : "​no",​ 
 +                    connInfo.isAuthenticated() ? "​yes"​ : "​no"​);​ 
 +    } else { 
 +      Serial.printf("​[BLE] Authentication failed; disconnecting %s\n", connInfo.getAddress().toString().c_str());​ 
 +      NimBLEDevice::​getServer()->​disconnect(connInfo.getConnHandle());​ 
 +    } 
 +  } 
 +}; 
 + 
 +class ValueCallbacks : public NimBLECharacteristicCallbacks { 
 +  void onRead(NimBLECharacteristic* characteristic,​ NimBLEConnInfo&​ connInfo) override { 
 +    Serial.printf("​[GATT] Read from %s on characteristic %s\n",​ 
 +                  connInfo.getAddress().toString().c_str(),​ 
 +                  characteristic->​getUUID().toString().c_str());​ 
 +  } 
 + 
 +  void onWrite(NimBLECharacteristic* characteristic,​ NimBLEConnInfo&​ connInfo) override { 
 +    std::string value = characteristic->​getValue();​ 
 +    Serial.printf("​[GATT] Write to %s from %s, %zu bytes\n",​ 
 +                  characteristic->​getUUID().toString().c_str(),​ 
 +                  connInfo.getAddress().toString().c_str(),​ 
 +                  value.size());​ 
 +  } 
 +}; 
 + 
 +ServerCallbacks g_serverCallbacks;​ 
 +ValueCallbacks g_valueCallbacks;​ 
 + 
 +bool initBME680() { 
 +  if (!g_bme.begin(BME680_I2C_ADDR)) { 
 +    Serial.println("​[BME680] Not found on 0x77, trying 0x76..."​);​ 
 +    if (!g_bme.begin(0x76)) { 
 +      Serial.println("​[BME680] Sensor not found. Check wiring/​power."​);​ 
 +      return false; 
 +    } 
 +  } 
 + 
 +  g_bme.setTemperatureOversampling(BME680_OS_8X);​ 
 +  g_bme.setHumidityOversampling(BME680_OS_2X);​ 
 +  g_bme.setPressureOversampling(BME680_OS_4X);​ 
 +  g_bme.setIIRFilterSize(BME680_FILTER_SIZE_3);​ 
 +  g_bme.setGasHeater(0,​ 0); // Disable gas heater for periodic environmental sampling 
 + 
 +  Serial.println("​[BME680] Sensor initialized"​);​ 
 +  return true; 
 +
 + 
 +bool readBME680(float&​ temperatureC,​ float& humidityPct,​ float& pressurePa) { 
 +  if (!g_bme.performReading()) { 
 +    Serial.println("​[BME680] Failed to perform reading"​);​ 
 +    return false; 
 +  } 
 + 
 +  temperatureC = g_bme.temperature;​ 
 +  humidityPct = g_bme.humidity;​ 
 +  pressurePa = g_bme.pressure;​ // Library returns Pascals 
 +  return true; 
 +
 + 
 +void setupSecurity() { 
 +  NimBLEDevice::​setSecurityAuth(true,​ true, true); // bonding, MITM, secure connections 
 +  NimBLEDevice::​setSecurityIOCap(BLE_HS_IO_DISPLAY_YESNO);​ 
 +  NimBLEDevice::​setSecurityPasskey(kSecurityPasskey);​ 
 +  NimBLEDevice::​setPower(ESP_PWR_LVL_P9);​ 
 +
 + 
 +void setupGattServer() { 
 +  NimBLEServer* server = NimBLEDevice::​createServer();​ 
 +  server->​setCallbacks(&​g_serverCallbacks,​ false); 
 + 
 +  NimBLEService* environmentalService = server->​createService(kEnvironmentalServiceUUID);​ 
 + 
 +  g_temperatureCharacteristic = environmentalService->​createCharacteristic( 
 +      kTemperatureCharUUID,​ 
 +      NIMBLE_PROPERTY::​READ | NIMBLE_PROPERTY::​READ_ENC | 
 +          NIMBLE_PROPERTY::​READ_AUTHEN | 
 +          NIMBLE_PROPERTY::​NOTIFY);​ 
 +  g_temperatureCharacteristic->​setCallbacks(&​g_valueCallbacks);​ 
 +  uint8_t tempPlaceholder[2] = {0xFF, 0x7F}; // NaN equivalent (0x7FFF) 
 +  g_temperatureCharacteristic->​setValue(tempPlaceholder,​ sizeof(tempPlaceholder));​ 
 + 
 +  g_humidityCharacteristic = environmentalService->​createCharacteristic( 
 +      kHumidityCharUUID,​ 
 +      NIMBLE_PROPERTY::​READ | NIMBLE_PROPERTY::​READ_ENC | 
 +          NIMBLE_PROPERTY::​READ_AUTHEN | 
 +          NIMBLE_PROPERTY::​NOTIFY);​ 
 +  g_humidityCharacteristic->​setCallbacks(&​g_valueCallbacks);​ 
 +  uint8_t humidityPlaceholder[2] = {0xFF, 0xFF}; 
 +  g_humidityCharacteristic->​setValue(humidityPlaceholder,​ sizeof(humidityPlaceholder));​ 
 + 
 +  g_pressureCharacteristic = environmentalService->​createCharacteristic( 
 +      kPressureCharUUID,​ 
 +      NIMBLE_PROPERTY::​READ | NIMBLE_PROPERTY::​READ_ENC | 
 +          NIMBLE_PROPERTY::​READ_AUTHEN | 
 +          NIMBLE_PROPERTY::​NOTIFY);​ 
 +  g_pressureCharacteristic->​setCallbacks(&​g_valueCallbacks);​ 
 +  uint8_t pressurePlaceholder[4] = {0xFF, 0xFF, 0xFF, 0xFF}; 
 +  g_pressureCharacteristic->​setValue(pressurePlaceholder,​ sizeof(pressurePlaceholder));​ 
 + 
 +  environmentalService->​start();​ 
 + 
 +  NimBLEAdvertising* advertising = NimBLEDevice::​getAdvertising();​ 
 +  NimBLEAdvertisementData advData; 
 +  advData.setName(kDeviceName);​ 
 +  advData.addServiceUUID(kEnvironmentalServiceUUID);​ 
 +  advData.setAppearance(0x0341);​ // Thermometer 
 +  advertising->​setAdvertisementData(advData);​ 
 + 
 +  NimBLEAdvertisementData scanData; 
 +  scanData.setName("​Env Sensing",​ false); 
 +  advertising->​setScanResponseData(scanData);​ 
 + 
 +  advertising->​setConnectableMode(BLE_GAP_CONN_MODE_UND);​ 
 +  advertising->​setDiscoverableMode(BLE_GAP_DISC_MODE_GEN);​ 
 + 
 +  if (advertising->​start()) { 
 +    Serial.println("​[BLE] Advertising environmental sensing service"​);​ 
 +  } else { 
 +    Serial.println("​[BLE] Failed to start advertising"​);​ 
 +  } 
 +
 + 
 +void updateEnvironmentalMeasurements() { 
 +  if (!g_isConnected || !g_bmeReady || 
 +      g_temperatureCharacteristic == nullptr || 
 +      g_humidityCharacteristic == nullptr || 
 +      g_pressureCharacteristic == nullptr) { 
 +    return; 
 +  } 
 + 
 +  const uint32_t now = millis(); 
 +  if (now - g_lastNotify < kNotifyIntervalMs) { 
 +    return; 
 +  } 
 +  g_lastNotify = now; 
 + 
 +  float temperatureC = 0.0f; 
 +  float humidityPct = 0.0f; 
 +  float pressurePa = 0.0f; 
 +  if (!readBME680(temperatureC,​ humidityPct,​ pressurePa)) { 
 +    return; 
 +  } 
 + 
 +  const int16_t temperatureHundredths = 
 +      static_cast<​int16_t>​(std::​lround(temperatureC * 100.0)); 
 +  const uint16_t humidityHundredths = 
 +      static_cast<​uint16_t>​(std::​lround(humidityPct * 100.0)); 
 +  const uint32_t pressureValue = static_cast<​uint32_t>​(std::​lround(pressurePa));​ 
 + 
 +  uint8_t temperaturePayload[2] = { 
 +      static_cast<​uint8_t>​(temperatureHundredths & 0xFF), 
 +      static_cast<​uint8_t>​((temperatureHundredths >> 8) & 0xFF), 
 +  }; 
 +  g_temperatureCharacteristic->​setValue(temperaturePayload,​ sizeof(temperaturePayload));​ 
 +  g_temperatureCharacteristic->​notify();​ 
 + 
 +  uint8_t humidityPayload[2] = { 
 +      static_cast<​uint8_t>​(humidityHundredths & 0xFF), 
 +      static_cast<​uint8_t>​((humidityHundredths >> 8) & 0xFF), 
 +  }; 
 +  g_humidityCharacteristic->​setValue(humidityPayload,​ sizeof(humidityPayload));​ 
 +  g_humidityCharacteristic->​notify();​ 
 + 
 +  uint8_t pressurePayload[4] = { 
 +      static_cast<​uint8_t>​(pressureValue & 0xFF), 
 +      static_cast<​uint8_t>​((pressureValue >> 8) & 0xFF), 
 +      static_cast<​uint8_t>​((pressureValue >> 16) & 0xFF), 
 +      static_cast<​uint8_t>​((pressureValue >> 24) & 0xFF), 
 +  }; 
 +  g_pressureCharacteristic->​setValue(pressurePayload,​ sizeof(pressurePayload));​ 
 +  g_pressureCharacteristic->​notify();​ 
 + 
 +  Serial.printf("​[ESS] Notified T=%.2fC H=%.2f%% P=%.2fhPa\n",​ 
 +                temperatureC,​ 
 +                humidityPct,​ 
 +                pressurePa / 100.0f); 
 +
 + 
 +} // namespace 
 + 
 +void setup() { 
 +  Serial.begin(115200);​ 
 +  while (!Serial) { 
 +    delay(10);​ 
 +  } 
 +  delay(1000);​ 
 +  Serial.println();​ 
 +  Serial.println("​[BOOT] ESP32-C6 Environmental Sensing Server"​);​ 
 + 
 +  Wire.begin(I2C_SDA_PIN,​ I2C_SCL_PIN);​ 
 +  g_bmeReady = initBME680();​ 
 +  if (!g_bmeReady) { 
 +    Serial.println("​[BOOT] BME680 not ready; characteristics will remain invalid"​);​ 
 +  } 
 + 
 +  NimBLEDevice::​init(kDeviceName);​ 
 +  setupSecurity();​ 
 +  setupGattServer();​ 
 +
 + 
 +void loop() { 
 +  updateEnvironmentalMeasurements();​ 
 +  delay(100);​ 
 +
 + 
 + 
 +</​code>​
 ===== Web BLE Application ===== ===== Web BLE Application =====
  
Line 531: Line 1165:
 Build the application in the tutorial and deploy the web page in your GitHub account. ​ Build the application in the tutorial and deploy the web page in your GitHub account. ​
  
-=== Assignment ===+===== Assignment ​=====
 <​note>​ Modify the web page and the BLE app to display the BME680 sensor data (temperature,​ pressure and humidity). </​note>​ <​note>​ Modify the web page and the BLE app to display the BME680 sensor data (temperature,​ pressure and humidity). </​note>​
iothings/laboratoare/2025/lab2.1759746844.txt.gz · Last modified: 2025/10/06 13:34 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