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,584 @@
/*
* 自然写互动课堂应用开发SDK软件 V1.0
* PenManager - Android端蓝牙点阵笔连接管理器
*
* 功能说明:
* 1. BLE 5.0蓝牙扫描与自动连接
* 2. GATT服务发现与特征值订阅
* 3. 点阵笔数据实时接收与解析
* 4. 多笔同时连接管理(最多支持60支)
* 5. 连接状态监控与自动重连
* 6. 电量/固件版本/设备信息查询
*/
package com.writech.sdk.android;
import android.bluetooth.BluetoothAdapter;
import android.bluetooth.BluetoothDevice;
import android.bluetooth.BluetoothGatt;
import android.bluetooth.BluetoothGattCallback;
import android.bluetooth.BluetoothGattCharacteristic;
import android.bluetooth.BluetoothGattDescriptor;
import android.bluetooth.BluetoothGattService;
import android.bluetooth.BluetoothManager;
import android.bluetooth.BluetoothProfile;
import android.bluetooth.le.BluetoothLeScanner;
import android.bluetooth.le.ScanCallback;
import android.bluetooth.le.ScanFilter;
import android.bluetooth.le.ScanResult;
import android.bluetooth.le.ScanSettings;
import android.content.Context;
import android.os.Handler;
import android.os.HandlerThread;
import android.os.ParcelUuid;
import android.util.Log;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CopyOnWriteArrayList;
/**
* 点阵笔蓝牙连接管理器
* 负责BLE扫描、连接、数据接收的全生命周期管理
*/
public class PenManager {
private static final String TAG = "WritechPenManager";
/* 自然写点阵笔GATT服务UUID(自定义) */
private static final UUID PEN_SERVICE_UUID =
UUID.fromString("0000FFE0-0000-1000-8000-00805F9B34FB");
/* 笔迹数据通知特征值UUID */
private static final UUID STROKE_DATA_CHAR_UUID =
UUID.fromString("0000FFE1-0000-1000-8000-00805F9B34FB");
/* 笔控制指令写入特征值UUID */
private static final UUID PEN_CONTROL_CHAR_UUID =
UUID.fromString("0000FFE2-0000-1000-8000-00805F9B34FB");
/* 设备信息特征值UUID(电量/固件版本) */
private static final UUID DEVICE_INFO_CHAR_UUID =
UUID.fromString("0000FFE3-0000-1000-8000-00805F9B34FB");
/* CCCD描述符UUID,用于启用通知 */
private static final UUID CCCD_UUID =
UUID.fromString("00002902-0000-1000-8000-00805F9B34FB");
/* 最大同时连接数 */
private static final int MAX_CONNECTIONS = 60;
/* 自动重连延迟(毫秒) */
private static final long RECONNECT_DELAY_MS = 3000;
/* 扫描超时时间(毫秒) */
private static final long SCAN_TIMEOUT_MS = 30000;
/* ========== 成员变量 ========== */
private final Context mContext;
private final BluetoothAdapter mBluetoothAdapter;
private BluetoothLeScanner mScanner;
/* 已连接的笔设备映射表(MAC地址 → GATT连接) */
private final Map<String, BluetoothGatt> mConnectedPens = new ConcurrentHashMap<>();
/* 等待重连的设备列表 */
private final Map<String, Integer> mReconnectAttempts = new ConcurrentHashMap<>();
/* 设备信息缓存(MAC地址 → 设备模型) */
private final Map<String, PenDeviceInfo> mDeviceInfoCache = new ConcurrentHashMap<>();
/* 数据回调监听器列表 */
private final List<PenDataListener> mDataListeners = new CopyOnWriteArrayList<>();
/* 连接状态监听器列表 */
private final List<PenConnectionListener> mConnectionListeners = new CopyOnWriteArrayList<>();
/* BLE操作专用线程 */
private HandlerThread mBleThread;
private Handler mBleHandler;
/* 扫描状态标志 */
private volatile boolean mIsScanning = false;
/* ========== 内部数据结构 ========== */
/** 笔设备信息缓存 */
private static class PenDeviceInfo {
String macAddress; /* MAC地址 */
String penName; /* 笔名称 */
String firmwareVersion; /* 固件版本 */
int batteryLevel; /* 电量百分比 */
long lastDataTimestamp; /* 最后一次收到数据的时间 */
boolean isWriting; /* 是否正在书写 */
}
/* ========== 对外回调接口 ========== */
/** 笔迹数据监听器 */
public interface PenDataListener {
/** 收到笔迹坐标数据 */
void onStrokeData(String penMac, int x, int y, int pressure, long timestamp);
/** 笔抬起事件(一笔结束) */
void onPenUp(String penMac, long timestamp);
/** 笔落下事件(一笔开始) */
void onPenDown(String penMac, long timestamp);
}
/** 连接状态监听器 */
public interface PenConnectionListener {
void onPenConnected(String penMac, String penName);
void onPenDisconnected(String penMac, int reason);
void onPenDiscovered(String penMac, String penName, int rssi);
void onBatteryUpdate(String penMac, int batteryPercent);
}
/* ========== 构造与初始化 ========== */
/**
* 创建笔管理器实例
* @param context Android上下文(需要蓝牙权限)
*/
public PenManager(Context context) {
mContext = context.getApplicationContext();
BluetoothManager btManager =
(BluetoothManager) mContext.getSystemService(Context.BLUETOOTH_SERVICE);
mBluetoothAdapter = btManager.getAdapter();
/* 创建BLE操作专用后台线程 */
mBleThread = new HandlerThread("WritechBLE");
mBleThread.start();
mBleHandler = new Handler(mBleThread.getLooper());
Log.i(TAG, "PenManager初始化完成,蓝牙状态: "
+ (mBluetoothAdapter.isEnabled() ? "已开启" : "未开启"));
}
/** 注册笔迹数据监听器 */
public void addDataListener(PenDataListener listener) {
if (listener != null && !mDataListeners.contains(listener)) {
mDataListeners.add(listener);
}
}
/** 移除笔迹数据监听器 */
public void removeDataListener(PenDataListener listener) {
mDataListeners.remove(listener);
}
/** 注册连接状态监听器 */
public void addConnectionListener(PenConnectionListener listener) {
if (listener != null && !mConnectionListeners.contains(listener)) {
mConnectionListeners.add(listener);
}
}
/* ========== BLE扫描 ========== */
/**
* 开始扫描附近的自然写点阵笔
* 使用低延迟模式扫描BLE设备,按服务UUID过滤
*/
public void startScan() {
if (mIsScanning) {
Log.w(TAG, "扫描已在进行中,忽略重复请求");
return;
}
if (!mBluetoothAdapter.isEnabled()) {
Log.e(TAG, "蓝牙未开启,无法扫描");
return;
}
mScanner = mBluetoothAdapter.getBluetoothLeScanner();
if (mScanner == null) {
Log.e(TAG, "获取BLE扫描器失败");
return;
}
/* 构建扫描过滤器:仅扫描包含自然写服务UUID的设备 */
ScanFilter filter = new ScanFilter.Builder()
.setServiceUuid(new ParcelUuid(PEN_SERVICE_UUID))
.build();
List<ScanFilter> filters = Collections.singletonList(filter);
/* 低延迟扫描设置(耗电较高,适合主动扫描场景) */
ScanSettings settings = new ScanSettings.Builder()
.setScanMode(ScanSettings.SCAN_MODE_LOW_LATENCY)
.setCallbackType(ScanSettings.CALLBACK_TYPE_ALL_MATCHES)
.setMatchMode(ScanSettings.MATCH_MODE_AGGRESSIVE)
.build();
mScanner.startScan(filters, settings, mScanCallback);
mIsScanning = true;
/* 设置扫描超时,避免长时间扫描耗电 */
mBleHandler.postDelayed(this::stopScan, SCAN_TIMEOUT_MS);
Log.i(TAG, "开始扫描自然写点阵笔...");
}
/** 停止BLE扫描 */
public void stopScan() {
if (mIsScanning && mScanner != null) {
mScanner.stopScan(mScanCallback);
mIsScanning = false;
Log.i(TAG, "停止扫描");
}
}
/** BLE扫描回调 */
private final ScanCallback mScanCallback = new ScanCallback() {
@Override
public void onScanResult(int callbackType, ScanResult result) {
BluetoothDevice device = result.getDevice();
String mac = device.getAddress();
String name = device.getName();
int rssi = result.getRssi();
if (name == null || name.isEmpty()) {
name = "WritechPen-" + mac.substring(mac.length() - 5);
}
/* 通知上层发现了新的笔设备 */
for (PenConnectionListener listener : mConnectionListeners) {
listener.onPenDiscovered(mac, name, rssi);
}
Log.d(TAG, "发现笔设备: " + name + " [" + mac + "] RSSI=" + rssi);
}
@Override
public void onScanFailed(int errorCode) {
mIsScanning = false;
Log.e(TAG, "BLE扫描失败,错误码: " + errorCode);
}
};
/* ========== BLE连接管理 ========== */
/**
* 连接指定MAC地址的点阵笔
* @param macAddress 设备MAC地址
*/
public void connectPen(String macAddress) {
if (mConnectedPens.size() >= MAX_CONNECTIONS) {
Log.w(TAG, "已达最大连接数 " + MAX_CONNECTIONS + ",拒绝新连接");
return;
}
if (mConnectedPens.containsKey(macAddress)) {
Log.w(TAG, "设备已连接: " + macAddress);
return;
}
BluetoothDevice device = mBluetoothAdapter.getRemoteDevice(macAddress);
/* 使用TRANSPORT_LE确保走BLE通道,autoConnect=false立即连接 */
device.connectGatt(mContext, false, mGattCallback, BluetoothDevice.TRANSPORT_LE);
Log.i(TAG, "正在连接笔设备: " + macAddress);
}
/** 断开指定笔的连接 */
public void disconnectPen(String macAddress) {
BluetoothGatt gatt = mConnectedPens.remove(macAddress);
if (gatt != null) {
gatt.disconnect();
gatt.close();
mReconnectAttempts.remove(macAddress);
Log.i(TAG, "已断开笔设备: " + macAddress);
}
}
/** 断开所有已连接的笔 */
public void disconnectAll() {
for (Map.Entry<String, BluetoothGatt> entry : mConnectedPens.entrySet()) {
entry.getValue().disconnect();
entry.getValue().close();
}
mConnectedPens.clear();
mReconnectAttempts.clear();
Log.i(TAG, "已断开所有笔设备");
}
/** 获取当前已连接的笔数量 */
public int getConnectedCount() {
return mConnectedPens.size();
}
/** 获取所有已连接笔的MAC地址列表 */
public List<String> getConnectedPenMacs() {
return new ArrayList<>(mConnectedPens.keySet());
}
/* ========== GATT回调处理 ========== */
/**
* GATT连接/数据回调
* 处理连接状态变化、服务发现、数据通知等所有BLE事件
*/
private final BluetoothGattCallback mGattCallback = new BluetoothGattCallback() {
@Override
public void onConnectionStateChange(BluetoothGatt gatt, int status, int newState) {
String mac = gatt.getDevice().getAddress();
if (newState == BluetoothProfile.STATE_CONNECTED) {
/* 连接成功,开始发现GATT服务 */
mConnectedPens.put(mac, gatt);
mReconnectAttempts.remove(mac);
gatt.discoverServices();
String name = gatt.getDevice().getName();
for (PenConnectionListener listener : mConnectionListeners) {
listener.onPenConnected(mac, name != null ? name : "Unknown");
}
Log.i(TAG, "笔设备连接成功: " + mac + ",正在发现服务...");
} else if (newState == BluetoothProfile.STATE_DISCONNECTED) {
/* 连接断开,尝试自动重连 */
mConnectedPens.remove(mac);
gatt.close();
for (PenConnectionListener listener : mConnectionListeners) {
listener.onPenDisconnected(mac, status);
}
Log.w(TAG, "笔设备断开: " + mac + ",状态码: " + status);
/* 自动重连逻辑(最多尝试5次) */
scheduleReconnect(mac);
}
}
@Override
public void onServicesDiscovered(BluetoothGatt gatt, int status) {
if (status != BluetoothGatt.GATT_SUCCESS) {
Log.e(TAG, "GATT服务发现失败: " + status);
return;
}
/* 查找自然写笔迹数据服务 */
BluetoothGattService penService = gatt.getService(PEN_SERVICE_UUID);
if (penService == null) {
Log.e(TAG, "未找到自然写笔服务,设备可能不兼容");
return;
}
/* 订阅笔迹数据通知特征值 */
BluetoothGattCharacteristic strokeChar =
penService.getCharacteristic(STROKE_DATA_CHAR_UUID);
if (strokeChar != null) {
gatt.setCharacteristicNotification(strokeChar, true);
/* 写入CCCD描述符启用通知 */
BluetoothGattDescriptor cccd = strokeChar.getDescriptor(CCCD_UUID);
if (cccd != null) {
cccd.setValue(BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE);
gatt.writeDescriptor(cccd);
}
Log.i(TAG, "已订阅笔迹数据通知");
}
/* 读取设备信息(电量、固件版本) */
BluetoothGattCharacteristic infoChar =
penService.getCharacteristic(DEVICE_INFO_CHAR_UUID);
if (infoChar != null) {
mBleHandler.postDelayed(() -> gatt.readCharacteristic(infoChar), 500);
}
}
@Override
public void onCharacteristicChanged(BluetoothGatt gatt,
BluetoothGattCharacteristic characteristic) {
String mac = gatt.getDevice().getAddress();
UUID charUuid = characteristic.getUuid();
if (STROKE_DATA_CHAR_UUID.equals(charUuid)) {
/* 收到笔迹数据通知,解析并分发 */
byte[] data = characteristic.getValue();
parseAndDispatchStrokeData(mac, data);
}
}
@Override
public void onCharacteristicRead(BluetoothGatt gatt,
BluetoothGattCharacteristic characteristic,
int status) {
if (status != BluetoothGatt.GATT_SUCCESS) return;
String mac = gatt.getDevice().getAddress();
UUID charUuid = characteristic.getUuid();
if (DEVICE_INFO_CHAR_UUID.equals(charUuid)) {
/* 解析设备信息数据 */
byte[] data = characteristic.getValue();
parseDeviceInfo(mac, data);
}
}
};
/* ========== 数据解析与分发 ========== */
/**
* 解析BLE收到的笔迹数据帧并分发给监听器
* 数据格式(7字节紧凑编码):
* [0-1] X坐标高16位 [2-3] Y坐标高16位
* [4] X低4位|Y低4位 [5] 压力高8位 [6] 压力低4位|标志
*/
private void parseAndDispatchStrokeData(String penMac, byte[] data) {
if (data == null || data.length < 7) {
return;
}
long timestamp = System.currentTimeMillis();
/* 检查帧类型标志(最低2位) */
int flags = data[6] & 0x03;
if (flags == 0x01) {
/* 笔落下事件 */
for (PenDataListener listener : mDataListeners) {
listener.onPenDown(penMac, timestamp);
}
return;
}
if (flags == 0x02) {
/* 笔抬起事件 */
for (PenDataListener listener : mDataListeners) {
listener.onPenUp(penMac, timestamp);
}
return;
}
/* 坐标数据帧(flags == 0x00 */
int xHigh = ((data[0] & 0xFF) << 8) | (data[1] & 0xFF);
int xLow = (data[4] >> 4) & 0x0F;
int x = (xHigh << 4) | xLow;
int yHigh = ((data[2] & 0xFF) << 8) | (data[3] & 0xFF);
int yLow = data[4] & 0x0F;
int y = (yHigh << 4) | yLow;
int pHigh = data[5] & 0xFF;
int pLow = (data[6] >> 4) & 0x0F;
int pressure = (pHigh << 4) | pLow;
/* 更新设备状态 */
PenDeviceInfo info = mDeviceInfoCache.get(penMac);
if (info != null) {
info.lastDataTimestamp = timestamp;
info.isWriting = true;
}
/* 分发到所有监听器 */
for (PenDataListener listener : mDataListeners) {
listener.onStrokeData(penMac, x, y, pressure, timestamp);
}
}
/** 解析设备信息特征值数据 */
private void parseDeviceInfo(String penMac, byte[] data) {
if (data == null || data.length < 4) return;
PenDeviceInfo info = mDeviceInfoCache.get(penMac);
if (info == null) {
info = new PenDeviceInfo();
info.macAddress = penMac;
mDeviceInfoCache.put(penMac, info);
}
/* 第一字节:电量百分比 */
info.batteryLevel = data[0] & 0xFF;
/* 第2-4字节:固件版本 major.minor.patch */
info.firmwareVersion = (data[1] & 0xFF) + "." + (data[2] & 0xFF)
+ "." + (data[3] & 0xFF);
/* 通知电量更新 */
for (PenConnectionListener listener : mConnectionListeners) {
listener.onBatteryUpdate(penMac, info.batteryLevel);
}
Log.i(TAG, "设备信息 [" + penMac + "] 电量:" + info.batteryLevel
+ "% 固件:" + info.firmwareVersion);
}
/* ========== 自动重连 ========== */
/** 安排自动重连(指数退避) */
private void scheduleReconnect(String macAddress) {
Integer attempts = mReconnectAttempts.getOrDefault(macAddress, 0);
if (attempts >= 5) {
Log.w(TAG, "设备 " + macAddress + " 重连次数已达上限,放弃重连");
mReconnectAttempts.remove(macAddress);
return;
}
mReconnectAttempts.put(macAddress, attempts + 1);
/* 指数退避:3s, 6s, 12s, 24s, 48s */
long delay = RECONNECT_DELAY_MS * (1L << attempts);
mBleHandler.postDelayed(() -> {
if (!mConnectedPens.containsKey(macAddress)) {
Log.i(TAG, "尝试重连设备: " + macAddress + "(第" + (attempts + 1) + "次)");
connectPen(macAddress);
}
}, delay);
}
/* ========== 控制指令发送 ========== */
/**
* 向笔发送控制指令
* @param macAddress 目标笔MAC
* @param command 指令字节数组
* @return 是否发送成功
*/
public boolean sendCommand(String macAddress, byte[] command) {
BluetoothGatt gatt = mConnectedPens.get(macAddress);
if (gatt == null) {
Log.w(TAG, "设备未连接,无法发送指令: " + macAddress);
return false;
}
BluetoothGattService service = gatt.getService(PEN_SERVICE_UUID);
if (service == null) return false;
BluetoothGattCharacteristic controlChar =
service.getCharacteristic(PEN_CONTROL_CHAR_UUID);
if (controlChar == null) return false;
controlChar.setValue(command);
controlChar.setWriteType(BluetoothGattCharacteristic.WRITE_TYPE_DEFAULT);
return gatt.writeCharacteristic(controlChar);
}
/** 查询笔电量 */
public int getBatteryLevel(String macAddress) {
PenDeviceInfo info = mDeviceInfoCache.get(macAddress);
return info != null ? info.batteryLevel : -1;
}
/* ========== 资源释放 ========== */
/** 释放PenManager资源 */
public void destroy() {
stopScan();
disconnectAll();
mDataListeners.clear();
mConnectionListeners.clear();
mDeviceInfoCache.clear();
if (mBleThread != null) {
mBleThread.quitSafely();
mBleThread = null;
}
Log.i(TAG, "PenManager资源已释放");
}
}