fix(ble): reliably expose and update BLE battery level (BAS) (#10622)

* fix(ble): reliably expose and update BLE battery level (BAS)

The Battery Service (0x180F / 0x2A19) is now wired up per the Bluetooth
BAS spec: the Battery Level characteristic always holds a valid 0-100
value and is pushed on change.

- NimBLE: seed an initial level at setup and cache the value on every
  update so a READ returns the current level even while disconnected;
  only notify when a client is connected.
- Power: mirror the battery level to the Battery Service from
  readPowerStatus() on change, so it updates independent of GPS/position
  events (previously the only push path was MeshService).

Also fixes two regressions the above would otherwise introduce:

- NimBLE use-after-free: BLEDevice::deinit(true) frees the GATT objects
  but left the global BatteryCharacteristic dangling. Several AdminModule
  paths (e.g. serial-config entry) deinit BLE while config.bluetooth.enabled
  stays true, so the periodic push would deref freed memory. Null the
  pointer in deinit().
- NRF52: guard blebas.write() on the nrf52Bluetooth instance so the new
  periodic push can't call it before the Battery Service is begun in setup().

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(ble): clamp BAS battery level to 0-100 and skip redundant updates

Address review feedback on the Battery Level characteristic (0x2A19):

- Clamp the value to the BAS-mandated 0-100 range at the platform write
  boundary (NimBLE seed + update, NRF52 update), so a misbehaving battery
  backend can't put an out-of-range value on the characteristic.
- Skip the write/notify when the level is unchanged, so repeated callers
  (e.g. MeshService refresh paths) don't emit redundant notifications.
- Simplify Power.cpp to a direct guarded call now that clamping and
  de-duplication live at the boundary, which also removes the implicit
  int->uint8_t narrowing.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-authored-by: Ben Meadors <benmmeadors@gmail.com>
This commit is contained in:
James Rich
2026-06-09 18:58:26 -05:00
committed by GitHub
co-authored by Claude Opus 4.8 Ben Meadors
parent e028663658
commit 6b3f975ba5
3 changed files with 38 additions and 5 deletions
+12 -2
View File
@@ -15,8 +15,9 @@ static BLECharacteristic fromRadio = BLECharacteristic(BLEUuid(FROMRADIO_UUID_16
static BLECharacteristic toRadio = BLECharacteristic(BLEUuid(TORADIO_UUID_16));
static BLECharacteristic logRadio = BLECharacteristic(BLEUuid(LOGRADIO_UUID_16));
static BLEDis bledis; // DIS (Device Information Service) helper class instance
static BLEBas blebas; // BAS (Battery Service) helper class instance
static BLEDis bledis; // DIS (Device Information Service) helper class instance
static BLEBas blebas; // BAS (Battery Service) helper class instance
static int lastBatteryLevel = -1; // last value written to BAS, to skip redundant writes/notifies
#ifndef BLE_DFU_SECURE
static BLEDfu bledfu; // DFU software update helper service
#else
@@ -336,6 +337,7 @@ void NRF52Bluetooth::setup()
LOG_INFO("Init the Battery Service");
blebas.begin();
blebas.write(0); // Unknown battery level for now
lastBatteryLevel = 0;
// Setup the Heart Rate Monitor service using
// BLEService and BLECharacteristic classes
LOG_INFO("Init the Mesh bluetooth service");
@@ -355,6 +357,14 @@ void NRF52Bluetooth::resumeAdvertising()
/// Given a level between 0-100, update the BLE attribute
void updateBatteryLevel(uint8_t level)
{
if (!nrf52Bluetooth) // skip until the Battery Service has been begun in setup()
return;
if (level > 100) // BAS battery level must stay within 0-100
level = 100;
if (level == lastBatteryLevel)
return;
lastBatteryLevel = level;
blebas.write(level);
}
void NRF52Bluetooth::clearBonds()