/// 自然写互动课堂手机端应用软件 V1.0 /// BLE蓝牙服务 - 教师端蓝牙连接点阵笔进行移动教学 /// /// 功能说明: /// 1. BLE设备扫描与发现(按自然写笔设备UUID过滤) /// 2. GATT连接与特征值订阅(实时接收笔迹坐标数据) /// 3. 7字节紧凑坐标数据解码(x:16bit, y:16bit, pressure:8bit, timestamp:16bit) /// 4. 多笔同时连接管理(教师端移动教学最多连接4支笔) /// 5. 自动重连与连接状态监控 /// 6. 设备电量读取与低电量告警 /// 7. 蓝牙权限检查与引导 /// 8. 笔迹数据缓冲与批量回调 import 'dart:async'; import 'dart:typed_data'; /* ========== BLE协议常量定义 ========== */ /// 自然写点阵笔BLE服务UUID class WritechBleUuids { /// 主服务UUID - 笔迹数据传输 static const String strokeServiceUuid = '6E400001-B5A3-F393-E0A9-E50E24DCCA9E'; /// 笔迹数据特征值UUID(Notify模式,笔到手机) static const String strokeDataCharUuid = '6E400003-B5A3-F393-E0A9-E50E24DCCA9E'; /// 命令写入特征值UUID(Write模式,手机到笔) static const String commandCharUuid = '6E400002-B5A3-F393-E0A9-E50E24DCCA9E'; /// 设备信息服务UUID(标准BLE Device Information Service) static const String deviceInfoServiceUuid = '0000180A-0000-1000-8000-00805F9B34FB'; /// 电池服务UUID(标准BLE Battery Service) static const String batteryServiceUuid = '0000180F-0000-1000-8000-00805F9B34FB'; /// 电池电量特征值UUID static const String batteryLevelCharUuid = '00002A19-0000-1000-8000-00805F9B34FB'; } /// BLE笔命令定义 class PenCommand { static const int cmdSetMode = 0x01; static const int cmdGetStatus = 0x02; static const int cmdSyncOffline = 0x03; static const int cmdSetName = 0x04; static const int cmdStartOta = 0x05; static const int cmdReset = 0xFF; } /* ========== 数据模型 ========== */ /// BLE笔设备信息 class PenDevice { final String deviceId; final String name; int rssi; int batteryLevel; String firmwareVersion; PenConnectionState state; DateTime? lastActiveTime; int offlineDataCount; PenDevice({ required this.deviceId, required this.name, this.rssi = -100, this.batteryLevel = -1, this.firmwareVersion = '', this.state = PenConnectionState.disconnected, this.lastActiveTime, this.offlineDataCount = 0, }); } /// 笔连接状态枚举 enum PenConnectionState { disconnected, connecting, connected, disconnecting, } /// 笔迹坐标点(从BLE数据解码后的结构化数据) class StrokePoint { final double x; final double y; final double pressure; final int timestamp; final bool isPenDown; const StrokePoint({ required this.x, required this.y, required this.pressure, required this.timestamp, required this.isPenDown, }); Map toJson() => { 'x': x, 'y': y, 'pressure': pressure, 'timestamp': timestamp, 'pen_down': isPenDown, }; } /// 笔迹数据回调事件 class StrokeDataEvent { final String deviceId; final List points; final int pageId; StrokeDataEvent({ required this.deviceId, required this.points, required this.pageId, }); } /* ========== BLE服务实现 ========== */ /// BLE蓝牙服务 - 管理点阵笔的蓝牙连接与数据传输 class BleConnectionService { /// 已连接或已发现的笔设备列表 final Map _devices = {}; /// 笔迹数据流控制器(向上层广播解码后的笔迹坐标) final StreamController _strokeStreamController = StreamController.broadcast(); /// 设备状态变化流 final StreamController _deviceStateController = StreamController.broadcast(); /// 扫描状态 bool _isScanning = false; /// 最大同时连接数(教师移动教学最多4支笔) static const int maxConnections = 4; /// 自动重连间隔(秒) static const int reconnectIntervalSec = 5; /// 数据缓冲区大小(累积到一定量后批量回调) static const int batchSize = 10; /// 设备活跃超时时间(毫秒) static const int activeTimeoutMs = 30000; /// 低电量告警阈值 static const int lowBatteryThreshold = 10; /// 重连计时器 final Map _reconnectTimers = {}; /// 电量查询计时器 Timer? _batteryCheckTimer; /// 笔迹数据缓冲区(按设备ID分组) final Map> _dataBuffers = {}; /// 外部可订阅的笔迹数据流 Stream get strokeStream => _strokeStreamController.stream; /// 外部可订阅的设备状态流 Stream get deviceStateStream => _deviceStateController.stream; /// 获取当前已连接设备数量 int get connectedCount => _devices.values.where((d) => d.state == PenConnectionState.connected).length; /// 获取所有已发现设备列表 List get discoveredDevices => _devices.values.toList(); /// 开始BLE扫描(发现周围的自然写点阵笔设备) /// 仅扫描包含自然写笔服务UUID的设备,过滤无关BLE设备 Future startScan({Duration timeout = const Duration(seconds: 10)}) async { if (_isScanning) { print('[BLE] 已在扫描中,忽略重复请求'); return; } // 检查蓝牙权限和状态 final hasPermission = await _checkBluetoothPermission(); if (!hasPermission) { print('[BLE] 蓝牙权限未授予,无法扫描'); return; } _isScanning = true; print('[BLE] 开始扫描自然写点阵笔设备...'); // 使用flutter_blue扫描指定服务UUID的设备 // 实际实现通过FlutterBluePlus.startScan() // 此处模拟扫描逻辑 Timer(timeout, () { stopScan(); }); } /// 停止BLE扫描 void stopScan() { if (!_isScanning) return; _isScanning = false; print('[BLE] 停止扫描'); } /// 处理扫描到的设备广播数据 /// 解析设备名称、信号强度、服务UUID void _onDeviceDiscovered(String deviceId, String name, int rssi, List serviceUuids) { // 仅处理包含自然写笔服务UUID的设备 if (!serviceUuids.contains(WritechBleUuids.strokeServiceUuid)) return; if (_devices.containsKey(deviceId)) { // 更新已知设备的RSSI _devices[deviceId]!.rssi = rssi; } else { // 发现新设备 final device = PenDevice( deviceId: deviceId, name: name.isNotEmpty ? name : '未知笔设备', rssi: rssi, ); _devices[deviceId] = device; print('[BLE] 发现新设备: $name (RSSI: $rssi)'); _deviceStateController.add(device); } } /// 连接指定的点阵笔设备 /// 建立GATT连接,发现服务,订阅笔迹数据特征值 Future connectDevice(String deviceId) async { final device = _devices[deviceId]; if (device == null) { print('[BLE] 未找到设备: $deviceId'); return false; } // 检查连接数限制 if (connectedCount >= maxConnections) { print('[BLE] 已达最大连接数限制 ($maxConnections)'); return false; } device.state = PenConnectionState.connecting; _deviceStateController.add(device); print('[BLE] 正在连接: ${device.name}'); try { // 步骤1: 建立BLE GATT连接 // 实际调用: FlutterBluePlus.connect(device, autoConnect: false) await Future.delayed(const Duration(milliseconds: 500)); // 模拟连接耗时 // 步骤2: 发现服务(查找笔迹数据服务和电池服务) await _discoverServices(deviceId); // 步骤3: 订阅笔迹数据Notify特征值 await _subscribeStrokeData(deviceId); // 步骤4: 读取初始电量 await _readBatteryLevel(deviceId); // 步骤5: 读取固件版本 await _readFirmwareVersion(deviceId); device.state = PenConnectionState.connected; device.lastActiveTime = DateTime.now(); _deviceStateController.add(device); // 初始化数据缓冲区 _dataBuffers[deviceId] = []; // 启动电量定时检查(每60秒读取一次电量) _startBatteryCheck(); print('[BLE] 连接成功: ${device.name}, 固件: ${device.firmwareVersion}, 电量: ${device.batteryLevel}%'); return true; } catch (e) { device.state = PenConnectionState.disconnected; _deviceStateController.add(device); print('[BLE] 连接失败: ${device.name}, 错误: $e'); // 设置自动重连计时器 _scheduleReconnect(deviceId); return false; } } /// 发现BLE服务列表 Future _discoverServices(String deviceId) async { // 实际调用: device.discoverServices() // 验证是否包含笔迹数据服务UUID print('[BLE] 服务发现完成: $deviceId'); } /// 订阅笔迹数据Notify特征值 /// 设置MTU为247字节以支持最大数据包 Future _subscribeStrokeData(String deviceId) async { // 步骤1: 请求MTU协商(247字节,支持每包最多34个坐标点) // 实际调用: device.requestMtu(247) // 步骤2: 启用Notify // 实际调用: characteristic.setNotifyValue(true) // 步骤3: 监听Notify数据流 // characteristic.onValueReceived.listen((data) => _onStrokeDataReceived(deviceId, data)) print('[BLE] 笔迹数据订阅成功: $deviceId'); } /// 处理接收到的BLE笔迹原始数据包 /// 每个数据包包含1-34个7字节坐标点 /// 7字节编码格式: [x_hi, x_lo, y_hi, y_lo, pressure, ts_hi, ts_lo] void _onStrokeDataReceived(String deviceId, Uint8List rawData) { final device = _devices[deviceId]; if (device == null) return; // 更新设备活跃时间 device.lastActiveTime = DateTime.now(); // 数据包最小长度: 3字节头 + 7字节坐标 = 10字节 if (rawData.length < 10) { print('[BLE] 数据包过短,丢弃: ${rawData.length}字节'); return; } // 解析数据包头部(3字节) final packetType = rawData[0]; // 包类型: 0x01=实时数据, 0x02=离线数据 final pageId = (rawData[1] << 8) | rawData[2]; // 点阵码页面ID final isPenDown = (packetType & 0x80) != 0; // 最高位标识落笔状态 // 验证CRC-16校验(数据包最后2字节) if (rawData.length > 5) { final payloadEnd = rawData.length - 2; final expectedCrc = (rawData[payloadEnd] << 8) | rawData[payloadEnd + 1]; final calculatedCrc = _calculateCrc16(rawData.sublist(0, payloadEnd)); if (expectedCrc != calculatedCrc) { print('[BLE] CRC校验失败,丢弃数据包'); return; } } // 解码坐标数据(从第3字节开始,每7字节一个坐标点) final points = []; final dataEnd = rawData.length - 2; // 排除末尾CRC for (int offset = 3; offset + 6 < dataEnd; offset += 7) { final point = _decodeStrokePoint(rawData, offset, isPenDown); points.add(point); } if (points.isEmpty) return; // 添加到缓冲区 final buffer = _dataBuffers[deviceId]; if (buffer != null) { buffer.addAll(points); // 缓冲区达到批量大小时回调 if (buffer.length >= batchSize) { final event = StrokeDataEvent( deviceId: deviceId, points: List.from(buffer), pageId: pageId, ); _strokeStreamController.add(event); buffer.clear(); } } } /// 解码单个7字节坐标点 /// 编码格式: x(16bit) + y(16bit) + pressure(8bit) + timestamp(16bit) StrokePoint _decodeStrokePoint(Uint8List data, int offset, bool isPenDown) { // X坐标(大端序,单位: 0.01mm,范围: 0-65535 即 0-655.35mm) final rawX = (data[offset] << 8) | data[offset + 1]; final x = rawX * 0.01; // Y坐标(同上) final rawY = (data[offset + 2] << 8) | data[offset + 3]; final y = rawY * 0.01; // 压力值(0-255,归一化到0.0-1.0) final rawPressure = data[offset + 4]; final pressure = rawPressure / 255.0; // 时间戳(毫秒增量,相对于笔迹起始) final timestamp = (data[offset + 5] << 8) | data[offset + 6]; return StrokePoint( x: x, y: y, pressure: pressure, timestamp: timestamp, isPenDown: isPenDown, ); } /// CRC-16 CCITT校验计算 int _calculateCrc16(Uint8List data) { int crc = 0xFFFF; for (int i = 0; i < data.length; i++) { crc ^= (data[i] << 8); for (int j = 0; j < 8; j++) { if ((crc & 0x8000) != 0) { crc = ((crc << 1) ^ 0x1021) & 0xFFFF; } else { crc = (crc << 1) & 0xFFFF; } } } return crc; } /// 读取设备电量 Future _readBatteryLevel(String deviceId) async { final device = _devices[deviceId]; if (device == null) return; // 实际调用: 读取Battery Service的Battery Level特征值 // device.batteryLevel = characteristic.value[0]; device.batteryLevel = 85; // 模拟值 // 低电量告警 if (device.batteryLevel > 0 && device.batteryLevel <= lowBatteryThreshold) { print('[BLE] 低电量告警: ${device.name} 电量 ${device.batteryLevel}%'); _deviceStateController.add(device); } } /// 读取固件版本号 Future _readFirmwareVersion(String deviceId) async { final device = _devices[deviceId]; if (device == null) return; // 读取Device Information Service的Firmware Revision特征值 device.firmwareVersion = '1.2.0'; } /// 启动电量定时检查 void _startBatteryCheck() { _batteryCheckTimer?.cancel(); _batteryCheckTimer = Timer.periodic(const Duration(seconds: 60), (_) { for (final entry in _devices.entries) { if (entry.value.state == PenConnectionState.connected) { _readBatteryLevel(entry.key); } } }); } /// 向笔设备发送命令 Future sendCommand(String deviceId, int command, {Uint8List? payload}) async { final device = _devices[deviceId]; if (device == null || device.state != PenConnectionState.connected) { print('[BLE] 设备未连接,无法发送命令'); return; } // 构造命令数据包: [cmd, payload_len, ...payload, crc_hi, crc_lo] final totalLen = 2 + (payload?.length ?? 0) + 2; final packet = Uint8List(totalLen); packet[0] = command; packet[1] = payload?.length ?? 0; if (payload != null) { packet.setRange(2, 2 + payload.length, payload); } final crc = _calculateCrc16(packet.sublist(0, totalLen - 2)); packet[totalLen - 2] = (crc >> 8) & 0xFF; packet[totalLen - 1] = crc & 0xFF; // 写入命令特征值 // 实际调用: commandCharacteristic.write(packet) print('[BLE] 发送命令: 0x${command.toRadixString(16)} -> ${device.name}'); } /// 请求同步离线数据(笔断线期间缓存的笔迹) Future syncOfflineData(String deviceId) async { await sendCommand(deviceId, PenCommand.cmdSyncOffline); print('[BLE] 已请求同步离线数据: $deviceId'); } /// 断开指定设备 Future disconnectDevice(String deviceId) async { final device = _devices[deviceId]; if (device == null) return; // 取消重连计时器 _reconnectTimers[deviceId]?.cancel(); _reconnectTimers.remove(deviceId); device.state = PenConnectionState.disconnecting; _deviceStateController.add(device); // 清空缓冲区中的残余数据 final buffer = _dataBuffers[deviceId]; if (buffer != null && buffer.isNotEmpty) { _strokeStreamController.add(StrokeDataEvent( deviceId: deviceId, points: List.from(buffer), pageId: 0, )); buffer.clear(); } // 断开GATT连接 // 实际调用: device.disconnect() device.state = PenConnectionState.disconnected; _deviceStateController.add(device); _dataBuffers.remove(deviceId); print('[BLE] 已断开设备: ${device.name}'); } /// 设置自动重连计时器 void _scheduleReconnect(String deviceId) { _reconnectTimers[deviceId]?.cancel(); _reconnectTimers[deviceId] = Timer( Duration(seconds: reconnectIntervalSec), () async { final device = _devices[deviceId]; if (device != null && device.state == PenConnectionState.disconnected) { print('[BLE] 尝试自动重连: ${device.name}'); await connectDevice(deviceId); } }, ); } /// 检查蓝牙权限(Android需要位置权限,iOS需要蓝牙使用描述) Future _checkBluetoothPermission() async { // Android: 检查 BLUETOOTH_SCAN, BLUETOOTH_CONNECT, ACCESS_FINE_LOCATION // iOS: 检查 CBManager authorization status return true; } /// 断开所有设备并释放资源 void dispose() { // 停止扫描 stopScan(); // 取消所有重连计时器 for (final timer in _reconnectTimers.values) { timer.cancel(); } _reconnectTimers.clear(); // 停止电量检查 _batteryCheckTimer?.cancel(); // 断开所有设备 for (final deviceId in _devices.keys.toList()) { disconnectDevice(deviceId); } // 关闭流控制器 _strokeStreamController.close(); _deviceStateController.close(); _devices.clear(); _dataBuffers.clear(); print('[BLE] BLE服务已销毁'); } }