software copyright

This commit is contained in:
jiahong
2026-03-22 15:24:40 +08:00
parent e303bb868a
commit 60f336e345
155 changed files with 127262 additions and 0 deletions
@@ -0,0 +1,491 @@
// 自然写互动课堂平板端应用软件 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();
}
}