/* * 自然写互动课堂应用开发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 mConnectedPens = new ConcurrentHashMap<>(); /* 等待重连的设备列表 */ private final Map mReconnectAttempts = new ConcurrentHashMap<>(); /* 设备信息缓存(MAC地址 → 设备模型) */ private final Map mDeviceInfoCache = new ConcurrentHashMap<>(); /* 数据回调监听器列表 */ private final List mDataListeners = new CopyOnWriteArrayList<>(); /* 连接状态监听器列表 */ private final List 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 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 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 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资源已释放"); } }