一、什么是星闪?

星闪(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. 权限先行:扫描前必须检查并申请权限(只要1个!)
  2. 资源释放:页面销毁时停止扫描、断开连接
  3. 状态同步:使用回调机制同步 UI 状态
  4. 多设备管理:使用 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)体验。

Logo

讨论HarmonyOS开发技术,专注于API与组件、DevEco Studio、测试、元服务和应用上架分发等。

更多推荐