A value a phone can read
A value a phone can read, and then the one extra property that stops the phone having to ask. The difference is invisible in the sketch and enormous on a battery.
Read is polite and expensive
A read is the phone asking. It has no way to know whether anything changed, so it asks on a timer — and every one of those exchanges wakes your radio whether or not there was news.
A notify is the board telling. Same characteristic, same value, one extra property and one descriptor; the phone subscribes once and then hears from you only when something moves.
For a sensor that changes every few seconds and a phone that wants to watch it, that is the difference between a coin cell lasting a week and lasting a year.
Which to use
Read for values that are looked up rather than watched: a serial number, a firmware version, a calibration constant. There is no reason to push those.
Notify for anything that changes and anyone is watching. Fire-and-forget — if the packet is lost, the next one carries the newer value anyway, which for a temperature is exactly right.
Indicate is notify with an acknowledgement, at the cost of a round trip per message. Use it when losing one update actually matters, which for sensor data it usually does not.
Send the standard shape when there is one
The SIG has assigned UUIDs for common measurements, and using them means a
generic app displays your value correctly with no configuration. Temperature is
0x2A6E: a signed 16-bit integer in hundredths of a degree, little-endian.
Sending 21.4 as five ASCII characters works, and every scanner on earth will
show it as five bytes of nothing.
The code
One service, one characteristic, both readable and notifying. The BLE2902 descriptor is what gives the phone somewhere to switch notifications on.
#include <BLEDevice.h>
#include <BLE2902.h>
#define SVC "0000181a-0000-1000-8000-00805f9b34fb" // environmental sensing
#define CHR "00002a6e-0000-1000-8000-00805f9b34fb" // temperature
BLECharacteristic *temp;
bool connected = false;
class Conn : public BLEServerCallbacks {
void onConnect(BLEServer *) override { connected = true; }
void onDisconnect(BLEServer *s) override { connected = false; s->startAdvertising(); }
};
void setup() {
Serial.begin(115200);
BLEDevice::init("tinkerblock-temp");
BLEServer *server = BLEDevice::createServer();
server->setCallbacks(new Conn());
BLEService *svc = server->createService(SVC);
temp = svc->createCharacteristic(
CHR, BLECharacteristic::PROPERTY_READ | BLECharacteristic::PROPERTY_NOTIFY);
temp->addDescriptor(new BLE2902()); // without this, no subscribe
svc->start();
server->getAdvertising()->start();
}
void loop() {
static int16_t last = 0;
int16_t c = 2140; // 21.40 °C, in hundredths
if (connected && c != last) {
temp->setValue((uint8_t *)&c, 2);
temp->notify(); // only when it moved
last = c;
}
delay(1000);
}Generate your own UUIDs rather than copying these. Two projects sharing a UUID is not fatal, but it makes every scanner on the bench show two devices claiming to be the same thing.
aioble is the readable way to do this in MicroPython. The write with notify=True is the same decision as calling notify() above.
import aioble, bluetooth, asyncio, struct
SVC = bluetooth.UUID(0x181A)
CHR = bluetooth.UUID(0x2A6E)
service = aioble.Service(SVC)
temp = aioble.Characteristic(service, CHR, read=True, notify=True)
aioble.register_services(service)
async def main():
last = None
asyncio.create_task(advertise())
while True:
c = 2140 # 21.40 °C
if c != last:
temp.write(struct.pack("<h", c), send_update=True)
last = c
await asyncio.sleep(1)
async def advertise():
while True:
async with await aioble.advertise(250_000, name="tinkerblock-temp",
services=[SVC]):
pass
asyncio.run(main())aioble is not in every firmware build. If the import fails, install it with mip, or fall back to the lower-level bluetooth module and considerably more code.
When it does not work
The BLE2902 descriptor is missing. Without it there is no client characteristic configuration to write to, so nRF Connect shows no subscribe arrow and notify() sends to nobody.
setValue must be called before notify, every time. notify() transmits whatever is currently stored — calling it after updating a local variable but not the characteristic sends the old bytes.
Advertising stops when a client connects and does not restart on its own. Call startAdvertising() from the disconnect callback, which is what the Conn class above is for.
A characteristic is bytes, not a string. A standard temperature characteristic is a signed 16-bit value in hundredths of a degree, little-endian; sending "21.4" as text is valid BLE and a generic app will show it as garbage.
The other direction, and the twenty bytes that arrive when you send thirty. Nothing reports the loss.
A value a phone can write →Edit this page — content/esp32/ble-read-and-notify.mdx
Discuss this article
Ask about this page. The answer stays here, on the page it belongs to, for whoever hits the same wall next.