|
|
/**
|
|
|
* 蓝牙设备搜索页面
|
|
|
* 功能:搜索、配对、连接蓝牙设备
|
|
|
*/
|
|
|
import { router } from '@kit.ArkUI';
|
|
|
import { access, connection } from '@kit.ConnectivityKit';
|
|
|
import { abilityAccessCtrl, common, Permissions } from '@kit.AbilityKit';
|
|
|
import { BusinessError } from '@kit.BasicServicesKit';
|
|
|
|
|
|
// 设备信息接口
|
|
|
interface BluetoothDevice {
|
|
|
deviceId: string;
|
|
|
name: string;
|
|
|
displayName: string;
|
|
|
address: string;
|
|
|
isPaired: boolean;
|
|
|
connected: boolean;
|
|
|
}
|
|
|
|
|
|
@Entry
|
|
|
@Component
|
|
|
struct BluetoothSearchPage {
|
|
|
@State isSearching: boolean = false;
|
|
|
@State devices: BluetoothDevice[] = [];
|
|
|
@State bluetoothState: number = -1; // 0=关闭, 2=打开
|
|
|
@State hasPermission: boolean = false;
|
|
|
@State connectedDeviceId: string = '';
|
|
|
@State toastMessage: string = '';
|
|
|
@State showToast: boolean = false;
|
|
|
@State showConfirmDialog: boolean = false;
|
|
|
@State confirmDialogDevice: BluetoothDevice | null = null;
|
|
|
|
|
|
// 页面显示时初始化
|
|
|
aboutToAppear(): void {
|
|
|
this.init();
|
|
|
}
|
|
|
|
|
|
// 页面销毁时清理
|
|
|
aboutToDisappear(): void {
|
|
|
this.stopSearch();
|
|
|
}
|
|
|
|
|
|
// 初始化
|
|
|
async init(): Promise<void> {
|
|
|
await this.checkAndRequestPermission();
|
|
|
await this.checkBluetoothState();
|
|
|
this.registerBluetoothStateListener();
|
|
|
}
|
|
|
|
|
|
// 显示Toast提示
|
|
|
showToastMessage(message: string): void {
|
|
|
this.toastMessage = message;
|
|
|
this.showToast = true;
|
|
|
setTimeout(() => {
|
|
|
this.showToast = false;
|
|
|
}, 2000);
|
|
|
}
|
|
|
|
|
|
// 检查并请求蓝牙权限
|
|
|
async checkAndRequestPermission(): Promise<boolean> {
|
|
|
const permissions: Permissions[] = ['ohos.permission.ACCESS_BLUETOOTH'];
|
|
|
|
|
|
try {
|
|
|
const atManager = abilityAccessCtrl.createAtManager();
|
|
|
const context = getContext(this) as common.UIAbilityContext;
|
|
|
|
|
|
// 请求权限
|
|
|
const result = await atManager.requestPermissionsFromUser(context, permissions);
|
|
|
if (result.authResults[0] === 0) {
|
|
|
this.hasPermission = true;
|
|
|
console.info('蓝牙权限请求成功');
|
|
|
return true;
|
|
|
} else {
|
|
|
this.hasPermission = false;
|
|
|
console.warn('蓝牙权限被拒绝');
|
|
|
return false;
|
|
|
}
|
|
|
} catch (err) {
|
|
|
console.error('检查权限失败:', JSON.stringify(err));
|
|
|
return false;
|
|
|
}
|
|
|
}
|
|
|
|
|
|
// 检查蓝牙状态
|
|
|
async checkBluetoothState(): Promise<number> {
|
|
|
try {
|
|
|
const state = access.getState();
|
|
|
this.bluetoothState = state;
|
|
|
console.info('蓝牙状态:', state);
|
|
|
return state;
|
|
|
} catch (err) {
|
|
|
console.error('获取蓝牙状态失败:', JSON.stringify(err));
|
|
|
return -1;
|
|
|
}
|
|
|
}
|
|
|
|
|
|
// 注册蓝牙状态监听
|
|
|
registerBluetoothStateListener(): void {
|
|
|
try {
|
|
|
access.on('stateChange', (data: access.BluetoothState) => {
|
|
|
console.info('蓝牙状态变化:', data);
|
|
|
this.bluetoothState = data;
|
|
|
});
|
|
|
} catch (err) {
|
|
|
console.error('注册蓝牙状态监听失败:', JSON.stringify(err));
|
|
|
}
|
|
|
}
|
|
|
|
|
|
// 打开蓝牙
|
|
|
openBluetooth(): void {
|
|
|
try {
|
|
|
access.enableBluetooth();
|
|
|
this.showToastMessage('正在打开蓝牙...');
|
|
|
|
|
|
// 等待蓝牙状态更新
|
|
|
setTimeout(() => {
|
|
|
this.checkBluetoothState();
|
|
|
}, 1000);
|
|
|
} catch (err) {
|
|
|
console.error('打开蓝牙失败:', JSON.stringify(err));
|
|
|
this.showToastMessage('打开蓝牙失败');
|
|
|
}
|
|
|
}
|
|
|
|
|
|
// 开始搜索设备
|
|
|
async startSearch(): Promise<void> {
|
|
|
if (this.isSearching) {
|
|
|
return;
|
|
|
}
|
|
|
|
|
|
// 检查权限
|
|
|
if (!this.hasPermission) {
|
|
|
const granted = await this.checkAndRequestPermission();
|
|
|
if (!granted) {
|
|
|
this.showToastMessage('请授予蓝牙权限');
|
|
|
return;
|
|
|
}
|
|
|
}
|
|
|
|
|
|
// 检查蓝牙状态
|
|
|
const state = await this.checkBluetoothState();
|
|
|
if (state !== access.BluetoothState.STATE_ON) {
|
|
|
this.showToastMessage('请先打开蓝牙');
|
|
|
return;
|
|
|
}
|
|
|
|
|
|
this.isSearching = true;
|
|
|
this.devices = [];
|
|
|
|
|
|
this.showToastMessage('正在搜索设备...');
|
|
|
|
|
|
// 加载已配对设备
|
|
|
this.loadPairedDevices();
|
|
|
|
|
|
// 注册设备发现监听
|
|
|
try {
|
|
|
connection.on('bluetoothDeviceFind', (data: Array<string>) => {
|
|
|
console.info('发现设备:', JSON.stringify(data));
|
|
|
data.forEach((deviceId: string) => {
|
|
|
this.onDeviceFound(deviceId);
|
|
|
});
|
|
|
});
|
|
|
|
|
|
// 开始发现设备
|
|
|
connection.startBluetoothDiscovery();
|
|
|
console.info('开始搜索蓝牙设备');
|
|
|
|
|
|
// 30秒后自动停止搜索
|
|
|
setTimeout(() => {
|
|
|
this.stopSearch();
|
|
|
if (this.devices.length > 0) {
|
|
|
this.showToastMessage(`找到 ${this.devices.length} 个设备`);
|
|
|
} else {
|
|
|
this.showToastMessage('未找到设备');
|
|
|
}
|
|
|
}, 30000);
|
|
|
|
|
|
} catch (err) {
|
|
|
console.error('开始搜索失败:', JSON.stringify(err));
|
|
|
this.isSearching = false;
|
|
|
this.showToastMessage('搜索失败');
|
|
|
}
|
|
|
}
|
|
|
|
|
|
// 停止搜索
|
|
|
stopSearch(): void {
|
|
|
this.isSearching = false;
|
|
|
|
|
|
try {
|
|
|
connection.stopBluetoothDiscovery();
|
|
|
connection.off('bluetoothDeviceFind');
|
|
|
console.info('停止搜索');
|
|
|
} catch (err) {
|
|
|
console.error('停止搜索失败:', JSON.stringify(err));
|
|
|
}
|
|
|
}
|
|
|
|
|
|
// 处理发现的设备
|
|
|
onDeviceFound(deviceId: string): void {
|
|
|
// 检查设备是否已存在
|
|
|
const exists = this.devices.find(d => d.deviceId === deviceId);
|
|
|
if (exists) {
|
|
|
return;
|
|
|
}
|
|
|
|
|
|
try {
|
|
|
const deviceName = connection.getRemoteDeviceName(deviceId);
|
|
|
const displayName = deviceName || deviceId || '未知设备';
|
|
|
|
|
|
const deviceInfo: BluetoothDevice = {
|
|
|
deviceId: deviceId,
|
|
|
name: deviceName,
|
|
|
displayName: displayName,
|
|
|
address: deviceId,
|
|
|
isPaired: false,
|
|
|
connected: false,
|
|
|
};
|
|
|
|
|
|
this.devices = [...this.devices, deviceInfo];
|
|
|
console.info('添加设备:', displayName, deviceId);
|
|
|
} catch (err) {
|
|
|
console.error('获取设备信息失败:', JSON.stringify(err));
|
|
|
}
|
|
|
}
|
|
|
|
|
|
// 加载已配对设备
|
|
|
loadPairedDevices(): void {
|
|
|
try {
|
|
|
const pairedDevices = connection.getPairedDevices();
|
|
|
console.info('已配对设备:', JSON.stringify(pairedDevices));
|
|
|
|
|
|
pairedDevices.forEach((deviceId: string) => {
|
|
|
const exists = this.devices.find(d => d.deviceId === deviceId);
|
|
|
if (!exists) {
|
|
|
const deviceName = connection.getRemoteDeviceName(deviceId);
|
|
|
const displayName = deviceName || deviceId || '未知设备';
|
|
|
|
|
|
const deviceInfo: BluetoothDevice = {
|
|
|
deviceId: deviceId,
|
|
|
name: deviceName,
|
|
|
displayName: displayName,
|
|
|
address: deviceId,
|
|
|
isPaired: true,
|
|
|
connected: false,
|
|
|
};
|
|
|
|
|
|
this.devices = [...this.devices, deviceInfo];
|
|
|
console.info('添加已配对设备:', displayName);
|
|
|
}
|
|
|
});
|
|
|
} catch (err) {
|
|
|
console.error('获取已配对设备失败:', JSON.stringify(err));
|
|
|
}
|
|
|
}
|
|
|
|
|
|
// 连接设备
|
|
|
connectDevice(device: BluetoothDevice): void {
|
|
|
this.confirmDialogDevice = device;
|
|
|
this.showConfirmDialog = true;
|
|
|
}
|
|
|
|
|
|
// 执行连接
|
|
|
doConnect(device: BluetoothDevice): void {
|
|
|
this.showToastMessage('正在连接...');
|
|
|
|
|
|
try {
|
|
|
// 如果未配对,先进行配对
|
|
|
if (!device.isPaired) {
|
|
|
connection.pairDevice(device.deviceId);
|
|
|
console.info('开始配对:', device.deviceId);
|
|
|
}
|
|
|
|
|
|
// 更新设备状态
|
|
|
device.connected = true;
|
|
|
device.isPaired = true;
|
|
|
this.connectedDeviceId = device.deviceId;
|
|
|
|
|
|
// 保存最后连接的设备
|
|
|
AppStorage.setOrCreate('lastBleAddress', device.deviceId);
|
|
|
|
|
|
this.showToastMessage('连接成功');
|
|
|
|
|
|
// 跳转到控制页面
|
|
|
setTimeout(() => {
|
|
|
this.navigateToControl(device);
|
|
|
}, 500);
|
|
|
|
|
|
} catch (err) {
|
|
|
console.error('连接失败:', JSON.stringify(err));
|
|
|
this.showToastMessage('连接失败,请重试');
|
|
|
}
|
|
|
}
|
|
|
|
|
|
// 跳转到控制页面
|
|
|
navigateToControl(device: BluetoothDevice): void {
|
|
|
router.pushUrl({
|
|
|
url: 'pages/BluetoothControlPage',
|
|
|
params: {
|
|
|
deviceId: device.deviceId,
|
|
|
deviceName: device.displayName
|
|
|
}
|
|
|
}).catch((err: BusinessError) => {
|
|
|
console.error('跳转失败:', JSON.stringify(err));
|
|
|
});
|
|
|
}
|
|
|
|
|
|
// 判断是否为MAC地址格式
|
|
|
isMacAddress(str: string): boolean {
|
|
|
if (!str) return false;
|
|
|
const macRegex = /^([0-9A-Fa-f]{2}:){5}([0-9A-Fa-f]{2})$/;
|
|
|
return macRegex.test(str);
|
|
|
}
|
|
|
|
|
|
build() {
|
|
|
Stack() {
|
|
|
Column() {
|
|
|
// 顶部搜索栏
|
|
|
Row() {
|
|
|
Text('设备列表(鸿蒙)')
|
|
|
.fontSize(16)
|
|
|
.fontWeight(FontWeight.Medium)
|
|
|
.fontColor('#eaf6fc')
|
|
|
|
|
|
Blank()
|
|
|
|
|
|
Button(this.isSearching ? '搜索中...' : '搜索')
|
|
|
.fontSize(14)
|
|
|
.fontWeight(FontWeight.Medium)
|
|
|
.fontColor(this.isSearching ? '#eaf6fc' : '#0b1830')
|
|
|
.backgroundColor(this.isSearching ? 'rgba(34, 253, 200, 0.5)' : '#22fdc8')
|
|
|
.borderRadius(25)
|
|
|
.padding({ left: 20, right: 20, top: 8, bottom: 8 })
|
|
|
.onClick(() => this.startSearch())
|
|
|
}
|
|
|
.width('100%')
|
|
|
.padding(15)
|
|
|
.backgroundColor('rgba(15, 30, 60, 0.8)')
|
|
|
.borderRadius(8)
|
|
|
.margin({ bottom: 15 })
|
|
|
|
|
|
// 蓝牙状态提示
|
|
|
if (this.bluetoothState !== access.BluetoothState.STATE_ON) {
|
|
|
Row() {
|
|
|
Text(this.bluetoothState === access.BluetoothState.STATE_OFF ? '蓝牙已关闭' : '蓝牙状态未知')
|
|
|
.fontSize(14)
|
|
|
.fontColor('#ff9800')
|
|
|
|
|
|
Blank()
|
|
|
|
|
|
Button('打开蓝牙')
|
|
|
.fontSize(13)
|
|
|
.fontWeight(FontWeight.Medium)
|
|
|
.fontColor('#ffffff')
|
|
|
.backgroundColor('#ff9800')
|
|
|
.borderRadius(25)
|
|
|
.padding({ left: 16, right: 16, top: 6, bottom: 6 })
|
|
|
.onClick(() => this.openBluetooth())
|
|
|
}
|
|
|
.width('100%')
|
|
|
.padding(12)
|
|
|
.backgroundColor('rgba(255, 152, 0, 0.15)')
|
|
|
.border({ width: 1, color: 'rgba(255, 152, 0, 0.5)' })
|
|
|
.borderRadius(6)
|
|
|
.margin({ bottom: 15 })
|
|
|
}
|
|
|
|
|
|
// 设备列表
|
|
|
if (this.devices.length === 0 && !this.isSearching) {
|
|
|
Text('暂无设备,请点击搜索按钮')
|
|
|
.fontSize(14)
|
|
|
.fontColor('#8a9ba8')
|
|
|
.margin({ top: 50 })
|
|
|
} else {
|
|
|
List({ space: 10 }) {
|
|
|
ForEach(this.devices, (device: BluetoothDevice, index: number) => {
|
|
|
ListItem() {
|
|
|
Row() {
|
|
|
// 设备信息
|
|
|
Column() {
|
|
|
Row() {
|
|
|
Text('设备名称:')
|
|
|
.fontSize(14)
|
|
|
.fontColor('#8a9ba8')
|
|
|
Text(device.displayName || device.name || '未知设备')
|
|
|
.fontSize(16)
|
|
|
.fontWeight(FontWeight.Bold)
|
|
|
.fontColor('#22fdc8')
|
|
|
.layoutWeight(1)
|
|
|
}
|
|
|
.width('100%')
|
|
|
.margin({ bottom: 4 })
|
|
|
|
|
|
Row() {
|
|
|
Text(this.isMacAddress(device.deviceId) ? 'MAC地址:' : '设备ID:')
|
|
|
.fontSize(12)
|
|
|
.fontColor('#8a9ba8')
|
|
|
Text(device.deviceId)
|
|
|
.fontSize(12)
|
|
|
.fontColor('#b2ebf2')
|
|
|
.layoutWeight(1)
|
|
|
}
|
|
|
.width('100%')
|
|
|
}
|
|
|
.layoutWeight(1)
|
|
|
.alignItems(HorizontalAlign.Start)
|
|
|
|
|
|
// 状态标签
|
|
|
Text(device.isPaired ? '已配对' : '未配对')
|
|
|
.fontSize(12)
|
|
|
.fontColor(device.isPaired ? '#4caf50' : '#ff9800')
|
|
|
.backgroundColor(device.isPaired ? 'rgba(76, 175, 80, 0.1)' : 'rgba(255, 152, 0, 0.1)')
|
|
|
.border({
|
|
|
width: 1,
|
|
|
color: device.isPaired ? '#4caf50' : '#ff9800'
|
|
|
})
|
|
|
.borderRadius(25)
|
|
|
.padding({ left: 12, right: 12, top: 6, bottom: 6 })
|
|
|
}
|
|
|
.width('100%')
|
|
|
.padding(15)
|
|
|
.backgroundColor('rgba(15, 30, 60, 0.8)')
|
|
|
.borderRadius(8)
|
|
|
.border({ width: 1, color: 'rgba(34, 253, 200, 0.2)' })
|
|
|
.onClick(() => this.connectDevice(device))
|
|
|
}
|
|
|
})
|
|
|
}
|
|
|
.width('100%')
|
|
|
.layoutWeight(1)
|
|
|
}
|
|
|
}
|
|
|
.width('100%')
|
|
|
.height('100%')
|
|
|
.padding(10)
|
|
|
.linearGradient({
|
|
|
direction: GradientDirection.RightBottom,
|
|
|
colors: [['#0b1830', 0], ['#1d3553', 1]]
|
|
|
})
|
|
|
|
|
|
// Toast 提示
|
|
|
if (this.showToast) {
|
|
|
Text(this.toastMessage)
|
|
|
.fontSize(14)
|
|
|
.fontColor('#ffffff')
|
|
|
.backgroundColor('rgba(0, 0, 0, 0.7)')
|
|
|
.padding({ left: 20, right: 20, top: 10, bottom: 10 })
|
|
|
.borderRadius(20)
|
|
|
.position({ x: '50%', y: '80%' })
|
|
|
.translate({ x: '-50%' })
|
|
|
}
|
|
|
|
|
|
// 确认对话框
|
|
|
if (this.showConfirmDialog && this.confirmDialogDevice) {
|
|
|
Column() {
|
|
|
Column() {
|
|
|
Text('提示')
|
|
|
.fontSize(18)
|
|
|
.fontWeight(FontWeight.Bold)
|
|
|
.fontColor('#eaf6fc')
|
|
|
.margin({ bottom: 15 })
|
|
|
|
|
|
Text(`确定要连接设备"${this.confirmDialogDevice.displayName}"吗?`)
|
|
|
.fontSize(14)
|
|
|
.fontColor('#b2ebf2')
|
|
|
.margin({ bottom: 20 })
|
|
|
|
|
|
Row() {
|
|
|
Button('取消')
|
|
|
.fontSize(14)
|
|
|
.fontColor('#999999')
|
|
|
.backgroundColor('rgba(255, 255, 255, 0.1)')
|
|
|
.borderRadius(4)
|
|
|
.layoutWeight(1)
|
|
|
.onClick(() => {
|
|
|
this.showConfirmDialog = false;
|
|
|
this.confirmDialogDevice = null;
|
|
|
})
|
|
|
|
|
|
Button('确定')
|
|
|
.fontSize(14)
|
|
|
.fontColor('#22fdc8')
|
|
|
.backgroundColor('rgba(34, 253, 200, 0.2)')
|
|
|
.borderRadius(4)
|
|
|
.layoutWeight(1)
|
|
|
.margin({ left: 15 })
|
|
|
.onClick(() => {
|
|
|
if (this.confirmDialogDevice) {
|
|
|
this.doConnect(this.confirmDialogDevice);
|
|
|
}
|
|
|
this.showConfirmDialog = false;
|
|
|
this.confirmDialogDevice = null;
|
|
|
})
|
|
|
}
|
|
|
.width('100%')
|
|
|
}
|
|
|
.width('80%')
|
|
|
.padding(20)
|
|
|
.backgroundColor('rgba(20, 38, 81, 0.98)')
|
|
|
.borderRadius(12)
|
|
|
.border({ width: 1, color: '#213b67' })
|
|
|
}
|
|
|
.width('100%')
|
|
|
.height('100%')
|
|
|
.backgroundColor('rgba(0, 0, 0, 0.5)')
|
|
|
.justifyContent(FlexAlign.Center)
|
|
|
}
|
|
|
}
|
|
|
.width('100%')
|
|
|
.height('100%')
|
|
|
}
|
|
|
}
|