如果你对 react-native-ble-plx 了解很少,那么你可以通过:react-native-ble-plx 权威指南 来进行了解和学习。
首先代码是 AI 帮我生成的,但是出现问题后我排查了很久。通过最后找到问题来看之前我也知道这个问题,只不过排查的时候就没注意,因为就是两行代码执行顺序不同而已。
我在前一家公司开发蓝牙 App 的时候就发现他们编写的代码就是先下发再监听,我当时就觉得不对,还修改了公司的代码,只不过我当时看到不对仅仅是因为我在看代码的时候觉得这样存在隐患,并不是说线上出现过问题啥的。
原本这点小问题是不值得分享的,但是有一点是值得深思的,那就是如果你觉得一段代码有问题,但是线上并没有出现问题,你应该怎么做,如果你是倾向于修改,那么你怎么说服领导修改,因为不少领导觉得只要没出现问题就不要动代码。
在看代码之前先说一下业务是怎样开展的。
设备端定义一个服务,这个服务下有两个特征值,一个负责写,一个负责监听。
写特征值是 App -> 设备 ,而监听(notify)就是 设备 -> 手机 。
假如 App 想要获取设备的信息,如: AT+deviceInfo=?\r\n ,这时手机需要负责监听的特征值拿到设备的信息。
下面我们看代码:
/**
* 向蓝牙设备写入数据
*/
async writeData(data: Command): Promise<any> {
const id = Date.now();
const sendStr = JSON.stringify({ id, ...data });
return new Promise((resolve, reject) => {
this.tasks.push({
payload: sendStr,
resolve,
reject,
id,
});
this._send();
});
}
/**
* 发送任务队列
*/
private async _send(): Promise<void> {
if (this.sending) {
return;
}
this.sending = true;
while (this.tasks.length > 0) {
const task = this.tasks.shift();
if (!task) continue;
this.currentTask = task;
let timer: number | null = null;
try {
if (!this.canWrite || !this.writeCharacteristic) {
task.reject?.(new Error('设备不支持写入或未正确连接'));
this.currentTask = null;
break;
}
const promise = Promise.race([
new Promise((resolve, reject) => {
const originalResolve = task.resolve;
task.resolve = (data: any) => {
originalResolve?.(data);
resolve(data);
timer && clearTimeout(timer);
};
const originalReject = task.reject;
task.reject = (error: any) => {
console.log('error-收到错误响应-----', error);
originalReject?.(error);
reject(error);
timer && clearTimeout(timer);
};
}),
new Promise((_, reject) => {
timer = setTimeout(() => {
task.reject?.(new Error('命令响应超时'));
reject(new Error('命令响应超时'));
}, 100000);
}),
]);
const Buffer = require('buffer').Buffer;
// 将字符串转换为 base64
const base64 = Buffer.from(task.payload, 'utf8').toString('base64');
await this.writeCharacteristic.writeWithResponse(base64);
// 等待响应或超时
await promise
// 任务完成
this.currentTask = null;
} catch (error) {
console.error('等待任务完成时出错:', error);
this.currentTask = null;
} finally {
timer && clearTimeout(timer);
}
}
this.sending = false;
}
这是一个加入队列的蓝牙指令发送函数,writeData只负责将要下发的指令保存到队列中,而 _send负责下发。其中问题就出现在:
const Buffer = require('buffer').Buffer;
// 将字符串转换为 base64
const base64 = Buffer.from(task.payload, 'utf8').toString('base64');
await this.writeCharacteristic.writeWithResponse(base64);
// 等待响应或超时
await promise
其中 promise 可以简单的理解为监听蓝牙返回。在上一家公司的时候由于获取设备信息啥的都需要一定的时间,所以下发之后再监听一般就不会遇到问题;这次我之所以遇到问题是因为设备端的 python 代码是我自己写的,我当时就是为了测试能够收到(设备端直接返回没啥逻辑),而且手机这边是通过:
setInterval(() => {
await bluetoothManager.writeData({
method: 'sendCommand',
params: command,
});
}, 150)
这样下发的,设备那边本来就是秒回的,当下发的多了就容易出现设备已经将数据返回给 App 了,此时 App 还没开始监听,由于我目前这个逻辑就导致程序一直被卡住,因为 writeData 的 promise 没有被消费,只要超时了才消费 reject 。
需要说明的是,每次运行程序后都会成功很多次才出现卡住的情况,我测试很多次前面都是成功的,其实这一点也是值得思考的,虽然先下发再监听存在逻辑隐患,但是既然每次下发都是先成功多次才出现的卡住,说明除了这个因素外还有因素影响着,只不过今天暂时不论,后面我思考清楚并且验证了再来。
刚开始我是没有注意到的,而是怀疑是设备蓝牙出问题了,因为我第一次编写设备端的代码,即便是简单的我都不自信。我看这段代码很久很久都没看出啥,虽然我怀疑是设备,但是我转念一想,按道理设备端只要成功过一次就不应该不会再成功,除非蓝牙断开了,但是很明显蓝牙并没有断开,通过其他 app 测试都是没有问题的,于是我才把全部的精力都放到 app 端,之前看 python 看得头皮发麻,说白了也没看出啥问题。然后我就一直重复说着卡住了卡住了,为啥会卡住呢,说着说着突然间想到会超时,说明是超时了才消费了 reject ,那为啥 resolve 没有消费,道理很简单就是没执行到 resolve ,那为啥执行不到,因为监听没收到设备发来的,那为啥又收不到呢,难道是设备没发,但是我刚才的分析设备端是没问题的。于是我又开始看日志,看代码,在我大脑里模拟执行,这下我想到了一种问题,就是先下发然后再监听存在隐患,万一下发后立马收到了消息,而此时还没开始监听,那岂不是永远也拿不到 resolve ,理论成立,于是我立马调换一下位置,然后再次测试,我就没有发现问题了。
其实有时候我们细想一下,破局的关键在于我静下心来分析一行行代码,之前没发现都是因为我觉得这里不应该有问题就没有特别仔细的查看代码,幸运的是我在恼怒之前静下来分析了代码,这个过程是享受的,再一次验证那句话“柳暗花明又一村”。
我现在越来越觉得很多事情一定要走到最后,特别是市面上已经证明过的,虽然每个人的条件不同,但是既然有人可以,你就要坚持走到最后一步,这样才有可能解决问题,而不是说环境不好,这不是压榨自己,而是我觉得这应该是在面对问题时应该有的态度,而且我在这个过程中也是快乐的,并不是那种很难受(被压榨的难受)。
现在说回上面的那句话,如果你发现逻辑问题,但是线上没有问题,你又想改代码,你应该怎么做,不可能直接就改,直接就改可能存在隐患,这个隐患就来自于你的猜测,理论和真实有时候是不一样的,我给出的解决方案就是真正找到你觉得会出现问题可复现路径,而不是假设,即便你的复现路径是上游突然返回的数据为空啥的都可以,即便你跟后端约定他们应该正常返回值。也就是说如果你觉得后端返回空会导致逻辑问题,那你就模拟出这种情况,然后看目前线上代码运行的结果是什么,结果是不是你猜测那样,这样你就有了说服力,如果不是你就应该思考你原本的逻辑是不是也存在漏洞。