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:42]
dan.tudose [Operation Example]
iothings:laboratoare:2025:lab2 [2025/10/07 13:44] (current)
dan.tudose [BLE Security and Pairing]
Line 525: Line 525:
  
  
-====== Set Up nRF Connect ​Mobile App to Broadcast ​the BLE Current Time Service (CTS) ======+==== 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>​ <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>​
Line 532: Line 532:
 === Part 1 — Create a CTS GATT Server (Android) === === Part 1 — Create a CTS GATT Server (Android) ===
  
-  - **Open nRF Connect → Menu ☰ → “Configure GATT server”.**+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).     * Tap **Add service**. From the list, pick **Current Time Service (0x1805)** (there’s a built-in preset).
  
-  - **Verify characteristics.**+2. **Verify characteristics.**
     * Inside the service, you should see:     * Inside the service, you should see:
       * **Current Time (0x2A2B)** — properties typically **Read**, **Notify**, and **Write (with response)**.       * **Current Time (0x2A2B)** — properties typically **Read**, **Notify**, and **Write (with response)**.
Line 541: Line 541:
     * If something’s missing, add it manually with the correct 16-bit UUIDs.     * If something’s missing, add it manually with the correct 16-bit UUIDs.
  
-  - **(Optional) Set an initial value manually** for testing:  ​+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**:     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`  ​     `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).     (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 === === Part 2 — Advertise it so Clients Can Find You ===
  
-  - **Go to “Advertiser”** (tab at the bottom) and **create a new set**:+1. **Go to “Advertiser”** (tab at the bottom) and **create a new set**:
     * Enable **Connectable** (so the client can connect).     * Enable **Connectable** (so the client can connect).
     * Add **Service UUID 0x1805** to the advertising data (helps clients filter).     * Add **Service UUID 0x1805** to the advertising data (helps clients filter).
     * Start advertising.     * Start advertising.
  
-  - **Start the server** (if your app version separates server start from advertising).+2. **Start the server** (if your app version separates server start from advertising).
     * Make sure your created CTS config is the active one.     * Make sure your created CTS config is the active one.
  
---- 
  
 === Part 3 — Connect from the Client and Sync Time === === Part 3 — Connect from the Client and Sync Time ===
  
-  - **Connect from your BLE client** ​(your DK/firmware or a second phone running nRF Connect as a scanner).+1. **Connect from your BLE client**.
     * Many CTS clients will **read 0x2A2B** on connect and may **subscribe to notifications**.     * 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.     * Some stacks require **bonding** before allowing CTS writes/​reads;​ accept pairing if prompted.
  
-  - **Confirm it works.**+2. **Confirm it works.**
     * On the client, you should see **Service 0x1805** with **Characteristic 0x2A2B**.     * 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.     * 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 578: 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.1759747327.txt.gz · Last modified: 2025/10/06 13:42 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