This shows you the differences between two versions of the page.
|
iothings:laboratoare:2022:lab7 [2023/11/20 21:33] dan.tudose [Mobile App] |
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 107: | Line 173: | ||
| For our testing purposes, we'll employ a free application named nRF Connect for Mobile, developed by Nordic Semi. This app is available on both Android ([[https://play.google.com/store/apps/details?id=no.nordicsemi.android.mcp&hl=en_US| Google Play Store]]) and iOS. To install the app, simply go to the Google Play Store or App Store, search for "nRF Connect for Mobile," and proceed with the installation. | For our testing purposes, we'll employ a free application named nRF Connect for Mobile, developed by Nordic Semi. This app is available on both Android ([[https://play.google.com/store/apps/details?id=no.nordicsemi.android.mcp&hl=en_US| Google Play Store]]) and iOS. To install the app, simply go to the Google Play Store or App Store, search for "nRF Connect for Mobile," and proceed with the installation. | ||
| + | ==== Sparrow BLE Service ==== | ||
| + | |||
| + | Here are the steps to create an BLE peripheral with an Environmental Sensing BLE service with temperature, humidity, and pressure, characteristics: | ||
| + | |||
| + | - 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). | ||
| + | - Add characteristics to that service: pressure (0x2A6D), temperature (0x2A6E) and humidity (0x246F) | ||
| + | - Add descriptors to the characteristics. | ||
| + | - Start the BLE server. | ||
| + | - Start advertising so BLE clients can connect and read the characteristics. | ||
| + | - Once a connection is established with a client, it will write new values on the characteristics and will notify the client, every time there’s a change. | ||
| + | |||
| + | |||
| + | 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> | ||