蓝牙命令分为一组
服务,例如
仪表盘服务、
武器服务 和
驱动控制服务。然后每个服务包含一组
特征,例如
仪表盘服务 包含
速度指示特征 和
油量指示特征。
每个服务要么用于接收 读取指令 来提供反馈(仪表盘服务及其特征),要么用于接收 写入指令 来执行操作(武器和驱动控制服务)。
请注意,每个服务和特征都可以使用其 UUID 进行标识。默认服务通常为 4 个字符长(例如,fa00),而其他服务则更长,看起来像这样 9a66fb00-0800-9191-11e4-012d1540cb8e。
与 BLE 设备通信的整体步骤
无论您使用什么技术与 BLE 设备通信,都必须遵循类似的一组步骤。
- 确保蓝牙已启用且可用
- 扫描区域内的 BLE 设备
- 允许用户选择设备并连接
- 使用服务 + 特征发送读取/写入命令
使用 Wowee Mip 机器人
为了向 MiP 机器人发送指令,我们需要使用 serviceUUID = 'fe05' 和 characteristicUUID = 'ffe9'。
例如,该协议提供了如何更改胸部 LED 颜色 的描述。
它包含该指令的代码 0x84,以及所需的三个字节值参数:红色、绿色和蓝色。
因此,如果我们想使用 RGB 值 FFAA88 调用它,该命令应类似于:'0x84,0xFF,0xAA,0x88'
NativeScript 蓝牙插件 API
设置
首先,我们需要将该插件添加到您的 NativeScript 项目中。
tns plugin add nativescript-bluetooth
要求
然后,我们需要请求该模块。
对于 Vanilla JS,请使用
var
bluetooth = require(
"nativescript-bluetooth"
);
对于 TypeScript(强烈推荐),请使用
import bluetooth = require(
'nativescript-bluetooth'
);
检查蓝牙是否可用
在我们开始与设备通信之前,我们需要
首先检查蓝牙是否已启用。
为此,只需调用 isBluetoothEnabled,它将返回一个包含布尔标志的 promise。
bluetooth.isBluetoothEnabled().then(
enabled => console.log(
"Enabled? "
+ enabled)
);
rxjs
我们可以使用
rxjs
稍微增强一下,创建一个
Observable
,每当蓝牙启用或禁用时,它都会发出一个事件。
import { Observable } from
'rxjs/Observable'
;
import
'rxjs/add/operator/distinctUntilChanged'
;
listenToBluetoothEnabled(): Observable<boolean> {
return
new
Observable(observer => {
bluetooth.isBluetoothEnabled()
.then(enabled => observer.next(enabled))
let intervalHandle = setInterval(
() => {
bluetooth.isBluetoothEnabled()
.then(enabled => observer.next(enabled))
}
, 1000);
// 取消订阅时停止每秒检查
return
() => clearInterval(intervalHandle);
})
.distinctUntilChanged();
}
然后订阅它,以便我们收到有关更改的通知
this
.listenToBluetoothEnabled()
.subscribe(enabled =>
this
.isBluetoothEnabled = enabled);
这为我们提供了一种优雅的机制来处理这种情况,并在蓝牙被禁用时提示用户启用蓝牙。
第二步,检查/请求应用程序权限以使用蓝牙
此步骤仅适用于 Android,因为 iOS 会自动处理此步骤。
要检查权限,请调用:bluetooth.hasCoarseLocationPermission(),它将返回一个包含结果的 promise。
bluetooth.hasCoarseLocationPermission()
.then(granted =>
console.log(
"Has Location Permission? "
+ granted);
);
如果为 false,则应通过调用 bluetooth.requestCoarseLocationPermission() 请求权限。
提示
我倾向于跳过权限检查,并在加载应用程序的第一个页面时始终调用请求函数。如果权限已授予,该插件足够智能,可以忽略我的请求。
例如,在 Angular 项目中,我从
ngOnInit
中执行此操作,如下所示:
ngOnInit() {
bluetooth.requestCoarseLocationPermission();
}
搜索
蓝牙准备好后,我们就可以开始搜索设备了。
为此,我们需要使用 startScanning 函数,并提供要扫描的时长(以 秒 为单位)以及每个发现的设备的回调函数,如 `onDiscovered`。
我们从 onDiscovered 中获得的两个最重要的信息是
- peripheral.name - 可用于显示给用户进行选择
- peripheral.UUID - 此设备的 UUID,用于连接到该设备。
我们将使用 onDiscovered 的结果来填充 devices 数组,您可以使用它在 listview 中显示数据。
private devices: any[] = [];
scan() {
this
.devices = [];
bluetooth.startScanning({
seconds: 3,
onDiscovered: (peripheral: Peripheral) => {
if
(peripheral.name) {
console.log(`UUID: ${peripheral.UUID} name: ${peripheral.name}`)
this
.devices.push(peripheral);
}
}
})
}
连接
如果一切顺利,您应该会看到设备列表,包括我们的 MiP 机器人。
要连接到它,我们需要调用 connect 函数,并提供
- UUID - 设备的 UUID
- onConnected 函数 - 一个回调函数,在连接完全建立时调用。使用此函数导航到包含设备控制的视图(例如,包含 goForward、goBack 按钮的视图)
- onDisconnected 函数 - 一个回调函数,在设备断开连接时调用。使用此函数返回扫描器视图
connect(UUID: string) {
bluetooth.connect({
UUID: UUID,
onConnected: (peripheral: Peripheral) => {
alert(
'Connected'
);
// 在此处您可以导航到控制器视图
},
onDisconnected: (peripheral: Peripheral) => {
alert(
'Device Disconnected'
);
// 在此处您可以导航到扫描视图
}
})
}
扫描页面完整示例
发送命令
连接到机器人后,我们可以开始发送一些指令了。
根据协议,我们可以使用以下参数告诉机器人向前和向后移动。
要以 20 的速度向前移动 500 毫秒,我们需要提供以下值
属性 |
值 |
十六进制 |
指令代码 |
0x71 |
0x71 |
速度 |
20 |
0x14 |
时间 |
500/7 = 71 |
0x47 |
我们可以通过两种不同的方式提供值
- 作为逗号分隔的十六进制值的字符串,例如:value: '0x71,0x14,0x47'
- 作为Uint8Array,包含数值,例如:value: new Uint8Array([0x71,0x14,0x47]) 或 value: new Uint8Array([0x71,20,71])
提示
字符串似乎是硬编码值的不错选择,但是
Uint8Array
当我们需要处理来自UI的持续值流时,会更好。
要发出命令,我们需要调用write或writeWithoutResponse函数,它们接受
- serviceUUID - 在我们的例子中是ffe5
- characteristicUUID - 在我们的例子中是ffe9
- peripheralUUID - 在我们的例子中是连接到的机器人的UUID
moveForward() {
bluetooth.writeWithoutResponse({
serviceUUID:
'ffe5'
,
characteristicUUID:
'ffe9'
,
peripheralUUID:
this
.deviceUUID,
value:
'0x71,0x14,0x47'
})
}
moveBack() {
bluetooth.writeWithoutResponse({
serviceUUID:
'ffe5'
,
characteristicUUID:
'ffe9'
,
peripheralUUID:
this
.deviceUUID,
value:
new
Uint8Array([0x72,0x20,0x40])
})
}
提示
writeWithoutResponse
比
write
快得多,因为它不等待设备的响应。大多数情况下,你应该使用
writeWithoutResponse
。只有当你想等待命令被连接的设备接收和处理时,你才应该使用
write
。对于MiP机器人来说,没有必要这样。
当我使用
Parrot Mambo无人机
之前,在发送任何飞行指令之前,我必须先通过发送4个配置命令来初始化它。每个命令必须在前面的命令完成后发送,这意味着我需要使用
write
并等待一个Promise,直到我可以调用下一个命令。
连续驱动
我相信从这一点你就可以弄清楚如何让机器人左转和右转。
四个带时间的驱动命令很容易实现,但是它们不能给你最好的驾驶体验。幸运的是,机器人还有另一个功能叫做连续驱动,它允许你每50毫秒发送一次驾驶指令,以获得更流畅的驾驶体验。
由于我们要多次调用连续驱动,所以最好把它封装在一个优雅的函数中。它将接受速度和转弯属性(范围从-1到1),将它们转换为十六进制,然后调用writeWithoutResponse函数。
注意,要向前移动,我们需要发送0到0x20之间的值,要后退,我们需要发送0x21到0x40之间的值。要向左转,我们需要发送0x61到0x80之间的值,要向右转,我们需要发送0x41到0x60之间的值
/**
* 将速度和转弯值转换为MiP期望的十六进制值
* 并发出连续驱动命令
* @param speed 向前移动[0到1],后退[-1到0]
* @param turn 左转[-1到0],右转[0到1]
*/
continuousDrive(speed: number, turn: number) {
const mipSpeed = (speed > 0) ?
this
.convertTo0x20(speed, 0) :
this
.convertTo0x20(speed, 0x20);
const mipTurn = (turn > 0) ?
this
.convertTo0x20(turn, 0x40) :
this
.convertTo0x20(turn, 0x60);
bluetooth.writeWithoutResponse({
serviceUUID:
this
.serviceUUID,
characteristicUUID:
this
.characteristicUUID,
peripheralUUID:
this
.deviceUUID,
value:
new
Uint8Array([0x78, mipSpeed, mipTurn])
});
}
convertTo0x20(val: number, offset: number) {
if
(val < 0) {
val = -val;
}
return
Math.floor(val * 0x20) + offset;
}
现在我们可以在循环中调用它,使机器人以圆形的方式向前左转。
setInterval(
() =>
this
.continuousDrive(0.5, -0.7)
,50);
或者,如果你有速度和转弯参数,你可以从UI更新它们,你可以像这样调用它
setInterval(
() =>
this
.continuousDrive(
this
.speed,
this
.turn)
,50);
加速度计
但实际上,要充分利用连续驱动,我们需要一些东西,让用户能够持续地向机器人提供反馈。
首先,让我们将插件添加到我们的项目中。
tns plugin add nativescript-accelerometer
使用加速度计相当简单。插件提供了两个函数
- startAccelerometerUpdates - 它接受一个回调函数,返回手机位置,包含x、y和z坐标
- stopAccelerometerUpdates - 它停止加速度计更新
然而,加速度计的更新频率与我们对连续驱动所需的每50毫秒不同。为了解决这个问题,我们将使用加速度计更新来更新转弯(使用data.x)和速度(使用data.y`)属性,并单独启动一个带`setInterval`的时间循环,该循环将每50毫秒调用一次连续驱动。
然后,我们需要一个单独的函数,允许我们停止加速度计更新和时间循环
private loopHandle: number =
null
;
startAccelerometerInterval() {
// 如果加速度计已处于活动状态,则忽略
if
(
this
.loopHandle) {
return
;
}
let turn: number = 0;
let speed: number = 0;
// 启动加速度计更新
startAccelerometerUpdates(
(data: AccelerometerData) => {
turn = data.x;
// 向左倾斜 [-1到0] 向右倾斜 [0到1]
speed = data.y;
// 向后倾斜 [-1到0] 向前倾斜 [0到1]
}
);
// 启动时间循环
this
.loopHandle = setInterval(() => {
this
.continuousDrive(speed, turn);
}, 50);
}
stopAccelerometerInterval() {
if
(
this
.loopHandle) {
stopAccelerometerUpdates();
clearInterval(
this
.loopHandle);
this
.loopHandle =
null
;
}
}
现在,如果你调用startAccelerometerInterval函数,你应该可以通过向前或向后倾斜它来使机器人向前或向后移动。要让它转弯,只需将它向左或向右倾斜。
多么酷啊?
带有rxjs Observable的加速度计
为了更刺激,让我们将加速度计功能封装在rxjs Observable中。
我们需要我们的Observable启动加速度计更新并发出所有值,并且在取消订阅时,它应该停止更新。
在订阅之前,我们需要使用 **sampleTime(50)**,它只会在每隔 50 毫秒时获取当前值,并忽略其间发射的所有其他值。
然后我们可以 **订阅**,并在每次更新时调用 **continuousDrive**。
请记住捕获从 **subscribe** 调用返回的 **订阅对象**。
停止更新很简单,只要调用 **订阅对象** 上的 **unsubscribe** 即可,因为取消订阅将负责停止加速度计更新。
startAccelerometerRxjs() {
// 创建一个发射加速度计数据的 Observable
const accelerometer$ =
new
Observable<AccelerometerData>(observer => {
startAccelerometerUpdates(
(data: AccelerometerData) => {
// 为每次更新发射值
observer.next(data);
}
)
// 在取消订阅时停止加速度计
return
() => stopAccelerometerUpdates();
});
this
.accelerometerSubscription = accelerometer$
// 每 50 毫秒接收当前值
.sampleTime(50)
// 订阅更新
.subscribe(data => {
this
.continuousDrive(data.y, data.x)
});
}
stopAccelerometerRxjs() {
this
.accelerometerSubscription.unsubscribe();
}
控制器页面完整示例
总结
使用 NativeScript 与机器人通信非常容易。唯一限制你的因素是机器人的 API 和你的想象力。
如果你发现这篇文章很有趣,并且想给你的朋友留下深刻印象,你应该给自己买一个支持 BLE 通信的机器人(在我的情况下是 **Wowwee MiP**),然后动手试试。
请记住公式:**扫描** -> **连接** -> **命令**