diff --git a/assets/images/PN2222ATransistor.png b/assets/images/PN2222ATransistor.png new file mode 100644 index 00000000..653a7a11 Binary files /dev/null and b/assets/images/PN2222ATransistor.png differ diff --git a/assets/images/PanelMountPotentiometer_NoCap.png b/assets/images/PanelMountPotentiometer_NoCap.png new file mode 100644 index 00000000..afb17d5b Binary files /dev/null and b/assets/images/PanelMountPotentiometer_NoCap.png differ diff --git a/assets/images/PanelMountPotentiometer_NoCap_150h.jpg b/assets/images/PanelMountPotentiometer_NoCap_150h.jpg new file mode 100644 index 00000000..82ec90a0 Binary files /dev/null and b/assets/images/PanelMountPotentiometer_NoCap_150h.jpg differ diff --git a/esp32/analog-input.md b/esp32/analog-input.md index 703118e5..0f60222f 100644 --- a/esp32/analog-input.md +++ b/esp32/analog-input.md @@ -3,7 +3,8 @@ layout: default title: L4: Analog input image: /esp32/assets/og/analog-input.jpg description: "Read a potentiometer on the ESP32 with analogRead() and build a knob-controlled LED dimmer. Explore the 12-bit ADC (0-4095), map() ranges, and which ADC pins to use." -parent: ESP32 +parent: Fundamentals +grand_parent: ESP32 has_toc: true # (on by default) usemathjax: true comments: true diff --git a/esp32/ble-bidirectional.md b/esp32/ble-bidirectional.md new file mode 100644 index 00000000..2bfa2823 --- /dev/null +++ b/esp32/ble-bidirectional.md @@ -0,0 +1,754 @@ +--- +layout: default +title: L5: Bidirectional BLE +description: "Control ESP32 hardware wirelessly from a phone or web browser over BLE: write characteristics to set NeoPixel color, stream sensor data back, and exchange text with the Nordic UART Service." +parent: Wireless +grand_parent: ESP32 +has_toc: true # (on by default) +usemathjax: false +comments: true +usetocbot: true +nav_order: 5 +--- +# {{ page.title | replace_first:'L','Lesson ' }} +{: .no_toc } + +## Table of Contents +{: .no_toc .text-delta } + +1. TOC +{:toc} +--- + +{: .warning } +> This lesson is in draft form. There are missing circuit diagrams, images, videos, and other content. + + + + + +In the [last lesson](ble-intro.md), you learned the fundamentals of BLE: the peripheral/central model, the GATT data hierarchy, and how to stream sensor data from the ESP32 to your phone and computer using notifications. Data flowed in one direction — *from* the ESP32 *to* the central. + +In this lesson, we'll close the loop. You'll learn how to send data in the *other* direction — from a phone or web browser *to* the ESP32 — to control hardware wirelessly. Along the way, you'll encounter BLE's **callback model** for handling incoming writes, build a **Web Bluetooth** interface that runs entirely in the browser, and learn about the **Nordic UART Service (NUS)** for serial-like text communication over BLE. + +{: .note } +> **In this lesson, you will learn:** +> - How to create a **writable** BLE characteristic and handle incoming data with callbacks +> - The difference between BLE's callback model and the `Serial.available()` polling pattern +> - How to combine readable, writable, and notify characteristics in a single service for bidirectional communication +> - How to build a **Web Bluetooth** web page that connects to the ESP32 from a browser — paralleling the [Web Serial](../communication/web-serial.md) approach but wireless +> - The **Nordic UART Service (NUS)** — a widely adopted convention for serial-like text communication over BLE + +{: .note } +> **Prerequisites:** This lesson builds directly on [Lesson 4: Introduction to BLE](ble-intro.md). You should be comfortable with BLE concepts (peripherals, centrals, GATT, services, characteristics, UUIDs, notifications) and have successfully completed Parts 1 and 2 from that lesson. + +## Part 1: Controlling the NeoPixel over BLE + +So far, data has flowed in one direction: from the ESP32 to the central. Now let's go the other direction — send data *from* your phone *to* the ESP32 to control hardware. In this part, you'll create a **writable** characteristic that accepts RGB color values and sets the onboard NeoPixel. You'll also learn the BLE **callback model** for handling incoming writes, which is fundamentally different from the `Serial.available()` polling pattern. + +The ESP32-S3 Feather has a built-in NeoPixel (WS2812B) RGB LED on `PIN_NEOPIXEL`, powered by `NEOPIXEL_POWER`. We used it in [Lesson 2: Blink](led-blink.md) and [Lesson 3: LED Fading](led-fade.md), so the NeoPixel setup should be familiar. + +### The Arduino code + + + +We'll extend the [Lesson 4](ble-intro.md) sensor streaming sketch to add a second characteristic for LED control—so the ESP32 simultaneously streams sensor data *and* accepts LED commands. This is the same bidirectional pattern from [Lesson 3, Part 5](bluetooth-web-serial.md#part-5-bidirectional-control--wireless-color-mixer), but over BLE with structured characteristics instead of a serial byte stream. The full source is available in our [Arduino GitHub repo](https://github.com/makeabilitylab/arduino/tree/master/ESP32/Bluetooth/BLENeoPixelControl). + +```cpp +/** + * BLENeoPixelControl: bidirectional BLE communication. + * Streams potentiometer data via notifications (peripheral → central) + * AND accepts RGB color commands via a writable characteristic + * (central → peripheral) to control the onboard NeoPixel. + * + * Circuit: + * - 10kΩ potentiometer on A5 (GPIO 8, ADC1) + * - Onboard NeoPixel (no external wiring needed) + * + * Works on: ESP32-S3 Feather (for the onboard NeoPixel). + * On the Huzzah32, substitute an external NeoPixel or LED. + * + * See: https://makeabilitylab.github.io/physcomp/esp32/ble + * + * By Jon E. Froehlich + * @jonfroehlich + * http://makeabilitylab.io + */ + +#include +#include +#include +#include +#include + +// Custom UUIDs +#define SERVICE_UUID "4fafc201-1fb5-459e-8fcc-c5c9c331914b" +#define SENSOR_CHAR_UUID "beb5483e-36e1-4688-b7f5-ea07361b26a8" +#define LED_CHAR_UUID "a3c87500-8ed3-4bdf-8a39-a01bebede295" + +const int POT_INPUT_PIN = A5; + +// NeoPixel setup — one pixel on the onboard NeoPixel pin +Adafruit_NeoPixel _pixel(1, PIN_NEOPIXEL, NEO_GRB + NEO_KHZ800); + +BLEServer* _pServer = NULL; +BLECharacteristic* _pSensorCharacteristic = NULL; +BLECharacteristic* _pLedCharacteristic = NULL; +bool _deviceConnected = false; + +unsigned long _lastSensorReadMs = 0; +const unsigned long SENSOR_READ_INTERVAL_MS = 100; + +class MyServerCallbacks : public BLEServerCallbacks { + void onConnect(BLEServer* pServer) { + _deviceConnected = true; + Serial.println("Central connected!"); + } + + void onDisconnect(BLEServer* pServer) { + _deviceConnected = false; + Serial.println("Central disconnected. Restarting advertising..."); + pServer->getAdvertising()->start(); + } +}; + +// Callback for when the central writes to the LED characteristic +class LedCallbacks : public BLECharacteristicCallbacks { + void onWrite(BLECharacteristic* pCharacteristic) { + String value = pCharacteristic->getValue(); + + if (value.length() >= 3) { + // Interpret the first 3 bytes as R, G, B + uint8_t r = (uint8_t)value[0]; + uint8_t g = (uint8_t)value[1]; + uint8_t b = (uint8_t)value[2]; + + Serial.print("Received RGB: "); + Serial.print(r); Serial.print(", "); + Serial.print(g); Serial.print(", "); + Serial.println(b); + + _pixel.setPixelColor(0, _pixel.Color(r, g, b)); + _pixel.show(); + } else { + Serial.print("Received write with "); + Serial.print(value.length()); + Serial.println(" bytes (expected 3 for RGB)."); + } + } +}; + +void setup() { + Serial.begin(115200); + Serial.println("Starting BLE NeoPixel Control..."); + + // Initialize NeoPixel + #if defined(NEOPIXEL_POWER) + pinMode(NEOPIXEL_POWER, OUTPUT); + digitalWrite(NEOPIXEL_POWER, HIGH); + #endif + _pixel.begin(); + _pixel.setBrightness(30); // keep it dim to avoid blinding you + _pixel.show(); // turn off (all zeros) + + // Initialize BLE + BLEDevice::init("ESP32-BLE-NeoPixel"); + _pServer = BLEDevice::createServer(); + _pServer->setCallbacks(new MyServerCallbacks()); + + BLEService* pService = _pServer->createService(SERVICE_UUID); + + // Sensor characteristic (Read + Notify) — streams potentiometer data + _pSensorCharacteristic = pService->createCharacteristic( + SENSOR_CHAR_UUID, + BLECharacteristic::PROPERTY_READ | + BLECharacteristic::PROPERTY_NOTIFY + ); + _pSensorCharacteristic->addDescriptor(new BLE2902()); + + // LED characteristic (Read + Write) — receives RGB color commands + _pLedCharacteristic = pService->createCharacteristic( + LED_CHAR_UUID, + BLECharacteristic::PROPERTY_READ | + BLECharacteristic::PROPERTY_WRITE + ); + _pLedCharacteristic->setCallbacks(new LedCallbacks()); + + // Start service and advertising + pService->start(); + BLEAdvertising* pAdvertising = BLEDevice::getAdvertising(); + pAdvertising->addServiceUUID(SERVICE_UUID); + pAdvertising->setScanResponse(true); + pAdvertising->start(); + + Serial.println("BLE server advertising. Connect with nRF Connect!"); +} + +void loop() { + unsigned long now = millis(); + + if (now - _lastSensorReadMs >= SENSOR_READ_INTERVAL_MS) { + _lastSensorReadMs = now; + + int potVal = analogRead(POT_INPUT_PIN); + Serial.print("Pot:"); + Serial.println(potVal); + + if (_deviceConnected) { + String valStr = String(potVal); + _pSensorCharacteristic->setValue(valStr.c_str()); + _pSensorCharacteristic->notify(); + } + } +} +``` + +The key new element is the `LedCallbacks` class. When the central writes to the LED characteristic, `onWrite()` fires automatically. We interpret the first three bytes of the written value as R, G, B and set the NeoPixel color accordingly. + +{: .highlight } +> **Callbacks vs. polling:** Notice the pattern: we don't poll for incoming data in `loop()` (like we do with `Serial.available()` or `SerialBT.available()` in [Lessons 2–3](bluetooth-serial.md)). Instead, BLE uses a **callback model**—the library calls our `onWrite()` function when data arrives. This is fundamentally different from the serial polling pattern you're used to, and it's one of the biggest code-level differences between Bluetooth Classic and BLE. + +### Try it out from your computer (Python) + +Here's a quick Python script that writes RGB values to the NeoPixel characteristic: + +```python +""" +ble_neopixel.py: Connects to the ESP32 and sets the NeoPixel color. + +Usage: python3 ble_neopixel.py +Then enter RGB values like: 255 0 128 + +Requires: bleak (pip3 install bleak) + +By Jon E. Froehlich +@jonfroehlich +http://makeabilitylab.io +""" + +import asyncio +from bleak import BleakScanner, BleakClient + +LED_CHAR_UUID = "a3c87500-8ed3-4bdf-8a39-a01bebede295" + +async def main(): + print("Scanning for ESP32-BLE-NeoPixel...") + devices = await BleakScanner.discover(timeout=5.0) + + target = None + for d in devices: + if d.name and "ESP32" in d.name: + target = d + break + + if not target: + print("ESP32 not found.") + return + + async with BleakClient(target.address) as client: + print(f"Connected to {target.name}!") + while True: + rgb = input("Enter R G B (0-255 each, or 'quit'): ") + if rgb.lower() == 'quit': + break + parts = rgb.split() + if len(parts) == 3: + r, g, b = int(parts[0]), int(parts[1]), int(parts[2]) + await client.write_gatt_char(LED_CHAR_UUID, bytes([r, g, b])) + print(f" Sent RGB: ({r}, {g}, {b})") + +asyncio.run(main()) +``` + +### Try it out from your phone (iPhone and Android) + +1. Upload the sketch. The NeoPixel should be off initially. +2. Open **nRF Connect** on your **iPhone or Android phone**. Scan and connect to `"ESP32-BLE-NeoPixel"`. +3. Expand the service. You should see **two** characteristics now. +4. Find the LED characteristic (the one with `a3c87500...` UUID). +5. Tap the **write arrow** (↑). In the write dialog, select **ByteArray** as the type, then enter `FF0000` (red), `00FF00` (green), or `0000FF` (blue). Tap **Send**. +6. Watch the NeoPixel change color! 🌈 + + + +{: .note } +> **nRF Connect write format:** When writing raw bytes in nRF Connect, select "ByteArray" (not "Text") and enter hex values without spaces or `0x` prefixes. `FF0000` = red, `00FF00` = green, `0000FF` = blue, `FF00FF` = magenta, `FFFFFF` = white. Each pair of hex digits is one byte (0–255). + +### Workbench demo + + + +## Part 2: Web Bluetooth + +So far we've used nRF Connect as our BLE central — it's great for debugging, but it doesn't give us a custom UI. What if you could control the NeoPixel from a **web page** with sliders and a color picker? What if you could plot sensor data in a live chart — all in the browser, all wireless? In this part, you'll build a single-page HTML/JavaScript app using the [Web Bluetooth API](https://developer.mozilla.org/en-US/docs/Web/API/Web_Bluetooth_API) that connects to your ESP32, subscribes to sensor notifications, and writes RGB values to the NeoPixel — paralleling the [Web Serial](../communication/web-serial.md) approach from the Communication module but over BLE. + +### Web Serial vs. Web Bluetooth + +| | Web Serial ([L2](../communication/web-serial.md)) | Web Bluetooth (this section) | +|---|---|---| +| Browser API | `navigator.serial` | `navigator.bluetooth` | +| Connect | `port.open({ baudRate })` | `device.gatt.connect()` | +| Send data | `writer.write(bytes)` | `characteristic.writeValue(bytes)` | +| Receive data | Read from stream | Subscribe to notifications | +| User gesture | Required to open port | Required to pair | +| Security | No HTTPS required | **Requires HTTPS** (or localhost) | +| Chrome/Edge | ✅ | ✅ | +| Firefox | ❌ | ⚠️ (behind flag) | +| Safari / iOS | ❌ | ❌ | +| Android Chrome | ✅ | ✅ | + +**Table.** Web Serial and Web Bluetooth have strikingly parallel structures. The main differences: Web Bluetooth requires HTTPS (or localhost), uses structured characteristics instead of raw byte streams, and is supported on Android but not iOS. +{: .fs-1 } + +{: .warning } +> **Web Bluetooth requires HTTPS or localhost.** It will not work from a `file://` URL. Use a local development server (VS Code's [Live Server](https://marketplace.visualstudio.com/items?itemName=ritwickdey.LiveServer) extension, or `python3 -m http.server`) or host your page on [GitHub Pages](https://pages.github.com/). Web Bluetooth works in **Chrome and Edge** on desktop and Android, but **not on iOS**—Apple's Safari (and iOS Chrome, which uses WebKit) does not support Web Bluetooth. For iOS users, nRF Connect provides similar functionality. + +### The web page + +We'll build a single HTML page (vanilla JavaScript, no frameworks—matching the style of the [Web Serial lesson](../communication/web-serial.md)) that: + +1. Connects to the ESP32's BLE service +2. Subscribes to potentiometer notifications and displays the live value +3. Has three sliders (R, G, B) that write to the LED characteristic to control the NeoPixel + +Make sure the Part 1 sketch is running on your ESP32 before testing this page. + +```html + + + + + + ESP32 BLE NeoPixel Controller + + + +

ESP32 BLE NeoPixel Controller

+ + +
Not connected
+ +
+

Sensor Data

+
+

Potentiometer reading (0–4095)

+ +

NeoPixel Color

+
+ + + 0 +
+
+ + + 0 +
+
+ + + 0 +
+
+
+ + + + +``` + +Let's walk through the JavaScript, step by step: + +**Step 1: `navigator.bluetooth.requestDevice()`** opens the browser's Bluetooth pairing dialog. We pass a `filters` array that limits the list to devices advertising our service UUID—so only our ESP32 appears. This is the BLE equivalent of `navigator.serial.requestPort()` from the [Web Serial lesson](../communication/web-serial.md). Like Web Serial, this call **requires a user gesture** (a button click)—you can't trigger it automatically on page load. + +**Step 2: `device.gatt.connect()`** establishes a GATT connection. This is analogous to `port.open()` in Web Serial—after this call, we can read and write data. + +**Steps 3–4: Getting the service and characteristic, subscribing to notifications.** We drill down through the GATT hierarchy: server → service → characteristic. Then `sensorChar.startNotifications()` tells the ESP32 we want to receive updates. We listen for `characteristicvaluechanged` events—each event delivers a `DataView` containing the raw bytes. Since our ESP32 sends the potentiometer value as a string, we decode it with `TextDecoder`. + +**Step 5: Getting the LED characteristic.** We store a reference to the LED characteristic so we can write to it later from the slider event handlers. + +**`sendColor()`** reads the three slider values, packs them into a `Uint8Array` of 3 bytes (R, G, B), and writes them to the LED characteristic with `ledCharacteristic.writeValue(data)`. This triggers the `onWrite()` callback on the ESP32, which sets the NeoPixel color. + +{: .note } +> **Spot the structural parallel.** In [Web Serial](../communication/web-serial.md), you write raw bytes to a `WritableStream`. In Web Bluetooth, you write raw bytes to a `BLECharacteristic`. The data format (a `Uint8Array`) is even the same! The key difference is that Web Bluetooth writes go to a *specific, named characteristic*—not a generic byte stream. This structure is what makes BLE self-describing and interoperable. + +### Try it out + +1. Make sure the Part 1 sketch is running on your ESP32. +2. Serve the HTML file from a local server (VS Code Live Server, or `python3 -m http.server`). Open it in Chrome. +3. Click **Connect to ESP32**. The browser shows a pairing dialog—select your ESP32 and click **Pair**. +4. The sensor value should appear and update in real time. +5. Drag the R, G, B sliders—the NeoPixel changes color as you move them! + + + +{: .note } +> **Throttling writes.** If you drag a slider quickly, `sendColor()` fires on every pixel of movement—potentially dozens of times per second. BLE can handle this, but rapid writes may occasionally fail with a "GATT operation already in progress" error. For a more robust implementation, you could debounce the slider input or use `requestAnimationFrame()` to batch writes. For this lesson, occasional errors are harmless. + +### Workbench demo + + + +## Part 3: Nordic UART Service (NUS) + +Throughout this lesson, we've worked directly with custom GATT services and characteristics — the fundamental BLE building blocks. But what if you just want to send text back and forth, like the serial bridge from [Lesson 2](bluetooth-serial.md)? In this part, you'll learn the **Nordic UART Service (NUS)** — a widely adopted convention that emulates serial communication over BLE using two characteristics. NUS bridges the gap between BLE's structured model and the simplicity of serial, and it's supported by most BLE terminal apps out of the box. + +NUS is a widely adopted convention (created by Nordic Semiconductor) that uses two BLE characteristics to emulate serial communication: + +- **RX Characteristic** (`6E400002-B5A3-F393-E0A9-E50E24DCCA9E`): the central *writes* data here to send it to the peripheral (from the peripheral's perspective, this is "received" data—hence "RX"). +- **TX Characteristic** (`6E400003-B5A3-F393-E0A9-E50E24DCCA9E`): the peripheral *notifies* data here to send it to the central (from the peripheral's perspective, this is "transmitted" data—hence "TX"). + +The naming is from the **peripheral's perspective**: RX = data coming *in* to the ESP32, TX = data going *out* from the ESP32. + +{: .note } +> NUS is not an official Bluetooth SIG standard—it's a convention created by Nordic Semiconductor that has become a de facto standard because so many apps support it. Apps like nRF Connect, nRF Toolbox, and many Bluetooth terminal apps automatically recognize the NUS UUIDs and provide a serial terminal interface. + +Here's a simple NUS example: + + + + +```cpp +/** + * BLEUartService: implements the Nordic UART Service (NUS) for + * serial-like text communication over BLE. Type text in nRF Connect's + * UART feature and it appears in Serial Monitor; type in Serial + * Monitor and it is sent over BLE. + * + * Works on: ESP32-S3 Feather, Huzzah32, or any ESP32 with BLE. + * + * See: https://makeabilitylab.github.io/physcomp/esp32/ble + * + * By Jon E. Froehlich + * @jonfroehlich + * http://makeabilitylab.io + */ + +#include +#include +#include +#include + +// Nordic UART Service UUIDs — these are a de facto standard +#define NUS_SERVICE_UUID "6E400001-B5A3-F393-E0A9-E50E24DCCA9E" +#define NUS_RX_CHAR_UUID "6E400002-B5A3-F393-E0A9-E50E24DCCA9E" +#define NUS_TX_CHAR_UUID "6E400003-B5A3-F393-E0A9-E50E24DCCA9E" + +BLEServer* _pServer = NULL; +BLECharacteristic* _pTxCharacteristic = NULL; +bool _deviceConnected = false; + +class MyServerCallbacks : public BLEServerCallbacks { + void onConnect(BLEServer* pServer) { + _deviceConnected = true; + Serial.println("Central connected!"); + } + void onDisconnect(BLEServer* pServer) { + _deviceConnected = false; + Serial.println("Disconnected. Restarting advertising..."); + pServer->getAdvertising()->start(); + } +}; + +// Called when the central writes to the RX characteristic (sending data to us) +class RxCallbacks : public BLECharacteristicCallbacks { + void onWrite(BLECharacteristic* pCharacteristic) { + String rxValue = pCharacteristic->getValue(); + if (rxValue.length() > 0) { + Serial.print("Received via BLE: "); + Serial.println(rxValue.c_str()); + } + } +}; + +void setup() { + Serial.begin(115200); + Serial.println("Starting BLE UART Service..."); + + BLEDevice::init("ESP32-BLE-UART"); + _pServer = BLEDevice::createServer(); + _pServer->setCallbacks(new MyServerCallbacks()); + + BLEService* pService = _pServer->createService(NUS_SERVICE_UUID); + + // TX characteristic — we notify data OUT to the central + _pTxCharacteristic = pService->createCharacteristic( + NUS_TX_CHAR_UUID, + BLECharacteristic::PROPERTY_NOTIFY + ); + _pTxCharacteristic->addDescriptor(new BLE2902()); + + // RX characteristic — the central writes data IN to us + BLECharacteristic* pRxCharacteristic = pService->createCharacteristic( + NUS_RX_CHAR_UUID, + BLECharacteristic::PROPERTY_WRITE + ); + pRxCharacteristic->setCallbacks(new RxCallbacks()); + + pService->start(); + BLEAdvertising* pAdvertising = BLEDevice::getAdvertising(); + pAdvertising->addServiceUUID(NUS_SERVICE_UUID); + pAdvertising->setScanResponse(true); + pAdvertising->start(); + + Serial.println("BLE UART ready. Connect with nRF Connect → UART."); +} + +void loop() { + // Forward USB Serial → BLE (via TX characteristic) + if (_deviceConnected && Serial.available()) { + String msg = Serial.readStringUntil('\n'); + _pTxCharacteristic->setValue(msg.c_str()); + _pTxCharacteristic->notify(); + Serial.print("Sent via BLE: "); + Serial.println(msg); + } +} +``` + +### Try it out + +1. Upload the sketch and open Serial Monitor at 115200 baud. +2. Open **nRF Connect** on your phone. Scan and connect to `"ESP32-BLE-UART"`. +3. In newer versions of nRF Connect, tap the **UART** icon (or navigate to the NUS service manually). You should see a chat-like interface. +4. Type a message in nRF Connect and tap **Send**. It should appear in Serial Monitor. +5. Type a message in Serial Monitor and press Enter. It should appear in nRF Connect's UART view. + +If your version of nRF Connect doesn't have the UART shortcut, you can do it manually: expand the NUS service, subscribe to notifications on the TX characteristic (`6E400003...`), and write text to the RX characteristic (`6E400002...`). + +{: .note } +> **NUS is "serial over BLE."** It gives you the familiar send/receive text experience of Bluetooth Classic's `SerialBT`, but running over BLE—so it works on the ESP32-S3, works with iPhones, and coexists with custom GATT services. Under the hood, it's still GATT: the NUS service has two characteristics, and data flows as writes and notifications. Understanding the GATT layer (the [previous lesson](ble-intro.md)) will help you debug NUS when things go wrong. + +If you want a `Serial`-like API over BLE without manually managing NUS characteristics, check out the [NuS-NimBLE-Serial](https://www.arduino.cc/reference/en/libraries/nus-nimble-serial/) library, which wraps NUS in familiar `.read()` and `.write()` methods. It requires the [NimBLE-Arduino](https://github.com/h2zero/NimBLE-Arduino) stack. + +{: .note } +> **A note on BLE security.** In this lesson, we use BLE's "Just Works" pairing mode, which requires no PIN and provides no protection against eavesdropping. This is fine for learning and for projects where the data isn't sensitive (potentiometer readings, LED colors). For production IoT devices that handle sensitive data—door locks, health monitors, payment systems—you'd want to explore passkey pairing or out-of-band (OOB) authentication. See the [Bluetooth SIG security overview](https://www.bluetooth.com/learn-about-bluetooth/key-attributes/bluetooth-security/) for more. + +## Exercises + +Want to go further? Here are some challenges to reinforce what you've learned: + +**Exercise 1: NeoPixel strip control.** Modify Part 1 to control the 5-LED NeoPixel stick from your kit instead of (or in addition to) the onboard NeoPixel. You could either send 15 bytes (5 × RGB) in a single write to set all LEDs at once, or add a fourth byte for the LED index (0–4) and set one LED per write. Build a Web Bluetooth page with five color pickers—one per LED. + +**Exercise 2: Multiple sensor characteristics.** Create a service with *three* characteristics: potentiometer data (notify), photoresistor data (notify), and LED brightness control (write). This requires reading two analog sensors and exposing each on its own characteristic. Build a Web Bluetooth dashboard that displays both sensor streams and includes a brightness slider for the LED. + +**Exercise 3: BLE servo control.** Create a writable characteristic that accepts a single byte (0–180) representing a servo angle. When the central writes a value, the ESP32 moves a servo motor to that position (using the [Servo library](../advancedio/servo.md)). Build a Web Bluetooth page with a slider to control the servo wirelessly. + +**Exercise 4: Web Bluetooth + p5.js.** Port the Web Bluetooth sensor display from Part 2 into [p5.js](https://p5js.org/). Use `createCanvas()` to draw a real-time visualization (bar chart, oscilloscope, *etc.*) of the incoming BLE sensor data. If you completed the [p5.js Serial lessons](../communication/p5js-serial.md), compare the code structure—how much carries over? (Hint: also check out [p5.ble.js](https://itpnyu.github.io/p5.ble.js/), a p5.js library specifically for Web Bluetooth.) + +**Exercise 5: Port a Bluetooth Classic bidirectional project to BLE.** If you completed the bidirectional LED control from [Lesson 3, Part 5](bluetooth-web-serial.md#part-5-bidirectional-control--wireless-color-mixer), rebuild it using BLE with writable and notify characteristics. Update the computer-side code to use Web Bluetooth instead of Web Serial. What changed? What stayed the same? + +## Lesson Summary + +In this lesson, you learned how to send data *to* the ESP32 over BLE and build browser-based interfaces for BLE devices. Here's what you covered: + +- **Writable characteristics** let the central send data to the peripheral. You created an RGB color control characteristic that accepts 3-byte payloads to set the onboard NeoPixel. +- **BLE uses a callback model** for handling incoming writes. Instead of polling with `Serial.available()` in `loop()`, you define a `BLECharacteristicCallbacks` class with an `onWrite()` method that the library calls automatically when data arrives. This is fundamentally different from the serial programming pattern. +- **Bidirectional communication** combines notify (peripheral → central) and write (central → peripheral) characteristics in a single service. The ESP32 can simultaneously stream sensor data and accept commands. +- **Web Bluetooth** lets you build browser-based interfaces for BLE devices using JavaScript — structurally parallel to the [Web Serial API](../communication/web-serial.md). It requires HTTPS (or localhost), works in Chrome/Edge on desktop and Android, but not on iOS Safari. +- **The Nordic UART Service (NUS)** provides serial-like text communication over BLE using standardized UUIDs. It's a practical bridge between the simplicity of serial and the universality of BLE — and is supported by most BLE terminal apps including nRF Connect's built-in UART mode. +- **NUS is still GATT under the hood.** It uses two characteristics (RX for writing to the peripheral, TX for notifications from the peripheral), with naming from the peripheral's perspective. + +## Resources + +- [Web Bluetooth API](https://developer.mozilla.org/en-US/docs/Web/API/Web_Bluetooth_API) — MDN Web Docs reference +- [Communicating with Bluetooth devices over JavaScript](https://developer.chrome.com/docs/capabilities/bluetooth) — Google's Web Bluetooth guide for Chrome +- [Nordic UART Service specification](https://docs.nordicsemi.com/bundle/ncs-latest/page/nrf/libraries/bluetooth_services/services/nus.html) — Nordic Semiconductor's NUS documentation +- [NuS-NimBLE-Serial Arduino library](https://www.arduino.cc/reference/en/libraries/nus-nimble-serial/) — a Serial-like API over BLE using NUS +- [p5.ble.js](https://itpnyu.github.io/p5.ble.js/) — a p5.js library for Web Bluetooth, from ITP/NYU +- [Create Apps for the ESP32 Using BLE Through P5](https://www.hackster.io/lemio/create-apps-for-the-esp32-using-ble-through-p5-55292d) — Hackster.io tutorial combining p5.js + ESP32 BLE +- [ESP32 Web Bluetooth (BLE) Getting Started Guide](https://randomnerdtutorials.com/esp32-web-bluetooth/) — Random Nerd Tutorials + +## Next Lesson + +With BLE under your belt, you've now covered all three major wireless communication technologies available on the ESP32: **WiFi** (cloud connectivity via [Lesson 1: IoT](iot.md)), **Bluetooth Classic** (wireless serial via [Lessons 2–3](bluetooth-serial.md)), and **BLE** (structured low-power wireless in [Lesson 4](ble-intro.md) and this lesson). From here, you might explore BLE HID (making your ESP32 act as a wireless keyboard, mouse, or game controller), deep sleep with BLE wake-up for battery-powered projects, or combining BLE with sensors like the ADXL343 accelerometer for motion-controlled wireless devices. The wireless world is yours! 🚀 + + diff --git a/esp32/ble-intro.md b/esp32/ble-intro.md new file mode 100644 index 00000000..157a97a7 --- /dev/null +++ b/esp32/ble-intro.md @@ -0,0 +1,743 @@ +--- +layout: default +title: L4: Introduction to BLE +description: "Learn Bluetooth Low Energy on the ESP32: the peripheral/central model, GATT services and characteristics, and streaming live sensor data to your phone via BLE notifications." +parent: Wireless +grand_parent: ESP32 +has_toc: true # (on by default) +usemathjax: false +comments: true +usetocbot: true +nav_order: 4 +--- +# {{ page.title | replace_first:'L','Lesson ' }} +{: .no_toc } + +## Table of Contents +{: .no_toc .text-delta } + +1. TOC +{:toc} +--- + +{: .warning } +> This lesson is in draft form. There are missing circuit diagrams, images, videos, and other content. + + + + + +In the [last two lessons](bluetooth-serial.md), we used Bluetooth Classic to create a wireless serial connection—simple, fast, and satisfying. But it came with real limitations: no iPhone support, no ESP32-S3 support, higher power consumption, and only one device at a time. In this lesson, we'll learn **Bluetooth Low Energy (BLE)**—the protocol that powers your Fitbit, your AirPods' pairing process, your smart thermostat, and billions of IoT devices worldwide. + +BLE is more complex than Bluetooth Classic. Instead of a simple serial byte stream, BLE organizes data into a structured model of **services** and **characteristics**. This takes some getting used to—but that structure is exactly what makes BLE so powerful and ubiquitous. And unlike Bluetooth Classic, BLE works on the ESP32-S3, works with iPhones, and—as we'll see—even works directly from a web browser. + +{: .note } +> **In this lesson, you will learn:** +> - What BLE is and how it fundamentally differs from Bluetooth Classic +> - The BLE communication model: **peripherals** and **centrals**, advertising and connecting +> - The GATT data model: **servers**, **services**, **characteristics**, and **UUIDs** +> - How to use the ESP32 BLE library to create a BLE peripheral that exposes sensor data +> - How to read and subscribe to BLE characteristics from a phone app ([nRF Connect](https://www.nordicsemi.com/Products/Development-tools/nrf-connect-for-mobile)) and from Python ([bleak](https://pypi.org/project/bleak/)) +> - How to stream real-time sensor data using BLE **notifications** +> - The 20-byte MTU payload limit and how to work within it + +**Did you skip the Bluetooth Classic lessons?** No problem. This lesson is self-contained—you don't need Bluetooth Classic experience to follow along. We'll briefly cover how BLE differs from Classic in the first section. If you want the full comparison, see [Lesson 2: Bluetooth Serial](bluetooth-serial.md) and [Lesson 3: Bluetooth Web Serial](bluetooth-web-serial.md). And unlike Bluetooth Classic, which is blocked on iOS and only works on the original ESP32, **BLE works with iPhones, Android phones, and the ESP32-S3**—so everyone can participate. + +## What is BLE? + +**Bluetooth Low Energy** (BLE) is a wireless communication protocol introduced in Bluetooth 4.0 (2010). Despite sharing the "Bluetooth" name with Bluetooth Classic, BLE is a completely different protocol stack designed from the ground up for **low-power, intermittent data exchange**. Where Bluetooth Classic was built for continuous streaming (music, file transfers, serial bridges), BLE was built for devices that send small amounts of data infrequently—a heart rate monitor broadcasting a reading every second, a door sensor reporting open/closed, a fitness tracker uploading step counts. + +This design priority—**extreme power efficiency**—is what makes BLE transformative for physical computing. A BLE sensor can run for months or even years on a coin cell battery. That's not possible with Bluetooth Classic or WiFi. + +{: .important } +> **BLE is not "wireless serial."** This is the single most important conceptual shift in this lesson. If you've used `Serial.println()` over USB or `SerialBT.println()` over Bluetooth Classic, you're used to a continuous byte stream—data flows like water through a pipe. BLE doesn't work that way. Instead, BLE organizes data into discrete, named **characteristics** that can be read, written, or subscribed to. Think less "serial port" and more "structured data API." + +If you completed [Lessons 2 and 3](bluetooth-serial.md), here's a quick comparison: + +| Feature | Bluetooth Classic (L2–L3) | BLE (this lesson) | +|---|---|---| +| Data model | Continuous byte stream | Structured characteristics | +| API feel | Like `Serial` | Like a REST API | +| Power | Higher | Very low | +| iOS support | ❌ (Apple blocks SPP) | ✅ | +| ESP32-S3 | ❌ | ✅ | +| Typical range | ~10m | ~10m | +| Max throughput | ~3 Mbps | Up to 2 Mbps (BLE 5.0 PHY), but practical throughput is much lower | +| Complexity | Very simple | More setup, more concepts | + +**Table.** Key differences between Bluetooth Classic (Lessons 2–3) and BLE (this lesson). BLE trades simplicity for universality, power efficiency, and structured data. +{: .fs-1 } + +## How BLE works + +BLE communication involves two fundamental concepts: **roles** (who talks to whom) and the **GATT data model** (how data is organized). Let's take these one at a time. + +### Peripherals and centrals + +Every BLE interaction has two roles: + +- A **peripheral** advertises its presence and hosts data. In our lessons, this is always the ESP32. Think of it as a weather station mounted on a wall—it has data (temperature, humidity) and it waits for someone to come read it. + +- A **central** scans for peripherals, initiates connections, and reads or writes data. In our lessons, this is your phone or laptop. Think of it as a person walking up to the weather station to check the temperature. + +The peripheral **advertises** by periodically broadcasting short packets (called advertisement packets) that say, in essence, "I'm here, my name is X, and I offer these services." The central **scans** for these packets, finds the peripheral, and can then **connect** to it for richer data exchange. + +{: .note } +> These roles are about who *initiates* the connection, not who sends data. Once connected, data flows in both directions—the central can read from the peripheral *and* write to it. The terms "peripheral" and "central" replace the older "slave" and "master" terminology that you may encounter in older documentation. + + + +### The GATT data model + +Once a central connects to a peripheral, how does it know what data is available? This is where **GATT** (Generic Attribute Profile) comes in. GATT defines how data is organized on a BLE peripheral, and it's the conceptual heart of BLE. + +Think of GATT as a structured bulletin board. The peripheral (ESP32) maintains a bulletin board organized into sections (**services**), and each section contains individual data items (**characteristics**). A central (your phone) walks up to the board, browses the sections, and reads or modifies specific items. + +Here's the hierarchy: + +```text +BLE Peripheral (GATT Server) + └── Service (e.g., "Sensor Data") ← a category of related data + ├── Characteristic (e.g., "Potentiometer") ← a single data point + │ ├── Value: 2847 ← the actual data + │ └── Properties: Read, Notify ← what you can do with it + └── Characteristic (e.g., "LED Color") + ├── Value: [255, 0, 128] + └── Properties: Read, Write +``` + +**Services** group related data. A peripheral can have multiple services—for example, one for sensor data and another for device information. Each service is identified by a **UUID** (more on this below). + +**Characteristics** are the individual data points within a service. Each characteristic has: + +- A **UUID** (a unique identifier—like a name or address for this data point) +- A **value** (the actual data—up to 512 bytes, though typically much smaller) +- **Properties** that define how the characteristic can be accessed: + - **Read**: the central can request the current value (like polling) + - **Write**: the central can set the value (like sending a command) + - **Notify**: the peripheral pushes updates to the central automatically when the value changes—this is the most efficient way to stream data, because the central doesn't have to keep asking + - **Indicate**: like Notify but the central sends an acknowledgment (rarely needed for our use cases) + +{: .note } +> **Why so much structure?** If the GATT model feels over-engineered for reading a potentiometer, that's because it was designed for a much broader world of devices—from heart rate monitors to smart locks to industrial sensors. The structure lets *any* BLE central discover what a peripheral offers without prior knowledge. Your phone's Bluetooth settings can show that a nearby device has a "Battery Service" at level 73% without needing a custom app—because "Battery Service" and "Battery Level" are standard UUIDs that every BLE stack understands. This interoperability is BLE's superpower. + +### UUIDs: identifying services and characteristics + +Every service and characteristic needs a unique identifier. BLE uses **UUIDs** (Universally Unique Identifiers) for this. + +**16-bit UUIDs** are reserved by the [Bluetooth SIG](https://www.bluetooth.com/) for standard, well-known services and characteristics. For example: +- `0x180F` = Battery Service +- `0x181A` = Environmental Sensing Service +- `0x2A19` = Battery Level characteristic +- `0x2A6E` = Temperature characteristic + +You can browse the full list in the [Bluetooth SIG Assigned Numbers](https://www.bluetooth.com/specifications/assigned-numbers/) document. + +**128-bit UUIDs** are for custom services and characteristics—anything you define for your own project. They look like this: `4fafc201-1fb5-459e-8fcc-c5c9c331914b`. You can generate your own at [uuidgenerator.net](https://www.uuidgenerator.net/). In this lesson, we'll use custom 128-bit UUIDs since we're defining our own sensor and LED control services. + +{: .note } +> **Don't be intimidated by UUIDs.** A 128-bit UUID is just a unique label—think of it like a URL or a barcode. You generate one, paste it into your code, and use the same one in your phone app or web page so both sides agree on which characteristic is which. You don't need to memorize them or understand their internal structure. + +### A note on BLE security + +In this lesson, we use BLE's "Just Works" pairing mode, which requires no PIN and provides no protection against eavesdropping. This is fine for learning and for projects where the data isn't sensitive (potentiometer readings, LED colors). For production IoT devices that handle sensitive data—door locks, health monitors, payment systems—you'd want to explore passkey pairing or out-of-band (OOB) authentication. See the [Bluetooth SIG security overview](https://www.bluetooth.com/learn-about-bluetooth/key-attributes/bluetooth-security/) for more. + +## Choosing between WiFi, Bluetooth Classic, and BLE + +Now that you understand the BLE concepts — peripherals, centrals, GATT, services, characteristics, and UUIDs — you have enough context to see where BLE fits alongside the other wireless technologies you've learned. If you've completed [Lesson 1 (WiFi/IoT)](iot.md) and [Lessons 2–3 (Bluetooth Classic)](bluetooth-serial.md), here's how all three compare: + +| | WiFi (L7) | Bluetooth Classic (L8) | BLE (this lesson) | +|---|---|---|---| +| Best for | Cloud/internet connectivity | Wireless serial replacement | Low-power sensors, phones, web apps | +| Range | Depends on router | ~10m | ~10m | +| Power | High | Medium | Very low | +| iPhone support | ✅ (via web) | ❌ | ✅ | +| ESP32 (original) | ✅ | ✅ | ✅ | +| ESP32-S3 | ✅ | ❌ | ✅ | +| Complexity | Medium (needs WiFi credentials) | Very simple | Higher (GATT model) | +| Browser API | Fetch / WebSocket | Web Serial (via virtual COM port) | Web Bluetooth | + +**Table.** Comparison of the three wireless technologies available on the ESP32. For most new projects, BLE is the default choice unless you need internet connectivity (WiFi) or a drop-in serial replacement (Bluetooth Classic). The original ESP32 supports all three; the ESP32-S3 supports WiFi and BLE but not Bluetooth Classic. +{: .fs-1 } + +## The ESP32 BLE library + +The ESP32 Arduino core includes a built-in BLE library ([source on GitHub](https://github.com/espressif/arduino-esp32/tree/master/libraries/BLE), [API docs](https://docs.espressif.com/projects/arduino-esp32/en/latest/api/ble.html)). No installation is needed—just `#include` the headers and go. + + + +The library is split across several header files, each providing a specific piece of the BLE puzzle: + +| Header | What it provides | +|---|---| +| `BLEDevice.h` | Top-level entry point. Initializes the BLE stack (call `BLEDevice::init()` once in `setup()`). | +| `BLEServer.h` | Creates a GATT server on the ESP32 and manages connections. | +| `BLEUtils.h` | Utility functions used internally by the library. Include it alongside the others. | +| `BLE2902.h` | The Client Characteristic Configuration Descriptor (CCCD). Required for any characteristic that supports **notifications** — without it, centrals cannot subscribe. You only need this header when using `PROPERTY_NOTIFY`. | + +You'll typically `#include` all four at the top of your sketch. The key *classes* you'll work with are: + +| Class | Purpose | +|---|---| +| `BLEDevice` | Initializes the BLE stack (call once in `setup()`) | +| `BLEServer` | Creates a GATT server on the ESP32 | +| `BLEService` | A service within the server (identified by UUID) | +| `BLECharacteristic` | A data point within a service (identified by UUID, has value + properties) | +| `BLEAdvertising` | Controls what the ESP32 broadcasts during advertising | +| `BLEServerCallbacks` | Callback class for connection/disconnection events | +| `BLECharacteristicCallbacks` | Callback class for read/write events on a characteristic | + +Don't worry about memorizing these—we'll introduce each one as we use it in the activities below. + +{: .note } +> **Alternative library: NimBLE-Arduino.** The default BLE library uses the Bluedroid stack, which consumes roughly 170KB of RAM and ~500KB of flash. An alternative called [NimBLE-Arduino](https://github.com/h2zero/NimBLE-Arduino) provides a lighter-weight BLE stack that uses approximately 60% less flash and 50% less RAM, with a similar (though not identical) API. For the ESP32-S3 with its 4MB flash and 2MB PSRAM, the memory savings are less critical—but if you're building a complex project that also uses WiFi, or targeting the ESP32-C3 with only 400KB SRAM, NimBLE is worth exploring. We use the default library in this lesson because it ships with the Arduino core, requires no installation, and is what most online tutorials reference. + +## Materials + +You'll need the following components. We use **[Adafruit's ESP32-S3 Feather](https://www.adafruit.com/product/5477)** but any ESP32 board with BLE support will work (including the Huzzah32). + +| Breadboard | ESP32 | LED | Resistor | Potentiometer | +| ---------- |:-----:|:-----:|:-----:|:-----:| +| ![Half-sized solderless breadboard]({{ site.baseurl }}/assets/images/Breadboard_Half.png) | ![Adafruit ESP32-S3 Feather board, top view](assets/images/Adafruit_ESP32-S3-5477-11-vertical-cropped.jpg) | ![Red 5mm LED]({{ site.baseurl }}/assets/images/RedLED_Fritzing.png) | ![220-ohm resistor, striped red-red-brown-gold]({{ site.baseurl }}/assets/images/Resistor220_Fritzing.png) | ![10kΩ rotary potentiometer]({{ site.baseurl }}/assets/images/PanelMountPotentiometer_NoCap_150h.jpg) | +| Breadboard | [ESP32-S3 Feather](https://www.adafruit.com/product/5477) | Red LED | 220Ω Resistor | 10kΩ Potentiometer | + +You will also need: + +- **Python 3** with the [bleak](https://pypi.org/project/bleak/) library installed (`pip3 install bleak`). Bleak is a cross-platform BLE library for Python—it works on macOS, Windows, and Linux. +- A **smartphone** (iPhone or Android) with the free [nRF Connect](https://www.nordicsemi.com/Products/Development-tools/nrf-connect-for-mobile) app by Nordic Semiconductor. Unlike Bluetooth Classic, **BLE works with iPhones**—so everyone can participate! Available on [iOS](https://apps.apple.com/app/nrf-connect-for-mobile/id1054362403) and [Android](https://play.google.com/store/apps/details?id=no.nordicsemi.android.mcp). + +{: .note } +> [nRF Connect](https://www.nordicsemi.com/Products/Development-tools/nrf-connect-for-mobile) is a professional-grade BLE debugging tool made by Nordic Semiconductor (a major BLE chip manufacturer). It lets you scan for BLE devices, inspect their services and characteristics, read values, write data, and subscribe to notifications. It's free, available on [iOS](https://apps.apple.com/app/nrf-connect-for-mobile/id1054362403) and [Android](https://play.google.com/store/apps/details?id=no.nordicsemi.android.mcp), and is the tool we'll use throughout this lesson. Alternatives include [LightBlue](https://punchthrough.com/lightblue/) (iOS/Android) and [BLE Scanner](https://play.google.com/store/apps/details?id=com.macdom.ble.blescanner) (Android). + +## Part 1: Advertising and discovery + +Let's start with the BLE equivalent of "Hello World": create a GATT server on the ESP32 with a single readable characteristic, advertise it, and discover it from both your computer and your phone. By the end of this part, you'll understand the full BLE setup lifecycle — initializing the stack, creating the GATT hierarchy, advertising, and handling connections — and you'll have read your first BLE characteristic from nRF Connect. + +### The Arduino code + + + +The full source is available in our [Arduino GitHub repo](https://github.com/makeabilitylab/arduino/tree/master/ESP32/Bluetooth/BLEHelloWorld). + +```cpp +/** + * BLEHelloWorld: creates a BLE GATT server with one service and one + * readable characteristic. The characteristic contains a greeting + * string that you can read from any BLE central (like nRF Connect). + * + * Works on: ESP32-S3 Feather, Huzzah32, or any ESP32 with BLE. + * + * See: https://makeabilitylab.github.io/physcomp/esp32/ble + * + * By Jon E. Froehlich + * @jonfroehlich + * http://makeabilitylab.io + */ + +#include +#include +#include + +// Custom UUIDs for our service and characteristic. +// Generated at https://www.uuidgenerator.net/ +#define SERVICE_UUID "4fafc201-1fb5-459e-8fcc-c5c9c331914b" +#define CHARACTERISTIC_UUID "beb5483e-36e1-4688-b7f5-ea07361b26a8" + +bool _deviceConnected = false; + +// Callback class to handle connection events +class MyServerCallbacks : public BLEServerCallbacks { + void onConnect(BLEServer* pServer) { + _deviceConnected = true; + Serial.println("Central connected!"); + } + + void onDisconnect(BLEServer* pServer) { + _deviceConnected = false; + Serial.println("Central disconnected. Restarting advertising..."); + + // IMPORTANT: restart advertising so other devices can find us again. + // Without this, the ESP32 goes silent after the first disconnection. + pServer->getAdvertising()->start(); + } +}; + +void setup() { + Serial.begin(115200); + Serial.println("Starting BLE Hello World..."); + + // Step 1: Initialize the BLE stack with a device name. + // This name appears when centrals scan for devices. + BLEDevice::init("ESP32-BLE"); + + // Step 2: Create a GATT server. + BLEServer* pServer = BLEDevice::createServer(); + pServer->setCallbacks(new MyServerCallbacks()); + + // Step 3: Create a service on the server (identified by UUID). + BLEService* pService = pServer->createService(SERVICE_UUID); + + // Step 4: Create a characteristic within the service. + // This characteristic is readable (PROPERTY_READ) — a central + // can request its value. + BLECharacteristic* pCharacteristic = pService->createCharacteristic( + CHARACTERISTIC_UUID, + BLECharacteristic::PROPERTY_READ + ); + + // Step 5: Set the initial value of the characteristic. + pCharacteristic->setValue("Hello from ESP32!"); + + // Step 6: Start the service (makes it visible to connected centrals). + pService->start(); + + // Step 7: Start advertising so centrals can discover us. + BLEAdvertising* pAdvertising = BLEDevice::getAdvertising(); + pAdvertising->addServiceUUID(SERVICE_UUID); // include our service UUID in ads + pAdvertising->setScanResponse(true); // allow a scan response packet + pAdvertising->start(); + + Serial.println("BLE server is advertising. Open nRF Connect and scan!"); +} + +void loop() { + // Nothing to do here yet — the BLE stack runs in the background. + // We'll add sensor reading and notifications in Part 2. + delay(1000); +} +``` + +Let's walk through the key steps: + +**Step 1: `BLEDevice::init("ESP32-BLE")`** initializes the Bluetooth stack and sets the device name that appears during scanning. This is analogous to `SerialBT.begin("ESP32-Bluetooth")` from [Lesson 2](bluetooth-serial.md), but the similarity ends here—BLE has no `println()` or `read()` on the device object. + +**Step 2: Creating the server and registering callbacks.** `BLEDevice::createServer()` creates a GATT server, and `pServer->setCallbacks(new MyServerCallbacks())` registers a **callback** — a function (or in this case, a class with methods) that the BLE library will call automatically when specific events happen, like a central connecting or disconnecting. If you've used event listeners in JavaScript or interrupt handlers on Arduino, callbacks are the same idea: instead of polling for events in `loop()`, you tell the library "call *this* function when *that* happens." We define `MyServerCallbacks` above `setup()` with two methods: `onConnect()` and `onDisconnect()`. + +**Steps 3–4: Creating the GATT hierarchy.** We create a **service** within the server (identified by `SERVICE_UUID`) and a **characteristic** within that service (identified by `CHARACTERISTIC_UUID`). The characteristic has `PROPERTY_READ`, meaning a central can request its value. This is the GATT structure we discussed earlier, built in code. + +**Step 5: Setting the value.** `pCharacteristic->setValue("Hello from ESP32!")` stores a string in the characteristic. When a central reads this characteristic, it receives this string. + +**Steps 6–7: Starting the service and advertising.** `pService->start()` activates the service so connected centrals can see it. `pAdvertising->start()` begins broadcasting advertisement packets. We include our service UUID in the advertisement (`addServiceUUID`) so centrals filtering by service can find us. + +**The `onDisconnect` callback revisited.** Look back at the `MyServerCallbacks` class we registered in Step 2. The `onDisconnect()` method contains a critical line: `pServer->getAdvertising()->start()`. This is because when a central disconnects, the ESP32 **stops advertising by default**. If you don't restart advertising in `onDisconnect()`, the ESP32 goes silent and no new centrals can find it. Always restart advertising after disconnection. + +### Discovering the ESP32 from your computer (Python) + +Let's start on the computer, where debugging is easiest. We'll use [bleak](https://pypi.org/project/bleak/) — a cross-platform BLE client library for Python that works on macOS, Windows, and Linux. Unlike [pySerial](https://pyserial.readthedocs.io/) (which we used for Bluetooth Classic in [Lesson 2](bluetooth-serial.md)), bleak speaks BLE natively — it connects directly to BLE peripherals, discovers their GATT services, and reads/writes characteristics using Python's `asyncio` for non-blocking I/O. If you haven't installed it yet: + +```bash +pip3 install bleak +``` + +Here's a script that scans for BLE devices, connects to the ESP32, and reads our characteristic: + +```python +""" +ble_discover.py: Scans for BLE devices, connects to the ESP32, +and reads the greeting characteristic. + +Requires: bleak (pip3 install bleak) + +By Jon E. Froehlich +@jonfroehlich +http://makeabilitylab.io +""" + +import asyncio +from bleak import BleakScanner, BleakClient + +SERVICE_UUID = "4fafc201-1fb5-459e-8fcc-c5c9c331914b" +CHARACTERISTIC_UUID = "beb5483e-36e1-4688-b7f5-ea07361b26a8" + +async def main(): + print("Scanning for BLE devices...") + devices = await BleakScanner.discover(timeout=5.0) + + target = None + for d in devices: + print(f" Found: {d.name} ({d.address})") + if d.name and "ESP32" in d.name: + target = d + + if target is None: + print("Could not find ESP32 BLE device. Is the sketch running?") + return + + print(f"\nConnecting to {target.name} ({target.address})...") + async with BleakClient(target.address) as client: + print(f"Connected: {client.is_connected}") + + # Read the greeting characteristic + value = await client.read_gatt_char(CHARACTERISTIC_UUID) + text = value.decode("utf-8") + print(f"Read from characteristic: {text}") + +asyncio.run(main()) +``` + +Run it: + +```bash +python3 ble_discover.py +``` + +You should see the ESP32 in the scan results and then read `"Hello from ESP32!"` from the characteristic. 🎉 + +{: .note } +> **Compare with pySerial from [Lesson 2](bluetooth-serial.md).** With Bluetooth Classic, you used `serial.Serial()` to open a virtual COM port—the same API as USB serial. With BLE, there's no virtual COM port; you use `bleak`'s `BleakClient` to connect directly to the device and read structured characteristics. This is the fundamental difference between the two Bluetooth flavors. + +### Discovering the ESP32 from your phone (iPhone and Android) + +Once you've confirmed the ESP32 is working from your computer, let's try it from your phone. **This works on both iPhones and Android phones**—unlike Bluetooth Classic, which was Android-only. + +1. On your **iPhone** or **Android phone**, open the **nRF Connect** app ([iOS](https://apps.apple.com/app/nrf-connect-for-mobile/id1054362403) / [Android](https://play.google.com/store/apps/details?id=no.nordicsemi.android.mcp)). +2. Tap **Scan** (top right). You should see `"ESP32-BLE"` in the list of discovered devices. + + + +3. Tap **Connect** next to `"ESP32-BLE"`. The app will connect and display the GATT server structure. You should see your custom service (listed by its UUID) with one characteristic underneath. + + + +4. Tap the **read arrow** (↓) next to the characteristic. You should see `"Hello from ESP32!"` appear as the value. You just read data from a BLE peripheral on your phone! + +{: .note } +> **What you're seeing in nRF Connect** is the GATT structure we built in code: one service containing one characteristic. nRF Connect shows the UUIDs for each. Since we used custom 128-bit UUIDs (not standard Bluetooth SIG UUIDs), nRF Connect displays them as "Unknown Service" and "Unknown Characteristic"—it doesn't know what our custom UUIDs mean. If we'd used a standard UUID like `0x181A` (Environmental Sensing), nRF Connect would display the name automatically. + +### Workbench demo + + + +## Part 2: Streaming sensor data with notifications + +Reading a static string is a good start, but the real power of BLE comes with **notifications** — the peripheral automatically pushes updates to the central whenever a value changes. In this part, you'll wire up a potentiometer, learn how to add `PROPERTY_NOTIFY` and the `BLE2902` descriptor, and stream live sensor data to both nRF Connect and a Python script. This is the BLE equivalent of `Serial.println(sensorValue)` — but structured and wireless. + +### The circuit + +Connect a 10kΩ potentiometer to the ESP32-S3 Feather on pin **A5** (GPIO 8), which is an ADC1 pin. This is the same potentiometer circuit from [Lesson 4: Analog Input](analog-input.md). + + + +
+Using the Huzzah32 instead? (click to expand) + +On the Huzzah32, use pin **A7** (GPIO 32), which is an ADC1 pin. ADC2 pins conflict with both WiFi and Bluetooth on the original ESP32, so always use ADC1 for analog input when using wireless features. + +
+ +### The Arduino code + + + +The full source is available in our [Arduino GitHub repo](https://github.com/makeabilitylab/arduino/tree/master/ESP32/Bluetooth/BLENotifySensor). + +```cpp +/** + * BLENotifySensor: reads a potentiometer and streams its value to + * connected BLE centrals using notifications. Open nRF Connect, + * connect, and subscribe to notifications to see live sensor data. + * + * Circuit: + * - 10kΩ potentiometer on A5 (GPIO 8, ADC1) for ESP32-S3 Feather + * (use A7 / GPIO 32 for the Huzzah32) + * + * Works on: ESP32-S3 Feather, Huzzah32, or any ESP32 with BLE. + * + * See: https://makeabilitylab.github.io/physcomp/esp32/ble + * + * By Jon E. Froehlich + * @jonfroehlich + * http://makeabilitylab.io + */ + +#include +#include +#include +#include + +// Custom UUIDs — same service UUID as Part 1, new characteristic UUID for sensor data +#define SERVICE_UUID "4fafc201-1fb5-459e-8fcc-c5c9c331914b" +#define SENSOR_CHAR_UUID "beb5483e-36e1-4688-b7f5-ea07361b26a8" + +const int POT_INPUT_PIN = A5; // GPIO 8, ADC1 on ESP32-S3 Feather + +BLEServer* _pServer = NULL; +BLECharacteristic* _pSensorCharacteristic = NULL; +bool _deviceConnected = false; + +// Timing for non-blocking sensor reads +unsigned long _lastSensorReadMs = 0; +const unsigned long SENSOR_READ_INTERVAL_MS = 100; // read sensor ~10x/sec + +class MyServerCallbacks : public BLEServerCallbacks { + void onConnect(BLEServer* pServer) { + _deviceConnected = true; + Serial.println("Central connected!"); + } + + void onDisconnect(BLEServer* pServer) { + _deviceConnected = false; + Serial.println("Central disconnected. Restarting advertising..."); + pServer->getAdvertising()->start(); + } +}; + +void setup() { + Serial.begin(115200); + Serial.println("Starting BLE Sensor Notify..."); + + // Initialize BLE + BLEDevice::init("ESP32-BLE-Sensor"); + _pServer = BLEDevice::createServer(); + _pServer->setCallbacks(new MyServerCallbacks()); + + // Create service + BLEService* pService = _pServer->createService(SERVICE_UUID); + + // Create characteristic with READ and NOTIFY properties + _pSensorCharacteristic = pService->createCharacteristic( + SENSOR_CHAR_UUID, + BLECharacteristic::PROPERTY_READ | + BLECharacteristic::PROPERTY_NOTIFY + ); + + // Add the BLE2902 descriptor — this is required for notifications. + // It allows the central to enable/disable notifications on this characteristic. + _pSensorCharacteristic->addDescriptor(new BLE2902()); + + // Start the service and begin advertising + pService->start(); + BLEAdvertising* pAdvertising = BLEDevice::getAdvertising(); + pAdvertising->addServiceUUID(SERVICE_UUID); + pAdvertising->setScanResponse(true); + pAdvertising->start(); + + Serial.println("BLE server advertising. Connect with nRF Connect!"); +} + +void loop() { + unsigned long now = millis(); + + if (now - _lastSensorReadMs >= SENSOR_READ_INTERVAL_MS) { + _lastSensorReadMs = now; + + int potVal = analogRead(POT_INPUT_PIN); + + // Always print to USB serial for debugging + Serial.print("Pot:"); + Serial.println(potVal); + + // If a BLE central is connected, update the characteristic and notify + if (_deviceConnected) { + // Convert the integer to a string and set it as the characteristic value. + // We could also send raw bytes for efficiency, but strings are easier + // to read in nRF Connect for learning purposes. + String valStr = String(potVal); + _pSensorCharacteristic->setValue(valStr.c_str()); + _pSensorCharacteristic->notify(); + } + } +} +``` + +There are two new elements here compared to Part 1: + +**`PROPERTY_NOTIFY`** tells the BLE stack that this characteristic supports notifications. When a central subscribes to notifications, it will receive an automatic update every time we call `notify()`. + +**`BLE2902` descriptor.** This is a BLE protocol requirement: the Client Characteristic Configuration Descriptor (CCCD), identified by UUID `0x2902`, is a small piece of metadata that the central uses to enable or disable notifications. Without it, the central cannot subscribe. The line `_pSensorCharacteristic->addDescriptor(new BLE2902())` adds this descriptor to our characteristic. + +**`_pSensorCharacteristic->notify()`** pushes the current value to all subscribed centrals. We call this after updating the value with `setValue()`. If no central is subscribed, `notify()` does nothing. + +### Reading notifications from your computer (Python) + +Here's a Python script that subscribes to the potentiometer notifications and displays them in real time: + +```python +""" +ble_sensor_reader.py: Connects to the ESP32 BLE sensor and subscribes +to potentiometer notifications. Displays values with a live ASCII bar. + +Requires: bleak (pip3 install bleak) + +By Jon E. Froehlich +@jonfroehlich +http://makeabilitylab.io +""" + +import asyncio +from bleak import BleakScanner, BleakClient + +SERVICE_UUID = "4fafc201-1fb5-459e-8fcc-c5c9c331914b" +SENSOR_CHAR_UUID = "beb5483e-36e1-4688-b7f5-ea07361b26a8" + +def on_notification(sender, data): + """Called each time the ESP32 sends a notification.""" + text = data.decode("utf-8").strip() + try: + value = int(text) + bar_length = int(value / 4095 * 50) + bar = '█' * bar_length + '░' * (50 - bar_length) + print(f"\r{bar} {value:4d}", end='', flush=True) + except ValueError: + print(f"\r{text}", end='', flush=True) + +async def main(): + print("Scanning for ESP32-BLE-Sensor...") + devices = await BleakScanner.discover(timeout=5.0) + + target = None + for d in devices: + if d.name and "ESP32" in d.name: + target = d + break + + if target is None: + print("Could not find ESP32. Is the sketch running?") + return + + print(f"Connecting to {target.name}...") + async with BleakClient(target.address) as client: + print(f"Connected! Turn the potentiometer.\n") + + # Subscribe to notifications + await client.start_notify(SENSOR_CHAR_UUID, on_notification) + + # Keep running until Ctrl+C + try: + while True: + await asyncio.sleep(1.0) + except KeyboardInterrupt: + print("\nStopping...") + await client.stop_notify(SENSOR_CHAR_UUID) + +asyncio.run(main()) +``` + +Run it and turn the potentiometer—you'll see a live bar chart updating in your terminal, with data arriving wirelessly over BLE: + +```bash +python3 ble_sensor_reader.py +``` + +{: .note } +> **Compare with the Python Bluetooth Classic script from [Lesson 2](bluetooth-serial.md).** In L2, you used `pyserial`'s `ser.readline()` to read data from a virtual COM port—a byte stream, just like USB serial. Here, you use `bleak`'s `start_notify()` to subscribe to a specific BLE characteristic—a callback fires each time the ESP32 pushes a new value. The data arrives structured and event-driven rather than as a continuous byte stream. + +### Reading notifications from your phone (iPhone and Android) + +Now try it from your phone: + +1. Open **nRF Connect** on your **iPhone** or **Android phone**. +2. Scan and connect to `"ESP32-BLE-Sensor"`. +3. Expand the service and find the sensor characteristic. +4. Tap the **triple-down-arrow** icon (⇊) to **subscribe to notifications**. +5. Turn the potentiometer—you should see the value updating in real time on your phone! + + + +{: .note } +> **Comparing with serial:** In the [Communication module](../communication/serial-intro.md), you call `Serial.println(sensorValue)` and bytes flow continuously through the USB cable at 115,200 bps. With BLE, you update a characteristic value and call `notify()`—the BLE stack delivers it at the negotiated connection interval (typically 7.5ms–4 seconds). BLE trades raw throughput for structured data, power efficiency, and wireless convenience. + +### The 20-byte payload limit + +Try changing the `setValue()` call to send a long string—something like `"Potentiometer reading is: " + String(potVal)`. You'll notice the value gets **truncated** in nRF Connect. Welcome to the 20-byte MTU limit! + +By default, BLE's ATT (Attribute Protocol) layer has a Maximum Transmission Unit (MTU) of 23 bytes. After 3 bytes of protocol overhead, that leaves **20 bytes** for your actual data. Any value longer than 20 bytes gets silently truncated. + +You can negotiate a larger MTU (up to 512 bytes) if both sides support it, but 20 bytes is the safe baseline that works with all BLE devices. For sensor data, this is rarely a problem—an integer like `"2847"` is only 4 bytes as a string (or 2 bytes as a raw `uint16_t`). But if you try to send long formatted strings, you'll hit this limit. + +{: .note } +> **Keep your BLE payloads compact.** Send numbers as short strings or raw bytes, not verbose text. If you need to send more than 20 bytes, either negotiate a larger MTU (call `BLEDevice::setMTU(185)` in `setup()`; both sides must agree), split the data across multiple characteristics, or send it in chunks. + +### Workbench demo + + + +## Exercises + +Want to go further? Here are some challenges to reinforce what you've learned: + +**Exercise 1: BLE range test.** With the notification sketch from Part 2 running, walk away from your ESP32 with nRF Connect open. At what distance do notifications stop arriving? How do walls and obstacles affect range? If you did the Bluetooth Classic range test in [Lesson 3, Exercise 4](bluetooth-web-serial.md#exercises), compare the two. Are they similar? + +**Exercise 2: Multiple sensor characteristics.** Create a service with *two* notify characteristics: one for a potentiometer and one for a photoresistor. Subscribe to both in nRF Connect and observe both values updating simultaneously. This is good practice for structuring your GATT services. + +**Exercise 3: Connection status NeoPixel.** Use the onboard NeoPixel to display BLE connection status: **blue** while advertising (waiting for a connection), **green** when a central is connected, and **red** briefly on disconnection before returning to blue. This is a common pattern in commercial BLE products. Implement it using the `onConnect()` and `onDisconnect()` callbacks. (Accessibility note: for colorblind users, consider also adding a blink pattern—*e.g.,* slow pulse for advertising, solid for connected, fast blink for disconnection.) + +**Exercise 4: Power comparison (research).** The ESP32-S3 Feather has a LiPoly battery connector and a MAX17048 battery monitor chip. Connect the 350mAh LiPoly battery from your kit and run a BLE sketch. How long does the battery last? Compare with a WiFi sketch (from the [IoT lesson](iot.md)). Which protocol consumes more power? For bonus points, use `BLEDevice::setPower()` to experiment with different transmit power levels and measure the effect on both range and battery life. + +**Exercise 5: Port a Bluetooth Classic project to BLE.** If you completed the potentiometer streaming project from [Lesson 3](bluetooth-web-serial.md), rebuild it using BLE. Replace `BluetoothSerial` with the BLE library, design your GATT service and characteristic, and update the computer-side code to use `bleak` instead of `pySerial`. What changed? What stayed the same? + +## Lesson Summary + +In this lesson, you learned the fundamentals of Bluetooth Low Energy — a structured, low-power wireless protocol that's fundamentally different from the serial-style Bluetooth Classic in [Lessons 2–3](bluetooth-serial.md). Here's what you covered: + +- **BLE is not wireless serial.** Instead of a continuous byte stream, BLE organizes data into structured **services** and **characteristics** with defined properties (read, write, notify). This structure enables interoperability across devices and applications. +- **BLE uses a peripheral/central model.** The ESP32 acts as a **peripheral** (advertising and hosting data), while your phone or laptop acts as a **central** (scanning, connecting, reading, and writing). Once connected, data flows in both directions. +- **GATT** (Generic Attribute Profile) is the data model at the heart of BLE. A GATT server contains **services** (categories of data), which contain **characteristics** (individual data points). Each service and characteristic is identified by a **UUID**. +- **Notifications** are the most efficient way to stream data. Instead of the central repeatedly polling, the peripheral pushes updates automatically when a value changes — dramatically reducing power consumption and latency. +- **The ESP32 BLE library** (`BLEDevice.h`) ships with the ESP32 Arduino core and requires no installation. It uses a **callback model** (not polling) for connection events — a different programming pattern than `Serial.available()`. +- **The `BLE2902` descriptor** must be added to any characteristic that supports notifications. Without it, centrals cannot subscribe. +- **The 20-byte MTU default** means BLE payloads should be kept compact. Send numbers as short strings or raw bytes, not verbose text. +- **After disconnection, the ESP32 stops advertising by default.** Always restart advertising in your `onDisconnect()` callback, or new centrals won't be able to find the device. +- **BLE works on the ESP32-S3, works with iPhones, and consumes dramatically less power than Bluetooth Classic or WiFi.** For most new wireless projects, BLE is the right default choice. + +## Resources + +- [ESP32 BLE Arduino library source and examples](https://github.com/espressif/arduino-esp32/tree/master/libraries/BLE) — the official library in the ESP32 Arduino core +- [ESP32 Arduino BLE API documentation](https://docs.espressif.com/projects/arduino-esp32/en/latest/api/ble.html) — Espressif's API reference +- [NimBLE-Arduino](https://github.com/h2zero/NimBLE-Arduino) — lighter-weight alternative BLE stack (~60% less flash, ~50% less RAM) +- [nRF Connect for Mobile](https://www.nordicsemi.com/Products/Development-tools/nrf-connect-for-mobile) — our recommended BLE debugging app (free, iOS + Android) +- [bleak](https://pypi.org/project/bleak/) — cross-platform BLE client library for Python +- [Bluetooth SIG Assigned Numbers](https://www.bluetooth.com/specifications/assigned-numbers/) — official list of standard service and characteristic UUIDs +- [Bluetooth SIG: Security Overview](https://www.bluetooth.com/learn-about-bluetooth/key-attributes/bluetooth-security/) — official overview of BLE security and pairing modes +- [Getting Started with ESP32 BLE on Arduino IDE](https://randomnerdtutorials.com/esp32-bluetooth-low-energy-ble-arduino-ide/) — Random Nerd Tutorials + +## Next Lesson + +In the [next lesson](ble-bidirectional.md), you'll learn how to send data in the *other* direction — from your phone or browser *to* the ESP32. You'll control the onboard NeoPixel over BLE, build a Web Bluetooth interface with sliders and a color picker, and learn about the Nordic UART Service (NUS) for serial-like text communication over BLE. Let's go! 🚀 + + diff --git a/esp32/bluetooth-serial.md b/esp32/bluetooth-serial.md new file mode 100644 index 00000000..44f7a244 --- /dev/null +++ b/esp32/bluetooth-serial.md @@ -0,0 +1,614 @@ +--- +layout: default +title: L2: Bluetooth Serial +description: "Send data wirelessly from the ESP32 with Bluetooth Classic's Serial Port Profile (SPP): pair on macOS or Windows to get a virtual COM port and reuse your existing pySerial and serial.js code." +parent: Wireless +grand_parent: ESP32 +has_toc: true # (on by default) +usemathjax: false +comments: true +usetocbot: true +nav_order: 2 +--- +# {{ page.title | replace_first:'L','Lesson ' }} +{: .no_toc } + +## Table of Contents +{: .no_toc .text-delta } + +1. TOC +{:toc} +--- + +{: .warning } +> This lesson is in draft form. There are missing circuit diagrams, images, videos, and other content. + + + + + +In the [last lesson](iot.md), you transmitted sensor data through WiFi, across the internet, and up to a cloud dashboard. But what if you just want to communicate with the laptop sitting right in front of you—**without a USB cable**? What if you could run the same Python scripts and terminal tools from the [Communication module](../communication/serial-intro.md), but wirelessly? + +In this lesson, we'll do exactly that using **Bluetooth**. And here's the fun part: the code on your computer is going to be *identical*. Bluetooth Classic's Serial Port Profile (SPP) creates a **virtual serial port** on your computer that looks and behaves exactly like a tethered USB serial connection. Your Python scripts, your terminal commands, your p5.js programs—they all work unchanged. The only difference is which port you select. ✨ + +{: .note } +> **In this lesson, you will learn:** +> - What Bluetooth is, its origin story, and why there are two very different flavors: Bluetooth Classic and Bluetooth Low Energy (BLE) +> - How the Serial Port Profile (SPP) creates a virtual serial port on your computer—making Bluetooth look exactly like a USB serial connection +> - How to use the `BluetoothSerial` library and why its API intentionally mirrors Arduino's built-in `Serial` +> - How to pair the ESP32 with your Mac or Windows computer and find the Bluetooth serial port +> - How to verify the connection using built-in OS tools (`cat`, `screen`, PowerShell)—no Python required +> - How to use Python and [pySerial](https://pyserial.readthedocs.io/) to communicate with the ESP32 over Bluetooth—using the same code patterns from the [Intro to Serial Communication lesson](../communication/serial-intro.md) +> - How to troubleshoot common Bluetooth Classic issues on macOS and Windows +> - Why Bluetooth Classic does **not** work on the ESP32-S3 and does **not** work with iPhones + +In the [next lesson](bluetooth-web-serial.md), we'll build on this foundation to stream live sensor data and create interactive [p5.js](https://p5js.org/) visualizations over Bluetooth using [Web Serial](../communication/web-serial.md). + +## What is Bluetooth? + +**Bluetooth is a short-range wireless communication** standard for exchanging data between devices over radio waves. It operates in the 2.4 GHz ISM band (the same frequency range as WiFi and your microwave oven) and is designed for low-power, close-range connections—typically within about 10 meters indoors. + +### A brief history + +Bluetooth was developed in the 1990s by [Ericsson](https://en.wikipedia.org/wiki/Ericsson) as a wireless replacement for RS-232 serial cables (the same serial communication we studied in [Lesson 1 of the Communication module](../communication/serial-intro.md)!). The name comes from [Harald Bluetooth](https://en.wikipedia.org/wiki/Harald_Bluetooth), a 10th-century Danish king who united warring Scandinavian tribes—a fitting metaphor for a technology designed to unite different devices. The Bluetooth logo is a [bind rune](https://en.wikipedia.org/wiki/Bind_rune) merging Harald's initials in a runic alphabet called [Younger Futhark](https://en.wikipedia.org/wiki/Younger_Futhark): ᚼ (Hagall, "H") and ᛒ (Bjarkan, "B"). + + + +### Two flavors: Classic and Low Energy + +When people say "Bluetooth," they might mean one of **two fundamentally different protocols** that happen to share a name: + +- **Bluetooth Classic** (also called BR/EDR, for "Basic Rate / Enhanced Data Rate") is the original Bluetooth. It was designed for **continuous data streaming**—wireless headphones, file transfers, or serial port emulation. It establishes a persistent connection and can push data at up to 3 Mbps at the radio level, though practical throughput for the Serial Port Profile is much lower (typically a few hundred kbps). This is the flavor we'll use in this lesson. + +- **Bluetooth Low Energy** (BLE, introduced in Bluetooth 4.0 in 2010) is a completely different protocol stack designed for **low-power, intermittent data exchange**—fitness trackers that run for months on a coin cell, sensors broadcasting a reading every few seconds. We'll cover BLE in [Lesson 4](ble-intro.md). + +Despite sharing the "Bluetooth" name, Classic and BLE are **not compatible with each other**. A BLE-only device cannot talk to a Bluetooth Classic device and vice versa. The original ESP32 supports **both**; the ESP32-S3 supports **BLE only**. + +| Feature | Bluetooth Classic (BR/EDR) | Bluetooth Low Energy (BLE) | +|---|---|---| +| Introduced | Bluetooth 1.0 (1999) | Bluetooth 4.0 (2010) | +| Design goal | Continuous streaming | Intermittent, low-power data | +| Data throughput | Up to 3 Mbps | ~1 Mbps (typically much less) | +| Power consumption | Higher | Very low (coin cell battery for months) | +| Connection model | Persistent stream (like serial) | Structured reads/writes/notifications | +| Range | ~10–30m (Class 2) | ~10–30m (similar) | +| Audio streaming | Yes (A2DP, HFP profiles) | Not originally (LE Audio added in BT 5.2) | +| iOS app support | **No** (Apple blocks SPP for third-party apps) | **Yes** | +| ESP32 (original) | ✅ | ✅ | +| **ESP32-S3** | **❌** | **✅** | +| ESP32-S2 | ❌ (no Bluetooth at all) | ❌ (no Bluetooth at all) | +| ESP32-C3, C6 | ❌ | ✅ | + +**Table.** Comparison of Bluetooth Classic and Bluetooth Low Energy. The original ESP32 supports both, but the ESP32-S3 only supports BLE. +{: .fs-1 } + +## Is Bluetooth Classic Still Used? + +Yes! Despite being over two decades old, Bluetooth Classic remains the dominant wireless audio protocol today. Every pair of wireless headphones you've likely used—Apple AirPods, Sony WH-1000XM series, Bose QuietComfort, JBL speakers—streams music over [A2DP](https://en.wikipedia.org/wiki/List_of_Bluetooth_profiles#Advanced_Audio_Distribution_Profile_(A2DP)), a Bluetooth Classic profile. Moreover, Bluetooth keyboards, mice, and game controllers also typically use [Classic's HID profile](https://en.wikipedia.org/wiki/List_of_Bluetooth_profiles#Human_Interface_Device_Profile_(HID)). Many modern devices are actually "dual-mode": AirPods, for example, stream audio over Bluetooth Classic while simultaneously using BLE for Apple's Find My network and proximity pairing. + +That said, Bluetooth Classic's expected replacement is underway—LE Audio (introduced in Bluetooth 5.2) brings a new, more efficient audio codec (LC3) and features like [Auracast broadcast audio](https://www.bluetooth.com/auracast/), and most new devices now ship as dual-mode during the transition. + +{: .note } +> **Why doesn't the ESP32-S3 support Bluetooth Classic?** Espressif designed the ESP32-S3 for IoT and edge AI workloads where BLE's low power consumption matters more than Classic's streaming capabilities. Dropping the Classic radio reduces the hardware die area, power consumption, and cost. If you try to compile a `BluetoothSerial` sketch on the ESP32-S3, you'll get the error: `Serial Bluetooth not available or not enabled. It is only available for the ESP32 chip.` This is a chip-level limitation, not a software bug. + +## The Serial Port Profile (SPP) + +One very cool and useful aspect of Bluetooth Classic is that we can make it act just like a serial cable, so all of our [Serial Communication lessons](../communication/) are relevant. This is done via the **[Serial Port Profile (SPP)](https://www.bluetooth.com/specifications/specs/html/?src=SPP_v1.2/out/en/index-en.html)**, which emulates a wired RS-232 serial port—exactly the kind of serial communication we've been doing over USB. + +When you pair the ESP32 with your computer over Bluetooth Classic, your operating system creates a **virtual serial port**—a COM port on Windows (*e.g.,* `COM8`) or a `/dev/tty.*` device on macOS (*e.g.,* `/dev/tty.ESP32-Bluetooth`). This virtual port behaves *identically* to the USB serial port you've been using all along. Any software that can open a serial port—the Arduino Serial Monitor, a Python script with [pySerial](https://pyserial.readthedocs.io/), a web browser using the [Web Serial API](../communication/web-serial.md), the [serial.js](https://github.com/makeabilitylab/js/blob/main/src/lib/serial/serial.js) library—can communicate over Bluetooth without any code changes. Just select the Bluetooth port instead of the USB port. + + + +This is a key insight of this lesson: **Bluetooth Classic SPP is a wireless serial cable.** So, everything you learned in the [Communication module](../communication/serial-intro.md)—data framing, parsing comma-separated values, terminal tools—works unchanged. The only difference is the transport: radio waves instead of copper wire. + +
+How SPP works under the hood (click to expand) + +SPP sits on top of several Bluetooth Classic protocol layers. At the bottom, the **L2CAP** (Logical Link Control and Adaptation Protocol) layer provides connection-oriented data channels over the Bluetooth radio. Above that, **RFCOMM** (Radio Frequency Communication) emulates RS-232 serial ports over L2CAP—this is the layer that makes Bluetooth look like a wired serial connection. SPP is a *profile* that defines how RFCOMM should be used for general-purpose serial communication. When your computer pairs with the ESP32, the **SDP** (Service Discovery Protocol) lets the computer discover that the ESP32 offers an SPP service, and the OS creates a virtual COM port backed by an RFCOMM channel. You don't need to know any of this to use SPP—the `BluetoothSerial` library handles it all—but it explains why the abstraction works so seamlessly. + +
+ +## Two Important Notes Before We Build + +Two important notes before we get started building: + +**This lesson requires the original ESP32** (like the [Adafruit Huzzah32](https://www.adafruit.com/product/3405) or [Espressif ESP32-DevKitC V4](https://docs.espressif.com/projects/esp-dev-kits/en/latest/esp32/esp32-devkitc/user_guide.html)), **not** the ESP32-S3. The ESP32-S3 does not have the hardware for Bluetooth Classic—the `BluetoothSerial` library will not compile on it. If you're taking one of our classes, you can borrow an ESP32 board from us. If you only have access to an ESP32-S3, skip ahead to [Lesson 4: Introduction to BLE](ble-intro.md), which works with both boards. + +**Apple iPhones will not work. 😢** Apple does not allow third-party apps to use Bluetooth Classic SPP on iOS, so **iPhones cannot connect to the ESP32 over Bluetooth Classic**. This lesson is entirely computer-based (Mac and Windows), so your phone type doesn't matter for Parts 1 and 2. If you have an **Android** phone, there's an optional bonus activity in Part 3. In [Lesson 4: BLE](ble-intro.md), we'll use a protocol that works with *everyone's* phone—including iPhones. + +## Materials + +You'll need the following components. This lesson uses the **original ESP32** (in our case, the [Adafruit Huzzah32 ESP32 Feather](https://www.adafruit.com/product/3591)), not the ESP32-S3. + +| ESP32 | +| :-----:| +| ![Adafruit Huzzah32 ESP32 Feather board, top view]({{ site.baseurl }}/assets/images/ESP32Huzzah32_Adafruit_vertical_h200.png) | +| [Huzzah32 ESP32 Feather](https://www.adafruit.com/product/3591) | + +You will also need: +- A **Mac or Windows computer** with Bluetooth (most modern laptops have Bluetooth built in) +- **Python 3** with [pySerial](https://pyserial.readthedocs.io/) installed (`pip3 install pyserial`) + +{: .note } +> **No external circuit components needed for this lesson!** We'll just use the ESP32 by itself to establish the wireless link. In the [next lesson](bluetooth-web-serial.md), we'll add a potentiometer and LED for sensor streaming and bidirectional control. + +{: .note } +> If you only have an ESP32-S3, you can skip ahead to [Lesson 4: Bluetooth Low Energy](ble-intro.md). + +## Part 1: Hello Bluetooth + +Let's cut the wire! ✂️🔌 + +In this first activity, we'll upload a Bluetooth serial sketch to the ESP32, pair it with your computer, and verify the connection using built-in OS tools—no Python or external dependencies. We'll just use your OS's built-in terminal. This way, if anything goes wrong, you'll know immediately whether it's a Bluetooth issue or a software issue. + +### The BluetoothSerial library + +The ESP32 Arduino core includes a built-in library called [`BluetoothSerial`](https://github.com/espressif/arduino-esp32/tree/master/libraries/BluetoothSerial) that handles all the Bluetooth Classic SPP complexity for you. No library installation is needed—just `#include "BluetoothSerial.h"` and you're ready to go. + +The library's API was **intentionally designed to mirror** Arduino's built-in `Serial` class. It provides the same `.begin()`, `.available()`, `.read()`, `.write()`, `.print()`, and `.println()` methods you already know. This means converting a wired serial sketch to Bluetooth is as simple as creating a `BluetoothSerial` object and using it alongside (or instead of) `Serial`. The rest of your code stays identical! 🎉 + +| Method | `Serial` (USB) | `SerialBT` (Bluetooth) | Notes | +|---|---|---|---| +| Initialize | `Serial.begin(115200)` | `SerialBT.begin("ESP32-BT")` | Serial takes a baud rate; SerialBT takes a device name (baud rate is negotiated by the Bluetooth stack) | +| Check for data | `Serial.available()` | `SerialBT.available()` | Identical — returns number of bytes waiting | +| Read a byte | `Serial.read()` | `SerialBT.read()` | Identical | +| Read until delimiter | `Serial.readStringUntil('\n')` | `SerialBT.readStringUntil('\n')` | Identical | +| Write bytes | `Serial.write(buf, len)` | `SerialBT.write(buf, len)` | Identical | +| Print text | `Serial.println("hello")` | `SerialBT.println("hello")` | Identical — also `.print()`, `.printf()` | +| Check connection | *(always connected)* | `SerialBT.connected()` | Returns `true` if a device is currently paired and connected | +| Connection events | *(n/a)* | `SerialBT.register_callback(cb)` | Optional callback for connect/disconnect events — no Serial equivalent | + +**Table.** Key API comparison between Arduino's built-in `Serial` and the `BluetoothSerial` library. Every read/write method is identical — only initialization and connection management differ. +{: .fs-1 } + +The key difference is in `.begin()`: while the traditional `Serial.begin()` takes a baud rate because it configures a physical UART, `SerialBT.begin()` takes a *device name* because the Bluetooth stack handles data rates internally. The other difference is that Bluetooth connections can come and go—unlike a USB cable, a Bluetooth device might walk out of range—so `BluetoothSerial` adds `connected()` and `register_callback()` for connection state management. We'll explore these in the [next lesson](bluetooth-web-serial.md) when we discuss what happens when a connection drops. + +{: .warning } +> Reminder: `BluetoothSerial` is **only available on the original ESP32 chip**. If you try to include it on an ESP32-S3 (or C3, S2, *etc.*), the sketch will not compile. + +### The Arduino code + +Let's start with the Arduino code. The following sketch creates a bidirectional bridge between the USB serial connection (to your computer via USB) and a Bluetooth serial connection (to your computer via Bluetooth). Anything sent over Bluetooth arrives on USB serial and vice versa. It also sends a periodic message so you can immediately see that data is flowing. When data is sent or received over Bluetooth, the built-in red LED (pin 13) flashes briefly as a visual heartbeat. + +The full source is available in our [Arduino GitHub repo](https://github.com/makeabilitylab/arduino/tree/master/ESP32/Bluetooth/HelloBluetooth). + +```cpp +#include "BluetoothSerial.h" + +// LED_BUILTIN is defined by the board package (e.g., pin 13 on the Huzzah32). +// We alias it here so you can easily swap in a different pin if needed. +const int LED_PIN = LED_BUILTIN; +const unsigned long LED_FLASH_MS = 60; // How long a single flash cycle takes +const unsigned int NUM_FLASHES = 3; + +BluetoothSerial SerialBT; + +unsigned long _lastMsgMs = 0; +unsigned long _msgCount = 0; +const unsigned long GREETING_INTERVAL_MS = 2000; + +void setup() { + Serial.begin(115200); + pinMode(LED_PIN, OUTPUT); + + // Initialize Bluetooth with a device name. You can choose any name you + // like — "ESP32-Bluetooth", "Jon's ESP32", "Pikachu", etc. This is the + // friendly name that appears when you scan for Bluetooth devices on your + // computer (pick something recognizable in a classroom full of ESP32s!). + SerialBT.begin("ESP32-Bluetooth"); + + Serial.println("Bluetooth started! You can now pair with 'ESP32-Bluetooth'."); + Serial.println("Open a Bluetooth serial connection to see messages."); + Serial.println("Anything you type here will be forwarded over Bluetooth (and vice versa).\n"); +} + +void loop() { + // Periodically send message updates + unsigned long now = millis(); + if (now - _lastMsgMs >= GREETING_INTERVAL_MS) { + _lastMsgMs = now; + _msgCount++; + + String msg = "Hello from ESP32! [Msg #" + String(_msgCount) + + " | Uptime: " + String(now / 1000.0, 1) + "s]"; + + // Check if Bluetooth Serial is connected + if (SerialBT.connected()) { + // Send Bluetooth message + SerialBT.println("[Bluetooth] " + msg); + flashLED(); // Visual confirmation: data sent over Bluetooth + } else { + // Send USB Serial message + Serial.println("[USB Serial] Waiting for Bluetooth connection..."); + } + Serial.println("[USB Serial] " + msg); + } + + // Forward everything received from USB Serial (e.g., typed in Serial Monitor) + // to the Bluetooth peer. We use read()/write() (byte-at-a-time) rather than + // readStringUntil() because it's non-blocking — the loop keeps running without + // waiting for a newline or timeout. + while (Serial.available()) { + SerialBT.write(Serial.read()); + } + + // Forward everything received over Bluetooth to USB Serial. + // The outer `if` avoids flashing when there's nothing to read, and + // ensures we flash once per burst of data rather than once per byte. + if (SerialBT.available()) { + while (SerialBT.available()) { + Serial.write(SerialBT.read()); + } + flashLED(); // Visual confirmation: data received over Bluetooth + } +} + +/** + * Briefly flashes the built-in LED. Uses a blocking delay, which is + * fine for a simple example — the ~180 ms pause won't noticeably affect + * the 2-second greeting interval or byte-at-a-time forwarding. + */ +void flashLED() { + for(int i=0; i< NUM_FLASHES; i++){ + digitalWrite(LED_PIN, HIGH); + delay(LED_FLASH_MS / 2); + digitalWrite(LED_PIN, LOW); + delay(LED_FLASH_MS / 2); + } +} +``` + +Notice how the code reads like a standard serial sketch — compare the `SerialBT` calls with the `Serial` calls and you'll see the API mirroring from the [table above](#the-bluetoothserial-library) in action. The one difference is `SerialBT.begin("ESP32-Bluetooth")`: instead of a baud rate, it takes a device name that will appear when you scan for Bluetooth devices on your computer. + +You can choose any name you like—"ESP32-Bluetooth", "MyPotentiometer", "Jon's ESP32", or even "Chewbacca". This is the friendly name that will appear in your computer's or phone's Bluetooth settings when scanning for nearby devices, so pick something recognizable (especially in a classroom full of ESP32s!). + +Upload this sketch to your ESP32 and open Serial Monitor at 115200 baud. You should see greeting messages appearing every 2 seconds, prefixed with `[USB Serial]`: + +```text +[USB Serial] Msg #1 | Uptime: 2.0s +[USB Serial] Msg #2 | Uptime: 4.0s +[USB Serial] Msg #3 | Uptime: 6.0s +``` + +This confirms the sketch is running. Now let's pair and see those messages arrive wirelessly. + +{: .note } +**What about the flickering orange LED?** On the Adafruit Huzzah32, you may notice an orange LED near the USB jack that flickers constantly. This is the CHG (charge) LED. It's hardwired to the LiPo battery charging circuit and is not controllable in code. It flickers when no battery is connected. You can safely ignore it. + +{: .note } +**Have an ESP32 with a built-in NeoPixel?** If you're using the [Adafruit ESP32 Feather V2](https://www.adafruit.com/product/5400) (or have an external NeoPixel wired up), check out [HelloBluetoothRGB](https://github.com/makeabilitylab/arduino/tree/master/ESP32/Bluetooth/HelloBluetoothRGB) — it flashes blue for Bluetooth send and green for Bluetooth receive, making it easy to distinguish data direction. + +### Pairing with your computer + +Before you can communicate over Bluetooth, you need to **pair** your computer with the ESP32. This is a one-time step—once paired, your computer will remember the device. + +#### macOS + +1. Open **System Settings → Bluetooth** (or click the Bluetooth icon in the menu bar). +2. Make sure Bluetooth is turned on. You should see `"ESP32-Bluetooth"` appear in the nearby devices list. +3. Click **Connect** next to `"ESP32-Bluetooth"`. macOS will pair with the device. +4. Once paired, macOS creates a virtual serial port. To find it, open **Terminal** and run: + +```bash +ls /dev/tty.*Bluetooth* +``` + +You should see something like `/dev/tty.ESP32-Bluetooth` or `/dev/tty.ESP32-BluetoothSPP`. This is your Bluetooth serial port. + + + +{: .note } +> **Troubleshooting macOS:** If the Bluetooth serial port doesn't appear, try unpairing and re-pairing the device. On some macOS versions, you may need to open **Terminal** and run `ls /dev/tty.*` before and after pairing to identify the new port. The port name varies by macOS version and Bluetooth stack, but it typically contains the device name. + +#### Windows + +1. Open **Settings → Bluetooth & devices** (or **Settings → Devices → Bluetooth & other devices** on Windows 10). +2. Click **Add device → Bluetooth**. Windows will scan for nearby devices. +3. Select `"ESP32-Bluetooth"` and click **Pair**. +4. Once paired, Windows creates a virtual COM port. To find it, open **Device Manager** (right-click the Start button → Device Manager) and expand **Ports (COM & LPT)**. Look for a `"Standard Serial over Bluetooth link"` entry—note its COM port number (e.g., `COM8`). + + + +{: .note } +> **Windows creates two COM ports** for each Bluetooth SPP pairing: one for outgoing and one for incoming connections. You want the **outgoing** port—this is the one that actually initiates the SPP data channel. If one connects but shows no data, try the other. You can check which is which in **Control Panel → Devices and Printers → right-click ESP32-Bluetooth → Properties → Services**. + +### Verifying the connection + +Now let's verify that data is actually flowing over Bluetooth. We'll **use built-in OS tools**—no Python, no additional installs—so if something goes wrong, you'll know immediately that it's a Bluetooth issue, not a software setup issue. + +#### macOS / Linux + +Open **Terminal** and run: + +```bash +cat /dev/tty.ESP32-Bluetooth +``` + +Replace the port name with whatever `ls /dev/tty.*Bluetooth*` showed you. You should immediately see greetings streaming in: + +```text +[Bluetooth] Msg #1 | Uptime: 2.0s +[Bluetooth] Msg #2 | Uptime: 4.0s +[Bluetooth] Msg #3 | Uptime: 6.0s +``` + +Press **Ctrl+C** to stop. + +Now try `screen`, which provides a more interactive serial terminal: + +```bash +screen /dev/tty.ESP32-Bluetooth 115200 +``` + +In `screen`, you can type characters that will be forwarded to the ESP32. To exit `screen`, press **Ctrl+A** then **K**, then confirm with **y**. On Mac, use the `control` key — labeled `⌃` — not `⌘ Command`. + +#### Windows + +If you use Windows, we've made a [PowerShell script](https://learn.microsoft.com/en-us/powershell/scripting/overview) called [`serial_reader.ps1`](https://github.com/makeabilitylab/arduino/blob/master/PowerShell/serial_reader.ps1) that reads from a COM port with zero dependencies. + +Download it and run from your PowerShell terminal: + +```powershell +# List available COM ports +.\serial_reader.ps1 + +# Connect to the Bluetooth COM port +.\serial_reader.ps1 -Port COM16 +``` + +{: .note } +> If you get an execution policy error, run this once first: +> ```powershell +> Set-ExecutionPolicy -Scope CurrentUser -ExecutionPolicy RemoteSigned +> ``` + +You should see messages streaming in: + +```text +Connected! Listening for data... + +[Bluetooth] Msg #1 | Uptime: 2.0s +[Bluetooth] Msg #2 | Uptime: 4.0s +[Bluetooth] Msg #3 | Uptime: 6.0s +``` + +Press **Ctrl+C** to stop. + +**If you see messages, congratulations—you're communicating wirelessly! 🎉** The ESP32 is sending data over Bluetooth, your computer is receiving it through a virtual serial port, and no USB cable was involved. This is the core magic of SPP. + +{: .warning } +> **Only one program can open a serial port at a time.** If you have Arduino's Serial Monitor open on the Bluetooth COM port, `cat`/`screen`/PowerShell won't be able to connect (and vice versa). Close Serial Monitor before trying the terminal commands—or keep Serial Monitor on the *USB* port (COM4) and use the terminal on the *Bluetooth* port. This is the same constraint from the [serial introduction](../communication/serial-intro.md#only-one-program-can-open-a-serial-port-at-a-time), just with two ports to manage. + +**If you don't see any data**, see the [Troubleshooting Bluetooth connections](#troubleshooting-bluetooth-connections) section below before moving on. + +### Workbench demo of Bluetooth terminal programs + + + + + +## Part 2: Connecting with Python + +Now that you've verified Bluetooth is working, let's connect from Python—using the same [pySerial](https://pyserial.readthedocs.io/) library you used in the [serial introduction lesson](../communication/serial-intro.md). This is where SPP really shines: your existing Python serial code works over Bluetooth with only a port name change. + +### Setting up pySerial + +If you don't already have pySerial installed, you'll need to install it. The recommended approach is to use a **virtual environment**, which keeps your project's dependencies isolated from your system Python: + +```bash +# Create a virtual environment (one-time setup) +python3 -m venv venv + +# Activate it +# macOS / Linux: +source venv/bin/activate +# Windows: +venv\Scripts\activate + +# Install pySerial +pip install pyserial +``` + +Once activated, you'll see `(venv)` at the beginning of your terminal prompt. You'll need to run the `activate` command each time you open a new terminal window to work on this project. + +{: .note } +> **Why a virtual environment?** Modern macOS (via Homebrew) and some Linux distributions block global `pip install` commands to protect system Python ([PEP 668](https://peps.python.org/pep-0668/)). You'll see an `externally-managed-environment` error if you try `pip install pyserial` without a venv. A virtual environment solves this cleanly and is good practice for any Python project. + +{: .note } +> **Windows users:** Use `python` instead of `python3` throughout this lesson. On Windows, the `python3` command often triggers a misleading Microsoft Store redirect instead of running Python. On macOS and Linux, either `python` or `python3` works, but `python3` is the safer choice to avoid accidentally invoking Python 2 on older systems. + +### Reading Bluetooth data with serial_reader.py + +We provide a simple Python script called [`serial_reader.py`](https://github.com/makeabilitylab/arduino/tree/master/Python/SerialReader) that connects to a serial port and prints whatever data arrives. It's the Python equivalent of the `cat` or PowerShell commands you just used—but now you're using pySerial, the same library you'll use for more sophisticated projects. + +First, list available ports to find your Bluetooth serial port: + +```bash +# macOS / Linux +python3 serial_reader.py --list + +# Windows +python serial_reader.py --list +``` + +On Windows, you should see something like: + +```text +Available serial ports: + COM1 - Communications Port (COM1) + COM4 - Silicon Labs CP210x USB to UART Bridge (COM4) + COM16 - Standard Serial over Bluetooth link (COM16) + COM17 - Standard Serial over Bluetooth link (COM17) +``` + +COM4 is your **tethered USB serial connection** (the CP210x chip on the Huzzah32). COM16 and COM17 are Bluetooth serial ports—Windows creates two for each Bluetooth SPP pairing: one for outgoing and one for incoming connections. You typically want the first one listed, but if it doesn't work, try the other. + +Now connect: + +```bash +# macOS / Linux +python3 serial_reader.py /dev/tty.ESP32-Bluetooth 115200 + +# Windows +python serial_reader.py COM16 115200 +``` + +You should see the same greetings you saw in Part 1: + +```text +Connected! Listening for data... + +[Bluetooth] Msg #42 | Uptime: 84.0s +[Bluetooth] Msg #43 | Uptime: 86.0s +[Bluetooth] Msg #44 | Uptime: 88.0s +``` + +Press **Ctrl+C** to stop. + +{: .note } +> **The baud rate parameter is ignored for Bluetooth virtual COM ports** on most operating systems. SPP negotiates its own data rate at the Bluetooth protocol level, so the `baudrate=115200` argument is passed to pySerial for API compatibility but doesn't actually set a baud rate the way it does for USB serial. You can pass any value and it will work—but we use `115200` to match our `Serial.begin(115200)` for consistency. + +### Sending data with serial_demo.py + +The `serial_reader.py` script only listens. To test bidirectional communication, use [`serial_demo.py`](https://github.com/makeabilitylab/arduino/blob/master/Python/Serial/serial_demo.py)—the interactive send-and-receive script from the [Communication module](../communication/serial-intro.md). It lets you type a number, sends it to the ESP32, and prints the echoed response: + +```bash +# macOS / Linux +python3 serial_demo.py /dev/tty.ESP32-Bluetooth 115200 + +# Windows +python serial_demo.py COM16 115200 +``` + +Type a number and press Enter—it will be sent to the ESP32 over Bluetooth, forwarded to USB Serial Monitor, and you can see it arrive wirelessly. You're communicating bidirectionally! 🎉 + +{: .note } +> **This is the point of SPP.** Both `serial_reader.py` and `serial_demo.py` were written for USB serial. They work over Bluetooth with only a port name change. The pySerial API, the `readline()` calls, the `write()` calls—everything is the same. Your operating system makes Bluetooth look like a wired serial connection. + +### Workbench demo of Python Bluetooth + + + +## Part 3: Android phone (optional bonus) + +If you have an **Android** phone, you can also communicate with the ESP32 using a Bluetooth terminal app—no laptop needed. This is an optional bonus activity since the rest of the lesson is computer-based. + +{: .note } +> **iPhone users:** You cannot use Bluetooth Classic SPP from an iPhone. Apple restricts Bluetooth Classic to system-level functions (audio, keyboards, *etc.*). Don't worry—in [Lesson 4: BLE](ble-intro.md), we'll use a protocol that works with both iOS and Android. + +1. On your Android phone, install the free [Serial Bluetooth Terminal](https://play.google.com/store/apps/details?id=de.kai_morich.serial_bluetooth_terminal) app by Kai Morich. +2. Go to **Settings → Bluetooth** and pair with `"ESP32-Bluetooth"` (the device name from the `HelloBluetooth` sketch). +3. Open the Serial Bluetooth Terminal app → **Devices** → select `"ESP32-Bluetooth"` → **Connect**. +4. You should see the periodic `[Bluetooth] Msg #N | Uptime: ...` greetings streaming in—just like in your terminal. Type a message and tap Send; it will be forwarded to the ESP32 and echoed back over USB Serial Monitor. + + + +{: .note } +> **This is the same SPP magic, but from your phone.** The Android app speaks Bluetooth Classic SPP the same way `cat` and pySerial do—byte stream over a virtual serial channel. We'll do more interesting things with this in the [next lesson](bluetooth-web-serial.md) once we've added a potentiometer and an LED to the circuit. + +## Troubleshooting Bluetooth connections + +Bluetooth Classic SPP is straightforward once it works, but the initial setup can be finicky—especially on Windows. Here are the most common issues and how to resolve them. + +### General issues + +**"Only one program can open a serial port at a time."** If Arduino's Serial Monitor, PuTTY, a Python script, or any other program has the Bluetooth COM port open, nothing else can use it. Close all other serial programs before trying a new one. You *can* have Serial Monitor open on the USB port (COM4) while using Python on the Bluetooth port (COM16)—they're separate ports. + +**"No data, but no error either."** The connection opened successfully but nothing appears. Make sure the Arduino sketch is actually running—check Serial Monitor on the USB port for `[USB Serial]` messages. If USB Serial is working but Bluetooth isn't, the issue is on the Bluetooth/OS side, not the Arduino side. + +**"Bluetooth connection drops frequently."** Bluetooth Classic SPP has a practical range of about 5–10 meters indoors. Walls, furniture, and other 2.4 GHz devices (WiFi routers, microwaves) reduce range and can cause interference. Move closer to the ESP32 and away from other wireless devices. + +### macOS-specific issues + +**Port doesn't appear after pairing.** Try unpairing and re-pairing the device. Run `ls /dev/tty.*` before and after pairing to spot the new port. The port name varies by macOS version but typically contains the device name (e.g., `/dev/tty.ESP32-Bluetooth`). + +**`externally-managed-environment` error when installing pySerial.** Modern macOS (via Homebrew) blocks global `pip install` to protect system Python. Use a virtual environment as described in [Part 2](#setting-up-pyserial). This is the correct fix—avoid using `--break-system-packages` as it can cause problems with future Homebrew updates. + +**`python3: command not found`.** Install Python 3 from [python.org](https://www.python.org/downloads/) or via Homebrew (`brew install python`). + +### Windows-specific issues + +**Two COM ports appear.** Windows creates two COM ports for each Bluetooth SPP pairing: one for outgoing and one for incoming. You want the **outgoing** port. If one connects but shows no data, try the other. Check which is which in **Control Panel → Devices and Printers → right-click ESP32-Bluetooth → Properties → Services**. + +**COM port hangs when connecting.** Some USB Bluetooth adapters—especially those with Realtek chipsets, such as the TP-Link UB500—have driver issues with Bluetooth Classic SPP on Windows. Symptoms include: pairing succeeds, COM ports appear, but the connection hangs or no data flows. If you experience this, try updating your adapter's drivers from the manufacturer's website, using your laptop's built-in Bluetooth adapter instead (if available), or testing with a different Bluetooth adapter. + +{: .note } +> **Isolating adapter issues.** If you're not sure whether the problem is your code or your Bluetooth adapter, use the [`serial_reader.ps1`](https://github.com/makeabilitylab/arduino/tree/master/PowerShell) PowerShell script—it has zero dependencies and uses Windows' built-in .NET serial classes. If the PowerShell script also can't receive data, the problem is your adapter or driver, not your Python code. You can also try [PuTTY](https://www.putty.org/) (Connection type: Serial, your COM port, 115200 baud, Flow control: None) as a third independent test. + +**`python` is not recognized / Microsoft Store redirect.** On Windows, use `python` instead of `python3`. If `python` isn't recognized, reinstall Python from [python.org](https://www.python.org/downloads/) and make sure **"Add Python to PATH"** is checked during installation. You can also disable the Microsoft Store redirect in **Settings → Apps → Advanced app settings → App execution aliases** by toggling off the `python.exe` and `python3.exe` aliases. + +**ESP32 shows "Not connected" in Bluetooth settings.** This is normal for Bluetooth Classic SPP on Windows. The "Connected" status only appears during an active data session, not just from pairing. The device is paired and ready—it will show "Connected" once you open the COM port. + +### Still stuck? + +If the tips above don't resolve your issue, try describing your problem to an AI assistant like [Claude](https://claude.ai) or [Gemini](https://gemini.google.com). Include the specific error messages you're seeing, your operating system, your Bluetooth adapter (built-in or external), and what you've already tried. These tools are especially helpful for debugging driver issues and platform-specific quirks. You can also ask on the course discussion board—chances are another student has hit the same issue. + +## Lesson Summary + +In this lesson, you cut the wire! Here's what you learned: + +- **Bluetooth** is a short-range wireless standard with two incompatible flavors: **Classic** (continuous streaming, what we used here) and **Low Energy / BLE** (low-power structured data, coming up next). +- **The `BluetoothSerial` library** ships with the ESP32 Arduino core and intentionally mirrors the `Serial` API. Converting wired serial code to Bluetooth requires minimal changes on both the Arduino and computer side. +- **Bluetooth Classic's Serial Port Profile (SPP)** creates a virtual serial port on your computer that behaves identically to a USB serial port. Any program that can talk to USB serial—Serial Monitor, `cat`/`screen`, PowerShell, pySerial—works unchanged over Bluetooth. Just select the Bluetooth port instead of the USB port. +- **Pairing** the ESP32 with your Mac or Windows computer is a one-time step. macOS creates a `/dev/tty.*` device; Windows creates a COM port (in fact, two—you usually want the first). +- **pySerial** connects to the Bluetooth serial port with the same API you already know—only the port name changes. +- **Android phones** can connect to Bluetooth Classic SPP using a terminal app, but **iPhones cannot**—Apple blocks SPP for third-party apps. +- **Bluetooth Classic only works on the original ESP32**, not the ESP32-S3, S2, C3, or C6. This is a hardware-level limitation. + +In the [next lesson](bluetooth-web-serial.md), we'll build on this foundation: add a potentiometer and LED to the circuit, stream live sensor data, and build interactive [p5.js](https://p5js.org/) visualizations using [Web Serial](../communication/web-serial.md)—all over Bluetooth. + +## Resources + +- [BluetoothSerial library source and examples](https://github.com/espressif/arduino-esp32/tree/master/libraries/BluetoothSerial) — the official library in the ESP32 Arduino core +- [ESP32 Arduino Bluetooth API docs](https://docs.espressif.com/projects/arduino-esp32/en/latest/api/bluetooth.html) — Espressif's API reference +- [pySerial documentation](https://pyserial.readthedocs.io/en/latest/) — the Python serial library used throughout our lessons +- [Serial Bluetooth Terminal](https://play.google.com/store/apps/details?id=de.kai_morich.serial_bluetooth_terminal) — our recommended Android app for Bluetooth serial (free, by Kai Morich) +- [Bluetooth SIG: Learn About Bluetooth](https://www.bluetooth.com/learn-about-bluetooth/tech-overview/) — official overview of Bluetooth technology +- [Random Nerd Tutorials: ESP32 Bluetooth Classic](https://randomnerdtutorials.com/esp32-bluetooth-classic-arduino-ide/) — additional Bluetooth Classic tutorials + +## Next Lesson + +In the [next lesson](bluetooth-web-serial.md), we'll add a potentiometer and LED to the circuit and use [Web Serial](../communication/web-serial.md) with [p5.js](https://p5js.org/) to build interactive visualizations and bidirectional controls—all running wirelessly over the Bluetooth serial link you just established. Let's go! 🚀 + + diff --git a/esp32/bluetooth-web-serial.md b/esp32/bluetooth-web-serial.md new file mode 100644 index 00000000..a9e6405e --- /dev/null +++ b/esp32/bluetooth-web-serial.md @@ -0,0 +1,579 @@ +--- +layout: default +title: L3: Bluetooth Web Serial +description: "Stream ESP32 sensor data over Bluetooth and visualize it in Python and the browser, then build a bidirectional color mixer where the web page and a potentiometer share control of a NeoPixel." +parent: Wireless +grand_parent: ESP32 +has_toc: true # (on by default) +usemathjax: false +comments: true +usetocbot: true +nav_order: 3 +--- +# {{ page.title | replace_first:'L','Lesson ' }} +{: .no_toc } + +## Table of Contents +{: .no_toc .text-delta } + +1. TOC +{:toc} +--- + +{: .warning } +> This lesson is in draft form. There are missing circuit diagrams, images, videos, and other content. + + + + + +In the [last lesson](bluetooth-serial.md), you established a wireless serial connection between the ESP32 and your computer using Bluetooth Classic's Serial Port Profile (SPP). You verified the wireless link with terminal tools and Python, but we weren't yet sending real sensor data. + +In this lesson, we'll add a **potentiometer** to the circuit, stream live sensor readings over Bluetooth, and visualize them three different ways: in Python, in a hosted **SerialTest** web app that needs no install, and in our familiar [p5.js](https://p5js.org/) sketches via [Web Serial](../communication/web-serial.md) and the [serial.js](https://github.com/makeabilitylab/js/blob/main/src/lib/serial/serial.js) library—the same tools from the [Communication module](../communication/index.md), but now wireless. Finally, we'll close the loop with a **bidirectional color mixer**: a browser hue picker controls a NeoPixel's color while the pot on the breadboard controls its brightness, with the browser UI staying in sync over the wireless link. + +{: .note } +> **In this lesson, you will learn:** +> - How to stream live analog sensor data over Bluetooth and visualize it in Python +> - How to use our [serial.js](https://github.com/makeabilitylab/js) library—and a hosted Web Serial test app—to inspect Bluetooth data in the browser with zero setup +> - How to reuse existing [p5.js](https://p5js.org/) + Web Serial sketches over a Bluetooth port without changing a single line of code +> - How to design a **bidirectional** interaction where each end of the wireless link owns a different part of the shared state +> - How to handle connection drops gracefully on both the ESP32 and the computer +> - When to choose Bluetooth Classic vs. BLE for your own projects + +{: .note } +> **Prerequisites:** This lesson builds directly on [Lesson 2: Bluetooth Serial](bluetooth-serial.md). You should have already paired your ESP32 with your computer and verified the connection works. The platform requirements from L2 still apply: you'll need an ESP32 with **Bluetooth Classic** (e.g., the original Huzzah32 or the Adafruit ESP32 Feather V2). The ESP32-S3 doesn't support Bluetooth Classic. **iPhone users**: phone-based activities in this lesson are Android-only; see [Lesson 4: BLE](ble-intro.md) for a phone-friendly alternative. See L2's [Two Important Notes Before We Build](bluetooth-serial.md#two-important-notes-before-we-build) for the full context. + +## Materials + +In addition to the materials from [Lesson 2](bluetooth-serial.md#materials), you'll need: + +| Breadboard | ESP32 | Potentiometer | NeoPixel | +| ---------- |:-----:|:-----:|:-----:| +| ![Half-sized solderless breadboard]({{ site.baseurl }}/assets/images/Breadboard_Half.png) | ![Adafruit ESP32 Feather, top view]({{ site.baseurl }}/assets/images/ESP32Huzzah32_Adafruit_vertical_h200.png) | ![10kΩ rotary potentiometer]({{ site.baseurl }}/assets/images/PanelMountPotentiometer_NoCap_150h.jpg) | WS2812B 8-LED stick | +| Breadboard | ESP32 with BT Classic (see below) | 10kΩ Potentiometer | NeoPixel (onboard or external) | + +**Choose one ESP32 board with Bluetooth Classic:** +- **Adafruit ESP32 Feather V2** ([product page](https://www.adafruit.com/product/5400)) — recommended. Has an **onboard NeoPixel**, so no extra wiring is needed for Part 5. +- **Adafruit Huzzah32 ESP32 Feather** ([product page](https://www.adafruit.com/product/3405)) — also works. For Part 5 you'll need an **external NeoPixel** (see below). +- Other original-ESP32 dev boards (SparkFun ESP32 Thing, Espressif ESP32-DevKitC, *etc.*) also work; check that the board has Bluetooth Classic (the ESP32-S3, S2, C3, and C6 do not). + +**NeoPixel options for Part 5:** +- If your board has an onboard NeoPixel (Feather V2), you're done—no extra parts. +- Otherwise, use any external WS2812B/SK6812 addressable LED: the **8-LED stick** in our course kits is perfect. You can also use a single NeoPixel, a strip, a ring, or a matrix—the code is the same, only the LED count changes. See the [Addressable LEDs lesson](../advancedio/addressable-leds.md) for background on the hardware and wiring conventions. + +You will also need: +- **Google Chrome** or **Microsoft Edge** (for the Web Serial activities in Parts 3–5)—same browser requirement as the [Web Serial lesson](../communication/web-serial.md) +- pySerial in a virtual environment, set up in [Lesson 2, Part 2](bluetooth-serial.md#setting-up-pyserial) + +## Part 1: Streaming sensor data + +Let's start by streaming live sensor data wirelessly. We'll read a potentiometer and send its value over Bluetooth, then visualize it in Python. + +### The circuit + +Connect a 10kΩ potentiometer to the ESP32 on pin **A7** (GPIO 32), which is an ADC1 pin. (On the original ESP32, ADC2 pins conflict with both WiFi *and* Bluetooth Classic, so always use ADC1 pins for analog input when using wireless features.) + + + +### The Arduino code + + + +The full source is available in our [Arduino GitHub repo](https://github.com/makeabilitylab/arduino/tree/master/ESP32/Bluetooth/BluetoothPot). + +```cpp +#include "BluetoothSerial.h" + +BluetoothSerial SerialBT; + +const int POT_INPUT_PIN = A7; // GPIO 32, an ADC1 pin +const int LED_PIN = LED_BUILTIN; // built-in LED; aliased for clarity +const int ADC_MAX = 4095; // ESP32 ADC is 12-bit (0..4095) +const int PWM_MAX = 255; // analogWrite() expects 0..255 by default +const bool MIRROR_DATA_TO_USB = false; // if true, also send data over USB Serial + +void setup() { + Serial.begin(115200); + SerialBT.begin("ESP32-PotSensor"); + Serial.println("Bluetooth started! Pair with 'ESP32-PotSensor' to see live data."); + Serial.println("The built-in LED brightness tracks the pot position.\n"); +} + +void loop() { + int potVal = analogRead(POT_INPUT_PIN); // 0..4095 (12-bit ADC on ESP32) + + // Normalize to 0.0–1.0 before sending + float normalized = potVal / (float)ADC_MAX; + + // Drive the built-in LED. analogWrite on the ESP32 wraps LEDC and expects 0..255. + int brightness = (int)(normalized * PWM_MAX); + analogWrite(LED_PIN, brightness); + + // Track Bluetooth connection state changes and report them over USB Serial. + static bool wasConnected = false; + bool isConnected = SerialBT.connected(); + if (isConnected != wasConnected) { + Serial.println(isConnected ? "[BT] Client connected." : "[BT] Client disconnected."); + wasConnected = isConnected; + } + + // Send the normalized value over Bluetooth — but only if a client is paired + if (isConnected) { + SerialBT.println(normalized, 4); + } + + // Also send data via USB serial for debugging + if(MIRROR_DATA_TO_USB){ + Serial.println(normalized, 4); + } +} +``` + +{: .note } +> **Pairing reminder:** Since this sketch uses a new device name (`ESP32-PotSensor`), you'll need to pair it from your computer's Bluetooth settings the first time. See [Lesson 2: Pairing with your computer](bluetooth-serial.md#pairing-with-your-computer) for instructions. + +### Reading the data in Python + +You already have two Python visualization scripts from the [Communication module](../communication/serial-intro.md) that work perfectly here without any code changes—they expect float values in the range 0.0–1.0, which is exactly what `BluetoothPot.ino` sends. + +{: .note } +> **Reminder: activate your venv first.** The scripts below assume you've activated the virtual environment you set up in [Lesson 2, Part 2](bluetooth-serial.md#setting-up-pyserial). Each time you open a new terminal: +> ```bash +> # macOS / Linux +> source venv/bin/activate +> # Windows +> venv\Scripts\activate +> ``` +> You'll see `(venv)` at the start of your prompt when it's active. Without it, the scripts will fail with `ModuleNotFoundError: No module named 'serial'`. + +**Terminal bar graph:** [`serial_bar_graph.py`](https://github.com/makeabilitylab/arduino/blob/master/Python/SerialBarGraph/serial_bar_graph.py) reads a float value per line (0.0–1.0) and renders a live ASCII bar chart in the terminal. To use it over Bluetooth, just pass the Bluetooth port as an argument: + +```bash +# macOS / Linux — with your venv active +python3 serial_bar_graph.py /dev/tty.ESP32-PotSensor 115200 + +# Windows — with your venv active +python3 serial_bar_graph.py COM8 115200 +``` + +**Matplotlib circle:** [`serial_draw_circle.py`](https://github.com/makeabilitylab/arduino/blob/master/Python/SerialCircle/serial_draw_circle.py) reads a float value per line and draws a circle whose radius is proportional to the value. Same idea—just pass the Bluetooth port: + +```bash +# macOS / Linux — with your venv active +python3 serial_draw_circle.py /dev/tty.ESP32-PotSensor 115200 + +# Windows — with your venv active +python3 serial_draw_circle.py COM8 115200 +``` + +Turn the potentiometer—you'll see the bar chart or circle updating in real time, with data arriving wirelessly. Compare this with the wired experience: same visualization, no cable. You may notice a slight delay compared to USB serial—Bluetooth Classic SPP typically adds 10–30ms of latency per packet, which is usually imperceptible for human-paced interaction but can matter for high-speed control loops. + +{: .note } +> These Python scripts were written for USB serial. They work over Bluetooth with no code changes—only the port argument differs. This is SPP's core value: your computer's operating system makes the Bluetooth connection look like a regular serial port. + +### Workbench demo + + + +## Part 2: Re-introducing serial.js + +So far we've sent data from the ESP32 to a Python script. Now let's bring the same data stream into the browser. Because your computer's Bluetooth serial port looks just like a USB serial port (that's SPP's whole job), the [Web Serial API](../communication/web-serial.md) works with it—and so does our [`serial.js`](https://github.com/makeabilitylab/js/blob/main/src/lib/serial/serial.js) wrapper library from the Communication module. **Every method and event you learned in the [Web Serial lesson](../communication/web-serial.md) works wirelessly without modification.** The only thing that changes is which port you select when you click "Connect." + +Before we dive back in, here's a quick reference table for `serial.js`. (We covered this in detail in the [Web Serial lesson](../communication/web-serial.md#our-serialjs-library)—revisit that if anything feels rusty.) + +### serial.js API reference + +| Category | API | Purpose | +|---|---|---| +| **Setup** | `new Serial()` | Create a `Serial` instance | +| **Setup** | `Serial.isWebSerialSupported()` | Static; returns `true` if the browser supports Web Serial | +| **Events** | `serial.on(SerialEvents.X, callback)` | Subscribe to an event | +| **Events** | `SerialEvents.CONNECTION_OPENED` | Port opened successfully | +| **Events** | `SerialEvents.CONNECTION_CLOSED` | Port closed (manual or dropped) | +| **Events** | `SerialEvents.DATA_RECEIVED` | A full line arrived; callback receives `(sender, line)` | +| **Events** | `SerialEvents.ERROR_OCCURRED` | I/O or connection error; callback receives `(sender, error)` | +| **Connect** | `await serial.connectAndOpen(filters, options)` | Prompts user for a port; must be called from a user gesture (e.g., a button click) | +| **Connect** | `await serial.autoConnectAndOpenPreviouslyApprovedPort(options)` | Silent reconnect to a port the user previously approved | +| **Connect** | `await serial.close()` | Close the port | +| **Write** | `await serial.writeLine(text)` | Send text followed by a newline (`\n`) | +| **Write** | `await serial.write(text)` | Send raw text (no newline) | +| **State** | `serial.isOpen()` | Boolean; is the port currently open? | +| **State** | `serial.state` | One of `SerialState.CLOSED`, `OPENING`, `OPEN`, `CLOSING` | + +To load the library, add this script tag to your HTML (we already use this pattern in the Communication module's p5.js sketches): + +```html + +``` + +After it loads, the `Serial`, `SerialEvents`, and `SerialState` symbols are available as globals—no `import` statements needed. + +{: .note } +> **Baud rate is ignored over Bluetooth SPP.** When you open a USB serial port, the baud rate sets the actual UART speed and must match what the Arduino called `Serial.begin()` with—otherwise you get garbled data. **Bluetooth SPP has no UART**: data travels over the radio link in packets, and the OS handles the framing for you. Chrome's port dialog and `serial.js` still ask for a baud rate (because they treat all serial ports uniformly), but for a Bluetooth port the value has no effect. Pick anything. We suggest matching the Arduino's USB `Serial.begin(115200)` value just to keep one number in your head. + +## Part 3: SerialTest — no install, no setup, just open and connect + +Before we wire up a p5.js sketch, let's verify the wireless link with the simplest possible browser tool: a hosted version of the Makeability Lab [**SerialTest**](https://makeabilitylab.github.io/js/src/apps/serial/SerialTest/) app. SerialTest is a small, generic Web Serial console: it has a Connect button, a baud-rate dropdown, a live log of received lines, and a text field for sending. No Node, no Live Server, no clone—just open the URL. + +**Try it now:** + +1. With the `BluetoothPot.ino` sketch from Part 1 running on your ESP32, open [makeabilitylab.github.io/js/src/apps/serial/SerialTest](https://makeabilitylab.github.io/js/src/apps/serial/SerialTest/) in Chrome or Edge. +2. Leave the baud rate set to whatever (remember: it's ignored for SPP). Click **Connect**. +3. In Chrome's port-picker dialog, select the **Bluetooth** port for `ESP32-PotSensor` (*not* the USB port). On macOS this looks like `/dev/tty.ESP32-PotSensor`; on Windows it's a `COM` port labeled with the Bluetooth device name. +4. The "Received" pane should immediately start filling with floats between 0 and 1. Turn the pot and watch the numbers update. +5. Type something into the Send field and click Send. Nothing will happen on the ESP32 (the Part 1 sketch doesn't read incoming data yet), but the data is going over the wireless link. We'll wire up the receive side in Part 5. + + + +This is the same SerialTest you used over USB in the Communication module—the *only* difference is which port you select. If this works, you have a confirmed wireless serial link and everything else in this lesson will work too. + +{: .note } +> **Why pick the Bluetooth port specifically when both are listed?** When your ESP32 is plugged into USB and paired over Bluetooth, Chrome shows *both* serial ports in the picker. Either one works for receiving data (the Arduino sends to both with `Serial.println` and `SerialBT.println`). For the rest of this lesson, **always pick the Bluetooth port**—that's the whole point of the wireless link, and it's how we'll verify Part 5 works without a USB cable. + +### Workbench demo + + + +## Part 4: Live visualization with p5.js + +SerialTest is a useful raw-data console, but our [p5.js](https://p5js.org/) sketches turn the same stream into something more interesting. Crucially, **every p5.js sketch from the Communication module works over Bluetooth without code changes**—because `serial.js` doesn't know or care whether it's connected to USB or Bluetooth. + +Open either of these existing sketches in the [p5.js Web Editor](https://editor.p5js.org/): + +- [p5.js Circle Visualization with Web Serial](https://editor.p5js.org/jonfroehlich/sketches/5Knw4tN1d) — circle size and color follow the pot +- [p5.js Sensor Graph with Web Serial](https://editor.p5js.org/jonfroehlich/sketches/Szs_sh4qI) — scrolling time-series plot + +Run either sketch, click Connect, and pick the `ESP32-PotSensor` Bluetooth port. Turn the pot. The visualization responds wirelessly. **The code is the same code from the [p5.js Serial lesson](../communication/p5js-serial.md)**—same `serial.js` import, same event callbacks, same `connectAndOpen()` call. The only difference is which port you select in the browser's picker. + +This is the entire point of SPP: existing Web Serial code works wirelessly without modification. + +### Workbench demo + + + +## Part 5: Bidirectional control — wireless color mixer + +Up to this point we've only sent data *from* the ESP32 *to* the computer. Now let's close the loop and send data both directions—and design a small interaction where each end of the wireless link **owns a different part of the shared state**. That's where bidirectional control gets interesting: if the browser already had a slider for everything, the physical knob would be pointless. We need to give the knob a job that the browser can't do. + +### The design: split ownership by affordance + +Hue is a visual choice—you want to *see* the colors you're picking from, so a color picker on a screen is the right tool. Brightness is an *amount*—a quantity along a single axis—and a rotary knob feels exactly right for that, the way a volume knob feels right on a stereo. So we split the responsibilities: + +- The **browser** owns the **hue** (0–360°). The user picks a color from a hue wheel. +- The **ESP32** owns the **brightness** (0.0–1.0). The user turns the potentiometer. +- A NeoPixel renders the combined color. + +This forces the wireless link to carry data in *both* directions: the browser must send its hue to the ESP32, and the ESP32 must report its brightness back so the browser's UI can show the actual color being displayed (a deep red at 30% brightness looks very different from the same deep red at full brightness). Without bidirectional communication, the browser's color preview would be a lie. + +### The circuit + +Keep the pot from Part 1 on A7. For the NeoPixel, you have two paths: + +**Path A (recommended): board with an onboard NeoPixel** — *e.g.*, the Adafruit ESP32 Feather V2. The NeoPixel is already wired internally to `PIN_NEOPIXEL` (GPIO 0). No extra wiring needed. + +**Path B: external NeoPixel** — *e.g.*, the 8-LED WS2812B/SK6812 stick in our course kits, or any single NeoPixel, ring, or strip. Wire it to the ESP32: +- Data line → **GPIO 27** +- VCC → **USB** pin (5V from USB) for sticks/strips, or **3V** for a single NeoPixel +- GND → **GND** + +See the [Addressable LEDs lesson](../advancedio/addressable-leds.md) for more on power, signal levels, and wiring conventions. + + + +### The Arduino code: BluetoothColorMixer + + + +The full source is in our [Arduino GitHub repo](https://github.com/makeabilitylab/arduino/tree/master/ESP32/Bluetooth/BluetoothColorMixer). The sketch handles both paths above—if your board defines `PIN_NEOPIXEL`, it uses the onboard pixel; otherwise it falls back to an external pixel on GPIO 27. You'll need the [Adafruit NeoPixel library](https://github.com/adafruit/Adafruit_NeoPixel) installed (Sketch → Include Library → Manage Libraries → "Adafruit NeoPixel"). + +```cpp +#include "BluetoothSerial.h" +#include + +#if !defined(CONFIG_BT_ENABLED) || !defined(CONFIG_BLUEDROID_ENABLED) +#error Bluetooth Classic is not enabled. This sketch requires the original ESP32 chip family. +#endif + +#if !defined(CONFIG_BT_SPP_ENABLED) +#error Serial Bluetooth (SPP) is unavailable on this chip variant. +#endif + +// --- NeoPixel configuration --- +// PIN_NEOPIXEL is defined automatically for boards with an onboard NeoPixel +// (e.g. the Adafruit ESP32 Feather V2). Otherwise, wire an external NeoPixel +// to GPIO 27. +#ifdef PIN_NEOPIXEL + const int NEOPIXEL_PIN = PIN_NEOPIXEL; + const int NUM_PIXELS = 1; + #ifdef NEOPIXEL_I2C_POWER + const int NEOPIXEL_POWER_PIN = NEOPIXEL_I2C_POWER; + #define HAS_NEOPIXEL_POWER_PIN + #endif +#else + const int NEOPIXEL_PIN = 27; // external NeoPixel data line + const int NUM_PIXELS = 8; // 8-LED stick from the kit +#endif + +const int POT_INPUT_PIN = A7; +const int ADC_MAX = 4095; +const unsigned long BRIGHTNESS_SEND_INTERVAL_MS = 50; +const float SMOOTHING_ALPHA = 0.15f; // exponential moving average; lower = smoother +const float BRIGHTNESS_CHANGE_THRESHOLD = 0.005f; // ~20 LSB on a 12-bit ADC + +BluetoothSerial SerialBT; +Adafruit_NeoPixel pixels(NUM_PIXELS, NEOPIXEL_PIN, NEO_GRB + NEO_KHZ800); + +int _hueDegrees = 0; // browser-owned +float _valueNormalized = 0.0f; // pot-owned, smoothed +float _lastSentBrightness = -1.0f; // sentinel: forces first send after (re)connect +unsigned long _lastBrightnessSendMs = 0; + +void setup() { + Serial.begin(115200); + + #ifdef HAS_NEOPIXEL_POWER_PIN + pinMode(NEOPIXEL_POWER_PIN, OUTPUT); + digitalWrite(NEOPIXEL_POWER_PIN, HIGH); // V2: enable onboard NeoPixel power + #endif + + pixels.begin(); + pixels.clear(); + pixels.show(); + + SerialBT.begin("ESP32-ColorMixer"); + Serial.println("Bluetooth started! Pair with 'ESP32-ColorMixer'."); +} + +void loop() { + // 1. Read pot → smoothed brightness + int potRaw = analogRead(POT_INPUT_PIN); + float potNorm = potRaw / (float)ADC_MAX; + _valueNormalized = SMOOTHING_ALPHA * potNorm + + (1.0f - SMOOTHING_ALPHA) * _valueNormalized; + + // 2. Receive hue updates from the browser + if (SerialBT.available()) { + String line = SerialBT.readStringUntil('\n'); + line.trim(); + if (line.length() > 0) { + _hueDegrees = constrain(line.toInt(), 0, 360); + } + } + + // 3. Render the NeoPixel(s) with hue × brightness + uint16_t hue16 = map(_hueDegrees, 0, 360, 0, 65535); + uint8_t val8 = (uint8_t)(_valueNormalized * 255.0f); + uint32_t color = pixels.gamma32(pixels.ColorHSV(hue16, 255, val8)); + for (int i = 0; i < NUM_PIXELS; i++) { + pixels.setPixelColor(i, color); + } + pixels.show(); + + // 4. Stream brightness back to the browser — throttled AND only on change. + // When the pot is sitting still, the wireless link stays idle. + unsigned long now = millis(); + bool isConnected = SerialBT.connected(); + if (!isConnected) { + _lastSentBrightness = -1.0f; // reset sentinel; next reconnect re-sends + } else if (now - _lastBrightnessSendMs >= BRIGHTNESS_SEND_INTERVAL_MS) { + if (fabsf(_valueNormalized - _lastSentBrightness) >= BRIGHTNESS_CHANGE_THRESHOLD) { + _lastBrightnessSendMs = now; + _lastSentBrightness = _valueNormalized; + SerialBT.println(_valueNormalized, 4); // e.g. "0.7321\n" + } + } +} +``` + +A few notes on the design: + +- **The wire protocol is asymmetric.** Outgoing (browser → ESP32) lines are integers (`180\n`). Incoming (ESP32 → browser) lines are floats (`0.7321\n`). Each direction carries only what that side owns. This is simpler than a single symmetric format like `"h, v\n"` and makes the ownership pattern explicit in the protocol. +- **The pot value is smoothed.** ESP32 ADCs are noisy (the bottom ~3 bits jitter under normal conditions). A one-line exponential moving average gives the brightness bar a calmer feel without adding perceptible lag. +- **Brightness is only sent on change.** We throttle to 20 Hz max *and* suppress sends entirely when the smoothed value hasn't moved by at least 0.005 (≈ 20 LSB on a 12-bit ADC). When the pot is sitting still, the wireless link is idle—no wasted bandwidth, no spurious browser updates. A sentinel value of `-1` for `_lastSentBrightness` guarantees a fresh send on every reconnect. + +{: .note } +> **Pairing reminder:** This sketch uses a new device name (`ESP32-ColorMixer`), so pair it from your computer's Bluetooth settings the first time. The Part 1 `ESP32-PotSensor` pairing is independent—you can keep both paired simultaneously, but only one can be connected at a time. + +### The browser side: SerialHueBrightnessTest + +We've built a small companion web app called [**SerialHueBrightnessTest**](https://makeabilitylab.github.io/js/src/apps/serial/SerialHueBrightnessTest/), hosted on GitHub Pages. Like SerialTest in Part 3, it needs no install—just open the link. + +The interface has two parts: +- A **hue wheel** (and a backup linear slider) for picking hue. This is the **browser-owned** state; whatever you select gets sent to the ESP32. +- A **brightness bar** that's read-only. This is the **device-owned** state; it fills up and empties as you turn the pot. + +The color swatch shows hue × brightness combined—so when the pot is near zero, the swatch goes dark, even if you've chosen a bright hue. The serial-format box at the bottom of the page shows the exact bytes flowing in each direction in real time, which is useful for debugging your own sketches later. + +To run it: + +1. Make sure `BluetoothColorMixer.ino` is running on your ESP32 and you've paired with `ESP32-ColorMixer`. +2. Open [makeabilitylab.github.io/js/src/apps/serial/SerialHueBrightnessTest](https://makeabilitylab.github.io/js/src/apps/serial/SerialHueBrightnessTest/) in Chrome or Edge. +3. Click **Connect** and pick the `ESP32-ColorMixer` Bluetooth port. +4. Drag around the hue wheel—the NeoPixel changes color. Turn the pot—the NeoPixel changes brightness and the on-screen brightness bar follows. + +Neither side can do this alone. The browser doesn't know how bright the LED is until the pot tells it. The ESP32 doesn't know what color to render until the browser tells it. Each side fills in what the other can't see. + + + +The source for the page is in our [JS GitHub repo](https://github.com/makeabilitylab/js/tree/main/src/apps/serial/SerialHueBrightnessTest)—it's a small fork of [SerialHueTest](https://github.com/makeabilitylab/js/tree/main/src/apps/serial/SerialHueTest) with the incoming-data parser changed from "hue" to "brightness," and a read-only brightness bar added below the hue slider. Worth a read if you're curious how the receive side works in practice. + +### Workbench demo + + + +### What happens when the connection drops? + +If you carry your laptop out of Bluetooth range (or the ESP32 loses power), the connection will drop. The `BluetoothSerial` library handles this gracefully on the ESP32 side—it will automatically start advertising again, so you can reconnect by re-opening the serial port from your computer. On the browser side, `serial.js` fires its `CONNECTION_CLOSED` event; in `SerialHueBrightnessTest` you'll see the brightness readout reset to `—` because we no longer know what the device is doing. If you need to detect the connection state inside the Arduino sketch, use `SerialBT.connected()` (we use this in the throttled-send block above) or register a callback with `SerialBT.register_callback()` for explicit connect/disconnect events. + +## Part 6: Android phone (optional bonus) + +If you have an **Android** phone, you can use the same Part 1 sketch (`BluetoothPot.ino`) to receive live pot data on your phone—no laptop needed. + +{: .note } +> **iPhone users:** You cannot use Bluetooth Classic SPP from an iPhone. In [Lesson 4: BLE](ble-intro.md), we'll use a protocol that works with both iOS and Android. + +Make sure the Part 1 `BluetoothPot.ino` sketch is running on your ESP32, then: + +1. On your Android phone, install the free [Serial Bluetooth Terminal](https://play.google.com/store/apps/details?id=de.kai_morich.serial_bluetooth_terminal) app by Kai Morich (you may already have it from [Lesson 2, Part 3](bluetooth-serial.md#part-3-android-phone-optional-bonus)). +2. Go to **Settings → Bluetooth** and pair with `ESP32-PotSensor`. +3. Open the Serial Bluetooth Terminal app → **Devices** → select your ESP32 → **Connect**. +4. You should see floating-point pot values (0.0000–1.0000) streaming. Turn the pot and watch the stream update. + + + +This is a one-way demo (ESP32 → phone), which is enough to show the wireless link working from a pocket-sized device. For sending data the *other* direction from Android, see **Exercise 5** below. + +## Gotchas and limitations + +**One connection at a time.** SPP is point-to-point. Only one device (your computer *or* your phone) can connect to the ESP32's Bluetooth serial at a time. + +**No iOS support.** Apple blocks Bluetooth Classic SPP for third-party apps. iPhone users can participate fully in the computer-based activities (Parts 1–5) but cannot connect from their phones. + +**No ESP32-S3.** Only the original ESP32 chip family supports Bluetooth Classic. The ESP32-S3, S2, C3, and C6 do not have the hardware. + +**Range and interference.** Expect reliable communication within about 5–10 meters indoors. Walls, furniture, and other 2.4 GHz devices (WiFi, microwaves) reduce range. + +**macOS Bluetooth port naming.** The virtual serial port name varies across macOS versions and can be long or cryptic. Use `ls /dev/tty.*Bluetooth*` or `ls /dev/tty.*ESP*` to find it. If the port disappears, unpair and re-pair the device. + +**Memory usage.** Bluetooth Classic consumes significant RAM. If you also need WiFi, consider using BLE instead—or be prepared for potential instability in complex sketches on the original ESP32's 520KB SRAM. + +## When to use Bluetooth Classic vs. BLE + +You've now seen what Bluetooth Classic can do—the next two lessons cover **Bluetooth Low Energy (BLE)**. Before moving on, here's a quick guide for choosing between them in your own projects: + +**Use Bluetooth Classic SPP when:** +- You want the simplest possible wireless serial—your existing serial code works unchanged +- You're working entirely from a computer (Mac or Windows) +- You're using an original-family ESP32 (Huzzah32, Feather V2) +- You don't need iPhone support + +**Use BLE ([Lesson 4](ble-intro.md)) when:** +- You're using the ESP32-S3 (or any non-original ESP32) +- You need iPhone / iOS compatibility +- You want to connect from a phone app that works on both platforms +- Power efficiency matters (battery-powered projects) +- You want to build a [Web Bluetooth](https://developer.mozilla.org/en-US/docs/Web/API/Web_Bluetooth_API) web app + +## Exercises + +**Exercise 1: Multi-sensor dashboard.** Modify the Arduino code to send comma-separated values from *two* sensors (potentiometer + photoresistor). Update a p5.js sketch to parse the CSV and visualize both streams—one as circle size, one as background color. This is the same parsing pattern from [p5.js Serial I/O](../communication/p5js-serial-io.md). + +**Exercise 2: Compare wired vs. wireless.** Open Arduino's Serial Plotter on the USB port while simultaneously running the Python sensor reader on the Bluetooth port. Both show the same data—one wired, one wireless. Can you measure any latency difference? Try it with `delay(10)` vs. `delay(100)` in the Arduino sketch. + +**Exercise 3: Move brightness ownership to the browser.** Take the `BluetoothColorMixer` sketch and make brightness browser-owned too: add a brightness slider to a fork of `SerialHueBrightnessTest`, change the wire format so the browser sends `"h, v\n"`, and have the Arduino just render whatever the browser sends. Then ask yourself: *what was the pot for?* This is the design choice we were avoiding in Part 5—do you prefer it? + +**Exercise 4: LED brightness from Android.** Write a stripped-down version of `BluetoothColorMixer` that uses a plain LED (with a 220Ω resistor) on GPIO 21 instead of a NeoPixel, and accepts a single integer (0–255) as the brightness command. Pair the Android Serial Bluetooth Terminal app with your ESP32, configure custom buttons (under **Settings → Buttons**) that send `0`, `128`, and `255`, and you've made a phone-based wireless dimmer. *(This is roughly what an earlier version of this lesson did with the full p5.js bidirectional demo—it's a nice minimal Android-friendly variant.)* + +**Exercise 5: Chat between two ESP32s.** Flash one ESP32 with the [`SerialToSerialBT`](https://github.com/espressif/arduino-esp32/blob/master/libraries/BluetoothSerial/examples/SerialToSerialBT/SerialToSerialBT.ino) example (peripheral) and another with [`SerialToSerialBTM`](https://github.com/espressif/arduino-esp32/blob/master/libraries/BluetoothSerial/examples/SerialToSerialBTM/SerialToSerialBTM.ino) (central). Build a two-way text chat. + +**Exercise 6: Range test.** With the Part 1 streaming sketch running, carry your laptop away from the ESP32. At what distance does the data start dropping out? When does the connection drop entirely? Test with and without walls between you and the ESP32. + +**Exercise 7: Replicate a Communication module project.** Pick any project from the [Communication module](../communication/index.md) (the paint app, the shape drawer, *etc.*) and run it over Bluetooth instead of USB. How much code did you have to change? (The answer should be: none—just a different port selection.) + +## Lesson Summary + +In this lesson, you went beyond Hello World to build real interactive Bluetooth projects: + +- **Streaming sensor data** over Bluetooth uses `SerialBT.println()`—the same pattern as USB serial, just wireless. Always use ADC1 pins (like A7) on the ESP32 when WiFi or Bluetooth are active. +- **The serial.js library works unchanged over Bluetooth.** Web Serial doesn't know or care which transport is underneath, so every method (`connectAndOpen`, `writeLine`, event subscriptions) behaves exactly the same. The only thing that changes is which port you select in Chrome's picker. +- **Baud rate has no effect over SPP**—there's no UART, just a radio link with OS-managed packetization. Set it to anything. +- **Hosted browser tools (`SerialTest`, `SerialHueBrightnessTest`) let students verify wireless links without any local setup.** Open the page, click Connect, pick the Bluetooth port. +- **Bidirectional control is most interesting when each side owns a different part of the state.** Our color mixer hands hue to the browser (a screen task) and brightness to the pot (a knob task), and the wireless link carries each piece in its own direction. +- **Connection drops are handled gracefully**: the ESP32 re-advertises automatically, `pySerial` raises `SerialException`, and `serial.js` fires `CONNECTION_CLOSED`. +- **Bluetooth Classic SPP has a practical range of 5–10 meters indoors**, supports one connection at a time, and adds 10–30ms of latency compared to USB. +- **For most new projects—especially on the ESP32-S3—BLE is the better default.** But Bluetooth Classic SPP is unbeatable when you want existing serial code to work wirelessly without modification. + +## Resources + +- [BluetoothSerial library source and examples](https://github.com/espressif/arduino-esp32/tree/master/libraries/BluetoothSerial) — official library in the ESP32 Arduino core +- [Adafruit NeoPixel library](https://github.com/adafruit/Adafruit_NeoPixel) — the library used in `BluetoothColorMixer` for driving the LED +- [Makeability Lab JS Library (serial.js)](https://github.com/makeabilitylab/js) — Web Serial wrapper, lives in the same repo as `SerialTest` and `SerialHueBrightnessTest` +- [SerialTest](https://makeabilitylab.github.io/js/src/apps/serial/SerialTest/) — hosted generic Web Serial console +- [SerialHueBrightnessTest](https://makeabilitylab.github.io/js/src/apps/serial/SerialHueBrightnessTest/) — hosted bidirectional color-mixer companion to this lesson +- [BluetoothColorMixer.ino](https://github.com/makeabilitylab/arduino/tree/master/ESP32/Bluetooth/BluetoothColorMixer) — the Part 5 Arduino sketch +- [Web Serial lesson](../communication/web-serial.md) — our introduction to Web Serial (the same API that works with Bluetooth COM ports) +- [p5.js Serial](../communication/p5js-serial.md) and [p5.js Serial I/O](../communication/p5js-serial-io.md) — the Communication module foundations this lesson builds on +- [Addressable LEDs lesson](../advancedio/addressable-leds.md) — background on NeoPixel hardware and wiring +- [Serial Bluetooth Terminal](https://play.google.com/store/apps/details?id=de.kai_morich.serial_bluetooth_terminal) — recommended Android app for Bluetooth serial (free, by Kai Morich) + +## Next Lesson + +In the [next lesson](ble-intro.md), we'll learn **Bluetooth Low Energy (BLE)**—the protocol that powers fitness trackers, smart home devices, and billions of IoT sensors. BLE works on the ESP32-S3, works with iPhones *and* Android phones, and introduces a structured data model that's more powerful than serial. The code is more complex, but the capabilities—and the universal device compatibility—are worth it. Let's go! 🚀 + + \ No newline at end of file diff --git a/esp32/capacitive-touch.md b/esp32/capacitive-touch.md index bd7f8fca..2bc9217e 100644 --- a/esp32/capacitive-touch.md +++ b/esp32/capacitive-touch.md @@ -2,7 +2,8 @@ layout: default title: L6: Capacitive Touch description: "Use the ESP32's built-in capacitive touch hardware with touchRead() to sense a finger on a bare pin, calibrate a threshold, and build a touch piano from foil or fruit." -parent: ESP32 +parent: Fundamentals +grand_parent: ESP32 has_toc: true # (on by default) usemathjax: true comments: true diff --git a/esp32/esp32-ide.md b/esp32/esp32-ide.md index 72e478b9..4db47e86 100644 --- a/esp32/esp32-ide.md +++ b/esp32/esp32-ide.md @@ -5,7 +5,7 @@ image: /esp32/assets/og/esp32-ide.jpg description: "Set up the Arduino IDE to program ESP32 boards: add Espressif's board package, select the board and port, upload a test sketch, and troubleshoot common upload issues." parent: ESP32 has_toc: true # (on by default) -nav_order: 9 +nav_order: 11 nav_exclude: false comments: true usetocbot: true diff --git a/esp32/esp32.md b/esp32/esp32.md index 1d138732..00fdb807 100644 --- a/esp32/esp32.md +++ b/esp32/esp32.md @@ -3,7 +3,8 @@ layout: default title: L1: Intro to the ESP32 description: "Meet the ESP32 system-on-a-chip: how it compares to the Arduino Uno and Leonardo, why it runs at 3.3V, the ESP32 vs. ESP32-S3, and the Adafruit Feather pinout." image: /esp32/assets/images/ESP32Boards_MakerAdvisor.png -parent: ESP32 +parent: Fundamentals +grand_parent: ESP32 has_toc: true # (on by default) usemathjax: true comments: true diff --git a/esp32/fundamentals.md b/esp32/fundamentals.md new file mode 100644 index 00000000..890a53e2 --- /dev/null +++ b/esp32/fundamentals.md @@ -0,0 +1,96 @@ +--- +layout: default +title: Fundamentals +description: "The ESP32 Fundamentals series: set up the board, blink and fade LEDs with the LEDC PWM peripheral, read the 12-bit ADC, play tones, and use built-in capacitive touch before going wireless." +parent: ESP32 +nav_order: 1 +has_toc: false # on by default +has_children: true +comments: true +usetocbot: true +--- +# ESP32 Fundamentals +{: .no_toc } + +## Table of Contents +{: .no_toc .text-delta } + +1. TOC +{:toc} +--- + + + +In this lesson series, you will learn the foundations of programming the ESP32: how it differs from the Arduino Uno and Leonardo you've used before, how to set up your development environment, and how to use the ESP32's hardware features—from GPIO and PWM to its 12-bit ADC and built-in capacitive touch sensing. By the end of this series, you'll be comfortable enough with the board to dive into [the Wireless series](wireless.md), where the ESP32 really shines. + +The lessons are interactive and designed to be completed **in order**. All ESP32 code is open source and in this [GitHub repository](https://github.com/makeabilitylab/arduino/tree/master/ESP32). + +{: .note } +> Before starting, we recommend completing both [Intro to Electronics](../electronics/index.md) and [Intro to Arduino](../arduino/index.md). We build on concepts from those modules—like voltage dividers, `digitalWrite`, `analogWrite`, and PWM—without re-explaining them in detail here. + +## Lessons + +### [Lesson 1: Introduction to the ESP32](esp32.md) + +Learn about the ESP32 platform, how it compares to the Arduino Uno and Leonardo, and how to set up your development environment. You'll get familiar with the pin diagram and important hardware differences like the 3.3V operating voltage. + +### [Lesson 2: Blinking an LED](led-blink.md) + +Write your first ESP32 program! The code is the same as the Arduino [Blink lesson](../arduino/led-blink.md)—the challenge here is getting comfortable with the new board and its pin layout. + +### [Lesson 3: Fading an LED with PWM](led-fade.md) + +Learn how to use PWM output on the ESP32 to fade an LED. This is where things start to diverge from Arduino: instead of `analogWrite`, the ESP32 uses the LEDC (LED Control) library, which gives you more control over PWM channels, frequencies, and resolutions. + +### [Lesson 4: Analog Input](analog-input.md) + +Use the ESP32's 12-bit ADC to read a potentiometer and control an LED's brightness—combining analog input with PWM output. + +### [Lesson 5: Playing Tones](tone.md) + +Learn how to play tones and melodies on the ESP32 using the `tone()` function (now supported in ESP32 Arduino core v3.x!) and the LEDC PWM library. + +### [Lesson 6: Capacitive Touch Sensing](capacitive-touch.md) + +The ESP32 has built-in capacitive touch sensing hardware—no external components needed! In this lesson, you'll use a bare wire (or aluminum foil) as a touch sensor to control an LED. + +## What's next? + +Once you've completed the Fundamentals lessons, move on to [Wireless](wireless.md) to learn about WiFi, Bluetooth Classic, and Bluetooth Low Energy (BLE)—the features that make the ESP32 truly powerful for IoT and physical computing projects! + + \ No newline at end of file diff --git a/esp32/index.md b/esp32/index.md index 935623d5..db74cda9 100644 --- a/esp32/index.md +++ b/esp32/index.md @@ -1,212 +1,108 @@ ---- -layout: default -title: ESP32 -description: "Program the WiFi- and Bluetooth-enabled ESP32 with Arduino: blink and fade LEDs with PWM, play tones, read analog input, sense capacitive touch, and build IoT projects." -image: /esp32/assets/images/ESP32Variants_FromS1-S3.png -nav_order: 6 -has_toc: false # on by default -has_children: true -comments: true -usetocbot: true ---- -# {{ page.title }} -{: .no_toc } - -## Table of Contents -{: .no_toc .text-delta } - -1. TOC -{:toc} ---- - -Welcome 👋 to the **ESP32** module! The [ESP32](https://www.espressif.com/en/products/socs/esp32) is a fast, low-cost, WiFi- and Bluetooth-enabled microcontroller that has become **the** platform for Internet of Things (IoT) projects. And the best part? You can program it with Arduino—so everything you learned in the [Intro to Arduino](../arduino/index.md) series carries over! In this module, you'll learn how the ESP32 differs from the Arduino boards you've used before, and you'll build projects that blink LEDs, fade lights with PWM, play tones, sense capacitive touch, and connect to the cloud ☁️. Let's go! 🚀 - -![A collage of ESP32 boards including the original ESP32, ESP32-S2, and ESP32-S3](assets/images/ESP32Variants_FromS1-S3.png) -**Figure.** The ESP32 family includes dozens of variants from Espressif and third-party manufacturers. They are fast (up to 240 MHz dual-core), have built-in WiFi and Bluetooth, and many development boards cost around $10 USD! -{: .fs-1 } - -{: .note } -> The ESP32 lessons assume you have completed both our [Intro to Electronics](../electronics/index.md) and [Intro to Arduino](../arduino/index.md) tutorial series. While not absolutely necessary, we build on concepts from those modules—like voltage dividers, `digitalWrite`, `analogWrite`, and PWM—without re-explaining them here. If this is your first time on our website, welcome 👋🏽—we recommend starting there! - - - -## Which boards do we use? - -For our tutorial series, we use **Adafruit's ESP32 boards** in the [Feather](https://learn.adafruit.com/adafruit-feather) form factor; however, you should be able to use almost any ESP32 board on the market and follow along (you might need to change pin numbers). Specifically, our lessons use: - -- The [**Adafruit ESP32-S3 Feather**](https://www.adafruit.com/product/5477) (4MB Flash, 2MB PSRAM) — our **primary board** for Spring 2026. Features native USB-C, WiFi, BLE 5.0, and an onboard NeoPixel. - -- The [**Adafruit Huzzah32 ESP32 Feather**](https://www.adafruit.com/product/3591) — uses the original ESP32. Our earlier videos and Fritzing diagrams reference this board, but all code transfers directly to the S3. - -Because both boards share the **Feather form factor** and use the same [ESP32 Arduino core](https://github.com/espressif/arduino-esp32), the lessons work with either board—you'll just need to consult the correct pin diagram. We'll note specific differences where they arise. See [Lesson 1](esp32.md) for detailed specs, pin diagrams, and a side-by-side comparison with the Arduino Uno. - -{: .highlight } -> You can find far cheaper ESP32 boards on [AliExpress](https://www.aliexpress.com/w/wholesale-esp32.html) or [Amazon](https://www.amazon.com/s?k=esp+32+board)—sometimes just a few dollars—and our lessons should work with them too. Adafruit boards cost more but offer reliable build quality, thorough documentation, and the [Feather ecosystem](https://learn.adafruit.com/adafruit-feather/featherwings) of stackable expansion boards ("FeatherWings"). - -### Chips, modules, and development boards - -It's worth clarifying the supply chain—and differences between **chips**, **modules**, and **development boards**—since the terminology can be confusing and the layering actually explains the price differences you'll see online. - -- **The chip:** **Espressif** designs the ESP32-S3 *chip* (the bare SoC). Working with bare silicon is difficult: it requires custom printed circuit boards (PCBs), complex surface-mount soldering, and precise RF antenna tuning. -- **The module:** To simplify manufacturing, Espressif packages the chip into *modules* (like the [ESP32-S3-WROOM-1](https://documentation.espressif.com/esp32-s3-wroom-1_wroom-1u_datasheet_en.pdf)). Modules add flash memory, an integrated antenna, and metal RF shielding. Crucially, they are pre-certified by the FCC, saving hardware designers from expensive regulatory testing. However, you still can't easily plug a module into a laptop or a breadboard. -- **The development board:** This is where the maker companies come in. They bridge the gap between industrial components and human-usable prototyping tools. They take the Espressif module and build a *development board* around it, adding the missing essentials: a USB connector, a USB interface for programming and serial communication, a 3.3V voltage regulator (since USB provides 5V), battery charging circuitry, and breadboard-friendly header pins. - -That's why an Adafruit Feather costs ~$18 while a bare Espressif module costs a few dollars. You're paying for the hardware that makes the chip accessible, standard form factors (like the plug-and-play [Feather ecosystem](https://learn.adafruit.com/adafruit-feather/featherwings)), and high-quality documentation. Because your code targets the underlying ESP32-S3 chip, it runs identically across all these boards—only the pin layout and onboard peripherals differ. We discuss the specific pin diagrams for our boards in [Lesson 1](esp32.md). - -## How does the Arduino Uno or Leonardo differ from ESP32? - -If you're coming from the [Intro to Arduino](../arduino/index.md) series, here are the key things to know upfront: - -{: .warning } -> The ESP32 runs on **3.3V power and logic**, not 5V like the Arduino Uno and Leonardo. This affects how you interface with sensors and LEDs—and you can damage your board by applying 5V to a GPIO pin! We'll cover this in detail in [Lesson 1](esp32.md). - -- **Way more computational power**: The ESP32 runs at up to 240 MHz with a 32-bit dual-core processor—15x faster than the 16 MHz, 8-bit ATmega chips in the Uno and Leonardo, with vastly more memory. -- **WiFi and Bluetooth built in**: No shields needed! This is what makes the ESP32 ideal for IoT projects. -- **More pins, more PWM**: The ESP32 has more GPIO pins, and *all* of them can do PWM (not just 6 like the Uno). -- **12-bit ADC**: The ESP32's analog-to-digital converter has 12-bit resolution (0–4095) compared to the Uno's 10-bit (0–1023). -- **Different PWM and tone APIs**: The ESP32 Arduino library uses a different approach for PWM output and tone generation—we'll walk you through it! -- **Native USB (ESP32-S3)**: The ESP32-S3 has native USB support, so it can act as a keyboard, mouse, MIDI device, or disk drive—no external USB-to-serial converter needed. -- **Capacitive touch sensing**: The ESP32 has built-in capacitive touch hardware—no external components required. - -## Programming the ESP32 - -For our learning series, we program the ESP32 using **Arduino (C/C++)**—specifically, Espressif's [open-source Arduino core](https://github.com/espressif/arduino-esp32) for the ESP32 family. This means most of your [prior Arduino learning](../arduino/) transfers directly (woohoo! 🎉). You can use the same Arduino IDE, the same `setup()`/`loop()` structure, and many of the same functions like `digitalRead`, `analogRead`, and `Serial.print`. - -The tradeoff is that the Arduino core is a **convenience layer** on top of the ESP32's native SDK. It doesn't expose all of the chip's features—you can see the [supported libraries here](https://docs.espressif.com/projects/arduino-esp32/en/latest/libraries.html)—and it adds some overhead compared to programming the chip directly. For our purposes, though, Arduino is a good choice: it lets us focus on learning physical computing concepts rather than wrestling with a new toolchain. - -That said, the ESP32 is completely **independent** of the Arduino ecosystem—just as you don't *have* to use Arduino to program the ATmega328P (used in the Uno) or the ATmega32u4 (used in the Leonardo), you don't have to use Arduino to program the ESP32. Here are some alternatives you may want to explore in the future: - -- **ESP-IDF (C/C++)**: Espressif's official [IoT Development Framework](https://docs.espressif.com/projects/esp-idf/en/stable/esp32/get-started/index.html) is a production-grade, FreeRTOS-based SDK that provides full access to the ESP32's hardware. It's what you'd likely use in industry—more powerful and efficient, but also more complex. If you want to try it, follow the [ESP-IDF Getting Started guide](https://docs.espressif.com/projects/esp-idf/en/stable/esp32/get-started/index.html). -- **CircuitPython / MicroPython**: Python-based alternatives that are great for rapid prototyping. See Adafruit's [CircuitPython guide](https://learn.adafruit.com/adafruit-esp32-s3-feather/circuitpython) for the ESP32-S3 Feather. -- **PlatformIO**: A professional [IDE and build system](https://platformio.org/) that supports ESP32 with both Arduino and ESP-IDF frameworks, and integrates with VS Code. - -## Lessons - -These lessons are interactive and designed to be completed **in order**. All ESP32 code is open source and in this [GitHub repository](https://github.com/makeabilitylab/arduino/tree/master/ESP32). - -### [Lesson 1: Introduction to the ESP32](esp32.md) - -Learn about the ESP32 platform, how it compares to the Arduino Uno and Leonardo, and how to set up your development environment. You'll get familiar with the pin diagram and important hardware differences like the 3.3V operating voltage. - -### [Lesson 2: Blinking an LED](led-blink.md) - -Write your first ESP32 program! The code is the same as the Arduino [Blink lesson](../arduino/led-blink.md)—the challenge here is getting comfortable with the new board and its pin layout. - -### [Lesson 3: Fading an LED with PWM](led-fade.md) - -Learn how to use PWM output on the ESP32 to fade an LED. This is where things start to diverge from Arduino: instead of `analogWrite`, the ESP32 uses the LEDC (LED Control) library, which gives you more control over PWM channels, frequencies, and resolutions. - -### [Lesson 4: Analog Input](analog-input.md) - -Use the ESP32's 12-bit ADC to read a potentiometer and control an LED's brightness—combining analog input with PWM output. - -### [Lesson 5: Playing Tones](tone.md) - -Learn how to play tones and melodies on the ESP32 using the `tone()` function (now supported in ESP32 Arduino core v3.x!) and the LEDC PWM library. - -### [Lesson 6: Capacitive Touch Sensing](capacitive-touch.md) - -The ESP32 has built-in capacitive touch sensing hardware—no external components needed! In this lesson, you'll use a bare wire (or aluminum foil) as a touch sensor to control an LED. - -### [Lesson 7: Internet of Things](iot.md) - -Connect your ESP32 to WiFi and upload sensor data to the cloud using [Adafruit IO](https://learn.adafruit.com/welcome-to-adafruit-io). This is where the ESP32 truly shines! ✨ - - - -## What's next? - -Once you've completed the ESP32 lessons, you'll have a solid foundation for building WiFi-connected, sensor-driven projects. Consider exploring more advanced topics like Bluetooth Low Energy (BLE), deep sleep for battery-powered projects, or building your own web server directly on the ESP32! \ No newline at end of file +--- +layout: default +title: ESP32 +description: "Program the WiFi- and Bluetooth-enabled ESP32 with Arduino: blink and fade LEDs with PWM, play tones, read analog input, sense capacitive touch, and build IoT and wireless projects." +image: /esp32/assets/images/ESP32Variants_FromS1-S3.png +nav_order: 6 +has_toc: false # on by default +has_children: true +comments: true +usetocbot: true +--- +# {{ page.title }} +{: .no_toc } + +## Table of Contents +{: .no_toc .text-delta } + +1. TOC +{:toc} +--- + +Welcome 👋 to the **ESP32** module! The [ESP32](https://www.espressif.com/en/products/socs/esp32) is a fast, low-cost, WiFi- and Bluetooth-enabled microcontroller that has become **the** platform for Internet of Things (IoT) projects. And the best part? You can program it with Arduino—so everything you learned in the [Intro to Arduino](../arduino/index.md) series carries over! + +In this module, you'll learn how the ESP32 differs from the Arduino boards you've used before, and you'll build projects that blink LEDs, fade lights with PWM, play tones, sense capacitive touch, connect to the cloud ☁️, and communicate wirelessly over Bluetooth 📡. Let's go! 🚀 + +![A collage of ESP32 boards including the original ESP32, ESP32-S2, and ESP32-S3](assets/images/ESP32Variants_FromS1-S3.png) +**Figure.** The ESP32 family includes dozens of variants from Espressif and third-party manufacturers. They are fast (up to 240 MHz dual-core), have built-in WiFi and Bluetooth, and many development boards cost around $10 USD! +{: .fs-1 } + +{: .note } +> The ESP32 lessons assume you have completed both our [Intro to Electronics](../electronics/index.md) and [Intro to Arduino](../arduino/index.md) tutorial series. While not absolutely necessary, we build on concepts from those modules—like voltage dividers, `digitalWrite`, `analogWrite`, and PWM—without re-explaining them here. If this is your first time on our website, welcome 👋🏽—we recommend starting there! + +The module is split into two sub-series: + +- **[Fundamentals](fundamentals.md)** — getting set up with the ESP32 board, blinking LEDs, fading with PWM, analog input, tone generation, and capacitive touch. This is the place to start if you're new to the ESP32. +- **[Wireless](wireless.md)** — what makes the ESP32 special: connecting to the cloud over WiFi, talking to your computer over Bluetooth Classic, and using Bluetooth Low Energy (BLE) for phone-friendly, low-power wireless projects. + + + +## Which boards do we use? + +For our tutorial series, we use **Adafruit's ESP32 boards** in the [Feather](https://learn.adafruit.com/adafruit-feather) form factor; however, you should be able to use almost any ESP32 board on the market and follow along (you might need to change pin numbers). Specifically, our lessons use: + +- The [**Adafruit ESP32-S3 Feather**](https://www.adafruit.com/product/5477) (4MB Flash, 2MB PSRAM) — our **primary board** for Spring 2026. Features native USB-C, WiFi, BLE 5.0, and an onboard NeoPixel. + +- The [**Adafruit Huzzah32 ESP32 Feather**](https://www.adafruit.com/product/3591) — uses the original ESP32. Our earlier videos and Fritzing diagrams reference this board, but all code transfers directly to the S3. + +Because both boards share the **Feather form factor** and use the same [ESP32 Arduino core](https://github.com/espressif/arduino-esp32), the lessons work with either board—you'll just need to consult the correct pin diagram. We'll note specific differences where they arise. See [Lesson 1: Introduction to the ESP32](esp32.md) for detailed specs, pin diagrams, and a side-by-side comparison with the Arduino Uno. + +{: .highlight } +> You can find far cheaper ESP32 boards on [AliExpress](https://www.aliexpress.com/w/wholesale-esp32.html) or [Amazon](https://www.amazon.com/s?k=esp+32+board)—sometimes just a few dollars—and our lessons should work with them too. Adafruit boards cost more but offer reliable build quality, thorough documentation, and the [Feather ecosystem](https://learn.adafruit.com/adafruit-feather/featherwings) of stackable expansion boards ("FeatherWings"). + +### Chips, modules, and development boards + +Before we begin, it's worth clarifying the differences between **chips**, **modules**, and **development boards**—since the terminology can be confusing and the layering actually explains the price differences you'll see online. + +- **The chip:** **Espressif** designs the ESP32-S3 *chip* (the bare SoC). Working with bare silicon is difficult: it requires custom printed circuit boards (PCBs), complex surface-mount soldering, and precise RF antenna tuning. +- **The module:** To simplify manufacturing, Espressif packages the chip into *modules* (like the [ESP32-S3-WROOM-1](https://documentation.espressif.com/esp32-s3-wroom-1_wroom-1u_datasheet_en.pdf)). Modules add flash memory, an integrated antenna, and metal RF shielding. Crucially, they are pre-certified by the FCC, saving hardware designers from expensive regulatory testing. However, you still can't easily plug a module into a laptop or a breadboard. +- **The development board:** This is where the maker companies like Adafruit and Sparkfun come in. They bridge the gap between industrial components and human-usable prototyping tools. They take the Espressif module and build a *development board* around it, adding the missing essentials: a USB connector, a USB interface for programming and serial communication, a 3.3V voltage regulator (since USB provides 5V), battery charging circuitry, and breadboard-friendly header pins. + +That's why an [Adafruit ESP32-S3 Feather](https://www.adafruit.com/product/5477) costs ~$18 while a bare Espressif module costs a few dollars. You're paying for the hardware that makes the chip accessible, standard form factors (like the plug-and-play [Feather ecosystem](https://learn.adafruit.com/adafruit-feather/featherwings)), and high-quality documentation. + +Because your code targets the underlying ESP32 or ESP32-S3 chip, it runs identically across all these boards—only the pin layout and onboard peripherals differ. We discuss the specific pin diagrams for our boards in [Lesson 1](esp32.md). + +## How does the Arduino Uno or Leonardo differ from ESP32? + +If you're coming from the [Intro to Arduino](../arduino/index.md) series, here are the key things to know upfront: + +{: .warning } +> The ESP32 runs on **3.3V power and logic**, not 5V like the Arduino Uno and Leonardo. This affects how you interface with sensors and LEDs—and you can damage your board by applying 5V to a GPIO pin! We'll cover this in detail in [Lesson 1](esp32.md). + +- **Way more computational power**: The ESP32 runs at up to 240 MHz with a 32-bit dual-core processor—15x faster than the 16 MHz, 8-bit ATmega chips in the Uno and Leonardo, with vastly more memory. +- **WiFi and Bluetooth built in**: No shields needed! This is what makes the ESP32 ideal for IoT projects. +- **More pins, more PWM**: The ESP32 has more GPIO pins, and *all* of them can do PWM (not just 6 like the Uno). +- **12-bit ADC**: The ESP32's analog-to-digital converter has 12-bit resolution (0–4095) compared to the Uno's 10-bit (0–1023). +- **Different PWM and tone APIs**: The ESP32 Arduino library uses a different approach for PWM output and tone generation—we'll walk you through it! +- **Native USB (ESP32-S3)**: The ESP32-S3 has native USB support, so it can act as a keyboard, mouse, MIDI device, or disk drive—no external USB-to-serial converter needed. +- **Capacitive touch sensing**: The ESP32 has built-in capacitive touch hardware—no external components required. + +## Programming the ESP32 + +For our learning series, we program the ESP32 using **Arduino (C/C++)**—specifically, Espressif's [open-source Arduino core](https://github.com/espressif/arduino-esp32) for the ESP32 family. This means most of your [prior Arduino learning](../arduino/) transfers directly (woohoo! 🎉). You can use the same Arduino IDE, the same `setup()`/`loop()` structure, and many of the same functions like `digitalRead`, `analogRead`, and `Serial.print`. + +The tradeoff is that the Arduino core is a **convenience layer** on top of the [ESP32's native SDK](https://docs.espressif.com/projects/esp-idf/en/stable/esp32/get-started/index.html). It doesn't expose all of the chip's features—you can see the [supported libraries here](https://docs.espressif.com/projects/arduino-esp32/en/latest/libraries.html)—and it adds some overhead compared to programming the chip directly. For our purposes, though, Arduino is a good choice: it lets us focus on learning physical computing concepts rather than wrestling with a new toolchain. + +That said, the ESP32 is completely **independent** of the Arduino ecosystem—just as you don't *have* to use Arduino to program the ATmega328P (used in the Uno) or the ATmega32u4 (used in the Leonardo), you don't have to use Arduino to program the ESP32. Here are some alternatives you may want to explore in the future: + +- **ESP-IDF (C/C++)**: Espressif's official [IoT Development Framework](https://docs.espressif.com/projects/esp-idf/en/stable/esp32/get-started/index.html) is a production-grade, FreeRTOS-based SDK that provides full access to the ESP32's hardware. It's what you'd likely use in industry—more powerful and efficient, but also more complex. If you want to try it, follow the [ESP-IDF Getting Started guide](https://docs.espressif.com/projects/esp-idf/en/stable/esp32/get-started/index.html). +- **CircuitPython / MicroPython**: Python-based alternatives that are great for rapid prototyping. See Adafruit's [CircuitPython guide](https://learn.adafruit.com/adafruit-esp32-s3-feather/circuitpython) for the ESP32-S3 Feather. +- **PlatformIO**: A professional [IDE and build system](https://platformio.org/) that supports ESP32 with both Arduino and ESP-IDF frameworks, and integrates with VS Code. + +## ESP32 Fundamentals + +🚦 **Start Here!** Begin with the [Fundamentals series](fundamentals.md) to get comfortable with the ESP32 board, its 3.3V logic, and its differences from the Arduino Uno/Leonardo. You'll blink LEDs, fade with PWM (using the new LEDC API), read analog input with the 12-bit ADC, play tones, and use the ESP32's built-in capacitive touch sensing—all foundational skills before moving on to wireless. + +**[Start the Fundamentals lessons →](fundamentals.md)** + +## ESP32 Wireless + +In the [Wireless series](wireless.md), you'll explore what makes the ESP32 truly special: building IoT projects with WiFi and Adafruit IO, wirelessly streaming sensor data over Bluetooth Classic SPP, and using Bluetooth Low Energy (BLE) to talk to iPhones, Android phones, and Chrome via Web Bluetooth. + +**[Start the Wireless lessons →](wireless.md)** + +All ESP32 code is open source and in this [GitHub repository](https://github.com/makeabilitylab/arduino/tree/master/ESP32). + +## What's next? + +Once you've completed both sub-series, you'll have a solid foundation for building WiFi-connected, Bluetooth-enabled, sensor-driven projects. Consider exploring more advanced topics like BLE HID devices (making your ESP32 act as a wireless keyboard or game controller), deep sleep for battery-powered projects, or building your own web server directly on the ESP32! diff --git a/esp32/iot.md b/esp32/iot.md index 298450a9..886a59d0 100644 --- a/esp32/iot.md +++ b/esp32/iot.md @@ -1,13 +1,14 @@ --- layout: default -title: L7: Internet of Things +title: L1: Internet of Things description: "Connect the ESP32 to the cloud: stream sensor data over WiFi to an Adafruit IO dashboard, compare REST and MQTT, control an LED remotely, and write non-blocking IoT code." -parent: ESP32 +parent: Wireless +grand_parent: ESP32 has_toc: true # (on by default) usemathjax: true comments: true usetocbot: true -nav_order: 7 +nav_order: 1 --- # {{ page.title | replace_first:'L','Lesson '}} {: .no_toc } diff --git a/esp32/led-blink.md b/esp32/led-blink.md index f3929741..d80ec6c5 100644 --- a/esp32/led-blink.md +++ b/esp32/led-blink.md @@ -3,7 +3,8 @@ layout: default title: L2: Blinking an LED image: /esp32/assets/og/led-blink.jpg description: "Write your first ESP32 sketch: blink the onboard LED with digitalWrite() and LED_BUILTIN, wire an external LED, try the Wokwi simulator, and light the onboard NeoPixel." -parent: ESP32 +parent: Fundamentals +grand_parent: ESP32 has_toc: true # (on by default) usemathjax: true comments: true diff --git a/esp32/led-fade.md b/esp32/led-fade.md index 9f83f884..f9fb1d3e 100644 --- a/esp32/led-fade.md +++ b/esp32/led-fade.md @@ -3,7 +3,8 @@ layout: default title: L3: LED Fading with PWM image: /esp32/assets/og/led-fade.jpg description: "Smoothly fade an LED on the ESP32 using its powerful LEDC PWM peripheral, understand frequency vs. duty-cycle resolution, and use analogWrite() on Arduino core v3.x." -parent: ESP32 +parent: Fundamentals +grand_parent: ESP32 has_toc: true # (on by default) usemathjax: true comments: true diff --git a/esp32/tone.md b/esp32/tone.md index ff981e90..b8bd5747 100644 --- a/esp32/tone.md +++ b/esp32/tone.md @@ -2,7 +2,8 @@ layout: default title: L5: Playing Tones description: "Make sound on the ESP32 with a passive piezo buzzer: play notes, scales, and melodies using tone() and the LEDC tone functions, and control pitch with a potentiometer." -parent: ESP32 +parent: Fundamentals +grand_parent: ESP32 has_toc: true # (on by default) usemathjax: true comments: true diff --git a/esp32/wireless.md b/esp32/wireless.md new file mode 100644 index 00000000..a4a9044e --- /dev/null +++ b/esp32/wireless.md @@ -0,0 +1,105 @@ +--- +layout: default +title: Wireless +description: "The ESP32 Wireless series: connect to the cloud over WiFi with Adafruit IO, stream data over Bluetooth Classic serial, and build phone- and browser-friendly projects with Bluetooth Low Energy (BLE)." +parent: ESP32 +nav_order: 2 +has_toc: false # on by default +has_children: true +comments: true +usetocbot: true +--- +# ESP32 Wireless +{: .no_toc } + +## Table of Contents +{: .no_toc .text-delta } + +1. TOC +{:toc} +--- + + + +In this lesson series, you will explore what makes the ESP32 truly special: **built-in wireless communication**. You'll connect to the cloud over WiFi, stream sensor data to your laptop wirelessly over Bluetooth Classic, build interactive [p5.js](https://p5js.org/) visualizations using [Web Serial](../communication/web-serial.md), and learn Bluetooth Low Energy (BLE)—the protocol behind billions of IoT devices that works on iPhones, Android phones, and directly from Chrome via [Web Bluetooth](https://developer.mozilla.org/en-US/docs/Web/API/Web_Bluetooth_API). 📡 + +The lessons are interactive and designed to be completed **in order**. All ESP32 code is open source and in this [GitHub repository](https://github.com/makeabilitylab/arduino/tree/master/ESP32). + +{: .note } +> Before starting, we recommend completing the [Fundamentals](fundamentals.md) series first. You should be comfortable with the ESP32 board layout, 3.3V logic, the LEDC PWM API, and `analogRead` on the 12-bit ADC. Several lessons here also reference the [Communication module](../communication/index.md)—particularly [Web Serial](../communication/web-serial.md), [p5.js + Serial](../communication/p5js-serial.md), and [p5.js + Serial I/O](../communication/p5js-serial-io.md). + +{: .warning } +> **A note on boards:** Lessons 2 and 3 (Bluetooth Classic) require the **original ESP32** since the ESP32-S3 lacks Bluetooth Classic hardware. All other lessons in this series work on both the original ESP32 and the ESP32-S3. If you only have an S3, you can skim or skip Lessons 2 and 3 and jump straight to Lesson 4 (BLE). + +## Lessons + +### [Lesson 1: Internet of Things](iot.md) + +Connect your ESP32 to WiFi and upload sensor data to the cloud using [Adafruit IO](https://learn.adafruit.com/welcome-to-adafruit-io). This is where the ESP32 truly shines! ✨ + +### [Lesson 2: Bluetooth Serial](bluetooth-serial.md) + +Cut the wire! Pair your ESP32 with your Mac or Windows computer using Bluetooth Classic's Serial Port Profile (SPP), which creates a virtual serial port that behaves exactly like a USB cable. Verify the wireless link with built-in OS tools (`cat`, `screen`, PowerShell) and Python's [pySerial](https://pyserial.readthedocs.io/)—the same library from the [Communication module](../communication/index.md). **Requires the original ESP32** (*e.g.,* [Adafruit Huzzah32](https://www.adafruit.com/product/3405?srsltid=AfmBOopMLfaARdO_FA2CcUqo7YmyJdwVWYZksdyQ8eakXbFqg3IALDRs), [SparkFun ESP32 Thing](https://www.sparkfun.com/sparkfun-esp32-thing.html), [Espressif ESP32-DevKitC](https://www.amazon.com/Espressif-ESP32-DevKitC-32E-Development-Board/dp/B09MQJWQN2?th=1)). + +### [Lesson 3: Bluetooth Web Serial](bluetooth-web-serial.md) + +Stream live sensor data over Bluetooth and visualize it in the browser. Build interactive [p5.js](https://p5js.org/) sketches using [Web Serial](../communication/web-serial.md) and the [serial.js](https://github.com/makeabilitylab/js/blob/main/src/lib/serial/serial.js) library—the same tools from the Communication module, but wireless. Close the loop with bidirectional control: a browser slider that dims an LED on your breadboard wirelessly. **Requires the original ESP32**. + +### [Lesson 4: Introduction to BLE](ble-intro.md) + +Learn **Bluetooth Low Energy (BLE)**—the protocol behind fitness trackers, smart home devices, and billions of IoT sensors. You'll learn the peripheral/central model, the GATT data hierarchy of services and characteristics, and how to stream live sensor data to your phone and computer using notifications. Works with the ESP32-S3 and iPhones! + +### [Lesson 5: Bidirectional BLE](ble-bidirectional.md) + +Send data in *both* directions over BLE. Control the onboard NeoPixel by writing to a BLE characteristic from your phone, build a **Web Bluetooth** interface with sliders and a color picker that runs entirely in the browser, and learn the **Nordic UART Service (NUS)** for serial-like text communication over BLE. + +## What's next? + +Once you've completed the Wireless lessons, you'll have a solid foundation for building WiFi-connected, Bluetooth-enabled, sensor-driven projects. Consider exploring more advanced topics like BLE HID devices (making your ESP32 act as a wireless keyboard or game controller), deep sleep for battery-powered projects, or building your own web server directly on the ESP32! + + \ No newline at end of file