其实我速通这个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},
};
通信也是正常的.
