请添加图片描述

前言

在万物互联的时代,设备间的连接速度与稳定性是决定用户体验的关键。华为推出的星闪(NearLink)技术,作为新一代近距离无线连接技术,旨在解决传统蓝牙和 Wi-Fi 在时延、功耗和连接数上的痛点。

本文将基于你提供的 ArkTS 代码,深入拆解如何在 HarmonyOS 应用中实现 NearLink 的核心功能:服务发现(扫描)SSAP 握手(连接)以及广播服务(Server 端)。我们将构建一个完整的“近场通信”调试面板,直观地展示从扫描设备到建立连接的底层逻辑。


📡 一、核心架构:状态管理与上下文绑定

在深入功能之前,我们需要理解该 Demo 的数据流向。代码使用了 @StorageLink 进行全局状态管理,这意味着即使页面刷新或跳转,连接状态和设备列表也能保持一致。

1. 关键状态定义

代码中定义了一系列与 NearLink 生命周期强相关的状态变量:

状态变量 类型 作用
nearLinkScanning boolean 标记当前是否正在扫描设备
nearLinkAdvertising boolean 标记当前是否正在广播(作为服务端)
nearLinkDevices Array 存储扫描到的设备列表
nearLinkConnectionState string 当前 SSAP 连接状态
nearLinkHandshakePhase string 握手阶段(如:配对中、连接中)
2. 上下文初始化

在页面即将出现时(aboutToAppear),必须绑定上下文并刷新能力,这是调用 NearLink 系统能力的前提:

aboutToAppear(): void {
  // 绑定 UIAbility 上下文,使 NearLinkSession 能访问系统服务
  NearLinkSession.bindContext(this.context)
  // 检查并刷新当前设备的星闪硬件能力
  NearLinkSession.refreshCapability()
}

🔍 二、服务发现:扫描与过滤

服务发现是近场通信的第一步。客户端需要扫描周围的广播包,找到目标设备。

1. 扫描控制逻辑

代码通过 TextInput 允许用户输入过滤前缀(默认为 SLE_),这在设备密集的环境中非常有用。

  • 开始扫描:调用 NearLinkSession.startScan(filter)
  • 停止扫描:调用 NearLinkSession.stopScan()
// 扫描控制区逻辑
Button(this.scanning ? '扫描中…' : '开始扫描')
  .onClick(() => {
    NearLinkSession.startScan(this.scanFilter)
    this.statusText = 'scan.startScan + deviceFound 回调收集中…'
  })
2. 设备列表渲染

扫描到的设备存储在 devices 数组中。UI 使用 ForEach 进行渲染,并提供了“选择”功能,将选中的设备地址存入 selectedAddress,为后续的连接做准备。

// 设备列表渲染片段
ForEach(this.devices, (item: NearLinkDeviceItem) => {
  Row({ space: 10 }) {
    Column({ space: 2 }) {
      Text(item.name) // 设备名称
      Text(`${item.address} · RSSI ${item.rssi}`) // 地址与信号强度
    }
    Button('选择')
      .backgroundColor(this.selectedAddress === item.address ? '#22C55E' : '#6366F1')
      .onClick(() => {
        this.selectedAddress = item.address // 记录目标设备
      })
  }
})

🤝 三、SSAP 握手:建立连接

SSAP(StarLink Service Access Point)是星闪技术中用于服务访问的核心协议。一旦选定了目标设备,就可以发起握手。

1. 客户端连接流程

代码展示了标准的客户端连接逻辑:

  1. 发起连接:调用 NearLinkSession.connectSelected()
  2. 状态监听:系统底层会经历 createClient -> connect -> connectionStateChange 的过程。
// 握手控制区
Button('发起连接')
  .onClick(() => {
    NearLinkSession.connectSelected()
    this.statusText = 'ssap.createClient → connect → connectionStateChange'
  })
2. 连接状态可视化

UI 实时展示连接状态和服务数量,帮助开发者判断握手是否成功。

Text(`连接: ${this.connectionState} · 服务数: ${this.serviceCount}`)

📡 四、广播服务:Server 端模拟

为了形成一个完整的通信闭环,设备不仅需要能“连别人”,还需要能“被别人连”。代码中的 AdvertiseSection 实现了服务端广播功能。

  • 服务 UUID:使用固定的 DEMO_SERVICE_UUID 标识服务。
  • 广播控制:通过 startAdvertisingstopAdvertising 控制广播的开启与关闭。
// 广播端逻辑
Button(this.advertising ? '广播中…' : '开始广播')
  .onClick(() => NearLinkSession.startAdvertising())

注意:在实际开发中,作为 Server 端时,还需要处理客户端的连接请求回调,而不仅仅是开启广播。


📊 五、底层链路可视化与日志

调试近场通信最困难的部分在于其“不可见性”。该 Demo 通过 FlowSectionLogSection 将底层协议栈的状态变化具象化。

1. 握手链路图谱

代码清晰地定义了星闪连接的五个关键阶段,这对于理解协议状态机非常有帮助:

  1. 扫描scan.startScanon(deviceFound)
  2. 配对manager.on(pairingStateChange)
  3. 连接ssap.createClientconnect()
  4. 握手connectionStateChangeCONNECTED
  5. 发现client.getServices() → 获取服务列表
2. 演示模式(Demo Mode)

代码中特别加入了 demoMode 的判断。如果硬件不支持星闪(如在普通模拟器上运行),系统会提示“演示模式”,并模拟握手流程。这是一种非常优秀的容错设计,保证了代码在没有专用硬件时依然可以演示 UI 逻辑。

if (this.demoMode) {
  Text('演示模式:硬件/模拟器不支持星闪,使用模拟握手流程')
    .fontColor('#FBBF24')
}

完整代码

import { common } from '@kit.AbilityKit'
import { DEMO_SERVICE_UUID } from '../nearlink/NearLinkTypes'
import { NearLinkBridge, NearLinkSession } from '../nearlink/NearLinkSession'
import { NearLinkDeviceItem, NearLinkLogItem } from '../nearlink/NearLinkTypes'



struct NearLinkDemo {
  ('nearLinkSupported') nearLinkSupported: boolean = false
  ('nearLinkState') nearLinkState: string = ''
  ('nearLinkScanning') scanning: boolean = false
  ('nearLinkAdvertising') advertising: boolean = false
  ('nearLinkHandshakePhase') handshakePhase: string = ''
  ('nearLinkConnectionState') connectionState: string = ''
  ('nearLinkDevices') devices: NearLinkDeviceItem[] = []
  ('nearLinkSelectedAddress') selectedAddress: string = ''
  ('nearLinkServiceCount') serviceCount: number = 0
  ('nearLinkLogs') logs: NearLinkLogItem[] = []
  ('nearLinkDemoMode') demoMode: boolean = false
   scanFilter: string = 'SLE_'
   statusText: string = 'SLE 协议栈:扫描发现 → 配对 → SSAP 握手 → 服务发现'
  private context = this.getUIContext().getHostContext() as common.UIAbilityContext

  aboutToAppear(): void {
    NearLinkSession.bindContext(this.context)
    NearLinkSession.refreshCapability()
  }

  build() {
    Scroll() {
      Column({ space: 16 }) {
        this.HeaderSection()
        this.StatusSection()
        this.ScanSection()
        this.DeviceSection()
        this.HandshakeSection()
        this.AdvertiseSection()
        this.FlowSection()
        this.LogSection()
      }
      .width('100%')
      .padding({ top: 36, left: 16, right: 16, bottom: 24 })
    }
    .width('100%')
    .height('100%')
    .backgroundColor('#020617')
    .scrollBar(BarState.Off)
  }

   HeaderSection() {
    Column({ space: 6 }) {
      Text('近场通信 NearLink')
        .fontSize(28)
        .fontWeight(FontWeight.Bold)
        .fontColor('#F8FAFC')
      Text('星闪 · 服务发现与 SSAP 握手')
        .fontSize(14)
        .fontColor('#94A3B8')
    }
    .width('100%')
    .alignItems(HorizontalAlign.Start)
  }

   StatusSection() {
    Column({ space: 6 }) {
      Row() {
        Text('星闪状态')
          .fontSize(13)
          .fontColor('#E2E8F0')
        Blank()
        Text(this.nearLinkState)
          .fontSize(12)
          .fontColor('#38BDF8')
      }
      .width('100%')

      Row() {
        Text('握手阶段')
          .fontSize(13)
          .fontColor('#E2E8F0')
        Blank()
        Text(this.handshakePhase)
          .fontSize(12)
          .fontColor('#A78BFA')
      }
      .width('100%')

      Text(this.statusText)
        .fontSize(12)
        .fontColor('#64748B')
        .width('100%')

      if (this.demoMode) {
        Text('演示模式:硬件/模拟器不支持星闪,使用模拟握手流程')
          .fontSize(11)
          .fontColor('#FBBF24')
          .width('100%')
      }
    }
    .width('100%')
    .padding(14)
    .backgroundColor('rgba(56,189,248,0.1)')
    .borderRadius(14)
  }

   ScanSection() {
    Column({ space: 10 }) {
      Text('服务发现 (Scan)')
        .fontSize(16)
        .fontWeight(FontWeight.Bold)
        .fontColor('#F8FAFC')
        .width('100%')

      TextInput({ text: this.scanFilter, placeholder: '设备名过滤(可选)' })
        .height(42)
        .fontSize(13)
        .backgroundColor('rgba(255,255,255,0.08)')
        .onChange((v: string) => this.scanFilter = v)

      Row({ space: 8 }) {
        Button(this.scanning ? '扫描中…' : '开始扫描')
          .layoutWeight(1)
          .height(42)
          .fontSize(12)
          .backgroundColor('#0EA5E9')
          .enabled(!this.scanning)
          .onClick(() => {
            NearLinkSession.startScan(this.scanFilter)
            this.statusText = 'scan.startScan + deviceFound 回调收集中…'
          })

        Button('停止')
          .layoutWeight(1)
          .height(42)
          .fontSize(12)
          .backgroundColor('#475569')
          .enabled(this.scanning)
          .onClick(() => NearLinkSession.stopScan())
      }
      .width('100%')
    }
    .width('100%')
    .padding(14)
    .backgroundColor('rgba(255,255,255,0.05)')
    .borderRadius(14)
    .alignItems(HorizontalAlign.Start)
  }

   DeviceSection() {
    Column({ space: 8 }) {
      Text(`发现设备 (${this.devices.length})`)
        .fontSize(16)
        .fontWeight(FontWeight.Bold)
        .fontColor('#F8FAFC')
        .width('100%')

      if (this.devices.length === 0) {
        Text('暂无设备 · 请开始扫描')
          .fontSize(12)
          .fontColor('#64748B')
      } else {
        ForEach(this.devices, (item: NearLinkDeviceItem) => {
          Row({ space: 10 }) {
            Column({ space: 2 }) {
              Text(item.name)
                .fontSize(13)
                .fontColor('#F8FAFC')
              Text(`${item.address} · RSSI ${item.rssi}`)
                .fontSize(11)
                .fontColor('#94A3B8')
            }
            .alignItems(HorizontalAlign.Start)
            .layoutWeight(1)

            Button('选择')
              .height(32)
              .fontSize(11)
              .backgroundColor(this.selectedAddress === item.address ? '#22C55E' : '#6366F1')
              .onClick(() => {
                this.selectedAddress = item.address
                NearLinkBridge.appendLog('选择', `${item.name} · ${item.address}`)
              })
          }
          .width('100%')
          .padding(10)
          .backgroundColor('rgba(255,255,255,0.04)')
          .borderRadius(8)
        }, (item: NearLinkDeviceItem) => item.id)
      }
    }
    .width('100%')
    .padding(14)
    .backgroundColor('rgba(255,255,255,0.05)')
    .borderRadius(14)
    .alignItems(HorizontalAlign.Start)
  }

   HandshakeSection() {
    Column({ space: 10 }) {
      Text('SSAP 握手 (Client)')
        .fontSize(16)
        .fontWeight(FontWeight.Bold)
        .fontColor('#F8FAFC')
        .width('100%')

      Text(`连接: ${this.connectionState} · 服务数: ${this.serviceCount}`)
        .fontSize(12)
        .fontColor('#94A3B8')
        .width('100%')

      Text(this.selectedAddress.length > 0 ? `目标: ${this.selectedAddress}` : '未选择目标设备')
        .fontSize(12)
        .fontColor('#A78BFA')
        .width('100%')

      Row({ space: 8 }) {
        Button('发起连接')
          .layoutWeight(1)
          .height(42)
          .fontSize(12)
          .backgroundColor('#7C3AED')
          .onClick(() => {
            NearLinkSession.connectSelected()
            this.statusText = 'ssap.createClient → connect → connectionStateChange'
          })

        Button('断开')
          .layoutWeight(1)
          .height(42)
          .fontSize(12)
          .backgroundColor('#DC2626')
          .onClick(() => NearLinkSession.disconnect())
      }
      .width('100%')
    }
    .width('100%')
    .padding(14)
    .backgroundColor('rgba(124,58,237,0.12)')
    .borderRadius(14)
    .alignItems(HorizontalAlign.Start)
  }

   AdvertiseSection() {
    Column({ space: 10 }) {
      Text('广播端 (Server 角色)')
        .fontSize(16)
        .fontWeight(FontWeight.Bold)
        .fontColor('#F8FAFC')
        .width('100%')

      Text(`UUID: ${DEMO_SERVICE_UUID}`)
        .fontSize(11)
        .fontColor('#64748B')
        .width('100%')

      Row({ space: 8 }) {
        Button(this.advertising ? '广播中…' : '开始广播')
          .layoutWeight(1)
          .height(42)
          .fontSize(12)
          .backgroundColor('#10B981')
          .enabled(!this.advertising)
          .onClick(() => NearLinkSession.startAdvertising())

        Button('停止广播')
          .layoutWeight(1)
          .height(42)
          .fontSize(12)
          .backgroundColor('#475569')
          .enabled(this.advertising)
          .onClick(() => NearLinkSession.stopAdvertising())
      }
      .width('100%')
    }
    .width('100%')
    .padding(14)
    .backgroundColor('rgba(16,185,129,0.1)')
    .borderRadius(14)
    .alignItems(HorizontalAlign.Start)
  }

   FlowSection() {
    Column({ space: 8 }) {
      Text('底层握手链路')
        .fontSize(16)
        .fontWeight(FontWeight.Bold)
        .fontColor('#F8FAFC')
        .width('100%')

      this.FlowRow('①', '扫描', 'scan.startScan → on(deviceFound)')
      this.FlowRow('②', '配对', 'manager.on(pairingStateChange)')
      this.FlowRow('③', '连接', 'ssap.createClient → connect()')
      this.FlowRow('④', '握手', 'connectionStateChange → CONNECTED')
      this.FlowRow('⑤', '发现', 'client.getServices() → SSAP 服务列表')
    }
    .width('100%')
    .padding(14)
    .backgroundColor('rgba(255,255,255,0.05)')
    .borderRadius(14)
    .alignItems(HorizontalAlign.Start)
  }

   LogSection() {
    Column({ space: 8 }) {
      Row() {
        Text('事件日志')
          .fontSize(16)
          .fontWeight(FontWeight.Bold)
          .fontColor('#F8FAFC')
        Blank()
        Text(`${this.logs.length} 条`)
          .fontSize(12)
          .fontColor('#38BDF8')
      }
      .width('100%')

      if (this.logs.length === 0) {
        Text('等待操作…')
          .fontSize(12)
          .fontColor('#64748B')
      } else {
        ForEach(this.logs, (item: NearLinkLogItem) => {
          Column({ space: 2 }) {
            Text(`[${item.time}] ${item.phase}`)
              .fontSize(11)
              .fontColor('#F472B6')
            Text(item.message)
              .fontSize(11)
              .fontColor('#CBD5E1')
          }
          .width('100%')
          .alignItems(HorizontalAlign.Start)
        }, (item: NearLinkLogItem) => item.id)
      }
    }
    .width('100%')
    .padding(14)
    .backgroundColor('rgba(255,255,255,0.05)')
    .borderRadius(14)
    .alignItems(HorizontalAlign.Start)
  }

   FlowRow(index: string, title: string, detail: string) {
    Row({ space: 10 }) {
      Text(index)
        .fontSize(12)
        .fontColor('#38BDF8')
        .width(20)
      Column({ space: 2 }) {
        Text(title)
          .fontSize(12)
          .fontColor('#E2E8F0')
        Text(detail)
          .fontSize(11)
          .fontColor('#64748B')
      }
      .alignItems(HorizontalAlign.Start)
      .layoutWeight(1)
    }
    .width('100%')
  }
}

在这里插入图片描述
在这里插入图片描述

总结

这份 ArkTS 代码不仅仅是一个简单的 UI 页面,它是一个完整的 NearLink 协议调试器。它涵盖了从上下文绑定设备扫描过滤SSAP 连接握手服务端广播的全链路逻辑。

对于开发者而言,理解这段代码的关键在于掌握 NearLinkSession 的调用时机以及如何处理异步的连接状态回调。通过这套逻辑,你可以构建出低时延、高可靠的星闪互联应用。

Logo

作为“人工智能6S店”的官方数字引擎,为AI开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐