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

585 lines
20 KiB
Java
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.
/*
* 自然写互动课堂应用开发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资源已释放");
}
}