* 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>