Files
2026-03-22 15:24:40 +08:00

553 lines
17 KiB
Dart
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/// 自然写互动课堂手机端应用软件 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<String, dynamic> toJson() => {
'x': x, 'y': y,
'pressure': pressure,
'timestamp': timestamp,
'pen_down': isPenDown,
};
}
/// 笔迹数据回调事件
class StrokeDataEvent {
final String deviceId;
final List<StrokePoint> points;
final int pageId;
StrokeDataEvent({
required this.deviceId,
required this.points,
required this.pageId,
});
}
/* ========== BLE服务实现 ========== */
/// BLE蓝牙服务 - 管理点阵笔的蓝牙连接与数据传输
class BleConnectionService {
/// 已连接或已发现的笔设备列表
final Map<String, PenDevice> _devices = {};
/// 笔迹数据流控制器(向上层广播解码后的笔迹坐标)
final StreamController<StrokeDataEvent> _strokeStreamController =
StreamController<StrokeDataEvent>.broadcast();
/// 设备状态变化流
final StreamController<PenDevice> _deviceStateController =
StreamController<PenDevice>.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<String, Timer> _reconnectTimers = {};
/// 电量查询计时器
Timer? _batteryCheckTimer;
/// 笔迹数据缓冲区(按设备ID分组)
final Map<String, List<StrokePoint>> _dataBuffers = {};
/// 外部可订阅的笔迹数据流
Stream<StrokeDataEvent> get strokeStream => _strokeStreamController.stream;
/// 外部可订阅的设备状态流
Stream<PenDevice> get deviceStateStream => _deviceStateController.stream;
/// 获取当前已连接设备数量
int get connectedCount =>
_devices.values.where((d) => d.state == PenConnectionState.connected).length;
/// 获取所有已发现设备列表
List<PenDevice> get discoveredDevices => _devices.values.toList();
/// 开始BLE扫描(发现周围的自然写点阵笔设备)
/// 仅扫描包含自然写笔服务UUID的设备,过滤无关BLE设备
Future<void> 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<String> 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<bool> 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<void> _discoverServices(String deviceId) async {
// 实际调用: device.discoverServices()
// 验证是否包含笔迹数据服务UUID
print('[BLE] 服务发现完成: $deviceId');
}
/// 订阅笔迹数据Notify特征值
/// 设置MTU为247字节以支持最大数据包
Future<void> _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 = <StrokePoint>[];
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<StrokePoint>.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<void> _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<void> _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<void> 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<void> syncOfflineData(String deviceId) async {
await sendCommand(deviceId, PenCommand.cmdSyncOffline);
print('[BLE] 已请求同步离线数据: $deviceId');
}
/// 断开指定设备
Future<void> 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<bool> _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服务已销毁');
}
}