From d2dcd3941124c81e35c8f3d6a8f7b2a5c6f5d979 Mon Sep 17 00:00:00 2001 From: Yudi Xiao <463708580@qq.com> Date: Wed, 10 Dec 2025 12:39:41 +0800 Subject: [PATCH] =?UTF-8?q?expo-ble=E6=A8=A1=E5=9D=97=E6=B5=8B=E8=AF=95dem?= =?UTF-8?q?o=20=E8=81=94=E8=B0=83BLE=E6=A8=A1=E5=9D=97?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 369 ++++++++++--------- app/(tabs)/explore.tsx | 369 ++++++------------- ble/example/BleExampleV2.tsx | 372 ------------------- {hooks => ble/hooks}/useBleExplorer.ts | 120 +++---- ble/index.ts | 1 + ble/manager/BlePeripheralManager.ts | 472 ------------------------- ble/package.json | 11 +- ble/protocol/Constants.ts | 8 +- ble/protocol/ProtocolManager.ts | 4 +- ble/protocol/types.ts | 17 - ble/services/BleProtocolService.ts | 3 +- ble/utils/CryptoUtils.ts | 80 ----- ble/utils/ProtocolUtilsV2.ts | 306 ---------------- bun.lock | 3 + eas.json | 1 - hooks/useBlePeripheral.ts | 323 ----------------- package.json | 1 + 17 files changed, 391 insertions(+), 2069 deletions(-) delete mode 100644 ble/example/BleExampleV2.tsx rename {hooks => ble/hooks}/useBleExplorer.ts (82%) delete mode 100644 ble/manager/BlePeripheralManager.ts delete mode 100644 ble/utils/CryptoUtils.ts delete mode 100644 ble/utils/ProtocolUtilsV2.ts delete mode 100644 hooks/useBlePeripheral.ts diff --git a/README.md b/README.md index eb8e7c3..df7326a 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,23 @@ +# 项目初始化 + +安装 bun https://bun.com/docs/installation#windows + +```bash +powershell -c "irm bun.sh/install.ps1|iex" +``` + +安装项目依赖 + +```bash +bun install +``` + +运行 dev项目 + +```bash +bun run start +````` + # 协议三端通信说明 ## 1. 数据格式 @@ -6,201 +26,217 @@ 所有数据包遵循以下格式(通常为大端序): -| 字段 (Field) | 长度 (Bytes) | 值 / 说明 | -| :--- | :--- | :--- | -| **HEADER** | 3 | `FEDCBA` | -| **CMD** | 2 | 命令字 (如 `E100`) | -| **DATA** | n | 数据内容 (0 ~ n 字节) | -| **CHECKSUM** | 1 | 校验和 | -| **TAIL** | 2 | `00EF` | +| 字段 (Field) | 长度 (Bytes) | 值 / 说明 | +|:-------------|:-----------|:----------------| +| **HEADER** | 3 | `FEDCBA` | +| **CMD** | 2 | 命令字 (如 `E100`) | +| **DATA** | n | 数据内容 (0 ~ n 字节) | +| **CHECKSUM** | 1 | 校验和 | +| **TAIL** | 2 | `00EF` | ### 1.2 错误反馈 (系统级) 当接收到的数据包格式错误时,设备会返回以下错误码: -| 错误类型 | CMD (上报) | 参数 (DATA) | 完整 HEX 示例 | -| :--- | :--- | :--- | :--- | -| HEADER 错误 | `E0E0` | `C0` | `FEDCBA E0E0 C0 00EF` | -| TAIL 错误 | `E0E0` | `C1` | `FEDCBA E0E0 C1 00EF` | -| CHECKSUM 错误 | `E0E2` | `C0` | `FEDCBA E0E2 C0 00EF` | -| CMD 不支持 | `E0E3` | `[CMD]` | `FEDCBA E0E3 [CMD] XX 00EF` | +| 错误类型 | CMD (上报) | 参数 (DATA) | 完整 HEX 示例 | +|:------------|:---------|:----------|:----------------------------| +| HEADER 错误 | `E0E0` | `C0` | `FEDCBA E0E0 C0 00EF` | +| TAIL 错误 | `E0E0` | `C1` | `FEDCBA E0E0 C1 00EF` | +| CHECKSUM 错误 | `E0E2` | `C0` | `FEDCBA E0E2 C0 00EF` | +| CMD 不支持 | `E0E3` | `[CMD]` | `FEDCBA E0E3 [CMD] XX 00EF` | --- ## 2. 蓝牙通道说明 -* **OTA 通道**: - * Write: `ae01` - * Notify: `ai02` (或 `ae02`) -* **APP 通信通道**: - * Write: `ae10` (MTU 建议设置为 517) - * Notify: `ae02` +* **OTA 通道**: + * Write: `ae01` + * Notify: `ai02` (或 `ae02`) +* **APP 通信通道**: + * Write: `ae10` (MTU 建议设置为 517) + * Notify: `ae02` --- ## 3. 命令详解 ### 3.1 绑定 (0xE100) + **方向**: APP -> 设备 -* **发送**: `FEDCBA E100 E1 00EF` -* **返回**: +* **发送**: `FEDCBA E100 E1 00EF` +* **返回**: -| 状态 | CMD | 参数 | 完整 HEX 示例 | -| :--- | :--- | :--- | :--- | +| 状态 | CMD | 参数 | 完整 HEX 示例 | +|:---|:-------|:-----|:----------------------| | 成功 | `E1A0` | `81` | `FEDCBA E1A0 81 00EF` | | 失败 | `E1A1` | `82` | `FEDCBA E1A1 82 00EF` | | 异常 | `E1A2` | `83` | `FEDCBA E1A2 83 00EF` | ### 3.2 绑定失败上报 (0xE200) + **方向**: APP -> 设备 (通常用于APP端绑定失败后通知设备) -* **发送**: `FEDCBA E200 XX 00EF` -* **返回**: +* **发送**: `FEDCBA E200 XX 00EF` +* **返回**: -| 状态 | CMD | 参数 | 完整 HEX 示例 | -| :--- | :--- | :--- | :--- | +| 状态 | CMD | 参数 | 完整 HEX 示例 | +|:---|:-------|:-----|:----------------------| | 成功 | `E2A0` | `XX` | `FEDCBA E2A0 XX 00EF` | | 失败 | `E2A1` | `XX` | `FEDCBA E2A1 XX 00EF` | | 异常 | `E2A2` | `XX` | `FEDCBA E2A2 XX 00EF` | ### 3.3 写用户 ID (0xE300) + **方向**: APP -> 设备 -* **发送**: `FEDCBA E300 [UserID] XX 00EF` - * `UserID`: 用户ID数据 -* **返回**: +* **发送**: `FEDCBA E300 [UserID] XX 00EF` + * `UserID`: 用户ID数据 +* **返回**: -| 状态 | CMD | 参数 | 完整 HEX 示例 | -| :--- | :--- | :--- | :--- | -| 成功 | `E3A0` | `83` | `FEDCBA E3A0 83 00EF` | -| 失败 | `E3A1` | `84` | `FEDCBA E3A1 84 00EF` | +| 状态 | CMD | 参数 | 完整 HEX 示例 | +|:------|:-------|:-----|:----------------------| +| 成功 | `E3A0` | `83` | `FEDCBA E3A0 83 00EF` | +| 失败 | `E3A1` | `84` | `FEDCBA E3A1 84 00EF` | | 异常(空) | `E3A3` | `85` | `FEDCBA E3A3 85 00EF` | ### 3.4 查询密锁 (0xE400) + **方向**: APP -> 设备 -* **发送**: `FEDCBA E400 E4 00EF` -* **返回**: `FEDCBA E4A0 [TOKEN] XX 00EF` - * `TOKEN`: 32位 (4字节) 密锁数据。 +* **发送**: `FEDCBA E400 E4 00EF` +* **返回**: `FEDCBA E4A0 [TOKEN] XX 00EF` + * `TOKEN`: 32位 (4字节) 密锁数据。 ### 3.5 设置时间 (0xE500) + **方向**: APP -> 设备 -* **发送**: `FEDCBA E500 [Time] XX 00EF` - * `Time`: 格式 `yyyymmddhhmmss` (14字节 ASCII) -* **返回**: +* **发送**: `FEDCBA E500 [Time] XX 00EF` + * `Time`: 格式 `yyyymmddhhmmss` (14字节 ASCII) +* **返回**: -| 状态 | CMD | 参数 | 完整 HEX 示例 | -| :--- | :--- | :--- | :--- | +| 状态 | CMD | 参数 | 完整 HEX 示例 | +|:---|:-------|:-----|:----------------------| | 成功 | `E5A0` | `85` | `FEDCBA E5A0 85 00EF` | | 失败 | `E5A1` | `86` | `FEDCBA E5A1 86 00EF` | | 异常 | `E5A2` | `87` | `FEDCBA E5A2 87 00EF` | ### 3.6 查询固件版本号 (0xE600) + **方向**: APP -> 设备 -* **发送**: `FEDCBA E600 E6 00EF` -* **返回**: `FEDCBA E6A0 [Version] XX 00EF` - * `Version`: 格式 `yyyymmddhh` (10字节 ASCII) +* **发送**: `FEDCBA E600 E6 00EF` +* **返回**: `FEDCBA E6A0 [Version] XX 00EF` + * `Version`: 格式 `yyyymmddhh` (10字节 ASCII) ### 3.7 查询资源版本号 (0xE700 / 0xD700) + **方向**: APP -> 设备 -* **版本 < V4.0**: - * 发送: `FEDCBA E700 E7 00EF` - * 返回: `FEDCBA E7A0 [ResVersion] XX 00EF` -* **版本 >= V4.0**: - * 发送: `FEDCBA D700 E7 00EF` - * 返回: `FEDCBA D7A0 [ResVersion] XX 00EF` +* **版本 < V4.0**: + * 发送: `FEDCBA E700 E7 00EF` + * 返回: `FEDCBA E7A0 [ResVersion] XX 00EF` +* **版本 >= V4.0**: + * 发送: `FEDCBA D700 E7 00EF` + * 返回: `FEDCBA D7A0 [ResVersion] XX 00EF` ### 3.8 设置资源版本号 (0xE800) + **方向**: APP -> 设备 -* **发送**: `FEDCBA E800 [TT] [ResVersion] XX 00EF` -* **返回**: +* **发送**: `FEDCBA E800 [TT] [ResVersion] XX 00EF` +* **返回**: -| 状态 | CMD | 参数 | 完整 HEX 示例 | -| :--- | :--- | :--- | :--- | +| 状态 | CMD | 参数 | 完整 HEX 示例 | +|:---|:-------|:-----|:----------------------| | 成功 | `E8A0` | `88` | `FEDCBA E8A0 88 00EF` | | 失败 | `E8A1` | `89` | `FEDCBA E8A1 89 00EF` | | 异常 | `E8A2` | `8A` | `FEDCBA E8A2 8A 00EF` | ### 3.9 解绑 (0xE900) + **方向**: APP -> 设备 -* **发送**: `FEDCBA E900 E0 00EF` -* **返回**: `FEDCBA E9A0 F3 00EF` (解绑成功) +* **发送**: `FEDCBA E900 E0 00EF` +* **返回**: `FEDCBA E9A0 F3 00EF` (解绑成功) ### 3.10 下载文件 / OTA (0xEA00) + **方向**: APP -> 设备 此命令流程包含请求传输、数据传输、传输结束、取消传输等状态。 #### A. 启动传输请求 -* **发送**: `FEDCBA EA00 [Type] [NameLen] [Name] [MD5] [Size] XX 00EF` - * `Type`: 文件类型 (1字节) - * `NameLen`: 文件名长度 (1字节) - * `Name`: 文件名 (N字节) - * `MD5`: 文件校验 (8字节) - * `Size`: 文件大小 (4字节, 16进制) -* **返回**: -| 状态 | CMD | 说明 | 附加数据 | -| :--- | :--- | :--- | :--- | -| 同意传输 | `EAA0` | 准备接收 | `8A` | -| 失败 | `EAA1` | 拒绝/错误 | `8B` | -| 异常 | `EAA2` | 参数错误 | `8C` | -| 文件相同 | `EAA3` | 无需传输 | `8D` | -| 断点续传 | `EAA4` | 从Offset开始 | `[Offset(4B)]` | -| OTA回复 | `EAA5` | OTA 错误码 | `[ErrCode]` | +* **发送**: `FEDCBA EA00 [Type] [NameLen] [Name] [MD5] [Size] XX 00EF` + * `Type`: 文件类型 (1字节) + * `NameLen`: 文件名长度 (1字节) + * `Name`: 文件名 (N字节) + * `MD5`: 文件校验 (8字节) + * `Size`: 文件大小 (4字节, 16进制) +* **返回**: + +| 状态 | CMD | 说明 | 附加数据 | +|:------|:-------|:----------|:---------------| +| 同意传输 | `EAA0` | 准备接收 | `8A` | +| 失败 | `EAA1` | 拒绝/错误 | `8B` | +| 异常 | `EAA2` | 参数错误 | `8C` | +| 文件相同 | `EAA3` | 无需传输 | `8D` | +| 断点续传 | `EAA4` | 从Offset开始 | `[Offset(4B)]` | +| OTA回复 | `EAA5` | OTA 错误码 | `[ErrCode]` | #### B. 文件数据传输 (Packet) -* **发送**: `FEDCBA EA01 [Seq] [Len] [Data] XX 00EF` - * `Seq`: 序列号 (2字节) - * `Len`: 数据长度 (2字节) - * `Data`: 数据内容 -* **返回 (每包/批量确认)**: `FEDCBA EAA6 [ErrCode] [Seq] [TotalLen] XX 00EF` + +* **发送**: `FEDCBA EA01 [Seq] [Len] [Data] XX 00EF` + * `Seq`: 序列号 (2字节) + * `Len`: 数据长度 (2字节) + * `Data`: 数据内容 +* **返回 (每包/批量确认)**: `FEDCBA EAA6 [ErrCode] [Seq] [TotalLen] XX 00EF` #### C. 传输完成 -* **发送**: `FEDCBA EA02 EC 00EF` -* **返回**: - * 成功: `FEDCBA EAA7 91 00EF` - * 失败: `FEDCBA EAA8 [ErrCode] XX 00EF` + +* **发送**: `FEDCBA EA02 EC 00EF` +* **返回**: + * 成功: `FEDCBA EAA7 91 00EF` + * 失败: `FEDCBA EAA8 [ErrCode] XX 00EF` #### D. 取消传输 -* **发送**: `FEDCBA EA03 ED 00EF` -* **返回**: `FEDCBA EAA9 [ErrCode] 8D 00EF` + +* **发送**: `FEDCBA EA03 ED 00EF` +* **返回**: `FEDCBA EAA9 [ErrCode] 8D 00EF` ### 3.11 查询设备 OTA 信息 (0xEB00) + **方向**: APP -> 设备 -* **查询 OTA 信息**: - * 发送: `FEDCBA EB00 E0 00EF` - * 返回: `FEDCBA EBA0 [Err] [Ver] XX 00EF` -* **清除 OTA 状态**: - * 发送: `FEDCBA EB01 EC 00EF` - * 返回: `FEDCBA EBA7 92 00EF` +* **查询 OTA 信息**: + * 发送: `FEDCBA EB00 E0 00EF` + * 返回: `FEDCBA EBA0 [Err] [Ver] XX 00EF` +* **清除 OTA 状态**: + * 发送: `FEDCBA EB01 EC 00EF` + * 返回: `FEDCBA EBA7 92 00EF` ### 3.12 设备信息查询 (0xED00) + **方向**: APP -> 设备 -* **发送**: `FEDCBA ED00 [TypeMask] XX 00EF` - * `TypeMask`: 2字节位掩码,每一位代表请求的信息类型。 -* **返回**: - * 成功: `FEDCBA EDA0 [Data] XX 00EF` - * 失败: `FEDCBA EDA1 ...` - * 异常: `FEDCBA EDA2 ...` +* **发送**: `FEDCBA ED00 [TypeMask] XX 00EF` + * `TypeMask`: 2字节位掩码,每一位代表请求的信息类型。 +* **返回**: + * 成功: `FEDCBA EDA0 [Data] XX 00EF` + * 失败: `FEDCBA EDA1 ...` + * 异常: `FEDCBA EDA2 ...` **支持的信息类型 (Bit定义)**: -| Bit | 信息内容 | 长度 (Bytes) | 说明 | -| :--- | :--- | :--- | :--- | -| 0 | 当前电量 | 1 | 0~100 | -| 1 | 当前音量 | 1 | 0~3 | -| 2 | SD卡挂载状态 | 1 | 0:未挂载, 1:挂载 | -| 3 | SD卡总容量 | 4 | 单位 KB | -| 4 | SD卡剩余容量 | 4 | 单位 KB | -| 5 | BLE MAC 地址 | 6 | Hex format | +| Bit | 信息内容 | 长度 (Bytes) | 说明 | +|:----|:-----------|:-----------|:------------| +| 0 | 当前电量 | 1 | 0~100 | +| 1 | 当前音量 | 1 | 0~3 | +| 2 | SD卡挂载状态 | 1 | 0:未挂载, 1:挂载 | +| 3 | SD卡总容量 | 4 | 单位 KB | +| 4 | SD卡剩余容量 | 4 | 单位 KB | +| 5 | BLE MAC 地址 | 6 | Hex format | --- @@ -212,78 +248,83 @@ V2 协议使用特定的帧头 `0xC7` (App发) / `0xB0` (设备发) 包裹 JSON 数据。 -| 字段 | 长度 (Bytes) | 说明 | -| :--- | :--- | :--- | -| **HEAD** | 1 | `0xC7` (App->Dev) / `0xB0` (Dev->App) | -| **TYPE** | 1 | 命令字 (如 `0x0D`) | -| **Subpage Total** | 2 | 分包总数 (大端) | -| **Current Page** | 2 | 当前包序号 (大端) | -| **Data Len** | 2 | 数据长度 (大端) | -| **DATA** | N | JSON 字符串 (UTF-8) | -| **CHECKSUM** | 1 | 校验和 (0 - Sum(0...End-1)) | +| 字段 | 长度 (Bytes) | 说明 | +|:------------------|:-----------|:--------------------------------------| +| **HEAD** | 1 | `0xC7` (App->Dev) / `0xB0` (Dev->App) | +| **TYPE** | 1 | 命令字 (如 `0x0D`) | +| **Subpage Total** | 2 | 分包总数 (大端) | +| **Current Page** | 2 | 当前包序号 (大端) | +| **Data Len** | 2 | 数据长度 (大端) | +| **DATA** | N | JSON 字符串 (UTF-8) | +| **CHECKSUM** | 1 | 校验和 (0 - Sum(0...End-1)) | ### 4.2 命令定义 (JSON Payload) 以下为 Data 字段中的 JSON 内容示例。 #### 4.2.1 激活状态 (0x01) -* **APP -> 设备**: (查询) -* **设备 -> APP**: - ```json - { - "type": 0x01, - "state": 0x00 // 0:未激活, 1:已激活 - } - ``` + +* **APP -> 设备**: (查询) +* **设备 -> APP**: + ```json + { + "type": 0x01, + "state": 0x00 // 0:未激活, 1:已激活 + } + ``` #### 4.2.2 协议版本 (0x07) -* **设备 -> APP**: - ```json - { - "type": 0x07, - "version": "2024.01" - } - ``` + +* **设备 -> APP**: + ```json + { + "type": 0x07, + "version": "2024.01" + } + ``` #### 4.2.3 时间同步 (0x08) -* **APP -> 设备**: - ```json - { - "type": 0x08, - "year": 2024, - "mon": 1, - "day": 1, - "hour": 12, - "min": 0, - "mes": 0 - } - ``` + +* **APP -> 设备**: + ```json + { + "type": 0x08, + "year": 2024, + "mon": 1, + "day": 1, + "hour": 12, + "min": 0, + "mes": 0 + } + ``` #### 4.2.4 设备信息上报 (0x0D) -* **设备 -> APP**: - ```json - { - "type": 0x0d, - "allspace": 1000, - "freespace": 500, - "devname": "Loom", - "size": 0, // 0:圆屏, 1:方屏 - "brand": 5 - } - ``` + +* **设备 -> APP**: + ```json + { + "type": 0x0d, + "allspace": 1000, + "freespace": 500, + "devname": "Loom", + "size": 0, // 0:圆屏, 1:方屏 + "brand": 5 + } + ``` #### 4.2.5 身份核对 (0x0E) -* **APP -> 设备** (发送 12位码): - ```json - { - "type": 0x0e, - "IdCheck": "xxxxxxxxxxxx" - } - ``` -* **设备 -> APP** (返回结果): - ```json - { - "type": 0x0e, - "Ret": 1 // 0:错误, 1:正确 - } - ``` + +* **APP -> 设备** (发送 12位码): + ```json + { + "type": 0x0e, + "IdCheck": "xxxxxxxxxxxx" + } + ``` +* **设备 -> APP** (返回结果): + ```json + { + "type": 0x0e, + "Ret": 1 // 0:错误, 1:正确 + } + ``` diff --git a/app/(tabs)/explore.tsx b/app/(tabs)/explore.tsx index 18280aa..cb8ba9e 100644 --- a/app/(tabs)/explore.tsx +++ b/app/(tabs)/explore.tsx @@ -1,18 +1,13 @@ import React from 'react'; -import {Alert, StyleSheet, Button, Switch, ScrollView} from 'react-native'; +import {StyleSheet, Button} from 'react-native'; import {ThemedText} from '@/components/themed-text'; import {ThemedView} from '@/components/themed-view'; -import {useBleExplorer} from '@/hooks/useBleExplorer'; -import {useBlePeripheral} from '@/hooks/useBlePeripheral'; -import {BLE_UUIDS, PROTOCOL_VERSION} from '@/ble'; +import {BLE_UUIDS, PROTOCOL_VERSION, useBleExplorer} from '@/ble'; import ParallaxScrollView from '@/components/parallax-scroll-view'; import {IconSymbol} from '@/components/ui/icon-symbol'; export default function TabTwoScreen() { - const [deviceMode, setDeviceMode] = React.useState<'central' | 'peripheral'>('central'); - - const { isScanning, isConnected, @@ -35,24 +30,8 @@ export default function TabTwoScreen() { updateActivationTime, sendIdentityCheck, transferSampleFile, - clearLogs, } = useBleExplorer(); - const { - isAdvertising, - connectedCentralCount, - logs: peripheralLogs, - deviceInfo: peripheralDeviceInfo, - loading: peripheralLoading, - error: peripheralError, - startAdvertising, - stopAdvertising, - getCharacteristicReadValue, - updateDeviceInfo, - resetDeviceInfo, - clearLogs: clearPeripheralLogs, - } = useBlePeripheral(); - return ( BLE Explorer - {/* Device Mode Selection */} + {/* Connection Status */} - Device Mode - - Central - { - if (value) { - // Switching to peripheral mode - stop central operations first - console.log('Switching to peripheral mode - stopping central operations'); - if (isScanning) { - await stopScan(); - // Add small delay to ensure cleanup - await new Promise(resolve => setTimeout(resolve, 500)); - } - if (isConnected) { - await disconnectDevice(); - // Add small delay to ensure cleanup - await new Promise(resolve => setTimeout(resolve, 500)); - } - } else { - // Switching to central mode - stop peripheral operations first - console.log('Switching to central mode - stopping peripheral operations'); - if (isAdvertising) { - await stopAdvertising(); - // Add small delay to ensure cleanup - await new Promise(resolve => setTimeout(resolve, 500)); - } - } - setDeviceMode(value ? 'peripheral' : 'central'); - }} - disabled={isScanning || isConnected || isAdvertising || peripheralLoading.advertising} - /> - Peripheral - - - {deviceMode === 'central' - ? 'Central mode: Scan for and connect to BLE peripherals' - : `Peripheral mode: Act as BLE receiver`} - + Connection Status + Scanning: {isScanning ? 'Yes' : 'No'} + Connected: {isConnected ? 'Yes' : 'No'} + Device: {connectedDevice?.name || 'None'} + {error && Error: {error}} - {deviceMode === 'central' ? ( - <> - {/* Connection Status */} - - Connection Status - Scanning: {isScanning ? 'Yes' : 'No'} - Connected: {isConnected ? 'Yes' : 'No'} - Device: {connectedDevice?.name || 'None'} - {error && Error: {error}} - - - {/* Discovered Devices */} - - Discovered Devices - Total devices - found: {discoveredDevices.length} - {discoveredDevices.length === 0 ? ( - No devices discovered yet. Start scanning to find devices. - ) : ( - - {discoveredDevices.map((item) => ( - - - - {item.name || 'Unknown Device'} - - {item.id} - {item.serviceUUIDs && item.serviceUUIDs.length > 0 && ( - - Services: {item.serviceUUIDs.join(', ')} - - )} - {item.connected && ( - Connected - )} - -