# 🧯 Handle Disconnects and Common BLE Pitfalls

## Learning Objectives

By the end of this lesson you will be able to:

- Explain what happens inside the ESP32 when a phone disconnects, and why the gadget can vanish from the scan list.
- Restart BLE advertising safely after every disconnect.
- Keep the gadget responsive by moving slow work out of BLE callbacks.
- Recognize and fix the classic beginner BLE bugs: flooding notifications, oversized payloads, and stale phone caches.

## Quick Review

In the last lesson you completed the full two-way loop from your phone: you scanned, connected, subscribed to notifications, watched values arrive, and wrote a command back to the ESP32, matching every phone-side event to a line in the Serial Monitor.

That working loop is your baseline. Today we cover what happens when that loop *breaks*, and how to make it recover on its own.

## Theory

### A BLE connection is a temporary rental

Your ESP32 is a **peripheral**: it advertises, a phone (**central**) connects, and the two exchange data until one side ends the link. When the link ends, the ESP32 does **not** automatically go back to advertising. The radio stack simply stops being discoverable, so your phone scans and finds nothing.

This confuses almost every beginner: the gadget worked, you walked away, and now it is "dead." It is not dead. It is invisible.

### The disconnect lifecycle

```mermaid
flowchart TD
    Boot["🔌<br/>Boot"] --> Adv["📡<br/>Advertising"]
    Adv -->|phone connects| Conn["🔗<br/>Connected"]
    Conn -->|phone leaves, out of range, app closed| Disc["✂️<br/>Disconnected"]
    Disc -->|startAdvertising from loop| Adv
    Disc -->|forgot to restart| Silent["🚫<br/>Invisible to phone"]
```

The fix is one call, `BLEDevice::startAdvertising()`, but *where* you call it matters.

### Keep callbacks short

`onConnect()` and `onDisconnect()` run inside the BLE stack task, not your `loop()`. Blocking there (a `delay()`, a slow `Serial` flood, a sensor read that takes 300 ms) stalls the stack, which can cause supervision timeouts or a watchdog reboot.

> [!IMPORTANT]
> The safe pattern is: the callback sets a **flag**, and `loop()` reads that flag and does the real work. Callbacks should finish in microseconds.

### Pitfalls that actually bite beginners

| Symptom | Usual cause | Fix |
| --- | --- | --- |
| Gadget disappears after first disconnect | Advertising never restarted | Set a flag in `onDisconnect`, call `BLEDevice::startAdvertising()` in `loop()` |
| Random reboots while connected | `delay()` or heavy work in callbacks or the main loop | Use `millis()` timing, no long blocking |
| `notify()` returns false | Not connected, phone not subscribed, or send queue full | Gate on connection and `getSubscribedCount()`, throttle the rate |
| Values cut off or garbled | Payload bigger than the default ATT size | Keep data at 20 bytes or less, or raise the MTU |
| Phone "cannot find service" after a firmware change | Phone caches the old GATT table | Forget the device in Bluetooth settings, reconnect |
| Device name missing from the scan list | Name too long for the 31-byte advertising packet | Shorten the name |

Two details worth memorizing:

- The default ATT payload is **23 bytes**, with **20 usable** for your data. Anything larger needs MTU negotiation (`BLEDevice::setMTU(185)` on the server side, plus a phone that asks for it).
- Disconnect reason `0x08` means **supervision timeout** (the phone stopped answering, often out of range), and `0x13` means the phone closed the connection on purpose.

## Worked Examples

### Example 1: Restart advertising the safe way

```cpp
#include <BLEDevice.h>
#include <BLEServer.h>

volatile bool clientConnected = false;
volatile bool needsRestart = false;

class ServerCallbacks : public BLEServerCallbacks {
  void onConnect(BLEServer* server) override {
    clientConnected = true;
    Serial.println("Phone connected");
  }
  void onDisconnect(BLEServer* server) override {
    clientConnected = false;
    needsRestart = true;            // flag only, no blocking calls
    Serial.println("Phone disconnected");
  }
};

void loop() {
  if (needsRestart) {
    needsRestart = false;
    delay(200);                     // let the stack release the link
    BLEDevice::startAdvertising();  // visible again
    Serial.println("Advertising restarted");
  }
  // sensor work here, no long delay()
}
```

The 200 ms settle delay is outside the callback, so it cannot stall the stack.

### Example 2: Gate and pace your notifications

```cpp
const unsigned long NOTIFY_PERIOD_MS = 200;   // 5 notifications per second
unsigned long lastNotify = 0;

void loop() {
  if (clientConnected && sensorChar->getSubscribedCount() > 0 &&
      millis() - lastNotify >= NOTIFY_PERIOD_MS) {
    lastNotify = millis();
    char payload[20];
    snprintf(payload, sizeof(payload), "%.1f", readTemperature());
    sensorChar->setValue((uint8_t*)payload, strlen(payload));
    if (!sensorChar->notify()) {
      Serial.println("notify failed, will retry");
    }
  }
}
```

Three guards in one place: connected, subscribed, and paced.

### Example 3: Diagnose "my phone can't see it"

<details><summary>Click to reveal the checklist</summary>

1. Is the Serial Monitor printing "Advertising restarted"? If not, the restart never ran.
2. Did you change any UUID since the last successful test? Then forget the device on the phone and reconnect.
3. Is another phone still holding the connection? Check `BLEDevice::getServer()->getConnectedCount()`.
4. Is the device name longer than about 29 characters? Shorten it and retest.
5. Does the board reboot when you connect? Look for a watchdog message, which points to blocking code.

</details>

## Discussion and Reflection

- Think about your own gadget's demo. If the phone disconnects mid-demo, how many seconds until it is discoverable again, and is that acceptable?
- Which parts of your current `loop()` could block for more than 100 ms? What would you replace them with?
- Why is it safer to check `getSubscribedCount()` than to assume the phone still wants notifications?

## Expert Corner

- **Rule of thumb:** after every disconnect, assume nothing survives. Connection, subscription state, and MTU all reset. Re-check each one.
- Never call `BLEDevice::deinit()` or re-run `BLEDevice::init()` to "fix" a disconnect. Restarting advertising is enough and far safer.
- Log the disconnect every time during development. A single line with the reason code saves hours of guessing.
- When you change the GATT layout, bump your testing habit: forget the device on the phone *before* you flash, not after you get confused.
- Keep the device name short and stable. Renaming it every build creates phantom entries in the phone's cache.

## Summary and Next Steps

You now know the core recovery rules: the ESP32 does not re-advertise on its own, callbacks must stay short, and notifications need connection, subscription, and pacing guards. Reconnect the mental model to last lesson's working loop and the gadget becomes something you can trust on a table in front of an audience.

Quick check with these:

```cloze
After the phone disconnects, the ESP32 stops advertising, so you must call {{c1::BLEDevice::startAdvertising()}} to become discoverable again.
BLE callbacks run in the BLE stack task, so keep them short and set a {{c2::flag}} that `loop()` acts on.
A default BLE notification carries at most {{c3::20}} usable bytes of your own data.
The ESP32 server callback that fires when the link is lost is {{c4::onDisconnect}}.
If the phone still shows old services after you changed a {{c5::UUID}}, clear the phone's GATT cache by forgetting the device.
```

```flashcard
[
  { "front": "What must you call after a disconnect so the phone can find the gadget again?", "back": "BLEDevice::startAdvertising()." },
  { "front": "Which server callback fires when the phone drops the link?", "back": "onDisconnect(BLEServer* server)." },
  { "front": "Why avoid delay() and long Serial prints inside BLE callbacks?", "back": "They block the BLE stack task and can cause supervision timeouts or watchdog reboots." },
  { "front": "Where should the advertising restart actually run?", "back": "In loop(), triggered by a flag that onDisconnect sets." },
  { "front": "What does notify() return, and what does false mean?", "back": "A bool; false means the notification was not queued (not connected, not subscribed, or queue full)." },
  { "front": "How do you check that the phone has subscribed to notifications?", "back": "BLECharacteristic::getSubscribedCount() > 0." },
  { "front": "Default ATT payload size before MTU negotiation?", "back": "23 bytes total, 20 usable for your data." },
  { "front": "Why throttle your notification rate?", "back": "The connection interval and phone queue limit throughput; flooding drops packets or the link." },
  { "front": "Phone shows old services after a firmware UUID change. First fix?", "back": "Forget the device in Bluetooth settings, then reconnect." },
  { "front": "What does disconnect reason 0x08 usually mean?", "back": "Supervision timeout, typically the phone went out of range or stopped responding." },
  { "front": "Why does a long device name sometimes not appear in scans?", "back": "The advertising packet is limited to 31 bytes, so long names get truncated or dropped." },
  { "front": "What must the phone do again after reconnecting?", "back": "Re-subscribe to the notification characteristic." }
]
```

Next up is the capstone, where you turn this reliable loop into a finished demo: a stable gadget, a clean phone-side walkthrough, and a short presentation of what it does.
