This shows you the differences between two versions of the page.
iothings:laboratoare:2025:lab2 [2025/09/28 17:47] dan.tudose created |
iothings:laboratoare:2025:lab2 [2025/09/28 18:16] (current) dan.tudose [Sparrow BLE Service] |
||
---|---|---|---|
Line 197: | Line 197: | ||
User config | User config | ||
============================ */ | ============================ */ | ||
- | static const char* DEVICE_NAME = "ESP32-C6 Env"; // shows in scan response | + | static const char* DEVICE_NAME = "ESP32-C6 Env"; |
- | #define I2C_SDA_PIN 21 // adjust for your board | + | #define I2C_SDA_PIN 21 |
#define I2C_SCL_PIN 22 | #define I2C_SCL_PIN 22 | ||
- | #define BME680_I2C_ADDR 0x77 // try 0x76 if needed | + | #define BME680_I2C_ADDR 0x77 |
- | static const uint32_t MEAS_INTERVAL_MS = 2000; // update period | + | static const uint32_t MEAS_INTERVAL_MS = 2000; // env update |
+ | static const uint32_t BATTERY_INTERVAL_MS = 60000; // battery update | ||
+ | |||
+ | /* Device Information strings */ | ||
+ | static const char* DIS_MANUFACTURER = "YourBrand"; | ||
+ | static const char* DIS_MODEL = "ESP32-C6 EnvSense"; | ||
+ | static const char* DIS_FW_REV = "1.0.0"; | ||
+ | static const char* DIS_SW_REV = "1.0.0"; | ||
+ | static const char* DIS_SN = "SN-000001"; | ||
+ | static const char* DIS_HW = "RevA"; | ||
/* ============================ | /* ============================ | ||
- | BLE UUIDs (Environmental Sensing) | + | UUIDs |
============================ */ | ============================ */ | ||
- | static NimBLEUUID ESS_UUID((uint16_t)0x181A); | + | // Environmental Sensing |
- | static NimBLEUUID TEMP_UUID((uint16_t)0x2A6E); // Temperature | + | static NimBLEUUID ESS_UUID ((uint16_t)0x181A); |
- | static NimBLEUUID PRES_UUID((uint16_t)0x2A6D); // Pressure | + | static NimBLEUUID TEMP_UUID((uint16_t)0x2A6E); |
- | static NimBLEUUID HUM_UUID ((uint16_t)0x2A6F); // Humidity | + | static NimBLEUUID PRES_UUID((uint16_t)0x2A6D); |
+ | static NimBLEUUID HUM_UUID ((uint16_t)0x2A6F); | ||
+ | |||
+ | // Battery Service | ||
+ | static NimBLEUUID BATT_SVC_UUID((uint16_t)0x180F); | ||
+ | static NimBLEUUID BATT_LVL_UUID((uint16_t)0x2A19); | ||
+ | |||
+ | // Device Information Service | ||
+ | static NimBLEUUID DIS_UUID ((uint16_t)0x180A); | ||
+ | static NimBLEUUID DIS_MFR_UUID ((uint16_t)0x2A29); | ||
+ | static NimBLEUUID DIS_MODEL_UUID((uint16_t)0x2A24); | ||
+ | static NimBLEUUID DIS_FW_UUID ((uint16_t)0x2A26); | ||
+ | static NimBLEUUID DIS_SW_UUID ((uint16_t)0x2A28); | ||
+ | static NimBLEUUID DIS_PNP_UUID ((uint16_t)0x2A50); | ||
/* ============================ | /* ============================ | ||
Globals | Globals | ||
============================ */ | ============================ */ | ||
- | NimBLEServer* gServer = nullptr; | + | Adafruit_BME680 bme; // I2C |
- | NimBLEService* gService = nullptr; | + | |
+ | NimBLEServer* gServer = nullptr; | ||
+ | |||
+ | /* Environmental Sensing */ | ||
+ | NimBLEService* gESS = nullptr; | ||
NimBLECharacteristic* gTempChar = nullptr; | NimBLECharacteristic* gTempChar = nullptr; | ||
NimBLECharacteristic* gPresChar = nullptr; | NimBLECharacteristic* gPresChar = nullptr; | ||
NimBLECharacteristic* gHumChar = nullptr; | NimBLECharacteristic* gHumChar = nullptr; | ||
- | Adafruit_BME680 bme; // I2C | + | /* Battery */ |
- | uint32_t lastMeas = 0; | + | NimBLEService* gBattSvc = nullptr; |
+ | NimBLECharacteristic* gBattChar = nullptr; | ||
+ | |||
+ | /* DIS */ | ||
+ | NimBLEService* gDIS = nullptr; | ||
+ | |||
+ | uint32_t lastEnv = 0; | ||
+ | uint32_t lastBatt = 0; | ||
/* ============================ | /* ============================ | ||
- | Helpers: encode per SIG formats (little-endian) | + | Helpers: GATT encoders |
============================ */ | ============================ */ | ||
static void setTempCentiDeg(NimBLECharacteristic* ch, float celsius) { | static void setTempCentiDeg(NimBLECharacteristic* ch, float celsius) { | ||
- | // sint16, 0.01 °C | + | int32_t raw = lroundf(celsius * 100.0f); // sint16, 0.01 °C |
- | int32_t raw = lroundf(celsius * 100.0f); | + | if (raw > 32767) raw = 32767; |
- | if (raw > 32767) raw = 32767; | + | |
if (raw < -32768) raw = -32768; | if (raw < -32768) raw = -32768; | ||
int16_t v = (int16_t)raw; | int16_t v = (int16_t)raw; | ||
Line 237: | Line 269: | ||
static void setHumidityCentiPct(NimBLECharacteristic* ch, float rh) { | static void setHumidityCentiPct(NimBLECharacteristic* ch, float rh) { | ||
- | // uint16, 0.01 %RH (clip 0..100%) | + | if (rh < 0) rh = 0; if (rh > 100) rh = 100; // uint16, 0.01 %RH |
- | if (rh < 0) rh = 0; | + | |
- | if (rh > 100) rh = 100; | + | |
uint32_t raw = lroundf(rh * 100.0f); | uint32_t raw = lroundf(rh * 100.0f); | ||
if (raw > 0xFFFF) raw = 0xFFFF; | if (raw > 0xFFFF) raw = 0xFFFF; | ||
Line 248: | Line 278: | ||
static void setPressureDeciPa(NimBLECharacteristic* ch, float pascals) { | static void setPressureDeciPa(NimBLECharacteristic* ch, float pascals) { | ||
- | // uint32, 0.1 Pa | + | if (pascals < 0) pascals = 0; // uint32, 0.1 Pa |
- | if (pascals < 0) pascals = 0; | + | |
uint64_t raw = llroundf(pascals * 10.0f); | uint64_t raw = llroundf(pascals * 10.0f); | ||
if (raw > 0xFFFFFFFFULL) raw = 0xFFFFFFFFULL; | if (raw > 0xFFFFFFFFULL) raw = 0xFFFFFFFFULL; | ||
uint32_t v = (uint32_t)raw; | uint32_t v = (uint32_t)raw; | ||
uint8_t buf[4] = { | uint8_t buf[4] = { | ||
- | (uint8_t)(v & 0xFF), | + | (uint8_t)(v & 0xFF), (uint8_t)((v >> 8) & 0xFF), |
- | (uint8_t)((v >> 8) & 0xFF), | + | (uint8_t)((v >> 16) & 0xFF), (uint8_t)((v >> 24) & 0xFF) |
- | (uint8_t)((v >> 16) & 0xFF), | + | |
- | (uint8_t)((v >> 24) & 0xFF) | + | |
}; | }; | ||
ch->setValue(buf, sizeof(buf)); | ch->setValue(buf, sizeof(buf)); | ||
} | } | ||
- | /* Add Characteristic Presentation Format (0x2904) */ | + | /* 0x2904 Presentation Format descriptor */ |
static void addCPF(NimBLECharacteristic* ch, uint8_t format, int8_t exponent, uint16_t unit) { | 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) | // 7 bytes: format, exponent, unit(LE), namespace(1=1), description(2=0) | ||
Line 274: | Line 301: | ||
/* ============================ | /* ============================ | ||
- | BLE setup (keeps your adv/scan-response style) | + | Battery level source (stub) |
+ | Replace with ADC/PMIC reading if available. | ||
+ | ============================ */ | ||
+ | static uint8_t getBatteryPercent() { | ||
+ | // TODO: read your actual battery voltage & map to 0..100 | ||
+ | return 100; // placeholder | ||
+ | } | ||
+ | |||
+ | /* ============================ | ||
+ | BLE setup | ||
============================ */ | ============================ */ | ||
void startBLE() { | void startBLE() { | ||
NimBLEDevice::init(DEVICE_NAME); | NimBLEDevice::init(DEVICE_NAME); | ||
- | NimBLEDevice::setPower(ESP_PWR_LVL_P9); // strong TX for gateways; lower if needed | + | NimBLEDevice::setPower(ESP_PWR_LVL_P9); |
- | NimBLEDevice::setSecurityAuth(false, false, true); // no bonding; SC on | + | NimBLEDevice::setSecurityAuth(false, false, true); // no bonding; SC=true |
- | gServer = NimBLEDevice::createServer(); | + | gServer = NimBLEDevice::createServer(); |
- | gService = gServer->createService(ESS_UUID); | + | |
- | // READ + NOTIFY for all three characteristics | + | /* --- Environmental Sensing Service --- */ |
- | auto props = (NIMBLE_PROPERTY::READ | NIMBLE_PROPERTY::NOTIFY); | + | gESS = gServer->createService(ESS_UUID); |
- | gTempChar = gService->createCharacteristic(TEMP_UUID, props); | + | auto propsRN = (NIMBLE_PROPERTY::READ | NIMBLE_PROPERTY::NOTIFY); |
- | gPresChar = gService->createCharacteristic(PRES_UUID, props); | + | |
- | gHumChar = gService->createCharacteristic(HUM_UUID, props); | + | |
- | // Presentation formats | + | gTempChar = gESS->createCharacteristic(TEMP_UUID, propsRN); |
+ | gPresChar = gESS->createCharacteristic(PRES_UUID, propsRN); | ||
+ | gHumChar = gESS->createCharacteristic(HUM_UUID, propsRN); | ||
+ | |||
+ | // CPF descriptors for units | ||
const uint8_t GATT_FORMAT_SINT16 = 0x0E; | const uint8_t GATT_FORMAT_SINT16 = 0x0E; | ||
const uint8_t GATT_FORMAT_UINT16 = 0x0D; | const uint8_t GATT_FORMAT_UINT16 = 0x0D; | ||
const uint8_t GATT_FORMAT_UINT32 = 0x10; | const uint8_t GATT_FORMAT_UINT32 = 0x10; | ||
addCPF(gTempChar, GATT_FORMAT_SINT16, -2, 0x272F); // Celsius | addCPF(gTempChar, GATT_FORMAT_SINT16, -2, 0x272F); // Celsius | ||
- | addCPF(gPresChar, GATT_FORMAT_UINT32, -1, 0x2724); // Pascal, 0.1 exponent | + | addCPF(gPresChar, GATT_FORMAT_UINT32, -1, 0x2724); // Pascal, 0.1 |
- | addCPF(gHumChar, GATT_FORMAT_UINT16, -2, 0x27AD); // Percentage | + | addCPF(gHumChar, GATT_FORMAT_UINT16, -2, 0x27AD); // % |
- | // Initialize with placeholder values so READ works before first measurement | + | // Initial values |
setTempCentiDeg(gTempChar, 0.0f); | setTempCentiDeg(gTempChar, 0.0f); | ||
setPressureDeciPa(gPresChar, 101325.0f); | setPressureDeciPa(gPresChar, 101325.0f); | ||
setHumidityCentiPct(gHumChar, 0.0f); | setHumidityCentiPct(gHumChar, 0.0f); | ||
- | gService->start(); | + | gESS->start(); |
+ | |||
+ | /* --- Battery Service --- */ | ||
+ | gBattSvc = gServer->createService(BATT_SVC_UUID); | ||
+ | gBattChar = gBattSvc->createCharacteristic(BATT_LVL_UUID, propsRN); | ||
+ | // Optional CPF for % (uint8, exponent 0) | ||
+ | // format for uint8 = 0x04 | ||
+ | addCPF(gBattChar, 0x04, 0, 0x27AD); | ||
+ | uint8_t lvl = getBatteryPercent(); | ||
+ | gBattChar->setValue(&lvl, 1); | ||
+ | gBattSvc->start(); | ||
+ | |||
+ | /* --- Device Information Service (all READ-only) --- */ | ||
+ | gDIS = gServer->createService(DIS_UUID); | ||
+ | gDIS->createCharacteristic(DIS_MFR_UUID, NIMBLE_PROPERTY::READ)->setValue(DIS_MANUFACTURER); | ||
+ | gDIS->createCharacteristic(DIS_MODEL_UUID, NIMBLE_PROPERTY::READ)->setValue(DIS_MODEL); | ||
+ | gDIS->createCharacteristic(DIS_FW_UUID, NIMBLE_PROPERTY::READ)->setValue(DIS_FW_REV); | ||
+ | gDIS->createCharacteristic(DIS_SW_UUID, NIMBLE_PROPERTY::READ)->setValue(DIS_SW_REV); | ||
+ | gDIS->createCharacteristic((uint16_t)0x2A25, NIMBLE_PROPERTY::READ)->setValue(DIS_SN); // Serial Number | ||
+ | gDIS->createCharacteristic((uint16_t)0x2A27, NIMBLE_PROPERTY::READ)->setValue(DIS_HW); // Hardware Rev | ||
+ | |||
+ | // PnP ID (7 bytes): Vendor ID Source (1=Bluetooth, 2=USB), Vendor ID (LE16), Product ID (LE16), Product Version (LE16) | ||
+ | uint8_t pnp[7] = { 0x02, 0x34, 0x12, 0x78, 0x56, 0x00, 0x01 }; // USB, VID 0x1234, PID 0x5678, ver 0x0100 | ||
+ | gDIS->createCharacteristic(DIS_PNP_UUID, NIMBLE_PROPERTY::READ)->setValue(pnp, sizeof(pnp)); | ||
+ | |||
+ | gDIS->start(); | ||
+ | /* --- Advertising (keeps your explicit payload approach) --- */ | ||
NimBLEAdvertising* adv = NimBLEDevice::getAdvertising(); | NimBLEAdvertising* adv = NimBLEDevice::getAdvertising(); | ||
- | // Advertise the standard Environmental Sensing Service UUID | + | // Advertise primary services (ESS + Battery). DIS is usually not advertised. |
adv->addServiceUUID(ESS_UUID); | adv->addServiceUUID(ESS_UUID); | ||
+ | adv->addServiceUUID(BATT_SVC_UUID); | ||
- | // Your explicit adv + scan-response payloads | ||
NimBLEAdvertisementData advData; | NimBLEAdvertisementData advData; | ||
advData.setFlags(0x06); // LE General Discoverable + BR/EDR Not Supported | advData.setFlags(0x06); // LE General Discoverable + BR/EDR Not Supported | ||
Line 316: | Line 379: | ||
NimBLEAdvertisementData scanData; | NimBLEAdvertisementData scanData; | ||
- | scanData.setName(DEVICE_NAME); | + | scanData.setName(DEVICE_NAME); // put name in scan response |
adv->setScanResponseData(scanData); | adv->setScanResponseData(scanData); | ||
- | // Appearance: Generic Thermometer (0x0300) | + | // Appearance: Generic Thermometer (helps some UIs) |
adv->setAppearance(0x0300); | adv->setAppearance(0x0300); | ||
NimBLEDevice::startAdvertising(); | NimBLEDevice::startAdvertising(); | ||
- | Serial.println("Advertising Environmental Sensing Service. Open nRF Connect -> Scan."); | + | Serial.println("Advertising: ESS + Battery + DIS (readable). Open nRF Connect -> Scan."); |
} | } | ||
/* ============================ | /* ============================ | ||
- | BME680 setup | + | Sensor setup |
============================ */ | ============================ */ | ||
bool startBME680() { | bool startBME680() { | ||
Line 334: | Line 397: | ||
Serial.println("[BME680] Not found on 0x77, trying 0x76..."); | Serial.println("[BME680] Not found on 0x77, trying 0x76..."); | ||
if (!bme.begin(0x76)) { | if (!bme.begin(0x76)) { | ||
- | Serial.println("[BME680] Sensor not found. Check wiring and power."); | + | Serial.println("[BME680] Sensor not found. Check wiring/power."); |
return false; | return false; | ||
} | } | ||
} | } | ||
- | |||
- | // Oversampling & filter for stable readings | ||
bme.setTemperatureOversampling(BME680_OS_8X); | bme.setTemperatureOversampling(BME680_OS_8X); | ||
bme.setHumidityOversampling(BME680_OS_2X); | bme.setHumidityOversampling(BME680_OS_2X); | ||
bme.setPressureOversampling(BME680_OS_4X); | bme.setPressureOversampling(BME680_OS_4X); | ||
bme.setIIRFilterSize(BME680_FILTER_SIZE_3); | bme.setIIRFilterSize(BME680_FILTER_SIZE_3); | ||
- | + | bme.setGasHeater(0, 0); // off | |
- | // Gas heater off (not needed for T/RH/P) | + | |
- | bme.setGasHeater(0, 0); | + | |
return true; | return true; | ||
} | } | ||
/* ============================ | /* ============================ | ||
- | Measurement + GATT update | + | Updates |
============================ */ | ============================ */ | ||
- | void updateMeasurements() { | + | void updateEnv() { |
if (!bme.performReading()) { | if (!bme.performReading()) { | ||
Serial.println("[BME680] performReading() failed"); | Serial.println("[BME680] performReading() failed"); | ||
return; | return; | ||
} | } | ||
- | + | float c = bme.temperature; | |
- | float c = bme.temperature; // °C | + | float h = bme.humidity; |
- | float h = bme.humidity; // %RH | + | float p = bme.pressure; |
- | float p = bme.pressure; // Pa | + | |
setTempCentiDeg(gTempChar, c); | setTempCentiDeg(gTempChar, c); | ||
Line 367: | Line 425: | ||
setPressureDeciPa(gPresChar, p); | setPressureDeciPa(gPresChar, p); | ||
- | // Send notifications to any subscribed client | + | gTempChar->notify(); |
- | gTempChar->notify(true); | + | gHumChar->notify(); |
- | gHumChar->notify(true); | + | gPresChar->notify(); |
- | gPresChar->notify(true); | + | |
- | Serial.printf("[DATA] T=%.2f°C RH=%.2f%% P=%.1f Pa\n", c, h, p); | + | Serial.printf("[ENV] T=%.2f°C RH=%.2f%% P=%.1f Pa\n", c, h, p); |
+ | } | ||
+ | |||
+ | void updateBattery() { | ||
+ | uint8_t lvl = getBatteryPercent(); | ||
+ | gBattChar->setValue(&lvl, 1); | ||
+ | gBattChar->notify(); | ||
+ | Serial.printf("[BATT] %u%%\n", lvl); | ||
} | } | ||
Line 383: | Line 447: | ||
if (!startBME680()) { | if (!startBME680()) { | ||
- | // We still start BLE so you can see/connect even if sensor is missing | + | // Proceed with BLE even if sensor is missing, so you can still connect |
} | } | ||
- | |||
startBLE(); | startBLE(); | ||
} | } | ||
Line 391: | Line 454: | ||
void loop() { | void loop() { | ||
uint32_t now = millis(); | uint32_t now = millis(); | ||
- | if (now - lastMeas >= MEAS_INTERVAL_MS) { | + | |
- | lastMeas = now; | + | if (now - lastEnv >= MEAS_INTERVAL_MS) { |
- | updateMeasurements(); | + | lastEnv = now; |
+ | updateEnv(); | ||
} | } | ||
+ | |||
+ | if (now - lastBatt >= BATTERY_INTERVAL_MS) { | ||
+ | lastBatt = now; | ||
+ | updateBattery(); | ||
+ | } | ||
+ | |||
delay(10); | delay(10); | ||
} | } | ||
Line 409: | Line 479: | ||
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. | 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. | ||
+ | |||
+ | You can also see a Battery Service and a Device Information Service. Battery is hardcoded for now at 100% and device information is also hardcoded. | ||
===== Web BLE Application ===== | ===== Web BLE Application ===== |