一、前言

宿舍关灯神器应该是一个已经被玩烂了的项目了,我也废话不多说。本篇文章就如标题所说,教大家如何开发一个鸿蒙NEXT版的APP来对单片机进行蓝牙通信。本文的关灯器将采用ESP32作为控制器。

二、材料准备

  1. ESP32
  2. SG90舵机
  3. 杜邦线
  4. 5v锂电池

设备组装

舵机上红色的线是接vin(也就是esp32上的5v引脚,不同的板子叫法不同),黑色的接GND,另外一个是信号线,随便接一个GPIO引脚即可,我这里接的是12,这个要记住后面要用。组装流程图

三、单片机代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
#include <BluetoothSerial.h>
#include <string>
#include <ESP32Servo.h>
using namespace std;

BluetoothSerial SerialBT;
Servo myservo;

int servoPin = 12;
int ini_pos = 90;
bool is_on = false;

void turn_mid_light()
{
myservo.write(90);
}

void turn_light()
{
myservo.write(90 + 45);
delay(500);
turn_mid_light();
}
void turn_off_light()
{
myservo.write(90 - 45);
delay(500);
turn_mid_light();
}

void setup() {
myservo.attach(servoPin);
Serial.begin(115200);
SerialBT.begin("ESP32_Server"); // 设定蓝牙服务器名称
int init_angle = myservo.read(); // 读取初始角度
if (init_angle != ini_pos)
{
myservo.write(ini_pos);
};
}

void loop() {
if(SerialBT.available()){
String cmd = SerialBT.readString();
Serial.print(cmd);
if(cmd == "switch"){
if(is_on){
turn_off_light();
is_on = false;
}else{
turn_light();
is_on = true;
}
}
}
}

下面我来解释一下这段代码:

首先引入了三个头文件<BluetoothSerial.h>(蓝牙库)、和<ESP32Servo.h>(控制舵机的库)。定义了蓝牙串口对象SerialBT和舵机对象myservo,以及servoPin(舵机引脚,就是组装时信号线接的位置)、ini_pos(初始位置角度)、is_on(表示灯是否开启的标志)。

接着定义了三个函数turn_mid_light(将舵机转到中间位置,避免影响人手正常开关灯)、turn_light(将舵机转到特定角度开启灯光,然后再转到中间位置)、turn_off_light(将舵机转到特定角度关闭灯光,然后再转到中间位置)。

在setup函数中,将舵机连接到指定引脚,初始化串口通信,设置蓝牙服务器名称,读取舵机初始角度,如果初始角度不是设定的初始位置角度,则将舵机转到初始位置。

在loop函数中,如果蓝牙串口有数据可读,就读取数据并存储在cmd字符串中。如果cmd等于 “switch”,根据is_on的状态来决定是开启灯光还是关闭灯光。

四、鸿蒙APP代码

1.蓝牙接口

我先不直接放代码,这里要先着重说一下鸿蒙蓝牙开发的方式,因为授之以鱼不如授之以渔,我解释了这一点以后,你在开发其他蓝牙应用的时候你就知道该怎么做了。

由于鸿蒙版本迭代快,api之间差异也不小,你在网上随便搜的教程指不定是哪个api的,并且写教程的人也没说,这就会造成许多误解。所以我会告诉大家我用的api版本,首先Dev eco studio版本是5.0.9.300,compatibleSdkVersion是 5.0.0(12)。

(由于这个项目是24年写的,现在整理的时候发现当前最新的api版本是16,但是主要的用法没有什么大变化。)

在华为官方文档中搜索的时候注意要选上你现在用的api版本

api选项

如果直接搜索蓝牙,会找到一些过时的文档,请注意看上边标注了从某某api后不再支持,如果用错了版本就会影响后面的开发。

image-20250404104246608

经过我的查找,我发现了这个接口@ohos.bluetooth.connection (蓝牙connection模块),这个就是标准蓝牙通信的接口,当然你在左侧的栏目中也会发现有蓝牙ble模块,这个对应的是低功耗蓝牙,不过我这个项目用的是经典蓝牙,如果想试试ble肯定也可以,只不过固件代码也得相应的更改,有兴趣的读者可以研究一下。

image-20250404104342520

2.部分代码

先引入一些必要的库,其中connection和socket是蓝牙连接和通信必要的组件。

1
2
3
4
5
6
import { router } from '@kit.ArkUI';
import { connection } from '@kit.ConnectivityKit';
import { socket } from '@kit.ConnectivityKit';
import { BusinessError } from '@kit.BasicServicesKit';
import { buffer } from '@kit.ArkTS';
import { promptAction } from '@kit.ArkUI';

下面来讲几个重要的函数:

  1. connection.getPairedDevices()

    这个是用于获取已配对设备的mac地址,返回是Array

  2. connection.getRemoteDeviceName()

    官方文档

    当我们拿到了mac地址后,我们想知道设备名称就要用这个函数,这个函数对于已配对设备和未配对都是一样的,只要传入地址就能返回设备名称。在api16中好像又新加进去了一个参数alias,不过是可选,我们不用管这个。

  3. connection.on(‘bluetoothDeviceFind’, Callback<Array>)

    这里可以看到有几个不同的on函数,其中第一个参数传入不同的字符串表示订阅不同的消息,这里我们当然要订阅发现设备的消息,也就是开启蓝牙扫描后只要发现了设备,就执行回调函数。

  4. connection.startBluetoothDiscovery();

    这个就不用多说了,就是开启扫描。

  5. socket.sppConnect(deviceId: string, options: SppOptions, callback: AsyncCallback): void

    连接指定地址的设备

  6. socket.sppWrite(clientSocket: number, data: ArrayBuffer): void

    发送字节数据

所以有了上面的代码,我们就可以分别写出以下功能:

检查已配对设备有无关灯器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
getDevice = (): boolean=>{
try{
let devices: Array<string> = connection.getPairedDevices()
for(let i=0; i<devices.length; i++){
if(connection.getRemoteDeviceName(devices[i])=="ESP32_Server"){
this.address = devices[i]
return true
}
}
} catch (err) {
console.error("errCode:" + err.code + ",errMessage:" + err.message)
}
return false
}

这里的设备名称是你自己设定的,根据你的固件代码进行实际调整。

扫描并配对

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
scanAndPair = ()=>{
try {
connection.on('bluetoothDeviceFind', (data)=>{
for(let i=0;i<data.length;i++){
let adrr = data[i]
if(adrr!=""){
let name = connection.getRemoteDeviceName(adrr)
if(name == "ESP32_Server"){
connection.pairDevice(adrr)
}
}
}
});
connection.startBluetoothDiscovery();
} catch (err) {
console.error("errCode:" + err.code + ",errMessage:" + err.message);
}
}

连接设备

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
connect = ()=>{
try{
socket.sppConnect(this.address, {uuid: '00001101-0000-1000-8000-00805f9b34fb', secure: false, type: 0},
(code: BusinessError, number: number)=>{
if (code) {
console.error('sppListen error, code is ' + code)
promptAction.showToast({message: '连接失败', duration: 2000})
return;
}
this.client_num = number
promptAction.showToast({message: '连接成功', duration: 2000})
this.state = "在线"
this.color = Color.Green
// this.sendCmd()
})
} catch (err) {
console.error("errCode:" + err.code + ",errMessage:" + err.message);
promptAction.showToast({message: '连接失败', duration: 2000})
}
}

发送消息

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
sendCmd = ()=>{
try{
socket.sppWrite(this.client_num, this.strToBuffer("switch"))
this.light_color==Color.Gray ? this.light_color = Color.Green : this.light_color = Color.Gray
socket.sppCloseClientSocket(this.client_num)
} catch (err) {
console.error("errCode:" + err.code + ",errMessage:" + err.message);
if(err.code == 2901054){
promptAction.showToast({message: '连接已丢失请重试', duration: 2000})
this.client_num = -1
this.state = "离线"
this.color = Color.Red
}
}
}

完整代码在此

https://github.com/1wpc/autolight_HMOS.git