| | |
| | | public class BleWifiConfiguratorUtils { |
| | | |
| | | private static final String TAG = "BleWifiConfigurator"; |
| | | |
| | | // 默认的 UUID(设备端需要定义以下服务和特征) |
| | | //这是主服务,包含所有配网相关的特征。 |
| | | public static final UUID DEFAULT_SERVICE_UUID = UUID.fromString("0000fff0-0000-1000-8000-00805f9b34fb"); |
| | | public static final UUID DEFAULT_SERVICE_UUID = UUID.fromString("000000ff-0000-1000-8000-00805f9b34fb"); |
| | | //特征值 1: 0000fff1-0000-1000-8000-00805f9b34fb (SSID) |
| | | //用途:写入 Wi-Fi 的 SSID 名称 |
| | | //属性:Write (可写) |
| | | //数据类型:UTF-8 字符串 |
| | | //示例:"MyWiFi_5G" |
| | | public static final UUID DEFAULT_SSID_CHAR_UUID = UUID.fromString("0000fff1-0000-1000-8000-00805f9b34fb"); |
| | | //特征值 2: 0000fff2-0000-1000-8000-00805f9b34fb (密码) |
| | | // 用途:写入 Wi-Fi 密码 |
| | | //属性:Write (可写) |
| | | //数据类型:UTF-8 字符串 |
| | | //示例:"password123" |
| | | public static final UUID DEFAULT_PWD_CHAR_UUID = UUID.fromString("0000fff2-0000-1000-8000-00805f9b34fb"); |
| | | public static final UUID DEFAULT_CHAR_UUID = UUID.fromString("0000ff01-0000-1000-8000-00805f9b34fb"); |
| | | //特征值 3: 0000fff3-0000-1000-8000-00805f9b34fb (通知,可选) |
| | | //用途:接收设备端的响应消息 |
| | | //属性:Notify (通知) |
| | | //数据类型:UTF-8 字符串 |
| | | //示例响应:"SUCCESS", "FAIL: Invalid SSID" |
| | | public static final UUID DEFAULT_NOTIFY_CHAR_UUID = UUID.fromString("0000fff3-0000-1000-8000-00805f9b34fb"); // 可选,用于接收设备响应 |
| | | public static final UUID DEFAULT_NOTIFY_CHAR_UUID = UUID.fromString("0000ff01-0000-1000-8000-00805f9b34fb"); // 可选,用于接收设备响应 |
| | | |
| | | // 上下文 |
| | | private Context context; |
| | |
| | | // GATT 相关 |
| | | private BluetoothGatt bluetoothGatt; |
| | | private BluetoothGattService targetService; |
| | | private BluetoothGattCharacteristic ssidCharacteristic; |
| | | private BluetoothGattCharacteristic pwdCharacteristic; |
| | | private BluetoothGattCharacteristic characteristic; |
| | | private BluetoothGattCharacteristic notifyCharacteristic; // 可选 |
| | | |
| | | // 自定义 UUID(允许调用者修改) |
| | | private UUID serviceUuid = DEFAULT_SERVICE_UUID; |
| | | private UUID ssidCharUuid = DEFAULT_SSID_CHAR_UUID; |
| | | private UUID pwdCharUuid = DEFAULT_PWD_CHAR_UUID; |
| | | private UUID charUuid = DEFAULT_CHAR_UUID; |
| | | private UUID notifyCharUuid = DEFAULT_NOTIFY_CHAR_UUID; |
| | | |
| | | // 回调接口 |
| | |
| | | // 内部状态 |
| | | private boolean isScanning = false; |
| | | private boolean isConnected = false; |
| | | private int writeState = 0; // 0: idle, 1: writing ssid, 2: writing pwd |
| | | private String ssidToWrite; |
| | | private String pwdToWrite; |
| | | private String dataToWrite; // 待写入的数据 |
| | | |
| | | // 用于将 BLE 回调抛到主线程 |
| | | private final Handler callbackHandler = new Handler(Looper.getMainLooper()); |
| | |
| | | } |
| | | |
| | | /** |
| | | * 设置自定义 SSID 特征 UUID(可选) |
| | | * 设置自定义特征 UUID(可选) |
| | | */ |
| | | public void setSsidCharUuid(UUID ssidCharUuid) { |
| | | this.ssidCharUuid = ssidCharUuid; |
| | | } |
| | | |
| | | /** |
| | | * 设置自定义密码特征 UUID(可选) |
| | | */ |
| | | public void setPwdCharUuid(UUID pwdCharUuid) { |
| | | this.pwdCharUuid = pwdCharUuid; |
| | | public void setCharUuid(UUID charUuid) { |
| | | this.charUuid = charUuid; |
| | | } |
| | | |
| | | /** |
| | |
| | | */ |
| | | public void setNotifyCharUuid(UUID notifyCharUuid) { |
| | | this.notifyCharUuid = notifyCharUuid; |
| | | } |
| | | |
| | | /** |
| | | * 检查蓝牙连接状态 |
| | | */ |
| | | public boolean getBluetoothStatus() { |
| | | return this.isConnected; |
| | | } |
| | | |
| | | // ==================== 权限检查 ==================== |
| | |
| | | public List<String> getMissingPermissions() { |
| | | List<String> missing = new ArrayList<>(); |
| | | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { |
| | | //在应用里--权限名称--附近的设备 |
| | | if (context.checkSelfPermission(android.Manifest.permission.BLUETOOTH_SCAN) != android.content.pm.PackageManager.PERMISSION_GRANTED) { |
| | | missing.add(android.Manifest.permission.BLUETOOTH_SCAN); |
| | | Log.d(TAG, "No Permissions: android.Manifest.permission.BLUETOOTH_SCAN"); |
| | |
| | | isScanning = true; |
| | | Log.d(TAG, "BLE scan started"); |
| | | } catch (Exception e) { |
| | | |
| | | Log.d(TAG, "BLE scan fail"); |
| | | } |
| | | } |
| | | |
| | |
| | | @Override |
| | | public void onScanResult(int callbackType, ScanResult result) { |
| | | BluetoothDevice device = result.getDevice(); |
| | | // // 关键:获取设备的蓝牙类型 |
| | | // int bluetoothType = device.getType(); |
| | | // switch (bluetoothType) { |
| | | // case BluetoothDevice.DEVICE_TYPE_CLASSIC: |
| | | // Log.d(TAG, "📞 经典蓝牙设备:" + device.getName()); |
| | | // // 例如:蓝牙耳机、车载音响 |
| | | // break; |
| | | // |
| | | // case BluetoothDevice.DEVICE_TYPE_LE: |
| | | // Log.d(TAG, "💡 低功耗蓝牙设备:" + device.getName()); |
| | | // // 例如:手环、智能灯、你的 HDL 设备 |
| | | // break; |
| | | // |
| | | // case BluetoothDevice.DEVICE_TYPE_DUAL: |
| | | // Log.d(TAG, "🔄 双模设备(支持两种):" + device.getName()); |
| | | // // 例如:高端蓝牙耳机 |
| | | // break; |
| | | // |
| | | // case BluetoothDevice.DEVICE_TYPE_UNKNOWN: |
| | | // Log.d(TAG, "❓ 未知类型"); |
| | | // break; |
| | | // } |
| | | |
| | | Log.d("===", "onDeviceFound: " + Objects.requireNonNull(result.getScanRecord()).getDeviceName()); |
| | | // if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { |
| | | // if (ActivityCompat.checkSelfPermission(context, Manifest.permission.BLUETOOTH_CONNECT) != PackageManager.PERMISSION_GRANTED) { |
| | |
| | | int rssi = result.getRssi(); |
| | | byte[] scanRecord = result.getScanRecord() != null ? result.getScanRecord().getBytes() : null; |
| | | if (scanListener != null) { |
| | | callbackHandler.post(() -> scanListener.onDeviceFound(device, rssi, scanRecord)); |
| | | callbackHandler.post(() -> scanListener.onDeviceFound(device, rssi, scanRecord, result)); |
| | | } |
| | | } |
| | | |
| | |
| | | // ==================== 连接相关 ==================== |
| | | |
| | | /** |
| | | * 连接指定的 BLE 设备(系统默认连接超时30秒 ) |
| | | * 连接指定的 BLE 设备(系统默认连接超时30秒) |
| | | * |
| | | * @param deviceAddress 目标设备BluetoothDevice |
| | | * @param listener 连接状态回调 |
| | |
| | | callbackHandler.post(() -> this.connectListener.onConnectionFailed("No permissions")); // 没有权限 |
| | | return; |
| | | } |
| | | |
| | | if (bluetoothGatt != null) { |
| | | bluetoothGatt.disconnect(); // 先断开连接 |
| | | bluetoothGatt.close(); |
| | | bluetoothGatt = null; |
| | | Log.d(TAG, "Connecting to 先断开旧蓝牙连接"); |
| | | } |
| | | BluetoothDevice device = BluetoothAdapter.getDefaultAdapter().getRemoteDevice(deviceAddress); |
| | | // 自动连接 = false,避免系统缓存连接 |
| | |
| | | } |
| | | isConnected = false; |
| | | targetService = null; |
| | | ssidCharacteristic = null; |
| | | pwdCharacteristic = null; |
| | | characteristic = null; |
| | | notifyCharacteristic = null; |
| | | } |
| | | |
| | |
| | | gatt.discoverServices(); |
| | | } else if (newState == BluetoothProfile.STATE_DISCONNECTED) { |
| | | isConnected = false; |
| | | // if (bluetoothGatt != null) { |
| | | // bluetoothGatt.close(); |
| | | // bluetoothGatt = null; |
| | | // } |
| | | if (connectListener != null) { |
| | | callbackHandler.post(() -> connectListener.onDisconnected()); |
| | | } |
| | | |
| | | Log.d(TAG, "Connected to GATT server, onDisconnected"); |
| | | }// |
| | | } |
| | | |
| | |
| | | notifyConnectionFailed("Service not found"); |
| | | return; |
| | | } |
| | | ssidCharacteristic = targetService.getCharacteristic(ssidCharUuid); |
| | | pwdCharacteristic = targetService.getCharacteristic(pwdCharUuid); |
| | | characteristic = targetService.getCharacteristic(charUuid); |
| | | notifyCharacteristic = targetService.getCharacteristic(notifyCharUuid); |
| | | if (ssidCharacteristic == null || pwdCharacteristic == null) { |
| | | boolean mtuRequestResult = gatt.requestMtu(512); // 默认设置请求最大 MTU,有设备可能不支持 |
| | | Log.d(TAG, "requestMtu(512) 结果:" + mtuRequestResult); |
| | | |
| | | if (characteristic == null) { |
| | | Log.e(TAG, "Required characteristics not found"); |
| | | notifyConnectionFailed("Characteristics not found"); |
| | | return; |
| | | } |
| | | |
| | | Log.e(TAG, "Required characteristics find"); |
| | | // 如果支持通知,可以开启通知(可选) |
| | | if (notifyCharacteristic != null) { |
| | | enableNotification(notifyCharacteristic); |
| | |
| | | boolean success = status == BluetoothGatt.GATT_SUCCESS; |
| | | if (!success) { |
| | | Log.e(TAG, "Write failed, status: " + status); |
| | | writeState = 0; |
| | | if (writeListener != null) { |
| | | callbackHandler.post(() -> writeListener.onWriteFailed(status)); |
| | | } |
| | | return; |
| | | } |
| | | |
| | | if (writeState == 1 && characteristic.getUuid().equals(ssidCharUuid)) { |
| | | // SSID 写入成功,开始写密码 |
| | | writeState = 2; |
| | | Log.d(TAG, "onCharacteristicWrite: 写入成功回复"); |
| | | if (characteristic.getUuid().equals(charUuid)) { |
| | | //写入成功 |
| | | if (writeListener != null) { |
| | | callbackHandler.post(() -> writeListener.onSsidWriteSuccess()); |
| | | } |
| | | writeCharacteristic(pwdCharacteristic, pwdToWrite); |
| | | } else if (writeState == 2 && characteristic.getUuid().equals(pwdCharUuid)) { |
| | | // 密码写入成功 |
| | | writeState = 0; |
| | | if (writeListener != null) { |
| | | callbackHandler.post(() -> writeListener.onPasswordWriteSuccess()); |
| | | callbackHandler.post(() -> writeListener.onWriteSuccess()); |
| | | callbackHandler.post(() -> writeListener.onWriteComplete(true)); |
| | | } |
| | | } |
| | |
| | | |
| | | @Override |
| | | public void onCharacteristicChanged(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic) { |
| | | Log.d(TAG, "onCharacteristicChanged: " + characteristic.getUuid()); |
| | | |
| | | //收到蓝牙回复的数 |
| | | if (characteristic.getUuid().equals(notifyCharUuid) && writeListener != null) { |
| | | byte[] value = characteristic.getValue(); |
| | | // Log.d(TAG, "打印十六进制: " + bytesToHexString(value)); |
| | | String response = new String(value, StandardCharsets.UTF_8); |
| | | callbackHandler.post(() -> writeListener.onDeviceResponse(response)); |
| | | } |
| | |
| | | |
| | | @Override |
| | | public void onDescriptorWrite(BluetoothGatt gatt, BluetoothGattDescriptor descriptor, int status) { |
| | | Log.d(TAG, "onDescriptorWrite: status=" + status + |
| | | ", descriptor=" + descriptor.getUuid()); |
| | | // 通知描述符写入完成,可以忽略 |
| | | // if (status == BluetoothGatt.GATT_SUCCESS) { |
| | | // Log.d(TAG, "通知开启成功,可以接收设备响应了"); |
| | | // } else { |
| | | // Log.e(TAG, "通知开启失败:" + status); |
| | | // if (writeListener != null) { |
| | | // callbackHandler.post(() -> writeListener.onWriteFailed(status)); |
| | | // } |
| | | // } |
| | | if (status == BluetoothGatt.GATT_SUCCESS) { |
| | | Log.d(TAG, "通知开启成功,可以接收设备响应了"); |
| | | } else { |
| | | Log.e(TAG, "通知开启失败:" + status); |
| | | if (writeListener != null) { |
| | | callbackHandler.post(() -> writeListener.onWriteFailed(status)); |
| | | } |
| | | } |
| | | } |
| | | |
| | | @Override |
| | | public void onMtuChanged(BluetoothGatt gatt, int mtu, int status) { |
| | | //todo 后面兼用mtc分包的,目前默认设置512字节 |
| | | if (status == BluetoothGatt.GATT_SUCCESS) { |
| | | // currentMtu = mtu; |
| | | Log.d(TAG, "✅ MTU 协商成功:当前 MTU = " + mtu + " 字节"); |
| | | Log.d(TAG, " 有效数据长度 = " + (mtu - 5) + " 字节"); |
| | | |
| | | // // MTU 设置成功后,标记连接成功 |
| | | // isConnected = true; |
| | | // if (connectListener != null) { |
| | | // callbackHandler.post(() -> { |
| | | // Log.d(TAG, "通知连接成功回调"); |
| | | // connectListener.onConnected(); |
| | | // }); |
| | | // } |
| | | } else { |
| | | // Log.w(TAG, "⚠️ MTU 协商失败,status: " + status + ",使用默认 MTU=23"); |
| | | //// currentMtu = 23; |
| | | // // 即使 MTU 失败,也认为连接成功(使用默认 MTU) |
| | | // isConnected = true; |
| | | // if (connectListener != null) { |
| | | // callbackHandler.post(() -> connectListener.onConnected()); |
| | | // } |
| | | } |
| | | } |
| | | |
| | | |
| | | }; |
| | | |
| | | @RequiresPermission(Manifest.permission.BLUETOOTH_CONNECT) |
| | |
| | | // 第 1 步:在协议栈层面开启通知 |
| | | boolean enabled = bluetoothGatt.setCharacteristicNotification(characteristic, true); |
| | | if (enabled) { |
| | | // 第 2 步:获取客户端特征配置描述符 (CCCD) |
| | | // ↑ 00002902 是标准的 CCCD 描述符 UUID |
| | | BluetoothGattDescriptor descriptor = characteristic.getDescriptor( |
| | | UUID.fromString("00002902-0000-1000-8000-00805f9b34fb")); |
| | | if (descriptor != null) { |
| | | // 第 3 步:设置描述符的值为 ENABLE_NOTIFICATION_VALUE, |
| | | // ↑ 这个值是 byte[]{0x01, 0x00},告诉设备端"我要订阅通知" |
| | | descriptor.setValue(BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE); |
| | | // 第 4 步:写入描述符到设备 |
| | | //↑ 只有写入成功后,告诉设备端,设备才会开始发送通知, |
| | | bluetoothGatt.writeDescriptor(descriptor); |
| | | } |
| | | // 根据 Android 版本和设备厂商动态调整延迟 |
| | | int delayMs = calculateNotificationDelay(); |
| | | callbackHandler.postDelayed(() -> { |
| | | |
| | | // 第 2 步:获取客户端特征配置描述符 (CCCD) |
| | | // ↑ 00002902 是标准的 CCCD 描述符 UUID |
| | | BluetoothGattDescriptor descriptor = characteristic.getDescriptor( |
| | | UUID.fromString("00002902-0000-1000-8000-00805f9b34fb")); |
| | | if (descriptor != null) { |
| | | // // 第 3 步:设置描述符的值 |
| | | // // 先检查特征属性,决定使用 NOTIFY 还是 INDICATE |
| | | int charProp = characteristic.getProperties(); |
| | | if ((charProp & BluetoothGattCharacteristic.PROPERTY_NOTIFY) > 0) { |
| | | descriptor.setValue(BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE); |
| | | Log.d(TAG, "使用 NOTIFICATION 模式"); |
| | | } else if ((charProp & BluetoothGattCharacteristic.PROPERTY_INDICATE) > 0) { |
| | | descriptor.setValue(BluetoothGattDescriptor.ENABLE_INDICATION_VALUE); |
| | | Log.d(TAG, "使用 INDICATION 模式"); |
| | | } else { |
| | | Log.e(TAG, "该特征不支持通知/指示"); |
| | | return; |
| | | } |
| | | // 第 3 步:设置描述符的值为 ENABLE_NOTIFICATION_VALUE, |
| | | // ↑ 这个值是 byte[]{0x01, 0x00},告诉设备端"我要订阅通知" |
| | | // descriptor.setValue(BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE); |
| | | // 第 4 步:写入描述符到设备 |
| | | //↑ 只有写入成功后,告诉设备端,设备才会开始发送通知, |
| | | bluetoothGatt.writeDescriptor(descriptor); |
| | | |
| | | Log.d(TAG, "writeDescriptor success"); |
| | | } |
| | | }, delayMs); |
| | | } |
| | | } |
| | | |
| | |
| | | // ==================== 写入凭证 ==================== |
| | | |
| | | /** |
| | | * 写入 Wi-Fi SSID 和密码(必须在连接成功后调用) |
| | | * 写入数据(必须在连接成功后调用) |
| | | * |
| | | * @param ssid Wi-Fi SSID |
| | | * @param password Wi-Fi 密码 |
| | | * @param data 数据 |
| | | * @param listener 写入结果回调 |
| | | */ |
| | | @RequiresPermission(Manifest.permission.BLUETOOTH_CONNECT) |
| | | public void writeCredentials(String ssid, String password, WriteListener listener) { |
| | | public void writeCredentials(String data, WriteListener listener) { |
| | | Log.d(TAG, "writeCredentials :准备写入"); |
| | | this.writeListener = listener; |
| | | if (!isConnected || ssidCharacteristic == null || pwdCharacteristic == null) { |
| | | if (!isConnected || characteristic == null) { |
| | | if (listener != null) { |
| | | callbackHandler.post(() -> listener.onWriteFailed(-1)); // 自定义错误码 |
| | | } |
| | | return; |
| | | } |
| | | this.ssidToWrite = ssid; |
| | | this.pwdToWrite = password; |
| | | |
| | | // 开始写 SSID |
| | | writeState = 1; |
| | | writeCharacteristic(ssidCharacteristic, ssid); |
| | | // 开始写 |
| | | writeCharacteristic(characteristic, data); |
| | | } |
| | | |
| | | @RequiresPermission(Manifest.permission.BLUETOOTH_CONNECT) |
| | | private void writeCharacteristic(BluetoothGattCharacteristic characteristic, String value) { |
| | | |
| | | Log.d(TAG, "writeCharacteristic :开始写入"); |
| | | if (!this.getMissingPermissions().isEmpty()) { |
| | | callbackHandler.post(() -> this.writeListener.onWriteFailed(-2)); // 没有权限 |
| | | return; |
| | |
| | | characteristic.setValue(value.getBytes(StandardCharsets.UTF_8)); |
| | | characteristic.setWriteType(BluetoothGattCharacteristic.WRITE_TYPE_DEFAULT); |
| | | bluetoothGatt.writeCharacteristic(characteristic); |
| | | Log.d(TAG, "writeCharacteristic :开始完成"); |
| | | } |
| | | |
| | | |
| | | |
| | | /** |
| | | * 根据 Android 版本和设备厂商计算延迟时间 |
| | | * Android 10/11 不需要延迟 原因:系统处理快,无严格检查 |
| | | * Android 12 需要100-200ms延迟 原因:系统需要时间准备 BLE 协议栈 |
| | | * Android 13 需要50-100ms延迟 原因:优化后速度稍快 |
| | | * Android 14 需要50ms延迟 原因:进一步优化 |
| | | */ |
| | | private int calculateNotificationDelay() { |
| | | int sdkVersion = Build.VERSION.SDK_INT; |
| | | |
| | | // Android 12 (SDK 31) 需要最长延迟 |
| | | if (sdkVersion == Build.VERSION_CODES.S) { |
| | | // 小米/红米设备可能需要更长延迟 |
| | | if ("Xiaomi".equals(Build.MANUFACTURER)) { |
| | | Log.d(TAG, "检测到小米设备,增加延迟到 200ms"); |
| | | return 200; |
| | | } |
| | | // 其他 Android 12 设备 |
| | | return 100; |
| | | } |
| | | |
| | | // Android 13+ 优化后延迟可以短一些 |
| | | if (sdkVersion >= Build.VERSION_CODES.TIRAMISU) { |
| | | return 50; |
| | | } |
| | | |
| | | // Android 11 及以下不需要延迟 |
| | | return 0; |
| | | } |
| | | |
| | | // ==================== 工具方法 ==================== |
| | |
| | | /** |
| | | * 检查蓝牙是否开启 |
| | | */ |
| | | private boolean checkBluetoothEnabled() { |
| | | public boolean checkBluetoothEnabled() { |
| | | return bluetoothAdapter != null && bluetoothAdapter.isEnabled(); |
| | | } |
| | | |
| | |
| | | /** |
| | | * 发现设备(可能多次回调) |
| | | */ |
| | | void onDeviceFound(BluetoothDevice device, int rssi, byte[] scanRecord); |
| | | void onDeviceFound(BluetoothDevice device, int rssi, byte[] scanRecord, ScanResult scanResult); |
| | | |
| | | /** |
| | | * 扫描失败 |
| | |
| | | |
| | | public interface WriteListener { |
| | | /** |
| | | * SSID 写入成功 |
| | | * 写入成功 |
| | | */ |
| | | void onSsidWriteSuccess(); |
| | | |
| | | /** |
| | | * 密码写入成功 |
| | | */ |
| | | void onPasswordWriteSuccess(); |
| | | void onWriteSuccess(); |
| | | |
| | | /** |
| | | * 全部写入完成(成功为 true) |