diff --git a/examples/companion_radio/MyMesh.cpp b/examples/companion_radio/MyMesh.cpp index e8c1914bad..fdecf9b2ff 100644 --- a/examples/companion_radio/MyMesh.cpp +++ b/examples/companion_radio/MyMesh.cpp @@ -144,6 +144,8 @@ #define AUTO_ADD_ROOM_SERVER (1 << 3) // 0x08 - auto-add Room Server (ADV_TYPE_ROOM) #define AUTO_ADD_SENSOR (1 << 4) // 0x10 - auto-add Sensor (ADV_TYPE_SENSOR) +#define BLE_ADV_UPDATE_INTERVAL_MS 30000 // Update BLE advertising every 30 seconds + void MyMesh::writeOKFrame() { uint8_t buf[1]; buf[0] = RESP_CODE_OK; @@ -237,6 +239,8 @@ void MyMesh::addToOfflineQueue(const uint8_t frame[], int len) { memcpy(offline_queue[offline_queue_len].buf, frame, len); offline_queue_len++; } + + updateBLEUnreadCount(); } int MyMesh::getFromOfflineQueue(uint8_t frame[]) { @@ -248,11 +252,19 @@ int MyMesh::getFromOfflineQueue(uint8_t frame[]) { for (int i = 0; i < offline_queue_len; i++) { // delete top item from queue offline_queue[i] = offline_queue[i + 1]; } + + updateBLEUnreadCount(); + return len; } return 0; // queue is empty } +void MyMesh::updateBLEUnreadCount() { + if (!_serial) return; + _serial->setUnreadCount((uint8_t)min(offline_queue_len, 255)); +} + float MyMesh::getAirtimeBudgetFactor() const { return _prefs.airtime_factor; } @@ -2158,6 +2170,8 @@ void MyMesh::checkSerialInterface() { } void MyMesh::loop() { + static unsigned long last_adv_update = 0; + BaseChatMesh::loop(); if (_cli_rescue) { @@ -2172,6 +2186,14 @@ void MyMesh::loop() { dirty_contacts_expiry = 0; } + // Periodically update battery level in BLE advertising + if (millis() - last_adv_update >= BLE_ADV_UPDATE_INTERVAL_MS) { + if (_serial) { + _serial->setBatteryMilliVolts(board.getBattMilliVolts()); + } + last_adv_update = millis(); + } + #ifdef DISPLAY_CLASS if (_ui) _ui->setHasConnection(_serial->isConnected()); #endif diff --git a/examples/companion_radio/MyMesh.h b/examples/companion_radio/MyMesh.h index aeff591cf4..3445604351 100644 --- a/examples/companion_radio/MyMesh.h +++ b/examples/companion_radio/MyMesh.h @@ -98,6 +98,8 @@ class MyMesh : public BaseChatMesh, public DataStoreHost { void loop(); void handleCmdFrame(size_t len); bool advert(); + + void updateBLEUnreadCount(); void enterCLIRescue(); int getRecentlyHeard(AdvertPath dest[], int max_num); diff --git a/src/helpers/BaseSerialInterface.h b/src/helpers/BaseSerialInterface.h index e60927654b..9c74789031 100644 --- a/src/helpers/BaseSerialInterface.h +++ b/src/helpers/BaseSerialInterface.h @@ -18,4 +18,7 @@ class BaseSerialInterface { virtual bool isWriteBusy() const = 0; virtual size_t writeFrame(const uint8_t src[], size_t len) = 0; virtual size_t checkRecvFrame(uint8_t dest[]) = 0; + + virtual void setUnreadCount(uint8_t count) {} + virtual void setBatteryMilliVolts(uint16_t millivolts) {} }; diff --git a/src/helpers/esp32/SerialBLEInterface.cpp b/src/helpers/esp32/SerialBLEInterface.cpp index dcfa0e1e34..49e9488f6a 100644 --- a/src/helpers/esp32/SerialBLEInterface.cpp +++ b/src/helpers/esp32/SerialBLEInterface.cpp @@ -50,7 +50,10 @@ void SerialBLEInterface::begin(const char* prefix, char* name, uint32_t pin_code pRxCharacteristic->setAccessPermissions(ESP_GATT_PERM_WRITE_ENC_MITM); pRxCharacteristic->setCallbacks(this); - pServer->getAdvertising()->addServiceUUID(SERVICE_UUID); + // Setup advertising with manufacturer data + pAdvertising = pServer->getAdvertising(); + pAdvertising->addServiceUUID(SERVICE_UUID); + applyAdvertisingData(); } // -------- BLESecurityCallbacks methods @@ -140,10 +143,10 @@ void SerialBLEInterface::enable() { // Start advertising - //pServer->getAdvertising()->setMinInterval(500); - //pServer->getAdvertising()->setMaxInterval(1000); + //pAdvertising->setMinInterval(500); + //pAdvertising->setMaxInterval(1000); - pServer->getAdvertising()->start(); + pAdvertising->start(); adv_restart_time = 0; } @@ -152,7 +155,7 @@ void SerialBLEInterface::disable() { BLE_DEBUG_PRINTLN("SerialBLEInterface::disable"); - pServer->getAdvertising()->stop(); + pAdvertising->stop(); pServer->disconnect(last_conn_id); pService->stop(); oldDeviceConnected = deviceConnected = false; @@ -223,8 +226,8 @@ size_t SerialBLEInterface::checkRecvFrame(uint8_t dest[]) { BLE_DEBUG_PRINTLN("SerialBLEInterface -> disconnecting..."); - //pServer->getAdvertising()->setMinInterval(500); - //pServer->getAdvertising()->setMaxInterval(1000); + //pAdvertising->setMinInterval(500); + //pAdvertising->setMaxInterval(1000); adv_restart_time = millis() + ADVERT_RESTART_DELAY; } else { @@ -232,7 +235,7 @@ size_t SerialBLEInterface::checkRecvFrame(uint8_t dest[]) { BLE_DEBUG_PRINTLN("SerialBLEInterface -> connecting..."); // connecting // do stuff here on connecting - pServer->getAdvertising()->stop(); + pAdvertising->stop(); adv_restart_time = 0; } oldDeviceConnected = deviceConnected; @@ -241,13 +244,78 @@ size_t SerialBLEInterface::checkRecvFrame(uint8_t dest[]) { if (adv_restart_time && millis() >= adv_restart_time) { if (pServer->getConnectedCount() == 0) { BLE_DEBUG_PRINTLN("SerialBLEInterface -> re-starting advertising"); - pServer->getAdvertising()->start(); // re-Start advertising + pAdvertising->start(); // re-Start advertising } adv_restart_time = 0; } + + // Apply pending advertising data changes when not connected + if (_advDataDirty && !deviceConnected) { + applyAdvertisingData(); + _advDataDirty = false; + } + return 0; } bool SerialBLEInterface::isConnected() const { return deviceConnected; //pServer != NULL && pServer->getConnectedCount() > 0; } + +void SerialBLEInterface::setUnreadCount(uint8_t count) { + if (_advStatus.unread_count == count) return; + _advStatus.unread_count = count; + onAdvStatusChanged(); +} + +void SerialBLEInterface::setBatteryMilliVolts(uint16_t millivolts) { + uint8_t encoded; + if (millivolts < 2500) { + encoded = 0; + } else if (millivolts > 5000) { + encoded = 250; + } else { + encoded = (millivolts - 2500) / 10; + } + if (_advStatus.battery_voltage == encoded) return; + _advStatus.battery_voltage = encoded; + onAdvStatusChanged(); +} + +void SerialBLEInterface::onAdvStatusChanged() { + if (deviceConnected) { + _advDataDirty = true; + } else if (_isEnabled) { + applyAdvertisingData(); + } +} + +void SerialBLEInterface::applyAdvertisingData() { + if (!pAdvertising) { + return; + } + + // Build manufacturer specific data: + // Bytes 0-1: Manufacturer ID (little-endian) + // Bytes 2-3: AdvertisingStatus struct + uint8_t mfr_data[4]; + mfr_data[0] = MESHCORE_MANUFACTURER_ID & 0xFF; // Manufacturer ID low byte + mfr_data[1] = (MESHCORE_MANUFACTURER_ID >> 8) & 0xFF; // Manufacturer ID high byte + mfr_data[2] = _advStatus.unread_count; + mfr_data[3] = _advStatus.battery_voltage; + + // Slave Connection Interval Range (AD type 0x12) + // min=40ms (0x0020), max=80ms (0x0040) + uint8_t conn_interval[] = {0x05, 0x12, 0x20, 0x00, 0x40, 0x00}; + + BLEAdvertisementData advData; + advData.setFlags(ESP_BLE_ADV_FLAG_GEN_DISC | ESP_BLE_ADV_FLAG_BREDR_NOT_SPT); + advData.setCompleteServices(BLEUUID(SERVICE_UUID)); + advData.setManufacturerData(std::string((char*)mfr_data, sizeof(mfr_data))); + advData.addData(std::string((char*)conn_interval, sizeof(conn_interval))); + + pAdvertising->setAdvertisementData(advData); + + BLE_DEBUG_PRINTLN("applyAdvertisingData: unread=%d, battery_voltage=%d", + _advStatus.unread_count, _advStatus.battery_voltage); +} diff --git a/src/helpers/esp32/SerialBLEInterface.h b/src/helpers/esp32/SerialBLEInterface.h index 965e90fd19..9f4d4ae8ae 100644 --- a/src/helpers/esp32/SerialBLEInterface.h +++ b/src/helpers/esp32/SerialBLEInterface.h @@ -5,11 +5,25 @@ #include #include #include +#include + +// Manufacturer ID for MeshCore (using 0xFFFF for development/testing) +// In production, you should register with Bluetooth SIG for a unique ID +#define MESHCORE_MANUFACTURER_ID 0xFFFF + +// Advertising data structure (packed into manufacturer specific data) +// Byte 0: unread message count (0-255) +// Byte 1: battery voltage encoded as (mV - 2500) / 10, range 0-250 (2500-5000 mV), 0xFF = unknown +struct AdvertisingStatus { + uint8_t unread_count; + uint8_t battery_voltage; +}; class SerialBLEInterface : public BaseSerialInterface, BLESecurityCallbacks, BLEServerCallbacks, BLECharacteristicCallbacks { BLEServer *pServer; BLEService *pService; BLECharacteristic * pTxCharacteristic; + BLEAdvertising *pAdvertising; bool deviceConnected; bool oldDeviceConnected; bool _isEnabled; @@ -18,6 +32,10 @@ class SerialBLEInterface : public BaseSerialInterface, BLESecurityCallbacks, BLE unsigned long _last_write; unsigned long adv_restart_time; + // Advertising status data + AdvertisingStatus _advStatus; + bool _advDataDirty; + struct Frame { uint8_t len; uint8_t buf[MAX_FRAME_SIZE]; @@ -52,6 +70,7 @@ class SerialBLEInterface : public BaseSerialInterface, BLESecurityCallbacks, BLE SerialBLEInterface() { pServer = NULL; pService = NULL; + pAdvertising = NULL; deviceConnected = false; oldDeviceConnected = false; adv_restart_time = 0; @@ -59,6 +78,9 @@ class SerialBLEInterface : public BaseSerialInterface, BLESecurityCallbacks, BLE _last_write = 0; last_conn_id = 0; send_queue_len = recv_queue_len = 0; + _advStatus.unread_count = 0; + _advStatus.battery_voltage = 0xFF; // unknown + _advDataDirty = false; } /** @@ -79,6 +101,13 @@ class SerialBLEInterface : public BaseSerialInterface, BLESecurityCallbacks, BLE bool isWriteBusy() const override; size_t writeFrame(const uint8_t src[], size_t len) override; size_t checkRecvFrame(uint8_t dest[]) override; + + void setUnreadCount(uint8_t count) override; + void setBatteryMilliVolts(uint16_t millivolts) override; + +private: + void onAdvStatusChanged(); + void applyAdvertisingData(); }; #if BLE_DEBUG_LOGGING && ARDUINO