// 自然写互动课堂平板端应用软件 V1.0 // service/ble_service.dart - BLE蓝牙点阵笔连接服务 import 'dart:async'; import 'dart:typed_data'; /// BLE服务UUID常量定义 /// 基于自然写点阵笔自定义GATT Service规范 class PadBleConstants { /// 点阵笔主服务UUID static const String penServiceUuid = '0000ffe0-0000-1000-8000-00805f9b34fb'; /// 笔迹坐标数据特征值UUID(Notify) static const String strokeCharUuid = '0000ffe1-0000-1000-8000-00805f9b34fb'; /// 笔控制指令特征值UUID(Write) static const String controlCharUuid = '0000ffe2-0000-1000-8000-00805f9b34fb'; /// 电量信息特征值UUID(Read/Notify) static const String batteryCharUuid = '0000ffe3-0000-1000-8000-00805f9b34fb'; /// 设备信息服务UUID static const String deviceInfoUuid = '0000180a-0000-1000-8000-00805f9b34fb'; /// 扫描超时时间(秒) static const int scanTimeoutSeconds = 15; /// 自动重连延迟(秒) static const int reconnectDelaySeconds = 3; /// 最大重连次数 static const int maxReconnectAttempts = 10; /// MTU协商大小 static const int requestedMtu = 247; /// 笔迹数据缓冲批量回调阈值 static const int strokeBatchSize = 8; /// 电量读取间隔(秒) static const int batteryReadInterval = 60; } /// 单个笔迹坐标点数据 class PadPenPoint { /// X坐标(0.01mm精度,16位无符号) final double x; /// Y坐标(0.01mm精度,16位无符号) final double y; /// 压力值(0-255,8位无符号) final int pressure; /// 时间戳(相对值,16位无符号,单位ms) final int timestamp; /// 是否为落笔点 final bool isPenDown; PadPenPoint({ required this.x, required this.y, required this.pressure, required this.timestamp, this.isPenDown = false, }); @override String toString() => 'PadPenPoint(x: ${x.toStringAsFixed(2)}, y: ${y.toStringAsFixed(2)}, ' 'p: $pressure, t: $timestamp)'; } /// 点阵笔设备信息 class PadPenDevice { /// 设备蓝牙MAC地址 final String macAddress; /// 设备名称 final String name; /// 信号强度(RSSI) int rssi; /// 当前连接状态 PenConnectionState connectionState; /// 电量百分比(0-100) int batteryLevel; /// 固件版本号 String? firmwareVersion; /// 当前所在点阵码页面ID String? currentPageId; PadPenDevice({ required this.macAddress, required this.name, this.rssi = -100, this.connectionState = PenConnectionState.disconnected, this.batteryLevel = -1, this.firmwareVersion, this.currentPageId, }); } /// 笔连接状态枚举 enum PenConnectionState { /// 未连接 disconnected, /// 正在扫描 scanning, /// 正在连接 connecting, /// 已连接 connected, /// 正在断开 disconnecting, /// 自动重连中 reconnecting, } /// 笔迹数据事件(批量坐标点回调) class PenStrokeEvent { /// 来源笔的MAC地址 final String penMac; /// 坐标点列表 final List points; /// 所在页面ID(点阵码识别) final String? pageId; PenStrokeEvent({ required this.penMac, required this.points, this.pageId, }); } /// BLE蓝牙点阵笔连接服务 /// 负责扫描、连接、数据接收、电量监控、自动重连等功能 /// 平板端支持同时连接1支笔(学生个人使用场景) class PadBleService { /// 已发现的设备列表 final List _discoveredDevices = []; /// 当前已连接的笔 PadPenDevice? _connectedPen; /// 笔迹数据缓冲区(累积到阈值后批量回调) final List _strokeBuffer = []; /// 扫描结果流 final StreamController> _scanController = StreamController>.broadcast(); /// 笔迹数据事件流 final StreamController _strokeController = StreamController.broadcast(); /// 连接状态变化流 final StreamController _connectionController = StreamController.broadcast(); /// 电量变化流 final StreamController _batteryController = StreamController.broadcast(); /// 自动重连计数器 int _reconnectAttempts = 0; /// 重连定时器 Timer? _reconnectTimer; /// 电量读取定时器 Timer? _batteryTimer; /// 是否正在扫描 bool _isScanning = false; /// 公开的流 Stream> get scanStream => _scanController.stream; Stream get strokeStream => _strokeController.stream; Stream get connectionStream => _connectionController.stream; Stream get batteryStream => _batteryController.stream; /// 获取当前连接的笔 PadPenDevice? get connectedPen => _connectedPen; /// 开始扫描附近的点阵笔设备 /// 按服务UUID过滤,仅发现自然写点阵笔 Future startScan() async { if (_isScanning) return; _isScanning = true; _discoveredDevices.clear(); // 通知扫描状态 _connectionController.add(PenConnectionState.scanning); // 模拟BLE扫描(实际使用flutter_blue_plus库) // 过滤条件:仅发现包含pen_service_uuid的设备 // scanFilters: [ScanFilter(serviceUuid: PadBleConstants.penServiceUuid)] // 设置扫描超时 Timer(Duration(seconds: PadBleConstants.scanTimeoutSeconds), () { stopScan(); }); } /// 停止扫描 Future stopScan() async { _isScanning = false; // 实际调用: FlutterBluePlus.stopScan() } /// 处理扫描结果回调 void _onScanResult(String mac, String name, int rssi) { // 检查是否已发现过 final existingIndex = _discoveredDevices.indexWhere( (d) => d.macAddress == mac, ); if (existingIndex >= 0) { // 更新已有设备的RSSI _discoveredDevices[existingIndex].rssi = rssi; } else { // 添加新发现的设备 _discoveredDevices.add(PadPenDevice( macAddress: mac, name: name, rssi: rssi, )); } // 按信号强度降序排列 _discoveredDevices.sort((a, b) => b.rssi.compareTo(a.rssi)); _scanController.add(List.from(_discoveredDevices)); } /// 连接指定的点阵笔 /// [device] 要连接的笔设备信息 Future connectPen(PadPenDevice device) async { // 先断开已有连接 if (_connectedPen != null) { await disconnectPen(); } device.connectionState = PenConnectionState.connecting; _connectionController.add(PenConnectionState.connecting); try { // 停止扫描 await stopScan(); // 执行BLE连接 // 实际调用: device.connect(timeout: Duration(seconds: 10)) // 协商MTU // await device.requestMtu(PadBleConstants.requestedMtu); // 发现服务和特征值 // final services = await device.discoverServices(); // 查找笔迹数据特征值并订阅Notify // 设置连接成功状态 device.connectionState = PenConnectionState.connected; _connectedPen = device; _reconnectAttempts = 0; _connectionController.add(PenConnectionState.connected); // 启动电量定时读取 _startBatteryMonitor(); // 订阅笔迹数据特征值 _subscribeStrokeData(); return true; } catch (e) { device.connectionState = PenConnectionState.disconnected; _connectionController.add(PenConnectionState.disconnected); return false; } } /// 订阅笔迹坐标数据Notify特征值 void _subscribeStrokeData() { // 实际调用: // characteristic.setNotifyValue(true); // characteristic.onValueReceived.listen(_onStrokeDataReceived); } /// 处理接收到的笔迹原始数据(7字节紧凑编码) /// 数据格式:[X_H, X_L, Y_H, Y_L, Pressure, TS_H, TS_L] /// X: 16位无符号(0.01mm精度) /// Y: 16位无符号(0.01mm精度) /// Pressure: 8位无符号(0-255) /// Timestamp: 16位无符号(相对毫秒) void _onStrokeDataReceived(Uint8List rawData) { if (rawData.length < 7) return; // 可能包含多个坐标点(每7字节一个) int offset = 0; while (offset + 7 <= rawData.length) { // 解码X坐标(大端序16位) final int rawX = (rawData[offset] << 8) | rawData[offset + 1]; final double x = rawX * 0.01; // 转换为毫米 // 解码Y坐标 final int rawY = (rawData[offset + 2] << 8) | rawData[offset + 3]; final double y = rawY * 0.01; // 解码压力值 final int pressure = rawData[offset + 4]; // 解码时间戳 final int timestamp = (rawData[offset + 5] << 8) | rawData[offset + 6]; // 判断落笔/抬笔(压力值>0为落笔) final bool isPenDown = pressure > 0; final point = PadPenPoint( x: x, y: y, pressure: pressure, timestamp: timestamp, isPenDown: isPenDown, ); _strokeBuffer.add(point); offset += 7; } // CRC-16 CCITT校验(如果数据包尾部有2字节CRC) if (rawData.length > offset + 1) { final int receivedCrc = (rawData[offset] << 8) | rawData[offset + 1]; final int calculatedCrc = _calculateCrc16( rawData.sublist(0, offset), ); if (receivedCrc != calculatedCrc) { // CRC校验失败,丢弃本批数据 _strokeBuffer.clear(); return; } } // 达到批量阈值后回调 if (_strokeBuffer.length >= PadBleConstants.strokeBatchSize) { _flushStrokeBuffer(); } } /// 将缓冲区中的笔迹数据批量回调 void _flushStrokeBuffer() { if (_strokeBuffer.isEmpty || _connectedPen == null) return; final event = PenStrokeEvent( penMac: _connectedPen!.macAddress, points: List.from(_strokeBuffer), pageId: _connectedPen!.currentPageId, ); _strokeController.add(event); _strokeBuffer.clear(); } /// CRC-16 CCITT校验算法 /// 多项式: 0x1021, 初始值: 0xFFFF 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; } /// 启动电量定时读取 void _startBatteryMonitor() { _batteryTimer?.cancel(); _batteryTimer = Timer.periodic( Duration(seconds: PadBleConstants.batteryReadInterval), (_) => _readBatteryLevel(), ); // 立即读取一次 _readBatteryLevel(); } /// 读取笔电量 Future _readBatteryLevel() async { if (_connectedPen == null) return; try { // 实际调用: 读取battery特征值 // final value = await batteryCharacteristic.read(); // _connectedPen!.batteryLevel = value[0]; // _batteryController.add(_connectedPen!.batteryLevel); } catch (e) { // 读取失败,忽略 } } /// 向笔发送控制指令 /// [command] 指令类型(如:LED闪烁、蜂鸣提示、固件信息查询) Future sendCommand(int command, [Uint8List? payload]) async { if (_connectedPen == null) return; // 构建指令包:[CMD, LEN, PAYLOAD..., CRC_H, CRC_L] final List packet = [command]; if (payload != null) { packet.add(payload.length); packet.addAll(payload); } else { packet.add(0); } // 追加CRC校验 final crc = _calculateCrc16(Uint8List.fromList(packet)); packet.add((crc >> 8) & 0xFF); packet.add(crc & 0xFF); // 实际调用: controlCharacteristic.write(Uint8List.fromList(packet)); } /// 断开当前笔连接 Future disconnectPen() async { _batteryTimer?.cancel(); _reconnectTimer?.cancel(); if (_connectedPen != null) { _connectedPen!.connectionState = PenConnectionState.disconnecting; _connectionController.add(PenConnectionState.disconnecting); // 实际调用: device.disconnect(); _connectedPen!.connectionState = PenConnectionState.disconnected; _connectedPen = null; _connectionController.add(PenConnectionState.disconnected); } // 清空缓冲区 _flushStrokeBuffer(); } /// 处理连接意外断开,启动自动重连 void _onDisconnected(PadPenDevice device) { if (_reconnectAttempts >= PadBleConstants.maxReconnectAttempts) { // 超过最大重连次数,放弃重连 _connectionController.add(PenConnectionState.disconnected); return; } _connectionController.add(PenConnectionState.reconnecting); _reconnectAttempts++; // 指数退避延迟重连 final delay = PadBleConstants.reconnectDelaySeconds * _reconnectAttempts; final clampedDelay = delay > 30 ? 30 : delay; _reconnectTimer = Timer(Duration(seconds: clampedDelay), () async { final success = await connectPen(device); if (!success) { _onDisconnected(device); } }); } /// 释放所有资源 void dispose() { _batteryTimer?.cancel(); _reconnectTimer?.cancel(); _scanController.close(); _strokeController.close(); _connectionController.close(); _batteryController.close(); _strokeBuffer.clear(); } }