This shows you the differences between two versions of the page.
iothings:laboratoare:2022:lab7 [2023/11/20 21:55] 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 123: | Line 189: | ||
<code C> | <code C> | ||
- | #include <BLEDevice.h> | + | #include <Arduino.h> |
- | #include <BLEServer.h> | + | #include <Wire.h> |
- | #include <BLEUtils.h> | + | #include <Adafruit_BME680.h> |
- | #include <BLE2902.h> | + | #include <NimBLEDevice.h> |
- | #include "Zanshin_BME680.h" | + | |
- | //BLE server name | + | /* ============================ |
- | #define bleServerName "Sparrow_BME680" | + | 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 | ||
- | // Default UUID for Environmental Sensing Service | + | /* ============================ |
- | // https://www.bluetooth.com/specifications/assigned-numbers/ | + | BLE UUIDs (Environmental Sensing) |
- | #define SERVICE_UUID (BLEUUID((uint16_t)0x181A)) | + | ============================ */ |
+ | 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 | ||
- | // Temperature Characteristic and Descriptor (default UUID) | + | /* ============================ |
- | // Check the default UUIDs here: https://www.bluetooth.com/specifications/assigned-numbers/ | + | Globals |
- | BLECharacteristic temperatureCharacteristic(BLEUUID((uint16_t)0x2A6E), BLECharacteristic::PROPERTY_NOTIFY); | + | ============================ */ |
- | BLEDescriptor temperatureDescriptor(BLEUUID((uint16_t)0x2902)); | + | NimBLEServer* gServer = nullptr; |
+ | NimBLEService* gService = nullptr; | ||
+ | NimBLECharacteristic* gTempChar = nullptr; | ||
+ | NimBLECharacteristic* gPresChar = nullptr; | ||
+ | NimBLECharacteristic* gHumChar = nullptr; | ||
- | // Humidity Characteristic and Descriptor (default UUID) | + | Adafruit_BME680 bme; // I2C |
- | BLECharacteristic humidityCharacteristic(BLEUUID((uint16_t)0x2A6F), BLECharacteristic::PROPERTY_NOTIFY); | + | uint32_t lastMeas = 0; |
- | BLEDescriptor humidityDescriptor(BLEUUID((uint16_t)0x2902)); | + | |
- | // Pressure Characteristic and Descriptor (default UUID) | + | /* ============================ |
- | BLECharacteristic pressureCharacteristic(BLEUUID((uint16_t)0x2A6D), BLECharacteristic::PROPERTY_NOTIFY); | + | Helpers: encode per SIG formats (little-endian) |
- | BLEDescriptor pressureDescriptor(BLEUUID((uint16_t)0x2902)); | + | ============================ */ |
+ | 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)); | ||
+ | } | ||
- | // Create a sensor object | + | static void setHumidityCentiPct(NimBLECharacteristic* ch, float rh) { |
- | BME680_Class BME680; | + | // 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)); | ||
+ | } | ||
- | bool deviceConnected = false; | + | 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)); | ||
+ | } | ||
- | //Setup callbacks onConnect and onDisconnect | + | /* Add Characteristic Presentation Format (0x2904) */ |
- | class MyServerCallbacks: public BLEServerCallbacks { | + | static void addCPF(NimBLECharacteristic* ch, uint8_t format, int8_t exponent, uint16_t unit) { |
- | void onConnect(BLEServer* pServer) { | + | // 7 bytes: format, exponent, unit(LE), namespace(1=1), description(2=0) |
- | deviceConnected = true; | + | uint8_t cpf[7] = { |
- | Serial.println("Device Connected"); | + | format, (uint8_t)exponent, |
+ | (uint8_t)(unit & 0xFF), (uint8_t)((unit >> 8) & 0xFF), | ||
+ | 0x01, 0x00, 0x00 | ||
}; | }; | ||
- | void onDisconnect(BLEServer* pServer) { | + | ch->createDescriptor("2904")->setValue(cpf, sizeof(cpf)); |
- | deviceConnected = false; | + | } |
- | Serial.println("Device Disconnected"); | + | |
+ | /* ============================ | ||
+ | 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; | ||
+ | } | ||
} | } | ||
- | }; | ||
- | void setup() { | + | // Oversampling & filter for stable readings |
- | // Start serial communication | + | bme.setTemperatureOversampling(BME680_OS_8X); |
- | Serial.begin(115200); | + | bme.setHumidityOversampling(BME680_OS_2X); |
+ | bme.setPressureOversampling(BME680_OS_4X); | ||
+ | bme.setIIRFilterSize(BME680_FILTER_SIZE_3); | ||
- | // Start BME sensor | + | // Gas heater off (not needed for T/RH/P) |
- | while (!BME680.begin(I2C_STANDARD_MODE)) { // Start BME680 using I2C, use first device found | + | bme.setGasHeater(0, 0); |
- | Serial.print(F("- Unable to find BME680. Trying again in 5 seconds.\n")); | + | return true; |
- | delay(5000); | + | } |
- | } // of loop until device is located | + | |
- | Serial.print(F("- Setting 16x oversampling for all sensors\n")); | + | |
- | BME680.setOversampling(TemperatureSensor, Oversample16); // Use enumerated type values | + | |
- | BME680.setOversampling(HumiditySensor, Oversample16); // Use enumerated type values | + | |
- | BME680.setOversampling(PressureSensor, Oversample16); // Use enumerated type values | + | |
- | Serial.print(F("- Setting IIR filter to a value of 4 samples\n")); | + | |
- | BME680.setIIRFilter(IIR4); // Use enumerated type values | + | |
- | Serial.print(F("- Setting gas measurement to 320\xC2\xB0\x43 for 150ms\n")); // "degC" symbols | + | |
- | BME680.setGas(320, 150); // 320 deg.C for 150 milliseconds | + | |
- | // Create the BLE Device | + | /* ============================ |
- | BLEDevice::init(bleServerName); | + | Measurement + GATT update |
+ | ============================ */ | ||
+ | void updateMeasurements() { | ||
+ | if (!bme.performReading()) { | ||
+ | Serial.println("[BME680] performReading() failed"); | ||
+ | return; | ||
+ | } | ||
- | // Create the BLE Server | + | float c = bme.temperature; // °C |
- | BLEServer *pServer = BLEDevice::createServer(); | + | float h = bme.humidity; // %RH |
- | pServer->setCallbacks(new MyServerCallbacks()); | + | float p = bme.pressure; // Pa |
- | // Create the BLE Service | + | setTempCentiDeg(gTempChar, c); |
- | BLEService *bmeService = pServer->createService(SERVICE_UUID); | + | setHumidityCentiPct(gHumChar, h); |
+ | setPressureDeciPa(gPresChar, p); | ||
- | // Create BLE Characteristics and corresponding Descriptors | + | // Send notifications to any subscribed client |
- | bmeService->addCharacteristic(&temperatureCharacteristic); | + | gTempChar->notify(true); |
- | temperatureCharacteristic.addDescriptor(&temperatureDescriptor); | + | gHumChar->notify(true); |
- | | + | gPresChar->notify(true); |
- | bmeService->addCharacteristic(&humidityCharacteristic); | + | |
- | humidityCharacteristic.addDescriptor(&humidityDescriptor); | + | |
- | bmeService->addCharacteristic(&pressureCharacteristic); | + | Serial.printf("[DATA] T=%.2f°C RH=%.2f%% P=%.1f Pa\n", c, h, p); |
- | pressureCharacteristic.addDescriptor(&pressureDescriptor); | + | } |
- | + | ||
- | // Start the service | + | |
- | bmeService->start(); | + | |
- | // Start advertising | + | /* ============================ |
- | pServer->getAdvertising()->start(); | + | Arduino entry points |
- | Serial.println("Waiting a client connection to notify..."); | + | ============================ */ |
+ | 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() { | void loop() { | ||
- | if (deviceConnected) { | + | uint32_t now = millis(); |
- | + | if (now - lastMeas >= MEAS_INTERVAL_MS) { | |
- | static int32_t temp, hum, pres, gas; // BME readings | + | lastMeas = now; |
- | + | updateMeasurements(); | |
- | BME680.getSensorData(temp, hum, pres, gas); // Get readings | + | |
- | + | ||
- | float t = (float)((int8_t)(temp / 100)); | + | |
- | float h = (float)((int8_t)(humidity / 1000)); | + | |
- | float p = (float)((int16_t)(pressure / 100)); | + | |
- | + | ||
- | + | ||
- | //Notify temperature reading | + | |
- | uint16_t temperature = (uint16_t)t; | + | |
- | //Set temperature Characteristic value and notify connected client | + | |
- | temperatureCharacteristic.setValue(temperature); | + | |
- | temperatureCharacteristic.notify(); | + | |
- | Serial.print("Temperature Celsius: "); | + | |
- | Serial.print(t); | + | |
- | Serial.println(" ºC"); | + | |
- | + | ||
- | //Notify humidity reading | + | |
- | uint16_t humidity = (uint16_t)h; | + | |
- | //Set humidity Characteristic value and notify connected client | + | |
- | humidityCharacteristic.setValue(humidity); | + | |
- | humidityCharacteristic.notify(); | + | |
- | Serial.print("Humidity: "); | + | |
- | Serial.print(h); | + | |
- | Serial.println(" %"); | + | |
- | + | ||
- | //Notify pressure reading | + | |
- | uint16_t pressure = (uint16_t)p; | + | |
- | //Set humidity Characteristic value and notify connected client | + | |
- | pressureCharacteristic.setValue(pressure); | + | |
- | pressureCharacteristic.notify(); | + | |
- | Serial.print("Pressure: "); | + | |
- | Serial.print(p); | + | |
- | Serial.println(" hPa"); | + | |
- | + | ||
- | delay(10000); | + | |
} | } | ||
+ | delay(10); | ||
} | } | ||
+ | |||
</code> | </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> |