Learning Goals 5 min
Yesterday's sketch had a single binary characteristic — on / off. Today you build a richer one: LED brightness as an 8-bit value (0–255), with read + write + notify properties wired up. The phone sets brightness; the Arduino also reports the brightness back to any other connected app via notifications. By the end of this lesson you will:
- Add a
BLEByteCharacteristicwith all three properties (BLERead | BLEWrite | BLENotify) and react in code when the central writes to it. - Push a new value back to subscribed centrals with
characteristic.writeValue()— the "phone gets notified when sensor changes" pattern. - Drive a real LED via PWM on a Nano 33 BLE's pin from your phone's slider control in the nRF Connect / Bluefruit app.
Warm-Up 10 min
Wire-up is one LED + 220 Ω resistor on a PWM-capable pin of your BLE board. On the Nano 33 BLE, any digital pin supports analogWrite at PWM. We'll use D3.
| Component | Nano 33 BLE pin |
|---|---|
| LED + 220 Ω | D3 → resistor → LED → GND |
Pre-class question
Your phone writes 128 to a brightness characteristic. How should the Arduino respond?
Reveal
Three things: (1) analogWrite(LED, 128) to physically dim the LED; (2) optionally update the characteristic's stored value to 128 so a subsequent read returns it (the library already does this for writes from the central — confirm by reading the docs); (3) optionally characteristic.writeValue(128) to notify any subscribed centrals of the new value (lets a second app stay in sync with the first).
New Concept · Read + write + notify on one characteristic 25 min
Characteristic properties revisited
| Property | Means |
|---|---|
BLERead | Central can ask "what's the current value?" on demand. |
BLEWrite | Central can set a new value. |
BLEWriteWithoutResponse | Central writes but doesn't need an acknowledgement. Faster, less reliable. |
BLENotify | Central can subscribe; peripheral pushes new values when they change. No acknowledgement. |
BLEIndicate | Like Notify but acknowledged. Slightly slower; guaranteed delivery. |
Combine with |:
BLEByteCharacteristic ledBright("...",
BLERead | BLEWrite | BLENotify);That single characteristic now supports all four operations.
The handler pattern in loop()
void loop() {
BLEDevice central = BLE.central();
if (!central) return;
while (central.connected()) {
// 1. If the central wrote a new value, react.
if (ledBright.written()) {
byte newValue = ledBright.value();
analogWrite(LED_PIN, newValue);
// Optional: notify subscribed centrals about the new value.
// (writeValue both stores AND pushes Notify to subscribers.)
ledBright.writeValue(newValue);
}
// 2. Optionally do other periodic work here.
}
}.written() returns true ONLY ONCE per write — checks and clears the flag. So one pass through the loop dispatches a write, the next pass returns false until the next write arrives. Standard event-driven pattern.
Pushing data to subscribed centrals
The peripheral controls notifications. To push a new sensor value:
int sensor = analogRead(A0);
byte mapped = map(sensor, 0, 1023, 0, 255);
ledBright.writeValue(mapped); // updates stored value AND notifies subscribersThe phone's app sees the new value pop up without polling — provided it's subscribed. Most BLE scanner apps have a "subscribe" toggle next to each Notify characteristic.
The full sketch shape
#include <ArduinoBLE.h>
const int LED_PIN = 3;
BLEService ledService("19B10000-E8F2-537E-4F6C-D104768A1214");
BLEByteCharacteristic ledBright("19B10001-E8F2-537E-4F6C-D104768A1214",
BLERead | BLEWrite | BLENotify);
void setup() {
Serial.begin(9600);
pinMode(LED_PIN, OUTPUT);
if (!BLE.begin()) {
Serial.println("# BLE init failed");
while (true) ;
}
BLE.setLocalName("LED-Dimmer");
BLE.setAdvertisedService(ledService);
ledService.addCharacteristic(ledBright);
BLE.addService(ledService);
ledBright.writeValue((byte)0);
BLE.advertise();
Serial.println("# Advertising as LED-Dimmer");
}
void loop() {
BLEDevice central = BLE.central();
if (!central) return;
Serial.print("# Connected: "); Serial.println(central.address());
while (central.connected()) {
if (ledBright.written()) {
byte v = ledBright.value();
analogWrite(LED_PIN, v);
Serial.print("# bright = "); Serial.println(v);
}
}
Serial.println("# Disconnected");
}Worked Example · Phone-controlled LED brightness 25 min
Step 1 — wire one LED + resistor to D3
Standard LED wiring. Long leg → D3, short leg through a 220 Ω resistor → GND.
Step 2 — upload the §3 sketch
Open Serial Monitor at 9600 baud; you should see "# Advertising as LED-Dimmer".
Step 3 — connect from phone
- Open nRF Connect (Android) or LightBlue (iOS).
- Scan; find "LED-Dimmer".
- Connect. Expand the service. Find the brightness characteristic.
- Tap the up-arrow icon to write a value. Enter
FF(hex 255). LED full brightness. - Write
80(128). LED half brightness. - Write
00. LED off.
Step 4 — subscribe to notifications
Tap the down-arrow icon next to the characteristic to subscribe. Now write a new value from another tab / scanner app — your subscribed view should auto-update with the new value. Cross-device sync via BLE.
Step 5 — drive the brightness from a sensor
Add a phototransistor or pot on A0 and report its reading via the same characteristic:
unsigned long lastReport = 0;
void loop() {
BLEDevice central = BLE.central();
if (!central) return;
while (central.connected()) {
if (ledBright.written()) {
byte v = ledBright.value();
analogWrite(LED_PIN, v);
}
// Periodically report the pot value too, for any subscribers
if (millis() - lastReport >= 200) {
lastReport = millis();
int raw = analogRead(A0);
byte mapped = map(raw, 0, 1023, 0, 255);
ledBright.writeValue(mapped);
analogWrite(LED_PIN, mapped);
}
}
}Now the pot controls the LED locally AND the phone sees the value update 5× per second via Notify. Real wearable-style telemetry.
Step 6 — use Bluefruit Connect for a real slider
Adafruit Bluefruit Connect (iOS + Android) has a built-in "Controller" view with sliders, pad controls and colour pickers. Set it to write to your characteristic's UUID and you get a real touchscreen slider that dims the LED with your finger — no code on the phone, no app development required. This is the standard "phone-side UI" pattern for BLE prototypes.
Try It Yourself 15 min
Goal: Rename the device by changing BLE.setLocalName("..."). Re-upload and re-scan from the phone — it appears with the new name.
Goal: Add a second characteristic for "LED colour" (R, G, B in 3 bytes). Drive an RGB LED accordingly. Service holds both characteristics; phone can adjust either independently.
Hint
BLECharacteristic ledColour("19B10002-...",
BLERead | BLEWrite, 3); // 3 bytes
void loop() {
// ... existing brightness handler ...
if (ledColour.written()) {
const uint8_t* rgb = (const uint8_t*)ledColour.value();
analogWrite(R_PIN, rgb[0]);
analogWrite(G_PIN, rgb[1]);
analogWrite(B_PIN, rgb[2]);
}
}BLECharacteristic (raw bytes, fixed length) is the right type for multi-byte values like RGB triples. The byte-typed ones (BLEByteCharacteristic) are restricted to single bytes.
Goal: Implement the standard Battery Service. Use the 16-bit UUIDs 0x180F (service) and 0x2A19 (battery level). Make the "battery level" just a percentage you fake from millis(). Now your device appears as a real battery in any battery-aware BLE app.
Hint
BLEService batteryService("180F");
BLEByteCharacteristic batteryLevel("2A19", BLERead | BLENotify);
// In loop, every 5s:
byte fakePct = 100 - ((millis() / 1000 / 60) % 100); // counts down 1% per minute
batteryLevel.writeValue(fakePct);This is exactly how real smartwatches expose their battery to phones — same UUIDs, same data type. Generic Battery Service is part of the standard BLE profiles published by the Bluetooth SIG.
Mini-Challenge · Sketch your project's BLE service 10 min
Take the plant-monitor design from L03-26 §6 and turn it into a real sketch outline:
- Declare the service and four characteristics with the UUIDs you noted.
- Stub out the four
setup()calls (add to service, etc.). - Stub out the four handlers in
loop(): read soil moisture and callwriteValueevery 30 s; same for temperature; check water-now writes and pulse a pin; update battery every 5 min. - Don't worry about the actual sensor code yet — just the BLE plumbing.
You don't have to upload this; it's a thinking exercise. The skeleton is what you'd hand a teammate to fill in the sensor code, or send to a future-you when you actually build the plant monitor.
Recap 5 min
One characteristic, three properties (BLERead | BLEWrite | BLENotify), gives you a full bidirectional control + status channel. .written() is single-shot; check it once per loop. .writeValue() both stores AND pushes Notify packets to subscribed centrals. Phone-side UI can be the nRF Connect scanner (writing hex) for prototyping, or Bluefruit Connect's built-in slider widget, or a custom app you build later. Tomorrow we combine everything from Cluster E into a real Bluetooth-controlled robot car — using the HC-05 chassis from L03-25.
BLEByteCharacteristic- A single-byte characteristic. Other types:
BLEIntCharacteristic,BLEFloatCharacteristic,BLEStringCharacteristic,BLECharacteristic(raw fixed-length bytes). - BLERead / BLEWrite / BLENotify
- The three most-used property flags. Combine with
|. Notify lets the peripheral push updates to subscribed centrals. .written()- True for one call after the central writes a new value. Single-shot; calling it clears the flag.
.value()- Returns the current stored value (the most recent write from the central, or the most recent
writeValue()from the peripheral). .writeValue(x)- Update the stored value AND push a Notify packet to any subscribed centrals.
- Subscribed central
- A central that has explicitly asked to be notified when a Notify-capable characteristic changes. Subscription state is per-characteristic, per-central.
- Standard service
- A pre-defined BLE service from the Bluetooth SIG with assigned short (16-bit) UUIDs — Heart Rate, Battery, Cycling Power, etc. Use them when your data fits the standard.
- Phone-side UI
- The app on the phone that reads/writes characteristics. Options: generic scanner (nRF Connect, LightBlue), prebuilt controller (Bluefruit Connect), or a custom app you build in React Native / Swift / Kotlin.
Homework 5 min
- If you have BLE hardware: upload the LED-Dimmer sketch and shoot a 20-second video of you dimming the LED from your phone.
- Save the sketch as
ble-dimmer.ino. - Read ahead to ARD-L03-28 (Bluetooth-Controlled Car). Tomorrow we build the BIG project — the chassis from L03-25 with a fancier phone-side joystick controller. You'll re-use the HC-05 chassis sketch (not BLE — Classic remains easier for high-bandwidth real-time joystick).
- Bring tomorrow: chassis, fully wired, batteries fresh.
Bring back next class:
- Saved BLE sketch + (optional) demo video.
- The 2WD chassis and the HC-05 ready to roll.