[Android速通]蓝牙通信

/ 0评 / 0

其实我速通这个Android,目的也是做这个,毕竟网上找到的太丑了,自己做又不会写,只能速通一下.

我会假设你同时有单片机知识,我用的是CH579作为蓝牙的外设目标,先对单片机关键代码进行一下读.

CH57X_BLEInit是初始化函数,具体看附属例程的文档有写,都是中文的了就不再解释了.然后是Peripheral_Init,看两个数据结构.

// GAP - 扫描响应数据(最大31字节)
static uint8 scanRspData[] = 
{
  // 完整设备名称部分
  0x12,   // 此数据段的长度(18字节)
  GAP_ADTYPE_LOCAL_NAME_COMPLETE,  // 表示这是完整的设备名称
  'S', 'i', 'm', 'p', 'l', 'e', ' ', 'P', 'e', 'r', 'i', 'p', 'h', 'e', 'r', 'a', 'l', // 设备名称:"Simple Peripheral"
  
  // 连接间隔范围参数
  0x05,   // 此数据段的长度(5字节)
  GAP_ADTYPE_SLAVE_CONN_INTERVAL_RANGE,  // 从设备连接间隔范围
  LO_UINT16(DEFAULT_DESIRED_MIN_CONN_INTERVAL),  // 期望的最小连接间隔(低字节),默认100ms
  HI_UINT16(DEFAULT_DESIRED_MIN_CONN_INTERVAL),  // 期望的最小连接间隔(高字节)
  LO_UINT16(DEFAULT_DESIRED_MAX_CONN_INTERVAL),  // 期望的最大连接间隔(低字节),默认1s
  HI_UINT16(DEFAULT_DESIRED_MAX_CONN_INTERVAL),  // 期望的最大连接间隔(高字节)
  
  // 发射功率级别
  0x02,   // 此数据段的长度(2字节)
  GAP_ADTYPE_POWER_LEVEL,  // 发射功率级别类型
  0       // 0dBm的发射功率
};

// GAP - 广播数据(最大31字节,通常保持较短以节省功耗)
static uint8 advertData[] =
{
  // 标志位:设置设备使用有限可发现模式
  // (每次广播30秒)而不是通用可发现模式(无限期广播)
  0x02,   // 此数据段的长度(2字节)
  GAP_ADTYPE_FLAGS,  // 标志位类型
  DEFAULT_DISCOVERABLE_MODE | GAP_ADTYPE_FLAGS_BREDR_NOT_SUPPORTED, // 有限可发现模式+不支持BR/EDR(传统蓝牙)
  
  // 服务UUID,通知中央设备此外设包含的服务
  0x03,   // 此数据段的长度(3字节)
  GAP_ADTYPE_16BIT_MORE,  // 表示有更多16位UUID(但不是全部)
  LO_UINT16(SIMPLEPROFILE_SERV_UUID),  // 简单配置文件服务UUID(低字节)
  HI_UINT16(SIMPLEPROFILE_SERV_UUID)   // 简单配置文件服务UUID(高字节)
};

事实上除了GAP_ADTYPE_FLAGS,所有字段都不是强制的,具体可以看蓝牙SIG标准文档.其中SIMPLEPROFILE_SERV_UUID是自定义的一个UUID,实际上我们用16位也是完全没问题的.

然后定义几个写的属性.但是实际写不是调用这个,而是GATT描述里强调的.

// 设置几个属性默认值,我们这次只用了charValue1,即实际会写到simpleProfileChar1

uint8 charValue1[SIMPLEPROFILE_CHAR1_LEN] = { 1 };
uint8 charValue2[SIMPLEPROFILE_CHAR2_LEN] = { 2 };
uint8 charValue3[SIMPLEPROFILE_CHAR3_LEN] = { 3 };
uint8 charValue4[SIMPLEPROFILE_CHAR4_LEN] = { 4 };
uint8 charValue5[SIMPLEPROFILE_CHAR5_LEN] = { 1, 2, 3, 4, 5 };
    
SimpleProfile_SetParameter( SIMPLEPROFILE_CHAR1, SIMPLEPROFILE_CHAR1_LEN, charValue1 );
SimpleProfile_SetParameter( SIMPLEPROFILE_CHAR2, SIMPLEPROFILE_CHAR2_LEN, charValue2 );
SimpleProfile_SetParameter( SIMPLEPROFILE_CHAR3, SIMPLEPROFILE_CHAR3_LEN, charValue3 );
SimpleProfile_SetParameter( SIMPLEPROFILE_CHAR4, SIMPLEPROFILE_CHAR4_LEN, charValue4 );
SimpleProfile_SetParameter( SIMPLEPROFILE_CHAR5, SIMPLEPROFILE_CHAR5_LEN, charValue5 );

看一下关于这个Serivice的Props.

static gattAttribute_t simpleProfileAttrTbl[] = 
{
  // Simple Profile Service
  { 
    { ATT_BT_UUID_SIZE, primaryServiceUUID }, /* type */
    GATT_PERMIT_READ,                         /* permissions */
    0,                                        /* handle */
    (uint8 *)&simpleProfileService            /* pValue */
  },

    // Characteristic 1 Declaration
    { 
      { ATT_BT_UUID_SIZE, characterUUID },
      GATT_PERMIT_READ, 
      0,
      &simpleProfileChar1Props 
    },

      // Characteristic Value 1
      { 
        { ATT_BT_UUID_SIZE, simpleProfilechar1UUID },
        GATT_PERMIT_READ | GATT_PERMIT_WRITE, 
        0, 
        simpleProfileChar1
      },

      // Characteristic 1 User Description
      { 
        { ATT_BT_UUID_SIZE, charUserDescUUID },
        GATT_PERMIT_READ, 
        0, 
        simpleProfileChar1UserDesp
      },      

    // Characteristic 2 Declaration
    { 
      { ATT_BT_UUID_SIZE, characterUUID },
      GATT_PERMIT_READ, 
      0,
      &simpleProfileChar2Props 
    },

      // Characteristic Value 2
      { 
        { ATT_BT_UUID_SIZE, simpleProfilechar2UUID },
        GATT_PERMIT_READ, 
        0, 
        simpleProfileChar2 
      },

      // Characteristic 2 User Description
      { 
        { ATT_BT_UUID_SIZE, charUserDescUUID },
        GATT_PERMIT_READ, 
        0, 
        simpleProfileChar2UserDesp 
      },           
    // 省略...
};

可以通过NRF Connect分析,和描述的一样.

红框位置可以读写,正好匹配simpleProfileChar1,我定义他的长度是20,因为我们是自定义程序,因此UUID不要碰撞别人可以随便写.暂时不说修改的事情,毕竟我这里不是重点讨论蓝牙.

先写一个主界面.

<LinearLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:id="@+id/main"
    android:orientation="vertical"
    android:padding="16dp">

    <ScrollView
        android:layout_width="match_parent"
        android:layout_height="0dp"
        android:layout_weight="1">

        <TextView
            android:id="@+id/device_info"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:textSize="16sp"/>
    </ScrollView>

    <EditText
        android:id="@+id/input_edit"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:hint="输入要发送的数据"/>

    <Button
        android:id="@+id/send_btn"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="发送数据"/>

    <Button
        android:id="@+id/notify_btn"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="开启通知"/>

</LinearLayout>

代码逻辑几个注意点.

代码相对比较简单,无非是普通读写,和Socket也没什么差别.

public class MainActivity extends AppCompatActivity {
    private static final String TAG = "HeartRateBLE";
    private BluetoothAdapter bluetoothAdapter;
    private BluetoothGatt bluetoothGatt;
    private BluetoothDevice targetDevice;

    private boolean isNotifyEnabled = false;
    private Queue<BluetoothGattCharacteristic> readQueue = new LinkedList<>();

    // 省略组件变量

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        // 省略固定初始化

        // 初始化UI组件

        // 初始化蓝牙适配器
        final BluetoothManager bluetoothManager = (BluetoothManager) getSystemService(Context.BLUETOOTH_SERVICE);
        bluetoothAdapter = bluetoothManager.getAdapter();

        // 设置按钮点击事件
        sendButton.setOnClickListener(v -> sendData());
        notifyButton.setOnClickListener(v -> toggleNotify());

        if (PermissionHelper.checkAndRequestPermissions(this)) {
            // 所有权限都已授予
            startBluetoothProcess();
        } else {
            // 省略权限相关代码
        }
    }

    @Override
    public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions,@NonNull int[] grantResults) {
        super.onRequestPermissionsResult(requestCode, permissions, grantResults);

        // 省略权限相关代码
    }

    private void startBluetoothProcess() {
        if (!checkBluetooth()) return;
        startScan();
    }

    private boolean checkBluetooth() {
        if (bluetoothAdapter == null || !bluetoothAdapter.isEnabled()) {
            // 按理说可以启动Activity来让用户启动.
            Toast.makeText(this, "请先启用蓝牙", Toast.LENGTH_SHORT).show();
            return false;
        }
        return true;
    }

    private void startScan() {
        // 开始BLE搜索
        bluetoothAdapter.startLeScan(leScanCallback);
    }

    private final BluetoothAdapter.LeScanCallback leScanCallback = new BluetoothAdapter.LeScanCallback() {
        @Override
        public void onLeScan(BluetoothDevice device, int rssi, byte[] scanRecord) {
            if (device.getName() != null && device.getName().contains("Simple")) {
                Toast.makeText(MainActivity.this,String.format("找到设备:%s",device.getAddress().toString()),Toast.LENGTH_LONG).show();
                targetDevice = device;
                bluetoothAdapter.stopLeScan(leScanCallback);
                connectToDevice();
            }
        }
    };

    private void connectToDevice() {
        if (targetDevice == null) return;
        bluetoothGatt = targetDevice.connectGatt(this, false, gattCallback);
    }

    private final BluetoothGattCallback gattCallback = new BluetoothGattCallback() {
        @Override
        public void onConnectionStateChange(BluetoothGatt gatt, int status, int newState) {
            if (newState == BluetoothProfile.STATE_CONNECTED) {
                gatt.discoverServices();
            }
        }

        @Override
        public void onServicesDiscovered(BluetoothGatt gatt, int status) {
            if (status == BluetoothGatt.GATT_SUCCESS) {
                processServices(gatt.getServices());
            }
        }

        @Override
        public void onCharacteristicRead(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic, int status) {
            if (status == BluetoothGatt.GATT_SUCCESS) {
                updateCharacteristicUI(characteristic);
            }
            // 无论成功与否都继续读取下一个,为什么要这么做,主要是不能一次性连续读,得返回一个读一个.
            readNextCharacteristic();
        }

        @Override
        public void onCharacteristicChanged(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic) {
            receiveNotifyData(characteristic);
        }
    };

    private void processServices(List<BluetoothGattService> services) {
        for (BluetoothGattService service : services) {
            String serviceUuid = service.getUuid().toString().toUpperCase();

            if (serviceUuid.equals("0000180A-0000-1000-8000-00805F9B34FB")) {
                readDeviceInfoCharacteristics(service);
            } else if (serviceUuid.equals("0000FFE0-0000-1000-8000-00805F9B34FB")) {
                setupCommunicationCharacteristics(service);
            }
        }
    }

    private void readDeviceInfoCharacteristics(BluetoothGattService service) {
        // 清空旧队列
        readQueue.clear();

        // 填充可读特征到队列
        for (BluetoothGattCharacteristic characteristic : service.getCharacteristics()) {
            if ((characteristic.getProperties() & BluetoothGattCharacteristic.PROPERTY_READ) != 0) {
                readQueue.add(characteristic);
            }
        }

        // 开始顺序读取
        readNextCharacteristic();
    }

    private void readNextCharacteristic() {
        if (!readQueue.isEmpty()) {
            BluetoothGattCharacteristic next = readQueue.poll();
            if (!bluetoothGatt.readCharacteristic(next)) {
                // 读取失败时继续下一个
                readNextCharacteristic();
            }
        }
    }

    private void updateCharacteristicUI(BluetoothGattCharacteristic characteristic) {
        runOnUiThread(() -> {
            String uuid = characteristic.getUuid().toString().toUpperCase();
            String value = new String(characteristic.getValue(), StandardCharsets.UTF_8);

            String info = deviceInfoText.getText().toString();
            info += "\n" + getCharacteristicName(uuid) + ": " + value;
            deviceInfoText.setText(info);
        });
    }

    private String getCharacteristicName(String uuid) {
        switch (uuid.substring(4, 8)) {
            case "2A23": return "设备ID";
            case "2A24": return "型号ID";
            case "2A25": return "序列号";
            case "2A26": return "固件版本";
            case "2A27": return "硬件版本";
            case "2A28": return "软件版本";
            case "2A29": return "制造商名称";
            default: return "未知特征";
        }
    }

    private BluetoothGattCharacteristic writeCharacteristic;
    private BluetoothGattCharacteristic notifyCharacteristic;

    private void setupCommunicationCharacteristics(BluetoothGattService service) {
        for (BluetoothGattCharacteristic characteristic : service.getCharacteristics()) {
            String uuid = characteristic.getUuid().toString().toUpperCase();

            if (uuid.equals("0000FFE1-0000-1000-8000-00805F9B34FB")) {
                writeCharacteristic = characteristic;
            } else if (uuid.equals("0000FFE4-0000-1000-8000-00805F9B34FB")) {
                notifyCharacteristic = characteristic;
            }
        }
    }

    private void sendData() {
        if (writeCharacteristic == null) return;

        String input = inputEditText.getText().toString();
        writeCharacteristic.setValue(input.getBytes(StandardCharsets.UTF_8));
        bluetoothGatt.writeCharacteristic(writeCharacteristic);
    }

    private void toggleNotify() {
        if (notifyCharacteristic == null) return;

        isNotifyEnabled = !isNotifyEnabled;
        setNotificationEnabled(notifyCharacteristic, isNotifyEnabled);
        notifyButton.setText(isNotifyEnabled ? "关闭通知" : "开启通知");
    }

    private void setNotificationEnabled(BluetoothGattCharacteristic characteristic, boolean enable) {
        bluetoothGatt.setCharacteristicNotification(characteristic, enable);

        BluetoothGattDescriptor descriptor = characteristic.getDescriptor(
                UUID.fromString("00002902-0000-1000-8000-00805F9B34FB"));
        if (descriptor != null) {
            descriptor.setValue(enable ?
                    BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE :
                    BluetoothGattDescriptor.DISABLE_NOTIFICATION_VALUE);
            bluetoothGatt.writeDescriptor(descriptor);
        }
    }

    private void receiveNotifyData(BluetoothGattCharacteristic characteristic) {

        runOnUiThread(() -> {
            String info = deviceInfoText.getText().toString();
            info += "\n收到通知: [" + characteristic.getUuid().toString() + "]" + bytesToHex(characteristic.getValue());
            deviceInfoText.setText(info);
        });
    }

    @Override
    protected void onDestroy() {
        super.onDestroy();
        if (bluetoothGatt != null) {
            bluetoothGatt.disconnect();
            bluetoothGatt.close();
        }
    }

    // 字节数组转十六进制工具方法
    private static String bytesToHex(byte[] bytes) {
        if (bytes == null) return "null";
        StringBuilder sb = new StringBuilder();
        for (byte b : bytes) {
            sb.append(String.format("%02X ", b));
        }
        return sb.toString().trim(); // 示例输出:"A0 B4 FF"
    }
}

最后界面效果


后话,有时候就一个收发通信就够了,精简后的GATT.

// Position of simpleProfilechar4 value in attribute array
#define SIMPLEPROFILE_CHAR4_VALUE_POS 4

static gattAttribute_t simpleProfileAttrTbl[] =
    {
        // Simple Profile Service
        {
            {ATT_BT_UUID_SIZE, primaryServiceUUID}, /* type */
            GATT_PERMIT_READ,                       /* permissions */
            0,                                      /* handle */
            (uint8 *)&simpleProfileService          /* pValue */
        },

        // Characteristic 1 Declaration
        {
            {ATT_BT_UUID_SIZE, characterUUID},
            GATT_PERMIT_READ,
            0,
            &simpleProfileChar1Props},

        // Characteristic Value 1
        {
            {ATT_BT_UUID_SIZE, simpleProfilechar1UUID},
            GATT_PERMIT_READ | GATT_PERMIT_WRITE,
            0,
            simpleProfileChar1},

        // Characteristic 4 Declaration
        {
            {ATT_BT_UUID_SIZE, characterUUID},
            GATT_PERMIT_READ,
            0,
            &simpleProfileChar4Props},

        // Characteristic Value 4
        {
            {ATT_BT_UUID_SIZE, simpleProfilechar4UUID},
            0,
            0,
            simpleProfileChar4},

        // Characteristic 4 configuration
        {
            {ATT_BT_UUID_SIZE, clientCharCfgUUID},
            GATT_PERMIT_READ | GATT_PERMIT_WRITE,
            0,
            (uint8 *)simpleProfileChar4Config},

};

通信也是正常的.

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注