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.
Basic 5 min
Goal: Rename the device by changing BLE.setLocalName("..."). Re-upload and re-scan from the phone — it appears with the new name.
Challenge 1 5 min
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.
Challenge 2 5 min
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.
Challenge 3 · The phone that loses every fight 10 min
Run the Step 5 sketch. Leave the pot still. From the phone, write FF. The LED flashes bright for a moment, then drops straight back. The phone's value also jumps back.
- Find the lines that undo the phone's write. Say how often they run.
- Change the sketch so the pot only takes over when someone turns it.
- Treat a change of more than 8 (on the 0–255 scale) as a real turn. Smaller changes are just noise.
- Keep the 200 ms report timer.
It works if a phone write of FF stays on until you turn the pot, and turning the pot still updates the phone.
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.
Extra Mission 5 min
Part 1 — Design a phone-tuned gadget
Pick something at home with a setting people change: a reading lamp, a fan, a night light, a fish-tank light. On paper, design a BLE version where the phone and a knob on the gadget can both change the setting.
Your design must include:
- The gadget's name and the one value the characteristic holds, with its range.
- Which properties it needs:
BLERead,BLEWrite,BLENotify. - The rule for who wins when the phone and the knob disagree.
- A wiring sketch: which pin the output uses, and which pin the knob uses.
Part 2 — Make it
Build your design on a BLE board, with an LED standing in for the output and a pot as the knob. Use one BLERead | BLEWrite | BLENotify characteristic and your "who wins" rule. No BLE board yet? Write the full sketch and check it compiles for a Nano 33 BLE.
Bring back next class: your design, your uploaded sketch, and a phone screenshot showing the value changing when you turn the knob.