30、综合实战(下):完成智能台灯App的蓝牙/WiFi控制部分,实现手机与硬件联动
好,上一章我们把光线传感器和PWM调光的底层逻辑打通了。现在,咱们来干点真正酷的事——让手机App和硬件“对话”。
说白了,就是你的手机能远程开关灯、调亮度,甚至根据环境光自动调节。我当年做第一个物联网项目时,看到手机上的滑块一拖,灯泡真的变亮了,那种感觉……嗯,比第一次跑通Hello World爽多了。
通信方案选型:蓝牙还是WiFi?
先别急着写代码。你得想清楚:用蓝牙还是WiFi?
我个人习惯这样判断:
- 蓝牙(BLE):手机和设备一对一,距离近(10米内),功耗低。适合台灯这种场景——你坐在旁边,手机控制就行。
- WiFi:可以远程控制,哪怕你人在公司也能关家里的灯。但功耗高,需要配网流程,开发复杂度也高一些。
我在项目中遇到过不少选型翻车的情况。有人做智能窗帘,非要上WiFi,结果用户说“我阳台没WiFi信号”……你想想看,多尴尬。
咱们这节课,以蓝牙BLE为例。因为入门简单,而且台灯场景下,手机离灯通常不超过5米。WiFi部分我会在最后给个扩展思路。
核心结论:台灯这种近距离、低功耗、实时控制场景,BLE是首选。WiFi适合需要远程访问或接入智能家居生态的场景。
蓝牙BLE通信架构
先画个图,让你对整个通信链路有个直观认识。
看到没?手机是客户端(GATT Client),ESP32是服务端(GATT Server)。它们通过“特征值”来交换数据。你写一个值进去,硬件就执行一个动作;硬件上报一个值,App就收到通知。
硬件端代码:ESP32上的BLE服务
先搞定硬件端。我用的是ESP32 + Arduino框架。代码不长,但每个细节都关键。
#include <BLEDevice.h>
#include <BLEUtils.h>
#include <BLEServer.h>
// 定义服务和特征值UUID
#define SERVICE_UUID "4fafc201-1fb5-459e-8fcc-c5c9c331914b"
#define CHAR_LIGHT_CTRL "beb5483e-36e1-4688-b7f5-ea07361b26a8"
#define CHAR_SENSOR_DATA "cba1d466-344c-4b0d-9b1e-6b3c3a1f2e8d"
BLECharacteristic *pLightChar;
BLECharacteristic *pSensorChar;
// PWM设置
const int ledPin = 2; // GPIO2
int currentBrightness = 0;
// 回调:处理App发来的控制指令
class MyCallbacks : public BLECharacteristicCallbacks {
void onWrite(BLECharacteristic *pCharacteristic) {
String value = pCharacteristic->getValue();
if (value.length() > 0) {
int brightness = value.toInt();
brightness = constrain(brightness, 0, 255);
currentBrightness = brightness;
analogWrite(ledPin, brightness);
Serial.println("亮度已更新: " + String(brightness));
}
}
};
void setup() {
Serial.begin(115200);
pinMode(ledPin, OUTPUT);
// 初始化BLE
BLEDevice::init("SmartLamp_ESP32");
BLEServer *pServer = BLEDevice::createServer();
BLEService *pService = pServer->createService(SERVICE_UUID);
// 控制特征值(可写)
pLightChar = pService->createCharacteristic(
CHAR_LIGHT_CTRL,
BLECharacteristic::PROPERTY_WRITE
);
pLightChar->setCallbacks(new MyCallbacks());
// 传感器数据特征值(可通知)
pSensorChar = pService->createCharacteristic(
CHAR_SENSOR_DATA,
BLECharacteristic::PROPERTY_NOTIFY
);
pService->start();
BLEAdvertising *pAdvertising = pServer->getAdvertising();
pAdvertising->start();
Serial.println("BLE服务已启动,等待连接...");
}
void loop() {
// 模拟读取光线传感器(实际项目中替换为I2C读取)
int lux = analogRead(34); // 假设接在GPIO34
lux = map(lux, 0, 4095, 0, 1000);
// 每2秒上报一次传感器数据
static unsigned long lastTime = 0;
if (millis() - lastTime > 2000) {
String data = String(lux);
pSensorChar->setValue(data.c_str());
pSensorChar->notify();
lastTime = millis();
}
delay(10);
}
个人经验:UUID别随便乱写。我早期项目里用过“1234”这种测试UUID,结果跟其他设备冲突,调试了一下午才发现。建议用在线UUID生成器,每次生成新的。
Android端:BLE扫描、连接与通信
Android端的代码稍微复杂一点。因为涉及权限、扫描、连接队列管理。我直接给你一个封装好的工具类,你拿去就能用。
// BLEManager.java - 核心管理类
public class BLEManager {
private BluetoothAdapter bluetoothAdapter;
private BluetoothLeScanner bleScanner;
private BluetoothGatt bluetoothGatt;
private BluetoothGattCharacteristic lightChar;
private BluetoothGattCharacteristic sensorChar;
private Context context;
private boolean isScanning = false;
// 回调接口
public interface BLEListener {
void onDeviceFound(BluetoothDevice device, int rssi);
void onConnected();
void onDisconnected();
void onSensorDataReceived(int lux);
}
public BLEManager(Context context) {
this.context = context;
BluetoothManager bluetoothManager =
(BluetoothManager) context.getSystemService(Context.BLUETOOTH_SERVICE);
bluetoothAdapter = bluetoothManager.getAdapter();
bleScanner = bluetoothAdapter.getBluetoothLeScanner();
}
// 开始扫描
public void startScan(BLEListener listener) {
if (isScanning) return;
isScanning = true;
ScanCallback scanCallback = new ScanCallback() {
@Override
public void onScanResult(int callbackType, ScanResult result) {
BluetoothDevice device = result.getDevice();
if (device.getName() != null &&
device.getName().equals("SmartLamp_ESP32")) {
listener.onDeviceFound(device, result.getRssi());
}
}
};
bleScanner.startScan(scanCallback);
}
// 连接设备
public void connect(BluetoothDevice device, BLEListener listener) {
bluetoothGatt = device.connectGatt(context, false,
new BluetoothGattCallback() {
@Override
public void onConnectionStateChange(BluetoothGatt gatt,
int status, int newState) {
if (newState == BluetoothProfile.STATE_CONNECTED) {
gatt.discoverServices();
listener.onConnected();
} else if (newState == BluetoothProfile.STATE_DISCONNECTED) {
listener.onDisconnected();
}
}
@Override
public void onServicesDiscovered(BluetoothGatt gatt, int status) {
BluetoothGattService service =
gatt.getService(UUID.fromString(SERVICE_UUID));
lightChar = service.getCharacteristic(
UUID.fromString(CHAR_LIGHT_CTRL));
sensorChar = service.getCharacteristic(
UUID.fromString(CHAR_SENSOR_DATA));
// 开启传感器通知
gatt.setCharacteristicNotification(sensorChar, true);
}
@Override
public void onCharacteristicChanged(BluetoothGatt gatt,
BluetoothGattCharacteristic characteristic) {
String data = characteristic.getStringValue(0);
int lux = Integer.parseInt(data.trim());
listener.onSensorDataReceived(lux);
}
});
}
// 发送亮度控制指令
public void setBrightness(int brightness) {
if (lightChar == null) return;
brightness = Math.max(0, Math.min(255, brightness));
lightChar.setValue(String.valueOf(brightness));
bluetoothGatt.writeCharacteristic(lightChar);
}
}
注意:Android 12以上需要动态申请 BLUETOOTH_SCAN、BLUETOOTH_CONNECT 权限。我踩过这个坑——在小米手机上扫描不到设备,折腾半天发现是权限弹窗被系统拦截了。记得在代码里处理权限回调。
UI层:控制面板的实现
UI我用了最简单的布局:一个SeekBar调亮度,一个TextView显示传感器数据。你可以在XML里这样写:
<!-- activity_main.xml -->
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:padding="24dp">
<TextView
android:text="智能台灯控制"
android:textSize="22sp"
android:textStyle="bold"
android:layout_marginBottom="32dp"/>
<TextView
android:text="当前亮度"
android:textSize="16sp"/>
<SeekBar
android:id="@+id/seekBar_brightness"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:max="255"
android:progress="128"/>
<TextView
android:id="@+id/text_brightness_value"
android:text="128"
android:textSize="18sp"
android:layout_marginTop="8dp"/>
<TextView
android:text="环境光传感器"
android:textSize="16sp"
android:layout_marginTop="32dp"/>
<TextView
android:id="@+id/text_sensor_lux"
android:text="等待数据..."
android:textSize="24sp"
android:textColor="#FF9800"/>
<Button
android:id="@+id/btn_connect"
android:text="连接设备"
android:layout_marginTop="32dp"/>
</LinearLayout>
在Activity里,把SeekBar的滑动事件和BLE的写入绑定起来:
// MainActivity.java 关键部分
SeekBar seekBar = findViewById(R.id.seekBar_brightness);
seekBar.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() {
@Override
public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
if (fromUser) {
bleManager.setBrightness(progress);
textBrightness.setText(String.valueOf(progress));
}
}
// 其他回调省略...
});
WiFi扩展思路
如果你想把台灯接入WiFi,思路其实类似:
- ESP32作为HTTP Server:监听一个端口,App通过HTTP GET/POST发送指令。
- MQTT协议:更适合生产环境。ESP32订阅一个Topic,App发布消息到该Topic。
- 配网问题:可以用SmartConfig或蓝牙辅助配网。我建议用蓝牙配网——先通过BLE把WiFi密码发给ESP32,它再自动连接。用户体验好很多。
我的建议:先跑通蓝牙版本,再考虑WiFi。一口吃不成胖子。蓝牙版本从扫描到控制,整个流程走通也就半天时间。WiFi版本涉及路由器、防火墙、公网IP等问题,调试起来会多花几倍时间。
调试技巧与避坑指南
最后,分享几个我实战中总结的调试技巧:
- 用nRF Connect App先测硬件:在写Android代码之前,先用这个App连接ESP32,手动读写特征值。确认硬件没问题,再写App端代码。这样能隔离问题。
- 日志要打全:ESP32的Serial.print和Android的Log.d,能打多细打多细。我曾经因为少打一行日志,查了一个小时才发现是UUID写错了。
- 注意MTU:BLE默认MTU是23字节,如果你要传大包(比如固件升级),记得协商MTU。控制台灯这种小数据,默认就够用。
- 重连机制:App切到后台再切回来,蓝牙可能会断开。记得在onResume里检查连接状态,断开了自动重连。
避坑指南:我曾经在华为手机上遇到一个问题——扫描不到任何BLE设备。后来发现是华为的“手机管家”把蓝牙扫描权限给禁了。用户层面根本不知道。所以,建议在App启动时检查权限,并引导用户去设置里开启。
好了,蓝牙/WiFi控制部分就讲到这里。你现在应该能自己写一个App,跟ESP32台灯硬件联动了。从传感器读取到PWM调光,再到手机远程控制,整个闭环已经打通。剩下的,就是优化体验和加功能了。
公众号:蓝海资料掘金营,微信deep3321