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

492 lines
13 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
// 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';
/// 笔迹坐标数据特征值UUIDNotify)
static const String strokeCharUuid = '0000ffe1-0000-1000-8000-00805f9b34fb';
/// 笔控制指令特征值UUIDWrite
static const String controlCharUuid = '0000ffe2-0000-1000-8000-00805f9b34fb';
/// 电量信息特征值UUIDRead/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<PadPenPoint> points;
/// 所在页面ID(点阵码识别)
final String? pageId;
PenStrokeEvent({
required this.penMac,
required this.points,
this.pageId,
});
}
/// BLE蓝牙点阵笔连接服务
/// 负责扫描、连接、数据接收、电量监控、自动重连等功能
/// 平板端支持同时连接1支笔(学生个人使用场景)
class PadBleService {
/// 已发现的设备列表
final List<PadPenDevice> _discoveredDevices = [];
/// 当前已连接的笔
PadPenDevice? _connectedPen;
/// 笔迹数据缓冲区(累积到阈值后批量回调)
final List<PadPenPoint> _strokeBuffer = [];
/// 扫描结果流
final StreamController<List<PadPenDevice>> _scanController =
StreamController<List<PadPenDevice>>.broadcast();
/// 笔迹数据事件流
final StreamController<PenStrokeEvent> _strokeController =
StreamController<PenStrokeEvent>.broadcast();
/// 连接状态变化流
final StreamController<PenConnectionState> _connectionController =
StreamController<PenConnectionState>.broadcast();
/// 电量变化流
final StreamController<int> _batteryController =
StreamController<int>.broadcast();
/// 自动重连计数器
int _reconnectAttempts = 0;
/// 重连定时器
Timer? _reconnectTimer;
/// 电量读取定时器
Timer? _batteryTimer;
/// 是否正在扫描
bool _isScanning = false;
/// 公开的流
Stream<List<PadPenDevice>> get scanStream => _scanController.stream;
Stream<PenStrokeEvent> get strokeStream => _strokeController.stream;
Stream<PenConnectionState> get connectionStream =>
_connectionController.stream;
Stream<int> get batteryStream => _batteryController.stream;
/// 获取当前连接的笔
PadPenDevice? get connectedPen => _connectedPen;
/// 开始扫描附近的点阵笔设备
/// 按服务UUID过滤,仅发现自然写点阵笔
Future<void> 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<void> 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<bool> 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<void> _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<void> sendCommand(int command, [Uint8List? payload]) async {
if (_connectedPen == null) return;
// 构建指令包:[CMD, LEN, PAYLOAD..., CRC_H, CRC_L]
final List<int> 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<void> 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();
}
}