This shows you the differences between two versions of the page.
iothings:laboratoare:2022:lab7 [2023/11/20 21:35] dan.tudose [Sparrow BLE Service] |
iothings:laboratoare:2022:lab7 [2025/09/28 17:46] (current) dan.tudose [UUID] |
||
---|---|---|---|
Line 72: | Line 72: | ||
In essence, the UUID serves the purpose of uniquely identifying information. For example, it can distinguish a specific service provided by a Bluetooth device. | In essence, the UUID serves the purpose of uniquely identifying information. For example, it can distinguish a specific service provided by a Bluetooth device. | ||
+ | ===== 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=1| nRF 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|}} | ||
===== Project Overview ===== | ===== Project Overview ===== | ||
Line 113: | Line 179: | ||
- Create a BLE device (server) with a name of your choice (we’ll call it ESP32_BME680, but you can call it any other name). | - Create a BLE device (server) with a name of your choice (we’ll call it ESP32_BME680, but you can call it any other name). | ||
- Create an Environmental Sensing service (UUID: 0x181A). | - Create an Environmental Sensing service (UUID: 0x181A). | ||
- | - Add characteristics to that service: | + | - Add characteristics to that service: pressure (0x2A6D), temperature (0x2A6E) and humidity (0x246F) |
- | * pressure: 0x2A6D | + | |
- | * temperature: 0x2A6E | + | |
- | * humidity: 0x246F | + | |
- Add descriptors to the characteristics. | - Add descriptors to the characteristics. | ||
- Start the BLE server. | - Start the BLE server. | ||
Line 123: | Line 186: | ||
- | Copy the following code to the Arduino IDE and upload it to your board. | + | Copy the following code to the Arduino IDE, modify it and upload it to your board. |
+ | |||
+ | <code C> | ||
+ | #include <Arduino.h> | ||
+ | #include <Wire.h> | ||
+ | #include <Adafruit_BME680.h> | ||
+ | #include <NimBLEDevice.h> | ||
+ | |||
+ | /* ============================ | ||
+ | User config | ||
+ | ============================ */ | ||
+ | static const char* DEVICE_NAME = "ESP32-C6 Env"; // shows in scan response | ||
+ | #define I2C_SDA_PIN 21 // adjust for your board | ||
+ | #define I2C_SCL_PIN 22 | ||
+ | #define BME680_I2C_ADDR 0x77 // try 0x76 if needed | ||
+ | static const uint32_t MEAS_INTERVAL_MS = 2000; // update period | ||
+ | |||
+ | /* ============================ | ||
+ | BLE UUIDs (Environmental Sensing) | ||
+ | ============================ */ | ||
+ | static NimBLEUUID ESS_UUID((uint16_t)0x181A); | ||
+ | static NimBLEUUID TEMP_UUID((uint16_t)0x2A6E); // Temperature | ||
+ | static NimBLEUUID PRES_UUID((uint16_t)0x2A6D); // Pressure | ||
+ | static NimBLEUUID HUM_UUID ((uint16_t)0x2A6F); // Humidity | ||
+ | |||
+ | /* ============================ | ||
+ | Globals | ||
+ | ============================ */ | ||
+ | NimBLEServer* gServer = nullptr; | ||
+ | NimBLEService* gService = nullptr; | ||
+ | NimBLECharacteristic* gTempChar = nullptr; | ||
+ | NimBLECharacteristic* gPresChar = nullptr; | ||
+ | NimBLECharacteristic* gHumChar = nullptr; | ||
+ | |||
+ | Adafruit_BME680 bme; // I2C | ||
+ | uint32_t lastMeas = 0; | ||
+ | |||
+ | /* ============================ | ||
+ | Helpers: encode per SIG formats (little-endian) | ||
+ | ============================ */ | ||
+ | static void setTempCentiDeg(NimBLECharacteristic* ch, float celsius) { | ||
+ | // sint16, 0.01 °C | ||
+ | int32_t raw = lroundf(celsius * 100.0f); | ||
+ | if (raw > 32767) raw = 32767; | ||
+ | if (raw < -32768) raw = -32768; | ||
+ | int16_t v = (int16_t)raw; | ||
+ | uint8_t buf[2] = { (uint8_t)(v & 0xFF), (uint8_t)((v >> 8) & 0xFF) }; | ||
+ | ch->setValue(buf, sizeof(buf)); | ||
+ | } | ||
+ | |||
+ | static void setHumidityCentiPct(NimBLECharacteristic* ch, float rh) { | ||
+ | // uint16, 0.01 %RH (clip 0..100%) | ||
+ | if (rh < 0) rh = 0; | ||
+ | if (rh > 100) rh = 100; | ||
+ | uint32_t raw = lroundf(rh * 100.0f); | ||
+ | if (raw > 0xFFFF) raw = 0xFFFF; | ||
+ | uint16_t v = (uint16_t)raw; | ||
+ | uint8_t buf[2] = { (uint8_t)(v & 0xFF), (uint8_t)((v >> 8) & 0xFF) }; | ||
+ | ch->setValue(buf, sizeof(buf)); | ||
+ | } | ||
+ | |||
+ | static void setPressureDeciPa(NimBLECharacteristic* ch, float pascals) { | ||
+ | // uint32, 0.1 Pa | ||
+ | if (pascals < 0) pascals = 0; | ||
+ | uint64_t raw = llroundf(pascals * 10.0f); | ||
+ | if (raw > 0xFFFFFFFFULL) raw = 0xFFFFFFFFULL; | ||
+ | uint32_t v = (uint32_t)raw; | ||
+ | uint8_t buf[4] = { | ||
+ | (uint8_t)(v & 0xFF), | ||
+ | (uint8_t)((v >> 8) & 0xFF), | ||
+ | (uint8_t)((v >> 16) & 0xFF), | ||
+ | (uint8_t)((v >> 24) & 0xFF) | ||
+ | }; | ||
+ | ch->setValue(buf, sizeof(buf)); | ||
+ | } | ||
+ | |||
+ | /* Add Characteristic Presentation Format (0x2904) */ | ||
+ | static void addCPF(NimBLECharacteristic* ch, uint8_t format, int8_t exponent, uint16_t unit) { | ||
+ | // 7 bytes: format, exponent, unit(LE), namespace(1=1), description(2=0) | ||
+ | uint8_t cpf[7] = { | ||
+ | format, (uint8_t)exponent, | ||
+ | (uint8_t)(unit & 0xFF), (uint8_t)((unit >> 8) & 0xFF), | ||
+ | 0x01, 0x00, 0x00 | ||
+ | }; | ||
+ | ch->createDescriptor("2904")->setValue(cpf, sizeof(cpf)); | ||
+ | } | ||
+ | |||
+ | /* ============================ | ||
+ | BLE setup (keeps your adv/scan-response style) | ||
+ | ============================ */ | ||
+ | void startBLE() { | ||
+ | NimBLEDevice::init(DEVICE_NAME); | ||
+ | NimBLEDevice::setPower(ESP_PWR_LVL_P9); // strong TX for gateways; lower if needed | ||
+ | NimBLEDevice::setSecurityAuth(false, false, true); // no bonding; SC on | ||
+ | |||
+ | gServer = NimBLEDevice::createServer(); | ||
+ | gService = gServer->createService(ESS_UUID); | ||
+ | |||
+ | // READ + NOTIFY for all three characteristics | ||
+ | auto props = (NIMBLE_PROPERTY::READ | NIMBLE_PROPERTY::NOTIFY); | ||
+ | gTempChar = gService->createCharacteristic(TEMP_UUID, props); | ||
+ | gPresChar = gService->createCharacteristic(PRES_UUID, props); | ||
+ | gHumChar = gService->createCharacteristic(HUM_UUID, props); | ||
+ | |||
+ | // Presentation formats | ||
+ | const uint8_t GATT_FORMAT_SINT16 = 0x0E; | ||
+ | const uint8_t GATT_FORMAT_UINT16 = 0x0D; | ||
+ | const uint8_t GATT_FORMAT_UINT32 = 0x10; | ||
+ | addCPF(gTempChar, GATT_FORMAT_SINT16, -2, 0x272F); // Celsius | ||
+ | addCPF(gPresChar, GATT_FORMAT_UINT32, -1, 0x2724); // Pascal, 0.1 exponent | ||
+ | addCPF(gHumChar, GATT_FORMAT_UINT16, -2, 0x27AD); // Percentage | ||
+ | |||
+ | // Initialize with placeholder values so READ works before first measurement | ||
+ | setTempCentiDeg(gTempChar, 0.0f); | ||
+ | setPressureDeciPa(gPresChar, 101325.0f); | ||
+ | setHumidityCentiPct(gHumChar, 0.0f); | ||
+ | |||
+ | gService->start(); | ||
+ | |||
+ | NimBLEAdvertising* adv = NimBLEDevice::getAdvertising(); | ||
+ | |||
+ | // Advertise the standard Environmental Sensing Service UUID | ||
+ | adv->addServiceUUID(ESS_UUID); | ||
+ | |||
+ | // Your explicit adv + scan-response payloads | ||
+ | NimBLEAdvertisementData advData; | ||
+ | advData.setFlags(0x06); // LE General Discoverable + BR/EDR Not Supported | ||
+ | adv->setAdvertisementData(advData); | ||
+ | |||
+ | NimBLEAdvertisementData scanData; | ||
+ | scanData.setName(DEVICE_NAME); | ||
+ | adv->setScanResponseData(scanData); | ||
+ | |||
+ | // Appearance: Generic Thermometer (0x0300) | ||
+ | adv->setAppearance(0x0300); | ||
+ | |||
+ | NimBLEDevice::startAdvertising(); | ||
+ | Serial.println("Advertising Environmental Sensing Service. Open nRF Connect -> Scan."); | ||
+ | } | ||
+ | |||
+ | /* ============================ | ||
+ | BME680 setup | ||
+ | ============================ */ | ||
+ | bool startBME680() { | ||
+ | Wire.begin(I2C_SDA_PIN, I2C_SCL_PIN); | ||
+ | if (!bme.begin(BME680_I2C_ADDR)) { | ||
+ | Serial.println("[BME680] Not found on 0x77, trying 0x76..."); | ||
+ | if (!bme.begin(0x76)) { | ||
+ | Serial.println("[BME680] Sensor not found. Check wiring and power."); | ||
+ | return false; | ||
+ | } | ||
+ | } | ||
+ | |||
+ | // Oversampling & filter for stable readings | ||
+ | bme.setTemperatureOversampling(BME680_OS_8X); | ||
+ | bme.setHumidityOversampling(BME680_OS_2X); | ||
+ | bme.setPressureOversampling(BME680_OS_4X); | ||
+ | bme.setIIRFilterSize(BME680_FILTER_SIZE_3); | ||
+ | |||
+ | // Gas heater off (not needed for T/RH/P) | ||
+ | bme.setGasHeater(0, 0); | ||
+ | return true; | ||
+ | } | ||
+ | |||
+ | /* ============================ | ||
+ | Measurement + GATT update | ||
+ | ============================ */ | ||
+ | void updateMeasurements() { | ||
+ | if (!bme.performReading()) { | ||
+ | Serial.println("[BME680] performReading() failed"); | ||
+ | return; | ||
+ | } | ||
+ | |||
+ | float c = bme.temperature; // °C | ||
+ | float h = bme.humidity; // %RH | ||
+ | float p = bme.pressure; // Pa | ||
+ | |||
+ | setTempCentiDeg(gTempChar, c); | ||
+ | setHumidityCentiPct(gHumChar, h); | ||
+ | setPressureDeciPa(gPresChar, p); | ||
+ | |||
+ | // Send notifications to any subscribed client | ||
+ | gTempChar->notify(true); | ||
+ | gHumChar->notify(true); | ||
+ | gPresChar->notify(true); | ||
+ | |||
+ | Serial.printf("[DATA] T=%.2f°C RH=%.2f%% P=%.1f Pa\n", c, h, p); | ||
+ | } | ||
+ | |||
+ | /* ============================ | ||
+ | Arduino entry points | ||
+ | ============================ */ | ||
+ | void setup() { | ||
+ | Serial.begin(115200); | ||
+ | while (!Serial) { delay(10); } | ||
+ | |||
+ | if (!startBME680()) { | ||
+ | // We still start BLE so you can see/connect even if sensor is missing | ||
+ | } | ||
+ | |||
+ | startBLE(); | ||
+ | } | ||
+ | |||
+ | void loop() { | ||
+ | uint32_t now = millis(); | ||
+ | if (now - lastMeas >= MEAS_INTERVAL_MS) { | ||
+ | lastMeas = now; | ||
+ | updateMeasurements(); | ||
+ | } | ||
+ | delay(10); | ||
+ | } | ||
+ | |||
+ | </code> | ||
+ | |||
+ | Upload the code to your board. After uploading, open the Serial Monitor, and restart the Sparrow by pressing the RST/EN button. | ||
+ | |||
+ | You should get a data reading messages in the Serial Monitor. | ||
+ | |||
+ | Then, go to your smartphone, open the nRF Connect app from Nordic, and start scanning for new devices. You should find a device called **ESP32-C6 Env**, this is the BLE server name you defined earlier. | ||
+ | |||
+ | Connect to it. You’ll see that it displays the Environmental Sensing service with the temperature, humidity, and pressure characteristics. Click on the down arrows to activate the notifications. | ||
+ | |||
+ | Then, click on the second icon (the one that looks like a " mark) at the left to change the format. You can change to unsigned int for all characteristics. You’ll start seeing the temperature, humidity, and pressure values being reported every 2 seconds. | ||
+ | |||
+ | ===== Web BLE Application ===== | ||
+ | |||
+ | Follow the tutorial [[https://randomnerdtutorials.com/esp32-web-bluetooth/| here]] to learn how to create a Web application that connects directly to your Sparrow ESP32 board. You can use the web app just like a normal phone application to send and receive information over BLE from your device. | ||
+ | |||
+ | <note important>Web BLE is not currently supported by iOS phones </note> | ||
+ | |||
+ | Build the application in the tutorial and deploy the web page in your GitHub account. | ||
+ | === Assignment === | ||
+ | <note> Modify the web page and the BLE app to display the BME680 sensor data (temperature, pressure and humidity). </note> |