ESP32/Bluetooth/62. A value a phone can write
Your chip
Your language
Bluetooth · 62 of 81

A value a phone can write

A characteristic the phone can write to, and the twenty bytes that actually arrive when you send thirty. The link drops the rest and reports success.

/esp32/ble-write-from-a-phone · arduino · S3

Twenty bytes, and no error

onWrite(), one packet
fits
Length of what you send14 bytes
Sent
14 B
Received
14 B
Error reported
none
Everything arrived. A write lands in onWrite() as bytes, not as a string — getValue() hands you a buffer, and it is your job to decide whether "on" and "ON" and "1" all mean the same thing.

BLE's default ATT payload is 23 bytes, three of which are header. Send more than twenty and the extra never leaves the phone — the write completes, onWrite fires, and your callback receives a truncated command it has no way to know was truncated.

Two ways out. Negotiate a bigger MTU (BLEDevice::setMTU(517), and the client has to agree, which iOS and Android both will) — or keep the protocol small enough that it never comes up. Short commands and a separate notify for results is the design that never has this problem.

The callback is not loop()

onWrite runs on the BLE stack's own task, with a small stack and a deadline. Anything slow there — a Serial.print at 9600, an HTTP request, a delay — either blocks the radio or crashes it.

Take a copy of the bytes, set a flag, return. The pattern is exactly the one an interrupt handler uses, and for exactly the same reason.

Bytes, not strings

getValue() returns a buffer and a length. It is worth working in those terms rather than converting to a String immediately, because the length is the one piece of information that tells you the truncation above happened.

It also forces the question a command protocol needs answered anyway: is on the same as ON the same as 1? Decide once, in one place, rather than in three if statements that disagree.

Nothing here is authenticated

A writable characteristic on an advertising board is a control anybody within range can operate. BLE has pairing and bonding for this and it is genuinely fiddly. If the write turns on a lamp, ignore the problem. If it opens a door, do not.

On your S3
ChipXtensa LX7 · 2 × 240 MHz
Board settingESP32S3 Dev Module
Default I2CSDA 8 · SCL 9
Watch out forThe port vanishes after upload

The code

The callback fires on the BLE task, not in loop. Treat it the way you would an interrupt — take a copy, set a flag, and do the work somewhere else.

writable.ino
#include <BLEDevice.h>

#define SVC "6e400001-b5a3-f393-e0a9-e50e24dcca9e"
#define CHR "6e400002-b5a3-f393-e0a9-e50e24dcca9e"

volatile bool haveCmd = false;
char cmd[64];

class OnWrite : public BLECharacteristicCallbacks {
  void onWrite(BLECharacteristic *c) override {
    std::string v = c->getValue();
    size_t n = min(v.size(), sizeof(cmd) - 1);
    memcpy(cmd, v.data(), n);
    cmd[n] = 0;
    haveCmd = true;                       // the work happens in loop()
  }
};

void setup() {
  Serial.begin(115200);
  pinMode(2, OUTPUT);
  BLEDevice::init("tinkerblock-ctl");
  BLEServer *s = BLEDevice::createServer();
  BLEService *svc = s->createService(SVC);
  svc->createCharacteristic(CHR,
    BLECharacteristic::PROPERTY_WRITE | BLECharacteristic::PROPERTY_WRITE_NR)
    ->setCallbacks(new OnWrite());
  svc->start();
  s->getAdvertising()->start();
}

void loop() {
  if (!haveCmd) return;
  haveCmd = false;
  Serial.printf("got %u bytes: %s\n", strlen(cmd), cmd);
  if (!strcmp(cmd, "on"))  digitalWrite(2, HIGH);
  if (!strcmp(cmd, "off")) digitalWrite(2, LOW);
}

getValue() hands you bytes with a length. Comparing them against a String works and hides the length, which is exactly the information you need when a command arrives truncated.

When it does not work

Long strings arrive cut off

The default ATT payload is twenty bytes, and anything past that is dropped by the link rather than by your code. onWrite still fires and the write is reported as successful. Negotiate a larger MTU on both ends, or keep commands short.

The board crashes inside onWrite

The callback runs on the BLE stack's task with a small stack, and anything slow or blocking there will fault. Copy the bytes, set a flag, and act in loop.

Writes work from nRF Connect and not from my app

Two write types exist. WRITE expects a response, WRITE_NR does not; a client using one against a characteristic that only offers the other fails silently. Declare both properties and neither client is wrong.

The value written earlier comes back on a read

The characteristic stores the last value written, so a read after a write returns it. If that is confusing, do not add PROPERTY_READ to a command characteristic — it has no meaning here.

Where this goes next

The older, heavier radio, and the reason it is still worth knowing about: it is the one that gives you a plain serial port to a laptop.

Classic Bluetooth serial

Edit this page — content/esp32/ble-write-from-a-phone.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.

Browse ESP32 on the forum