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,673 @@
// 自然写互动课堂平板端应用软件 V1.0
// service/api_service.dart - 云平台API服务(Dio HTTP客户端)
import 'dart:async';
import 'dart:convert';
import 'dart:io';
import 'package:dio/dio.dart';
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
import 'package:crypto/crypto.dart';
/// 云平台API基础路径配置
class ApiConfig {
/// 生产环境API地址
static const String productionBaseUrl = 'https://api.writech.com/v1';
/// 测试环境API地址
static const String stagingBaseUrl = 'https://staging-api.writech.com/v1';
/// 连接超时时间(毫秒)
static const int connectTimeout = 15000;
/// 接收超时时间(毫秒)
static const int receiveTimeout = 30000;
/// Token刷新路径
static const String refreshTokenPath = '/auth/refresh';
/// 最大重试次数
static const int maxRetryCount = 3;
/// HMAC签名密钥标识
static const String hmacKeyId = 'writech-pad-v1';
}
/// API响应数据统一封装
class ApiResponse<T> {
final int code;
final String message;
final T? data;
final String? requestId;
ApiResponse({
required this.code,
required this.message,
this.data,
this.requestId,
});
/// 判断请求是否成功
bool get isSuccess => code == 0 || code == 200;
/// 从JSON解析响应
factory ApiResponse.fromJson(
Map<String, dynamic> json,
T Function(dynamic)? fromData,
) {
return ApiResponse(
code: json['code'] ?? -1,
message: json['message'] ?? '未知错误',
data: json['data'] != null && fromData != null
? fromData(json['data'])
: json['data'] as T?,
requestId: json['request_id'],
);
}
}
/// 离线请求队列项
class OfflineRequest {
final String id;
final String method;
final String path;
final Map<String, dynamic>? data;
final DateTime createdAt;
int retryCount;
OfflineRequest({
required this.id,
required this.method,
required this.path,
this.data,
required this.createdAt,
this.retryCount = 0,
});
/// 序列化为JSON用于本地持久化
Map<String, dynamic> toJson() => {
'id': id,
'method': method,
'path': path,
'data': data,
'created_at': createdAt.toIso8601String(),
'retry_count': retryCount,
};
/// 从JSON反序列化
factory OfflineRequest.fromJson(Map<String, dynamic> json) {
return OfflineRequest(
id: json['id'],
method: json['method'],
path: json['path'],
data: json['data'],
createdAt: DateTime.parse(json['created_at']),
retryCount: json['retry_count'] ?? 0,
);
}
}
/// 平板端云平台API服务
/// 负责与云平台的所有HTTP通信,包括:
/// - JWT双令牌认证与自动刷新
/// - HMAC-SHA256请求签名
/// - 离线请求队列暂存
/// - 学生简化登录(班级+姓名/学号)
class PadApiService {
late final Dio _dio;
final FlutterSecureStorage _secureStorage = const FlutterSecureStorage();
/// 当前访问令牌
String? _accessToken;
/// 刷新令牌
String? _refreshToken;
/// Token刷新锁,防止并发刷新
Completer<bool>? _refreshCompleter;
/// 离线请求队列
final List<OfflineRequest> _offlineQueue = [];
/// 网络状态标志
bool _isOnline = true;
/// API事件流控制器(登录状态变化等)
final StreamController<String> _eventController =
StreamController<String>.broadcast();
/// API事件流
Stream<String> get eventStream => _eventController.stream;
/// 单例实例
static PadApiService? _instance;
/// 获取单例
static PadApiService get instance {
_instance ??= PadApiService._internal();
return _instance!;
}
/// 私有构造函数,初始化Dio客户端
PadApiService._internal() {
_dio = Dio(BaseOptions(
baseUrl: ApiConfig.productionBaseUrl,
connectTimeout: Duration(milliseconds: ApiConfig.connectTimeout),
receiveTimeout: Duration(milliseconds: ApiConfig.receiveTimeout),
headers: {
'Content-Type': 'application/json',
'X-Client-Platform': 'pad',
'X-Client-Version': '1.0.0',
},
));
// 添加请求拦截器:自动附加Token和HMAC签名
_dio.interceptors.add(InterceptorsWrapper(
onRequest: _onRequest,
onResponse: _onResponse,
onError: _onError,
));
// 从安全存储恢复令牌
_restoreTokens();
}
/// 从安全存储恢复上次保存的令牌
Future<void> _restoreTokens() async {
_accessToken = await _secureStorage.read(key: 'access_token');
_refreshToken = await _secureStorage.read(key: 'refresh_token');
}
/// 请求拦截器:附加Authorization头和HMAC签名
void _onRequest(
RequestOptions options,
RequestInterceptorHandler handler,
) {
// 附加JWT访问令牌
if (_accessToken != null) {
options.headers['Authorization'] = 'Bearer $_accessToken';
}
// 生成HMAC-SHA256请求签名
final timestamp = DateTime.now().millisecondsSinceEpoch.toString();
options.headers['X-Timestamp'] = timestamp;
final signature = _generateSignature(
options.method,
options.path,
timestamp,
options.data,
);
options.headers['X-Signature'] = signature;
handler.next(options);
}
/// 响应拦截器:统一处理响应
void _onResponse(
Response response,
ResponseInterceptorHandler handler,
) {
handler.next(response);
}
/// 错误拦截器:处理401自动刷新Token、离线暂存等
Future<void> _onError(
DioException error,
ErrorInterceptorHandler handler,
) async {
// 网络不可用时,将请求加入离线队列
if (error.type == DioExceptionType.connectionError ||
error.type == DioExceptionType.connectionTimeout) {
_isOnline = false;
_enqueueOfflineRequest(error.requestOptions);
handler.reject(error);
return;
}
// 401未授权:尝试刷新Token后重试
if (error.response?.statusCode == 401) {
final refreshSuccess = await _refreshAccessToken();
if (refreshSuccess) {
// Token刷新成功,使用新Token重试原请求
final retryOptions = error.requestOptions;
retryOptions.headers['Authorization'] = 'Bearer $_accessToken';
try {
final response = await _dio.fetch(retryOptions);
handler.resolve(response);
return;
} catch (retryError) {
// 重试也失败了
}
} else {
// Token刷新失败,通知登出
_eventController.add('token_expired');
}
}
handler.reject(error);
}
/// 生成HMAC-SHA256请求签名
/// 签名内容: METHOD\nPATH\nTIMESTAMP\nBODY_SHA256
String _generateSignature(
String method,
String path,
String timestamp,
dynamic body,
) {
// 计算请求体SHA256哈希
String bodyHash = '';
if (body != null) {
final bodyStr = body is String ? body : jsonEncode(body);
bodyHash = sha256.convert(utf8.encode(bodyStr)).toString();
}
// 拼接签名原文
final signContent = '$method\n$path\n$timestamp\n$bodyHash';
final hmacKey = utf8.encode(ApiConfig.hmacKeyId);
final hmac = Hmac(sha256, hmacKey);
final digest = hmac.convert(utf8.encode(signContent));
return digest.toString();
}
/// 刷新访问令牌
/// 使用Completer防止并发多次刷新
Future<bool> _refreshAccessToken() async {
// 如果已经在刷新中,等待结果
if (_refreshCompleter != null) {
return _refreshCompleter!.future;
}
_refreshCompleter = Completer<bool>();
try {
if (_refreshToken == null) {
_refreshCompleter!.complete(false);
return false;
}
// 发送刷新请求(不经过拦截器避免死循环)
final response = await Dio().post(
'${ApiConfig.productionBaseUrl}${ApiConfig.refreshTokenPath}',
data: {'refresh_token': _refreshToken},
);
if (response.statusCode == 200 && response.data['code'] == 0) {
_accessToken = response.data['data']['access_token'];
_refreshToken = response.data['data']['refresh_token'];
// 持久化新令牌到安全存储
await _secureStorage.write(
key: 'access_token',
value: _accessToken,
);
await _secureStorage.write(
key: 'refresh_token',
value: _refreshToken,
);
_refreshCompleter!.complete(true);
return true;
}
_refreshCompleter!.complete(false);
return false;
} catch (e) {
_refreshCompleter!.complete(false);
return false;
} finally {
_refreshCompleter = null;
}
}
/// 将失败的请求加入离线队列
void _enqueueOfflineRequest(RequestOptions options) {
final offlineReq = OfflineRequest(
id: DateTime.now().microsecondsSinceEpoch.toString(),
method: options.method,
path: options.path,
data: options.data is Map ? options.data : null,
createdAt: DateTime.now(),
);
_offlineQueue.add(offlineReq);
}
/// 网络恢复后,批量重发离线队列中的请求
Future<void> flushOfflineQueue() async {
if (_offlineQueue.isEmpty) return;
_isOnline = true;
final pendingRequests = List<OfflineRequest>.from(_offlineQueue);
_offlineQueue.clear();
for (final req in pendingRequests) {
try {
if (req.retryCount >= ApiConfig.maxRetryCount) continue;
req.retryCount++;
switch (req.method.toUpperCase()) {
case 'POST':
await _dio.post(req.path, data: req.data);
break;
case 'PUT':
await _dio.put(req.path, data: req.data);
break;
case 'DELETE':
await _dio.delete(req.path);
break;
default:
await _dio.get(req.path);
}
} catch (e) {
// 重发失败的请求重新加入队列
if (req.retryCount < ApiConfig.maxRetryCount) {
_offlineQueue.add(req);
}
}
}
}
// ============================================================
// 学生登录接口(简化登录,班级+姓名/学号)
// ============================================================
/// 学生简化登录(无需手机号,仅班级+姓名或学号)
Future<ApiResponse<Map<String, dynamic>>> studentLogin({
required String schoolCode,
required String classId,
required String studentName,
String? studentNo,
}) async {
try {
final response = await _dio.post('/auth/student/login', data: {
'school_code': schoolCode,
'class_id': classId,
'student_name': studentName,
'student_no': studentNo,
'device_type': 'pad',
});
final result = ApiResponse<Map<String, dynamic>>.fromJson(
response.data,
(data) => data as Map<String, dynamic>,
);
// 保存登录令牌
if (result.isSuccess && result.data != null) {
_accessToken = result.data!['access_token'];
_refreshToken = result.data!['refresh_token'];
await _secureStorage.write(
key: 'access_token',
value: _accessToken,
);
await _secureStorage.write(
key: 'refresh_token',
value: _refreshToken,
);
}
return result;
} on DioException catch (e) {
return ApiResponse(code: -1, message: e.message ?? '网络请求失败');
}
}
/// 教师登录(手机号+验证码)
Future<ApiResponse<Map<String, dynamic>>> teacherLogin({
required String phone,
required String verifyCode,
}) async {
try {
final response = await _dio.post('/auth/teacher/login', data: {
'phone': phone,
'verify_code': verifyCode,
'device_type': 'pad',
});
final result = ApiResponse<Map<String, dynamic>>.fromJson(
response.data,
(data) => data as Map<String, dynamic>,
);
if (result.isSuccess && result.data != null) {
_accessToken = result.data!['access_token'];
_refreshToken = result.data!['refresh_token'];
await _secureStorage.write(
key: 'access_token',
value: _accessToken,
);
await _secureStorage.write(
key: 'refresh_token',
value: _refreshToken,
);
}
return result;
} on DioException catch (e) {
return ApiResponse(code: -1, message: e.message ?? '网络请求失败');
}
}
// ============================================================
// 作业相关接口
// ============================================================
/// 获取学生待完成作业列表
Future<ApiResponse<List<dynamic>>> getHomeworkList({
int page = 1,
int pageSize = 20,
String? status,
}) async {
try {
final response = await _dio.get('/homework/list', queryParameters: {
'page': page,
'page_size': pageSize,
if (status != null) 'status': status,
});
return ApiResponse<List<dynamic>>.fromJson(
response.data,
(data) => data as List<dynamic>,
);
} on DioException catch (e) {
return ApiResponse(code: -1, message: e.message ?? '获取作业列表失败');
}
}
/// 下载作业详情(含题目内容,支持离线作答)
Future<ApiResponse<Map<String, dynamic>>> downloadHomework(
String homeworkId,
) async {
try {
final response = await _dio.get('/homework/detail/$homeworkId');
return ApiResponse<Map<String, dynamic>>.fromJson(
response.data,
(data) => data as Map<String, dynamic>,
);
} on DioException catch (e) {
return ApiResponse(code: -1, message: e.message ?? '下载作业失败');
}
}
/// 提交作业(含笔迹数据)
Future<ApiResponse<Map<String, dynamic>>> submitHomework({
required String homeworkId,
required List<Map<String, dynamic>> strokePages,
int? timeCostSeconds,
}) async {
try {
final response = await _dio.post('/homework/submit', data: {
'homework_id': homeworkId,
'stroke_pages': strokePages,
'time_cost': timeCostSeconds,
'submit_time': DateTime.now().toIso8601String(),
});
return ApiResponse<Map<String, dynamic>>.fromJson(
response.data,
(data) => data as Map<String, dynamic>,
);
} on DioException catch (e) {
// 离线时暂存提交请求
if (!_isOnline) {
_enqueueOfflineRequest(e.requestOptions);
}
return ApiResponse(code: -1, message: e.message ?? '提交作业失败');
}
}
/// 获取作业批改结果
Future<ApiResponse<Map<String, dynamic>>> getHomeworkResult(
String homeworkId,
) async {
try {
final response = await _dio.get('/homework/result/$homeworkId');
return ApiResponse<Map<String, dynamic>>.fromJson(
response.data,
(data) => data as Map<String, dynamic>,
);
} on DioException catch (e) {
return ApiResponse(code: -1, message: e.message ?? '获取批改结果失败');
}
}
// ============================================================
// 字帖练习接口
// ============================================================
/// 获取字帖模板列表(按年级/学科分类)
Future<ApiResponse<List<dynamic>>> getCopybookTemplates({
required String grade,
String? subject,
int page = 1,
}) async {
try {
final response = await _dio.get('/copybook/templates', queryParameters: {
'grade': grade,
'subject': subject,
'page': page,
});
return ApiResponse<List<dynamic>>.fromJson(
response.data,
(data) => data as List<dynamic>,
);
} on DioException catch (e) {
return ApiResponse(code: -1, message: e.message ?? '获取字帖失败');
}
}
/// 上传练字笔迹评分
Future<ApiResponse<Map<String, dynamic>>> submitPracticeStroke({
required String templateId,
required String character,
required List<Map<String, dynamic>> strokes,
}) async {
try {
final response = await _dio.post('/copybook/evaluate', data: {
'template_id': templateId,
'character': character,
'strokes': strokes,
});
return ApiResponse<Map<String, dynamic>>.fromJson(
response.data,
(data) => data as Map<String, dynamic>,
);
} on DioException catch (e) {
return ApiResponse(code: -1, message: e.message ?? '提交练字评分失败');
}
}
// ============================================================
// 错题本接口
// ============================================================
/// 获取错题列表(按知识点/科目分类)
Future<ApiResponse<List<dynamic>>> getErrorBookList({
String? subject,
String? knowledgePoint,
int page = 1,
int pageSize = 20,
}) async {
try {
final response = await _dio.get('/error-book/list', queryParameters: {
if (subject != null) 'subject': subject,
if (knowledgePoint != null) 'knowledge_point': knowledgePoint,
'page': page,
'page_size': pageSize,
});
return ApiResponse<List<dynamic>>.fromJson(
response.data,
(data) => data as List<dynamic>,
);
} on DioException catch (e) {
return ApiResponse(code: -1, message: e.message ?? '获取错题本失败');
}
}
// ============================================================
// 学情与学习计划接口
// ============================================================
/// 获取学生个人学情概览
Future<ApiResponse<Map<String, dynamic>>> getStudentProfile() async {
try {
final response = await _dio.get('/profile/student/overview');
return ApiResponse<Map<String, dynamic>>.fromJson(
response.data,
(data) => data as Map<String, dynamic>,
);
} on DioException catch (e) {
return ApiResponse(code: -1, message: e.message ?? '获取学情失败');
}
}
/// 获取学习计划列表
Future<ApiResponse<List<dynamic>>> getStudyPlans() async {
try {
final response = await _dio.get('/study-plan/list');
return ApiResponse<List<dynamic>>.fromJson(
response.data,
(data) => data as List<dynamic>,
);
} on DioException catch (e) {
return ApiResponse(code: -1, message: e.message ?? '获取学习计划失败');
}
}
/// 更新学习计划进度
Future<ApiResponse<void>> updateStudyPlanProgress({
required String planId,
required String taskId,
required double progress,
}) async {
try {
final response = await _dio.put('/study-plan/progress', data: {
'plan_id': planId,
'task_id': taskId,
'progress': progress,
'update_time': DateTime.now().toIso8601String(),
});
return ApiResponse<void>.fromJson(response.data, null);
} on DioException catch (e) {
return ApiResponse(code: -1, message: e.message ?? '更新进度失败');
}
}
/// 登出,清除本地令牌
Future<void> logout() async {
try {
await _dio.post('/auth/logout');
} catch (_) {
// 忽略登出请求失败
}
_accessToken = null;
_refreshToken = null;
await _secureStorage.delete(key: 'access_token');
await _secureStorage.delete(key: 'refresh_token');
_eventController.add('logged_out');
}
/// 释放资源
void dispose() {
_eventController.close();
_dio.close();
}
}
@@ -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();
}
}