HarmonyOS 星闪快速实战
星闪(NearLink)是华为研发的新一代短距离无线通信技术,可以理解为"华为版蓝牙"(仅限我们目前用的,有对标WiFi的版本),但比蓝牙更快、更稳、更省电。低延迟、高带宽、低功耗。当前只能连接华为设备,不能连 iPhone 等其他设备设备还不多,在发展中开发星闪应用需要什么?一台华为手机(运行 HarmonyOS 5.0+),星闪支持较好一个星闪设备(或另一台华为手机)DevEco Studio
·
一、什么是星闪?
星闪(NearLink) 是华为研发的新一代短距离无线通信技术,可以理解为"华为版蓝牙"(仅限我们目前用的,有对标WiFi的版本),但比蓝牙更快、更稳、更省电。
星闪的优点:
- 低延迟、高带宽、低功耗。
星闪的缺点:
- 当前只能连接华为设备,不能连 iPhone 等其他设备
- 设备还不多,在发展中
开发星闪应用需要什么?
- 一台华为手机(运行 HarmonyOS 5.0+),星闪支持较好
- 一个星闪设备(或另一台华为手机)
- DevEco Studio 开发工具
二、第一步:导入需要的模块
// ====== common/NearLinkManager.ets ======
// 这个文件放在 entry/src/main/ets/common/ 目录下
// 星闪功能模块
import { scan, advertising, ssap, constant } from '@kit.NearLinkKit';
// scan: 扫描设备
// advertising: 发送广播(可选)
// ssap: 星闪应用层协议,类似蓝牙的 GATT
// constant: 常量定义
// 权限管理模块
import { abilityAccessCtrl, Permissions, common } from '@kit.AbilityKit';
// 错误处理模块
import { BusinessError } from '@kit.BasicServicesKit';
// 日志模块(可选)
import { hilog } from '@kit.PerformanceAnalysisKit';
三、第二步:配置权限
星闪只需要一个权限,比蓝牙简单多了!
3.1 静态权限声明
// ====== entry/src/main/module.json5 ======
// 只需要这一个权限,是不是超简单?
{
"module": {
"requestPermissions": [
{
"name": "ohos.permission.ACCESS_NEARLINK", // 星闪权限(就这一个!)
"reason": "$string:nearlink_permission_reason",
"usedScene": {
"abilities": ["EntryAbility"],
"when": "inuse"
}
}
]
}
}
3.2 权限说明字符串
// ====== entry/src/main/resources/base/element/string.json ======
{
"string": [
{
"name": "nearlink_permission_reason",
"value": "需要星闪权限以连接和控制星闪设备"
}
]
}
3.3 动态权限申请(弹窗让用户确认)
// ====== common/PermissionManager.ets ======
import { abilityAccessCtrl, Permissions, common } from '@kit.AbilityKit';
export class PermissionManager {
private static instance: PermissionManager;
private context?: common.UIAbilityContext;
public static getInstance(): PermissionManager {
if (!PermissionManager.instance) {
PermissionManager.instance = new PermissionManager();
}
return PermissionManager.instance;
}
public setContext(context: common.UIAbilityContext): void {
this.context = context;
}
/**
* 检查并申请星闪权限
* @returns 是否已授权
*/
public async checkAndRequestNearLinkPermissions(): Promise<boolean> {
if (!this.context) {
console.error('Context not set');
return false;
}
const permission: Permissions = 'ohos.permission.ACCESS_NEARLINK';
try {
const atManager = abilityAccessCtrl.createAtManager();
// 检查权限
const grantStatus = await atManager.checkAccessToken(
this.context.applicationInfo.accessTokenId,
permission
);
if (grantStatus !== abilityAccessCtrl.GrantStatus.PERMISSION_GRANTED) {
// 权限未授予,请求用户授权
const requestResult = await atManager.requestPermissionsFromUser(
this.context,
[permission]
);
if (requestResult.authResults[0] !== abilityAccessCtrl.GrantStatus.PERMISSION_GRANTED) {
console.error('NearLink permission denied by user');
return false;
}
}
console.info('NearLink permission granted');
return true;
} catch (error) {
console.error(`Permission request failed: ${error}`);
return false;
}
}
}
3.4 在 EntryAbility 中初始化(应用启动时设置)
// ====== EntryAbility.ets ======
import { AbilityConstant, UIAbility, Want } from '@kit.AbilityKit';
import { window } from '@kit.ArkUI';
import { PermissionManager } from '../common/PermissionManager';
import { NearLinkManager } from '../common/NearLinkManager';
export default class EntryAbility extends UIAbility {
onCreate(want: Want, launchParam: AbilityConstant.LaunchParam): void {
console.info('EntryAbility onCreate');
}
onWindowStageCreate(windowStage: window.WindowStage): void {
// 设置上下文
PermissionManager.getInstance().setContext(this.context);
NearLinkManager.getInstance().setContext(this.context);
windowStage.loadContent('pages/Index', (err) => {
if (err.code) {
console.error('Failed to load content');
return;
}
});
}
}
四、第三步:SSAP 客户端实现(核心代码)
SSAP 是什么? 星闪应用层协议(StarShine Application Protocol),类似蓝牙的 GATT,用来定义数据如何传输。
术语对照:
| 蓝牙 | 星闪 | 说明 |
|---|---|---|
| GATT | SSAP | 应用层协议 |
| Characteristic | Property | 数据容器 |
| Service | Service | 服务分组 |
4.1 SSAP 客户端管理器
// ====== common/SSAPClient.ets ======
// 这个文件放在 entry/src/main/ets/common/ 目录下
import { ssap, constant } from '@kit.NearLinkKit';
import { BusinessError } from '@kit.BasicServicesKit';
// ========== 数据类型定义 ==========
// 连接状态枚举
export enum ConnectionState {
DISCONNECTED = 0, // 已断开
CONNECTING = 1, // 连接中...
CONNECTED = 2, // 已连接
DISCONNECTING = 3 // 断开中...
}
// 属性权限枚举(这个属性能读/能写/能通知)
export enum PropertyPermission {
READ = 0x01, // 可读
WRITE = 0x02, // 可写
NOTIFY = 0x04 // 可接收通知
}
// 连接状态变化信息
export interface ConnectionChangeState {
address: string;
state: ConnectionState;
}
// 属性通知信息
export interface PropertyNotification {
address: string;
serviceId: string;
propertyId: string;
value: ArrayBuffer;
}
// 发现的服务
export interface DiscoveredService {
serviceId: string;
properties: DiscoveredProperty[];
}
// 发现的属性
export interface DiscoveredProperty {
propertyId: string;
permissions: PropertyPermission[];
}
// 客户端信息
interface ClientInfo {
address: string;
client: ssap.Client;
services: Array<ssap.Service>;
connectionState: ConnectionState;
}
export class SSAPClient {
private static instance: SSAPClient;
private clientMap: Map<string, ClientInfo> = new Map();
// 回调函数
private connectionStateCallbacks: ((state: ConnectionChangeState) => void)[] = [];
private propertyNotificationCallbacks: ((notification: PropertyNotification) => void)[] = [];
private constructor() {}
public static getInstance(): SSAPClient {
if (!SSAPClient.instance) {
SSAPClient.instance = new SSAPClient();
}
return SSAPClient.instance;
}
/**
* 连接到 SSAP 服务端(连接星闪设备)
* @param address 设备地址
* @returns 是否连接成功
*/
public async connectToServer(address: string): Promise<boolean> {
try {
// 检查是否已连接,防止重复连接
if (this.clientMap.has(address)) {
const existing = this.clientMap.get(address);
if (existing?.connectionState === ConnectionState.CONNECTED) {
console.info(`已经连接了这个设备: ${address}`);
return true;
}
}
console.info(`正在连接星闪设备: ${address}`);
// 创建 SSAP 客户端
const client: ssap.Client = ssap.createClient(address);
const clientInfo: ClientInfo = {
address: address,
client: client,
services: [],
connectionState: ConnectionState.CONNECTING
};
this.clientMap.set(address, clientInfo);
// 注册连接状态变化回调
client.on('connectionStateChange', (data: ssap.ConnectionChangeState) => {
this.handleConnectionStateChange(address, data);
});
// 注册属性变化回调
client.on('propertyChange', (data: ssap.Property) => {
this.handlePropertyChange(address, data);
});
// 连接
await client.connect();
console.info(`Connection request sent: ${address}`);
return true;
} catch (error) {
console.error(`Connect failed: ${error}`);
this.clientMap.delete(address);
return false;
}
}
/**
* 处理连接状态变化
*/
private handleConnectionStateChange(address: string, data: ssap.ConnectionChangeState): void {
const clientInfo = this.clientMap.get(address);
if (!clientInfo) return;
let newState: ConnectionState;
if (data.state === constant.ConnectionState.STATE_CONNECTED) {
newState = ConnectionState.CONNECTED;
clientInfo.connectionState = newState;
// 连接成功后获取服务列表
this.getServices(address);
} else if (data.state === constant.ConnectionState.STATE_DISCONNECTED) {
newState = ConnectionState.DISCONNECTED;
clientInfo.connectionState = newState;
clientInfo.services = [];
} else if (data.state === constant.ConnectionState.STATE_CONNECTING) {
newState = ConnectionState.CONNECTING;
clientInfo.connectionState = newState;
} else if (data.state === constant.ConnectionState.STATE_DISCONNECTING) {
newState = ConnectionState.DISCONNECTING;
clientInfo.connectionState = newState;
} else {
newState = ConnectionState.DISCONNECTED;
}
// 通知所有订阅者
const changeState: ConnectionChangeState = { address, state: newState };
this.connectionStateCallbacks.forEach(callback => callback(changeState));
}
/**
* 处理属性变化
*/
private handlePropertyChange(address: string, data: ssap.Property): void {
const notification: PropertyNotification = {
address: address,
serviceId: data.serviceUuid,
propertyId: data.propertyUuid,
value: data.value
};
this.propertyNotificationCallbacks.forEach(callback => callback(notification));
}
/**
* 获取服务列表
*/
private async getServices(address: string): Promise<void> {
try {
const clientInfo = this.clientMap.get(address);
if (!clientInfo) return;
const services: Array<ssap.Service> = await clientInfo.client.getServices();
clientInfo.services = services;
console.info(`Discovered ${services.length} services`);
// 自动设置属性通知
for (const service of services) {
for (const prop of service.properties) {
await this.subscribePropertyNotification(address, service.serviceUuid, prop.propertyUuid);
}
}
} catch (error) {
console.error(`Get services failed: ${error}`);
}
}
/**
* 读取属性值
*/
public async readProperty(address: string, serviceId: string, propertyId: string): Promise<ArrayBuffer | null> {
try {
const clientInfo = this.clientMap.get(address);
if (!clientInfo || clientInfo.services.length === 0) {
console.error('Client not found or services not discovered');
return null;
}
const property: ssap.Property = {
serviceUuid: serviceId,
propertyUuid: propertyId,
value: new ArrayBuffer(1)
};
const result: ssap.Property = await clientInfo.client.readProperty(property);
return result.value;
} catch (error) {
console.error(`Read property failed: ${error}`);
return null;
}
}
/**
* 写入属性值(双重写入策略)
*/
public async writeProperty(address: string, serviceId: string, propertyId: string, value: ArrayBuffer): Promise<boolean> {
try {
const clientInfo = this.clientMap.get(address);
if (!clientInfo || clientInfo.services.length === 0) {
console.error('Client not found or services not discovered');
return false;
}
const property: ssap.Property = {
serviceUuid: serviceId,
propertyUuid: propertyId,
value: value
};
// 双重写入策略
try {
await clientInfo.client.writeProperty(property, ssap.PropertyWriteType.WRITE);
console.info('Write property success (WRITE mode)');
return true;
} catch (writeError) {
console.warn('WRITE mode failed, trying WRITE_NO_RESPONSE');
await clientInfo.client.writeProperty(property, ssap.PropertyWriteType.WRITE_NO_RESPONSE);
console.info('Write property success (WRITE_NO_RESPONSE mode)');
return true;
}
} catch (error) {
console.error(`Write property failed: ${error}`);
return false;
}
}
/**
* 订阅属性通知
*/
public async subscribePropertyNotification(address: string, serviceId: string, propertyId: string): Promise<boolean> {
try {
const clientInfo = this.clientMap.get(address);
if (!clientInfo) return false;
const property: ssap.Property = {
serviceUuid: serviceId,
propertyUuid: propertyId,
value: new ArrayBuffer(1)
};
await clientInfo.client.setPropertyNotification(property, true);
return true;
} catch (error) {
console.error(`Subscribe notification failed: ${error}`);
return false;
}
}
/**
* 断开连接
*/
public async disconnectFromServer(address: string): Promise<void> {
try {
const clientInfo = this.clientMap.get(address);
if (!clientInfo) return;
await clientInfo.client.disconnect();
clientInfo.client.off('propertyChange');
clientInfo.client.off('connectionStateChange');
clientInfo.client.close();
this.clientMap.delete(address);
console.info(`Disconnected from: ${address}`);
} catch (error) {
console.error(`Disconnect failed: ${error}`);
}
}
/**
* 获取发现的服务
*/
public getDiscoveredServices(address: string): DiscoveredService[] {
const clientInfo = this.clientMap.get(address);
if (!clientInfo) return [];
return clientInfo.services.map((service: ssap.Service): DiscoveredService => ({
serviceId: service.serviceUuid,
properties: service.properties.map((prop: ssap.Property): DiscoveredProperty => ({
propertyId: prop.propertyUuid,
permissions: [PropertyPermission.READ, PropertyPermission.WRITE, PropertyPermission.NOTIFY]
}))
}));
}
// 回调管理
public onConnectionStateChange(callback: (state: ConnectionChangeState) => void): void {
this.connectionStateCallbacks.push(callback);
}
public onPropertyNotification(callback: (notification: PropertyNotification) => void): void {
this.propertyNotificationCallbacks.push(callback);
}
}
四、第三步:星闪管理器(核心代码)
这是整个星闪功能的核心,包含扫描、连接、收发数据等所有功能。
// ====== common/NearLinkManager.ets ======
import { scan, advertising } from '@kit.NearLinkKit';
import { common } from '@kit.AbilityKit';
import { BusinessError } from '@kit.BasicServicesKit';
import { SSAPClient, ConnectionState, DiscoveredService, ConnectionChangeState, PropertyNotification } from './SSAPClient';
// 扫描结果接口
export interface ScanResult {
deviceId: string;
deviceName: string;
address: string;
rssi: number;
}
// 扫描过滤器接口
export interface ScanFilter {
deviceName?: string;
address?: string;
}
export class NearLinkManager {
private static instance: NearLinkManager;
private context?: common.UIAbilityContext;
private isScanning: boolean = false;
private scanResults: Map<string, ScanResult> = new Map();
private connectedDevice?: ScanResult;
// 回调函数
private onDeviceFoundCallback?: (device: ScanResult) => void;
private connectionStateCallbacks: ((device: ScanResult, state: ConnectionState) => void)[] = [];
private onDataReceivedCallback?: (data: ArrayBuffer) => void;
// SSAP客户端
private ssapClient: SSAPClient = SSAPClient.getInstance();
private constructor() {
this.setupSSAPClientCallback();
}
public static getInstance(): NearLinkManager {
if (!NearLinkManager.instance) {
NearLinkManager.instance = new NearLinkManager();
}
return NearLinkManager.instance;
}
public setContext(context: common.UIAbilityContext): void {
this.context = context;
}
/**
* 设置 SSAP 客户端回调
*/
private setupSSAPClientCallback(): void {
// 连接状态变化回调
this.ssapClient.onConnectionStateChange((state: ConnectionChangeState) => {
if (this.connectedDevice && this.connectionStateCallbacks.length > 0) {
let connectionState: ConnectionState;
switch (state.state) {
case 0: connectionState = ConnectionState.DISCONNECTED; break;
case 1: connectionState = ConnectionState.CONNECTING; break;
case 2: connectionState = ConnectionState.CONNECTED; break;
case 3: connectionState = ConnectionState.DISCONNECTING; break;
default: connectionState = ConnectionState.DISCONNECTED;
}
this.connectionStateCallbacks.forEach(callback => {
callback(this.connectedDevice!, connectionState);
});
}
});
// 属性通知回调
this.ssapClient.onPropertyNotification((notification: PropertyNotification) => {
if (this.onDataReceivedCallback) {
this.onDataReceivedCallback(notification.value);
}
});
}
/**
* 开始扫描
*/
public startScan(filters?: ScanFilter[]): void {
if (this.isScanning) {
console.warn('Already scanning');
return;
}
try {
this.isScanning = true;
this.scanResults.clear();
// 注册设备发现回调
scan.on('deviceFound', (data: Array<scan.ScanResults>) => {
data.forEach((result: scan.ScanResults) => {
const device: ScanResult = {
deviceId: result.address,
deviceName: result.deviceName || '未知设备',
address: result.address,
rssi: result.rssi || -100
};
// 去重
if (!this.scanResults.has(device.address)) {
this.scanResults.set(device.address, device);
if (this.onDeviceFoundCallback) {
this.onDeviceFoundCallback(device);
}
}
});
});
// 配置扫描参数
const scanOptions: scan.ScanOptions = {
scanMode: 2 // 平衡扫描模式
};
// 构建过滤器
const scanFilters: scan.ScanFilters[] = [];
if (filters && filters.length > 0) {
filters.forEach(filter => {
const nearLinkFilter: scan.ScanFilters = {};
if (filter.deviceName) {
nearLinkFilter.deviceName = filter.deviceName;
}
if (filter.address) {
nearLinkFilter.address = filter.address;
}
scanFilters.push(nearLinkFilter);
});
}
const filtersToUse = scanFilters.length > 0 ? scanFilters : [({} as scan.ScanFilters)];
scan.startScan(filtersToUse, scanOptions)
.then(() => {
console.info('NearLink scan started');
})
.catch((err: BusinessError) => {
console.error(`Start scan failed: ${err.message}`);
this.isScanning = false;
});
} catch (error) {
console.error(`Start scan exception: ${error}`);
this.isScanning = false;
}
}
/**
* 停止扫描
*/
public stopScan(): void {
if (!this.isScanning) return;
try {
scan.stopScan()
.then(() => console.info('NearLink scan stopped'))
.catch((err: BusinessError) => console.error(`Stop scan failed: ${err.message}`));
scan.off('deviceFound');
this.isScanning = false;
} catch (error) {
console.error(`Stop scan exception: ${error}`);
}
}
/**
* 连接设备
*/
public async connectDevice(device: ScanResult): Promise<boolean> {
try {
this.connectedDevice = device;
const success = await this.ssapClient.connectToServer(device.address);
return success;
} catch (error) {
console.error(`Connect device failed: ${error}`);
this.connectedDevice = undefined;
return false;
}
}
/**
* 断开连接
*/
public async disconnectDevice(): Promise<void> {
if (!this.connectedDevice) return;
try {
await this.ssapClient.disconnectFromServer(this.connectedDevice.address);
this.connectedDevice = undefined;
} catch (error) {
console.error(`Disconnect failed: ${error}`);
}
}
/**
* 发送数据
*/
public async sendData(serviceId: string, propertyId: string, data: ArrayBuffer): Promise<boolean> {
if (!this.connectedDevice) return false;
return await this.ssapClient.writeProperty(
this.connectedDevice.address,
serviceId,
propertyId,
data
);
}
// 回调设置
public setOnDeviceFoundCallback(callback: (device: ScanResult) => void): void {
this.onDeviceFoundCallback = callback;
}
public setOnConnectionStateChangedCallback(callback: (device: ScanResult, state: ConnectionState) => void): void {
if (!this.connectionStateCallbacks.includes(callback)) {
this.connectionStateCallbacks.push(callback);
}
}
public setOnDataReceivedCallback(callback: (data: ArrayBuffer) => void): void {
this.onDataReceivedCallback = callback;
}
// Getter
public getIsScanning(): boolean {
return this.isScanning;
}
public getConnectedDevice(): ScanResult | undefined {
return this.connectedDevice;
}
public getDiscoveredServices(): DiscoveredService[] {
if (!this.connectedDevice) return [];
return this.ssapClient.getDiscoveredServices(this.connectedDevice.address);
}
}
六、UI 组件实现
6.1 设备卡片组件
// ====== pages/NearLinkConnectionPage.ets ======
import { ScanResult, ConnectionState } from '../common/NearLinkManager';
@Component
struct NearLinkDeviceCard {
@Prop device: ScanResult;
@Prop isConnected: boolean = false;
onConnect?: () => void;
private getSignalColor(rssi: number): string {
if (rssi >= -50) return '#4CAF50';
if (rssi >= -70) return '#FF9800';
return '#FF5722';
}
build() {
Row() {
// 左侧:设备信息
Column() {
Row() {
Text(this.device.deviceName)
.fontSize(16)
.fontWeight(FontWeight.Medium)
.fontColor(this.isConnected ? '#9C27B0' : '#333333')
.layoutWeight(1)
// 星闪标识
Text('NearLink')
.fontSize(10)
.fontColor('#9C27B0')
.backgroundColor('rgba(156, 39, 176, 0.1)')
.padding({ left: 6, right: 6, top: 2, bottom: 2 })
.borderRadius(4)
}
.width('100%')
Row() {
Text(this.device.address)
.fontSize(12)
.fontColor('#999999')
.layoutWeight(1)
Text(`${this.device.rssi} dBm`)
.fontSize(12)
.fontColor(this.getSignalColor(this.device.rssi))
}
.width('100%')
.margin({ top: 4 })
}
.layoutWeight(1)
.alignItems(HorizontalAlign.Start)
// 连接按钮
Button(this.isConnected ? '断开' : '连接')
.fontSize(14)
.backgroundColor(this.isConnected ? '#FF5722' : '#9C27B0')
.fontColor(Color.White)
.height(36)
.width(70)
.margin({ left: 12 })
.onClick(() => {
if (this.onConnect) {
this.onConnect();
}
})
}
.width('100%')
.padding(16)
.backgroundColor(Color.White)
.borderRadius(12)
.shadow({ radius: 4, color: 'rgba(0,0,0,0.1)', offsetY: 2 })
}
}
6.2 设备列表页面
// ====== pages/NearLinkConnectionPage.ets ======
import { NearLinkManager, ScanResult, ConnectionState } from '../common/NearLinkManager';
import { PermissionManager } from '../common/PermissionManager';
import { promptAction } from '@kit.ArkUI';
@Entry
@Component
struct NearLinkConnectionPage {
@State discoveredDevices: ScanResult[] = [];
@State isScanning: boolean = false;
@State connectedDevice: ScanResult | null = null;
private nearLinkManager: NearLinkManager = NearLinkManager.getInstance();
aboutToAppear(): void {
// 设置设备发现回调
this.nearLinkManager.setOnDeviceFoundCallback((device: ScanResult) => {
const exists = this.discoveredDevices.find(d => d.address === device.address);
if (!exists) {
this.discoveredDevices = [...this.discoveredDevices, device];
}
});
// 设置连接状态回调
this.nearLinkManager.setOnConnectionStateChangedCallback((device: ScanResult, state: ConnectionState) => {
if (state === ConnectionState.CONNECTED) {
this.connectedDevice = device;
promptAction.showToast({ message: `已连接: ${device.deviceName}` });
} else if (state === ConnectionState.DISCONNECTED) {
this.connectedDevice = null;
promptAction.showToast({ message: '设备已断开' });
}
});
}
aboutToDisappear(): void {
this.nearLinkManager.stopScan();
}
private async startScan(): Promise<void> {
// 检查并申请权限
const hasPermission = await PermissionManager.getInstance().checkAndRequestNearLinkPermissions();
if (!hasPermission) {
promptAction.showToast({ message: '请授予星闪权限后重试' });
return;
}
this.discoveredDevices = [];
this.nearLinkManager.startScan();
this.isScanning = true;
// 10秒后自动停止扫描
setTimeout(() => {
this.stopScan();
}, 10000);
}
private stopScan(): void {
this.nearLinkManager.stopScan();
this.isScanning = false;
}
private async onDeviceClick(device: ScanResult): Promise<void> {
if (this.connectedDevice?.address === device.address) {
await this.nearLinkManager.disconnectDevice();
} else {
this.stopScan();
await this.nearLinkManager.connectDevice(device);
}
}
build() {
Column() {
// 标题栏
Row() {
Text('星闪设备')
.fontSize(20)
.fontWeight(FontWeight.Bold)
Blank()
Button(this.isScanning ? '停止扫描' : '开始扫描')
.fontSize(14)
.backgroundColor(this.isScanning ? '#FF5722' : '#9C27B0')
.onClick(() => {
if (this.isScanning) {
this.stopScan();
} else {
this.startScan();
}
})
}
.width('100%')
.padding(16)
// 扫描状态
if (this.isScanning) {
Row() {
LoadingProgress()
.width(20)
.height(20)
.color('#9C27B0')
Text('正在扫描星闪设备...')
.fontSize(14)
.fontColor('#666666')
.margin({ left: 8 })
}
.width('100%')
.justifyContent(FlexAlign.Center)
.padding(16)
}
// 设备列表
if (this.discoveredDevices.length === 0) {
Column() {
Text('⚡')
.fontSize(48)
.margin({ bottom: 16 })
Text(this.isScanning ? '正在搜索星闪设备...' : '未发现设备')
.fontSize(16)
.fontColor('#999999')
}
.width('100%')
.height(200)
.justifyContent(FlexAlign.Center)
} else {
List({ space: 12 }) {
ForEach(this.discoveredDevices, (device: ScanResult) => {
ListItem() {
NearLinkDeviceCard({
device: device,
isConnected: this.connectedDevice?.address === device.address,
onConnect: () => this.onDeviceClick(device)
})
}
}, (device: ScanResult) => device.address)
}
.width('100%')
.layoutWeight(1)
.padding({ left: 16, right: 16 })
}
}
.width('100%')
.height('100%')
.backgroundColor('#F5F5F5')
}
}
6.3 服务/属性卡片组件
// ====== pages/NearLinkDeviceDetailPage.ets ======
import { NearLinkManager, DiscoveredService, DiscoveredProperty } from '../common/NearLinkManager';
import { router } from '@kit.ArkUI';
@Entry
@Component
struct NearLinkDeviceDetailPage {
@State services: DiscoveredService[] = [];
private nearLinkManager: NearLinkManager = NearLinkManager.getInstance();
aboutToAppear(): void {
this.services = this.nearLinkManager.getDiscoveredServices();
}
private importUUID(serviceId: string, propertyId: string): void {
router.pushUrl({
url: 'pages/SettingsPage',
params: {
importServiceUUID: serviceId,
importCharacteristicUUID: propertyId
}
});
}
@Builder
ServiceCard(service: DiscoveredService, index: number) {
Column() {
// 服务标题
Row() {
Text(`服务 ${index + 1}`)
.fontSize(14)
.fontWeight(FontWeight.Medium)
.fontColor('#333333')
Blank()
Text('SSAP Service')
.fontSize(12)
.fontColor('#9C27B0')
.backgroundColor('rgba(156, 39, 176, 0.1)')
.padding({ left: 8, right: 8, top: 2, bottom: 2 })
.borderRadius(4)
}
.width('100%')
.margin({ bottom: 8 })
// 服务 UUID
Text(service.serviceId)
.fontSize(12)
.fontColor('#666666')
.width('100%')
.margin({ bottom: 12 })
// 属性列表
if (service.properties.length > 0) {
Text('属性列表')
.fontSize(13)
.fontColor('#999999')
.width('100%')
.margin({ bottom: 8 })
ForEach(service.properties, (prop: DiscoveredProperty, propIndex: number) => {
Row() {
Column() {
Text(`属性 ${propIndex + 1}`)
.fontSize(12)
.fontColor('#333333')
Text(prop.propertyId)
.fontSize(11)
.fontColor('#999999')
.maxLines(1)
.textOverflow({ overflow: TextOverflow.Ellipsis })
}
.layoutWeight(1)
.alignItems(HorizontalAlign.Start)
Button('导入')
.fontSize(12)
.height(28)
.backgroundColor('#9C27B0')
.fontColor(Color.White)
.onClick(() => {
this.importUUID(service.serviceId, prop.propertyId);
})
}
.width('100%')
.padding(8)
.backgroundColor('#F5F5F5')
.borderRadius(4)
.margin({ bottom: 4 })
}, (prop: DiscoveredProperty) => prop.propertyId)
}
}
.width('100%')
.padding(16)
.backgroundColor(Color.White)
.borderRadius(12)
.shadow({ radius: 2, color: 'rgba(0,0,0,0.1)', offsetY: 1 })
}
build() {
Column() {
// 标题栏
Row() {
Button('返回')
.backgroundColor(Color.Transparent)
.fontColor('#9C27B0')
.onClick(() => router.back())
Text('设备详情')
.fontSize(18)
.fontWeight(FontWeight.Medium)
.layoutWeight(1)
.textAlign(TextAlign.Center)
Text('')
.width(60)
}
.width('100%')
.padding(16)
// 服务列表
if (this.services.length === 0) {
Column() {
Text('未发现服务')
.fontSize(16)
.fontColor('#999999')
}
.width('100%')
.height(200)
.justifyContent(FlexAlign.Center)
} else {
Scroll() {
Column({ space: 12 }) {
ForEach(this.services, (service: DiscoveredService, index: number) => {
this.ServiceCard(service, index)
}, (service: DiscoveredService) => service.serviceId)
}
.width('100%')
.padding(16)
}
.layoutWeight(1)
}
}
.width('100%')
.height('100%')
.backgroundColor('#F5F5F5')
}
}
6.4 UUID 配置页面
// ====== pages/NearLinkSettingsPage.ets ======
import { router } from '@kit.ArkUI';
@Entry
@Component
struct NearLinkSettingsPage {
@State serviceUUID: string = '';
@State propertyUUID: string = '';
@State filterEnabled: boolean = false;
@State filterDeviceName: string = '';
aboutToAppear(): void {
const params = router.getParams() as Record<string, string>;
if (params) {
if (params.importServiceUUID) {
this.serviceUUID = params.importServiceUUID;
}
if (params.importCharacteristicUUID) {
this.propertyUUID = params.importCharacteristicUUID;
}
}
}
@Builder
UUIDConfigSection() {
Column() {
Text('UUID 配置')
.fontSize(16)
.fontWeight(FontWeight.Medium)
.alignSelf(ItemAlign.Start)
.margin({ bottom: 16 })
// 服务 UUID
Row() {
Text('服务UUID')
.fontSize(14)
.fontColor('#666666')
.width(80)
TextInput({ placeholder: '请输入服务UUID', text: this.serviceUUID })
.layoutWeight(1)
.margin({ left: 12, right: 12 })
.onChange((value: string) => {
this.serviceUUID = value;
})
Button('重置')
.fontSize(12)
.backgroundColor('#F5F5F5')
.fontColor('#9C27B0')
.width(60)
.height(32)
.onClick(() => {
this.serviceUUID = '';
})
}
.width('100%')
.margin({ bottom: 12 })
// 属性 UUID
Row() {
Text('属性UUID')
.fontSize(14)
.fontColor('#666666')
.width(80)
TextInput({ placeholder: '请输入属性UUID', text: this.propertyUUID })
.layoutWeight(1)
.margin({ left: 12, right: 12 })
.onChange((value: string) => {
this.propertyUUID = value;
})
Button('重置')
.fontSize(12)
.backgroundColor('#F5F5F5')
.fontColor('#9C27B0')
.width(60)
.height(32)
.onClick(() => {
this.propertyUUID = '';
})
}
.width('100%')
.margin({ bottom: 8 })
Text('UUID格式:37bea880-fc70-11ea-b720-00000000fdee')
.fontSize(12)
.fontColor('#999999')
.alignSelf(ItemAlign.Start)
}
.width('100%')
.padding(16)
.backgroundColor(Color.White)
.borderRadius(12)
}
build() {
Column() {
// 标题栏
Row() {
Button('返回')
.backgroundColor(Color.Transparent)
.fontColor('#9C27B0')
.onClick(() => router.back())
Text('星闪设置')
.fontSize(18)
.fontWeight(FontWeight.Medium)
.layoutWeight(1)
.textAlign(TextAlign.Center)
Button('保存')
.backgroundColor(Color.Transparent)
.fontColor('#9C27B0')
.onClick(() => router.back())
}
.width('100%')
.padding(16)
Scroll() {
Column({ space: 16 }) {
this.UUIDConfigSection()
}
.width('100%')
.padding(16)
}
.layoutWeight(1)
}
.width('100%')
.height('100%')
.backgroundColor('#F5F5F5')
}
}
七、广播发送(高级用法,可选)
什么是广播? 让你的手机也能被其他星闪设备发现,类似于使你的手机变成一个星闪设备。
什么时候用? 当你需要手机对手机通信,或者你在开发服务端功能时。
// ====== common/NearLinkManager.ets(补充)======
/**
* 开始广播
*/
public startAdvertising(): void {
try {
const setting: advertising.AdvertisingSettings = {
interval: 160,
power: 2
};
const manufactureData: advertising.ManufacturerData = {
manufacturerId: 4567,
manufacturerData: new Uint8Array([1, 2, 3, 4]).buffer
};
const serviceData: advertising.ServiceData = {
serviceUuid: '37bea880-fc70-11ea-b720-00000000fdee',
serviceData: new Uint8Array([5, 6, 7, 8]).buffer
};
const advData: advertising.AdvertisingData = {
serviceUuids: ['37bea880-fc70-11ea-b720-00000000fdee'],
manufacturerData: [manufactureData],
serviceData: [serviceData],
includeDeviceName: true
};
const advertisingParams: advertising.AdvertisingParams = {
advertisingSettings: setting,
advertisingData: advData
};
advertising.startAdvertising(advertisingParams)
.then((handle: number) => {
console.info(`Advertising started, handle: ${handle}`);
})
.catch((err) => {
console.error(`Start advertising failed: ${err.message}`);
});
} catch (error) {
console.error(`Start advertising exception: ${error}`);
}
}
/**
* 停止广播
*/
public stopAdvertising(handle: number): void {
try {
advertising.stopAdvertising(handle)
.then(() => console.info('Advertising stopped'))
.catch((err) => console.error(`Stop advertising failed: ${err.message}`));
} catch (error) {
console.error(`Stop advertising exception: ${error}`);
}
}
八、最佳实践(踩坑总结)
8.1 必须遵守的规则
- 权限先行:扫描前必须检查并申请权限(只要1个!)
- 资源释放:页面销毁时停止扫描、断开连接
- 状态同步:使用回调机制同步 UI 状态
- 多设备管理:使用 Map 管理多个连接
8.2 常见问题排查
| 问题 | 可能原因 | 解决方法 |
|---|---|---|
| 扫描不到设备 | 权限未授予 | 检查应用设置里的权限 |
| 扫描不到设备 | 设备不支持星闪 | 确认是华为星闪设备 |
| 连接失败 | 设备已被其他手机连接 | 断开其他连接后重试 |
| 读写失败 | UUID 不对 | 检查服务UUID和属性UUID |
8.3 写入数据的技巧
// 双重写入策略:先尝试 WRITE,失败后尝试 WRITE_NO_RESPONSE
public async sendData(serviceId: string, propId: string, data: ArrayBuffer): Promise<boolean> {
try {
// 第一次尝试:带响应的写入
await this.ssapClient?.writeProperty(prop, ssap.PropertyWriteType.WRITE);
return true;
} catch (error) {
try {
// 第二次尝试:不带响应的写入
await this.ssapClient?.writeProperty(prop, ssap.PropertyWriteType.WRITE_NO_RESPONSE);
return true;
} catch (e) {
return false;
}
}
}
九、学习资源
十、常见问题 FAQ
Q1: 我的设备支持星闪吗?
如何确认:
- HarmonyOS 6设备下拉控制中心,检查WLAN下方是不是有星闪标识
Q2: 星闪和蓝牙可以同时用吗?
可以! 它们使用不同的无线频段,互不干扰。你可以同时连接一个蓝牙设备和一个星闪设备。
Q4: 星闪设备在哪买?
目前星闪设备还不太普及,可以购买WS63或者BS21E开发板作为初体验
作者声明:个人拙作,仅总结了本人开发经验,专业性指导请参考官方文档,欢迎各位大佬斧正。本文档不是最简星闪调试工具开发文档,代码截取自成熟应用,欢迎下载易管闪联(星闪端,蓝牙端正在突破3.5)体验。
更多推荐



所有评论(0)