# waerable_pack **Repository Path**: chen22eer/waerable_pack ## Basic Information - **Project Name**: waerable_pack - **Description**: No description available - **Primary Language**: Unknown - **License**: Not specified - **Default Branch**: main - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 0 - **Forks**: 0 - **Created**: 2026-07-21 - **Last Updated**: 2026-07-23 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README # EmoLink 基于 BLE 的近场社交匹配系统。手环 (GATT Client) 扫描并连接手机 (GATT GATT Server),交换多维度用户画像,通过多对多匹配引擎发现附近"同频"的人,双击手环确认后即时断开连接。 ## 系统总览 ``` ┌─────────────────────────────────────────────────────────────────┐ │ 多对多 BLE 网络拓扑 │ │ │ │ ┌──────────┐ ┌──────────┐ ┌──────────┐ │ │ │ 手环 A │ │ 手环 B │ │ 手环 C │ │ │ │(GATT Client)│ │(GATT Client)│ │(GATT Client)│ │ │ └────┬─────┘ └────┬─────┘ └────┬─────┘ │ │ │ BLE Scan │ │ │ │ ▼ ▼ ▼ │ │ ┌──────────┐ ┌──────────┐ ┌──────────┐ │ │ │ 手机 1 │ │ 手机 2 │ │ 手机 3 │ │ │ │(GATT Server)│ │(GATT Server)│ │(GATT Server)│ │ │ └──────────┘ └──────────┘ └──────────┘ │ └─────────────────────────────────────────────────────────────────┘ ``` | 设备 | BLE 角色 | 职责 | |------|----------|------| | 手环 (Xiaomi Watch S5) | GATT Client | 扫描、连接、读写特征值、振动反馈 | | 手机 (Android) | GATT Server | 广播、接受连接、聚合数据、匹配计算 | ## 核心架构原则 - **边缘计算优先**:HRV 原始数据仅在手环端处理,匹配逻辑在手机端完成,数据绝不上云 - **隐私保护模式**:隐私 ON 所有数据仅存手机本地 (SQLite);OFF 可选云端同步 (Node.js + MySQL) - **多对多拓扑**:一个手机可同时服务多个手环,匹配引擎聚合多手环数据 ## 匹配流程 ``` 【阶段1:近场匹配】 手环扫描 → 连接信号最强的手机 → 依次写入 3 个 Profile (work/sport/social) → 手机匹配引擎任一 Profile 匹配即成功 → Notify 手环振动 → 用户双击确认 → 记录状态 → 立即断开 【阶段2:加好友】 隐私模式: 手机通知手环扫描连接对方手机 (3次×10s) → 读取确认状态 → 双方都确认=成功 非隐私模式: 手机上传匹配记录到云端 → 双方确认后推送联系方式 ``` ## 存储策略 | 模式 | 存储位置 | 适用场景 | |------|----------|----------| | 隐私保护 OFF (默认) | 云端 Node.js + MySQL | 需要多设备同步 | | 隐私保护 ON | 手机本地 SQLite 加密 | 数据不出手机 | 无论哪种模式,HRV 原始数据永远不出手环/不上云。 ## 技术栈 | 层级 | 技术 | 说明 | |------|------|------| | 手机 UI | Flutter/Dart | 跨平台 UI 框架 | | 手机 BLE | Android Native (Kotlin) | GATT Server 实现 | | 手机匹配 | Dart | 本地匹配引擎 | | 手机存储 | SharedPreferences / SQLite | 本地存储 | | 手环 UI | Xiaomi Vela JS | 手表应用框架 | | 手环 BLE | Xiaomi Vela BLE API | GATT Client | | 手环传感 | Vela Sensor API | HRV/运动采集 | | 云端 (可选) | Node.js + MySQL | 多设备同步 | ## 性能约束 | 指标 | 目标值 | |------|--------| | BLE 扫描延迟 | < 2s | | 连接建立 | < 3s | | 匹配计算 | < 500ms | | 振动响应 | < 200ms | | 并发连接 | 8 (单手机最大) | --- # BLE 协议规范 ## GATT 服务定义 Base UUID: `0000XXXX-0000-1000-8000-00805f9b34fb` | Characteristic | UUID | Properties | 说明 | |----------------|------|------------|------| | Profile | `0000FF02-...` | Read, Write | 用户画像读写 (支持多 Profile) | | Match Result | `0000FF03-...` | Read, Notify | 匹配结果通知 | | Control | `0000FF05-...` | Write, Indicate | 控制指令 | | Status | `0000FF06-...` | Read, Notify | 手机状态上报 | | Band Status | `0000FF07-...` | Read, Notify | 手环确认状态读取 | ## Profile 数据格式 (0xFF02) 手环写入自身画像,支持多 Profile,手环依次写入多个。 ``` Offset Length Field Description 0 1 Message Type 固定值 0x01 (PROFILE_WRITE) 1 1 Profile Version 协议版本 (当前 0x01) 2 1 Profile Index Profile 序号 (0=work, 1=social, 2=sport) 3 1 Flags Bit 0: Has HRV / Bit 1: Has Motion / Bit 2: Has Tags / Bit 3: Is Active 4 2 HRV Score HRV 压力分数 (0-100, 大端) 6 1 Motion Type 0x00=None / 0x01=Walking / 0x02=Running / 0x03=Cycling / 0x04=Swimming / 0x05=Yoga 7 1 Tag Count 标签数量 N 8 N×2 Tags 兴趣标签 ID 列表 (每标签 2 字节, 大端) 8+N×2 4 Timestamp 设备时间戳 (Unix 秒, 大端) 12+N×2 16 Band UUID 手环应用层 UUID (16字节) ``` ## Match Result 数据格式 (0xFF03) ``` Offset Length Field Description 0 1 Message Type 固定值 0x02 (MATCH_RESULT) 1 1 Match Type 0x01=HRV 同频 / 0x02=运动偶遇 / 0x03=标签同频 2 1 Match Score 匹配度 (0-100) 3 1 Matched Profile 命中的 Profile (0/1/2) 4 16 Partner UUID 对端手环应用层 UUID 20 1 Partner Motion 对端运动类型 21 1 Partner HRV 对端 HRV 等级 (0-3) 22 4 Expiry 匹配有效期 (Unix 秒) ``` ## Control 指令 (0xFF05) **手环 → 手机:** | Cmd ID | Description | |--------|-------------| | 0x10 | Request Match | | 0x11 | Confirm Match | | 0x12 | Reject Match | | 0x13 | Send Vibrate | | 0x20 | Heartbeat | | 0x30 | Disconnect Request | **手机 → 手环:** | Cmd ID | Description | |--------|-------------| | 0x90 | Match Confirmed | | 0x91 | Match Cancelled | | 0x92 | Vibrate Pattern (2 bytes) | | 0xA0 | Heartbeat ACK | | 0xB0 | Disconnect ACK | ## Band Status (0xFF07) 手环确认状态读取,用于加好友时验证双方确认状态。 ``` 读取响应 (手环 → 手机): Offset Length Field Description 0 1 Message Type 固定值 0x06 (BAND_STATUS) 1 1 My User ID Len 我的用户ID长度 (通常为16) 2 16 My User ID 我的应用层 UUID 18 1 Match Count 待确认匹配数 N 19 N×17 Pending Matches 每个匹配: ├── 16B: Partner UUID └── 1B: Flags (Bit 0: My Confirmed / Bit 1: Partner Confirmed) 写入请求 (手环 → 手机) - 确认状态更新: 0 1 Message Type 固定值 0x16 (CONFIRM_UPDATE) 1 16 Partner UUID 对方手环 UUID 17 1 Confirmed 0x01=已确认 / 0x00=取消 ``` ## 匹配算法 ### HRV 同频 ```dart double calculateHrvCoupling(int hrvA, int hrvB) { final diff = (hrvA - hrvB).abs(); if (diff <= 10) return 100 - (diff * 2); // 高匹配 (80-100) if (diff <= 20) return 80 - ((diff - 10) * 3); // 中匹配 (50-79) if (diff <= 30) return 50 - ((diff - 20) * 3); // 低匹配 (20-49) return (20 - (diff - 30) * 2).clamp(0, 20); } ``` ### 标签匹配 (Jaccard) ```dart double calculateTagMatch(List tagsA, List tagsB) { final setA = tagsA.toSet(); final setB = tagsB.toSet(); return (setA.intersection(setB).length / setA.union(setB).length * 100); } ``` ### 综合匹配 权重: HRV 40% + 标签 30% + 运动 30%。阈值 >= 50 分即匹配成功。 ## 安全规范 - BLE 配对: LE Secure Connections (LESC), Just Works - 设备使用随机地址,应用层 UUID 标识,不暴露 MAC - 用户匿名 ID 为 SHA-256 哈希,定期轮换 - 仅交换脱敏后的匹配信号 --- # 手机端 GATT Server 设计 ## 分层架构 ``` ┌─────────────────────────────────────────┐ │ Flutter UI Layer │ │ Home / Profile / Friends / Settings │ ├─────────────────────────────────────────┤ │ Platform Channel Layer │ │ MethodChannel / EventChannel │ ├─────────────────────────────────────────┤ │ Android Native Layer │ │ GATT Server + Advertiser + MatchEngine │ ├─────────────────────────────────────────┤ │ Android BLE Stack │ └─────────────────────────────────────────┘ ``` ## 模块职责 | 模块 | 文件 | 职责 | |------|------|------| | GattServerManager | `android/.../ble/GattServerManager.kt` | GATT Server 生命周期 | | AdvertiserManager | `android/.../ble/AdvertiserManager.kt` | BLE 广播 | | MatchEngine | `android/.../matching/MatchEngine.kt` | 匹配算法 | | ProfileStore | `android/.../store/ProfileStore.kt` | 用户画像存储 | | BlePlugin | `android/.../plugin/BlePlugin.kt` | Platform Channel 桥接 | ## 初始化流程 ``` App 启动 ├── 1. 检查蓝牙权限 (BLUETOOTH_SCAN / ADVERTISE / CONNECT) ├── 2. 获取 BluetoothManager ├── 3. 打开 GATT Server (openGattServer) ├── 4. 添加 EmoLink Service ├── 5. 开始 BLE 广播 (startAdvertising) └── 6. 通知 Flutter 层 EventChannel({"status": "ready"}) ``` ## 广播参数 ```kotlin val settings = AdvertiseSettings.Builder() .setAdvertiseMode(AdvertiseSettings.ADVERTISE_MODE_LOW_LATENCY) .setTxPowerLevel(AdvertiseSettings.ADVERTISE_TX_POWER_HIGH) .setConnectable(true) .setTimeout(0) .build() val data = AdvertiseData.Builder() .setIncludeDeviceName(false) .setIncludeTxPowerLevel(false) .addServiceUuid(ParcelUuid(UUID_EMOLINK_SERVICE)) .addManufacturerData(0x0157, buildManufacturerData()) // 小米厂商 ID .build() ``` 广播数据格式: ``` Byte 0-1: Service UUID (0xFF01) Byte 2: 协议版本 (0x01) Byte 3: 设备类型 (0x01=手机) Byte 4: 状态标志 (Bit 0: 可接受连接 / Bit 1: 有匹配结果) Byte 5-8: 设备匿名 ID (哈希) ``` ## MatchEngine ```kotlin class MatchEngine { private val matchHistory = mutableMapOf() // 5分钟去重 private val bandProfiles = mutableMapOf>() fun onProfileUpdate(updated: BandProfile, all: Map): List { // 存储该手环的 Profile // 与其他手环的所有 Profile 比对 // 任一 Profile 匹配上就算成功 // 整体分数 = HRV*0.4 + 标签*0.3 + 运动*0.3 >= 50 } } ``` ## Flutter Platform Channel ```dart class BleBridge { static const _channel = MethodChannel('com.emolink/ble'); static const _eventChannel = EventChannel('com.emolink/ble/events'); static Future initGattServer() async { ... } static Future startAdvertising() async { ... } static Future stopAdvertising() async { ... } static Future notifyBand(String deviceId, String charUuid, List data) async { ... } static Future> getConnectedDevices() async { ... } static Stream get events { ... } } ``` ## 错误处理 | 场景 | 处理策略 | |------|----------| | GATT Server 打开失败 | 重试 3 次,间隔 2s | | 广播启动失败 | 降级为低功耗模式 | | 设备连接数超限 | 拒绝新连接 | | 通知发送失败 | 缓存队列,延迟重发 | --- # 手环端 GATT Client 设计 ## 目标设备 - **Xiaomi Watch S5** (唯一支持设备) - Xiaomi Vela OS + Vela JS 应用框架 ## 技术约束 - Vela JS 仅支持 GATT Client 角色 - BLE API 仅支持 `createScanner()` 和 `createGattClientDevice()` - **不支持后台运行**,应用需在前台 ## 手环职责 1. 扫描附近手机 2. 连接信号最强的手机 3. 依次写入 3 个 Profile (work/sport/social) 4. 接收匹配通知 → 振动提醒 5. 用户双击确认 → 记录状态 → 立即断开 6. 下次连接时上报确认状态 **不在手环上做的:** 聊天、加好友、查看详细 Profile → 全部在手机 App ## 模块划分 ``` band/ ├── app.js # 应用入口 ├── config.js # 配置常量 ├── ble/ │ ├── scanner.js # BLE 扫描 │ ├── client.js # GATT 客户端 │ └── protocol.js # 协议编解码 ├── sensor/ │ ├── hrv.js # HRV 数据采集 │ └── motion.js # 运动检测 ├── feedback/ │ ├── vibration.js # 振动反馈 │ └── display.js # 显示反馈 └── ui/ ├── home.js # 主界面 └── match.js # 匹配结果界面 ``` ## BLE 扫描 ```javascript import bluetoothBLE from '@system.bluetooth.ble' const scanner = bluetoothBLE.createScanner() scanner.startBLEScan({ filters: [{ serviceUuid: '0000FF01-0000-1000-8000-00805f9b34fb' }], options: { dutyMode: 0 } // SCAN_MODE_LOW_POWER }) scanner.subscribeBLEDeviceFind({ callback: (results) => { for (const device of results) { // 过滤: Service UUID + RSSI >= -80 + 小米厂商 ID 0x0157 + 设备类型 0x01 onDeviceFound(device) } } }) ``` ## GATT Client ```javascript import bluetoothBLE from '@system.bluetooth.ble' const UUID_EMOLINK_SERVICE = '0000FF01-0000-1000-8000-00805f9b34fb' const UUID_PROFILE = '0000FF02-0000-1000-8000-00805f9b34fb' const UUID_MATCH_RESULT = '0000FF03-0000-1000-8000-00805f9b34fb' const UUID_CONTROL = '0000FF05-0000-1000-8000-00805f9b34fb' const client = bluetoothBLE.createGattClientDevice(deviceId, addressType) // 连接状态监听 client.onBLEConnectionStateChange = (state) => { ... } // 特征值变化 (Notify) client.onBLECharacteristicChange = (characteristic) => { ... } await client.connect() const services = await client.getServices() await client.setNotifyCharacteristicChanged({ characteristic, enable: true }) await client.writeCharacteristicValue({ characteristic }) await client.disconnect() ``` ## 协议编解码 ```javascript // Profile 编码 (work/sport/social) function encodeProfile(profile, profileIndex, bandUuid) { // 0x01 + version + index + flags + hrvScore(2B) + motionType + tagCount + tags(N×2B) + timestamp(4B) + bandUuid(16B) } // 匹配结果解码 function decodeMatchResult(data) { return { matchType, matchScore, matchedProfile, partnerUuid, partnerMotion, partnerHrvGrade, expiry } } ``` ## 振动反馈 ```javascript const VibrationPattern = { MATCH_DISCOVER: [200, 100, 200], // 心跳式双振 MATCH_CONFIRM: [500], // 长振一次 MESSAGE_NOTIFY: [100, 50, 100, 50, 100], MATCH_REJECT: [100, 100, 100] // 三连等距振 } ``` ## 限制与注意事项 - **后台运行限制**: Vela JS 应用不支持后台运行,需保持前台 - **并发连接**: 手环端同时只能连接一个 GATT Server - **功耗控制**: 扫描使用 `SCAN_MODE_LOW_POWER`,连接后减少频率,空闲主动断开 --- # Android 权限配置 ## 权限列表 | 权限 | API Level | 用途 | |------|-----------|------| | `BLUETOOTH_SCAN` | 31+ | BLE 扫描 | | `BLUETOOTH_ADVERTISE` | 31+ | BLE 广播 | | `BLUETOOTH_CONNECT` | 31+ | BLE 连接 | | `ACCESS_FINE_LOCATION` | ≤30 | BLE 扫描 (旧版) | ## AndroidManifest.xml ```xml ``` ## 动态权限请求 ```dart // Flutter 侧 (permission_handler) static Future requestAll() async { final statuses = await [ Permission.bluetoothScan, Permission.bluetoothAdvertise, Permission.bluetoothConnect, ].request(); return statuses.values.every((s) => s.isGranted); } ``` ```kotlin // Kotlin 侧 class BlePermissionHandler(private val activity: Activity) { companion object { val ANDROID_12_PERMISSIONS = arrayOf( BLUETOOTH_SCAN, BLUETOOTH_ADVERTISE, BLUETOOTH_CONNECT ) val LEGACY_PERMISSIONS = arrayOf(ACCESS_FINE_LOCATION) } fun hasRequiredPermissions(): Boolean { return if (Build.VERSION.SDK_INT >= S) { ANDROID_12_PERMISSIONS.all { check(it) == GRANTED } } else { LEGACY_PERMISSIONS.all { check(it) == GRANTED } } } } ``` ## 前台服务 ```kotlin class GattServerService : Service() { override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { startForeground(NOTIFICATION_ID, buildNotification()) return START_STICKY } } ``` ## FAQ - **Q: 为什么需要 BLUETOOTH_SCAN 而不是 BLUETOOTH?** A: Android 12 将旧权限拆分为三个细粒度权限。 - **Q: 为什么需要定位权限?** A: Android 11 及以下,BLE 扫描被视为定位操作。Android 12+ 声明 `neverForLocation` 后不需要。 - **Q: 用户拒绝权限后怎么办?** A: 引导到系统设置页手动开启 (`openAppSettings()`)。 --- # 参考资料 ## Xiaomi Vela JS 开发框架 手环/手表端 (Xiaomi Watch S5) 使用 Vela JS 应用框架开发,以下为官方 API 参考: - **通用语法与 JS 接口**: https://iot.mi.com/vela/quickapp/zh/features/grammar.html - 接口声明、导入模块、同步/异步 API 调用、通用错误码 - 蓝牙接口: `@system.bluetooth.ble` - 振动接口: `@system.vibrator` - 传感器接口: `@system.sensor` - 数据存储: `@system.storage` - **UI 组件参考**: https://iot.mi.com/vela/quickapp/zh/components/ - 容器组件: div, list, scroll, stack, swiper - 基础组件: text, image, progress, chart - 表单组件: input, picker, switch, slider - 通用样式、颜色配置、动画 ## 项目文档 | 文档 | 说明 | |------|------| | `docs/architecture.md` | 系统架构设计 | | `docs/ble-protocol.md` | BLE 协议规范 | | `docs/phone-gatt-server.md` | 手机端 GATT Server 详细设计 | | `docs/band-gatt-client.md` | 手环端 GATT Client 详细设计 | | `docs/android-permissions.md` | Android 权限配置指南 |