BLE
BLE(Bluetooth Low Energy,低功耗藍芽),是藍芽4.0版本中加入的一個特性。
Android在4.3版(API 18)時,開始支援BLE。以下是BLE的一些特性:
- 和經典藍芽(Bluetooth 2.0、3.0)相比,具有省電的優勢。
- 體積小
- 價格便宜
- iOS、Android、Windows都支援
關於BLE更詳細的介紹,請參考維基百科。
開發APP部分
前置作業
要在Android APP上使用BLE的API,必須要開啟以下幾個權限:
- android.permission.BLUETOOTH
- android.permission.BLUETOOTH_ADMIN
- android.permission.ACCESS_FINE_LOCATION
除此之外,還需要在Android Manifest.xml中聲明使用到的裝置功能:
- android.hardware.bluetooth_le
由於,ACCESS_FINE_LOCATION屬於危險權限之一(詳細請看我的另一篇文章),因此需要在程式裡加入動態申請權限的功能,如下:
前置作業到目前為止都已經完成,接著就可以開始使用BLE了。
搜尋裝置
要和BLE裝置連線,必須要知道該裝置的硬體識別碼,也就是MAC Address。
由於我們並不知道裝置的硬體識別碼是多少,因此必須要先經過搜尋的階段,以下是搜尋並以列表方式呈現裝置清單的示範:
public class ScanActivity extends AppCompatActivity {
private LeDeviceListAdapter mLeDeviceListAdapter;
private BluetoothAdapter mBluetoothAdapter;
private boolean isScanning;
private Handler mHandler;
private static final int REQUEST_LOCATION = 0x0;
private static final int REQUEST_ENABLE_BT = 0x1;
// 10 秒後停止掃描
private static final long SCAN_INTERVAL = 10000;
// UI 元件
private Button btnScan;
private ListView listView;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_scan);
mHandler = new Handler();
btnScan = (Button) findViewById(R.id.button_scan);
listView = (ListView) findViewById(R.id.list_view);
// 檢查設備是否支援 BLE
if (!getPackageManager().hasSystemFeature(PackageManager.FEATURE_BLUETOOTH_LE)) {
Toast.makeText(this, "你的裝置不支援 BLE", Toast.LENGTH_SHORT).show();
finish();
}
// 初始化藍芽 Adapter
final BluetoothManager bluetoothMgr = (BluetoothManager) getSystemService(Context.BLUETOOTH_SERVICE);
mBluetoothAdapter = bluetoothMgr.getAdapter();
if (mBluetoothAdapter == null) {
Toast.makeText(this, "你的裝置部支援藍芽", Toast.LENGTH_SHORT).show();
finish();
}
// 初始化掃描裝置清單
mLeDeviceListAdapter = new LeDeviceListAdapter();
listView.setAdapter(mLeDeviceListAdapter);
listView.setOnItemClickListener(new AdapterView.OnItemClickListener() {
@Override
public void onItemClick(AdapterView<?> adapterView, View view, int position, long l) {
final BluetoothDevice device = mLeDeviceListAdapter.getDevice(position);
if (device == null) return;
final Intent intent = new Intent(ScanActivity.this, MainActivity.class);
intent.putExtra(MainActivity.EXTRAS_DEVICE_NAME, device.getName());
intent.putExtra(MainActivity.EXTRAS_DEVICE_ADDRESS, device.getAddress());
if (isScanning) {
mBluetoothAdapter.stopLeScan(mLeScanCallback);
isScanning = false;
}
//startActivity(intent);
}
});
// 加入按鈕事件
btnScan.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
mLeDeviceListAdapter.clear();
scanLeDevice(true);
}
});
}
@Override
protected void onResume() {
super.onResume();
// 如果藍芽功能關閉,則打開藍芽設定
if (!mBluetoothAdapter.isEnabled()) {
if (!mBluetoothAdapter.isEnabled()) {
Intent enableBtIntent = new Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE);
startActivityForResult(enableBtIntent, REQUEST_ENABLE_BT);
}
}
checkLocationPermission();
}
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
// 如果使用者沒開啟藍芽,則離開APP
if (requestCode == REQUEST_ENABLE_BT && resultCode == Activity.RESULT_CANCELED) {
finish();
return;
}
super.onActivityResult(requestCode, resultCode, data);
}
@Override
protected void onPause() {
super.onPause();
scanLeDevice(false);
mLeDeviceListAdapter.clear();
}
@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
switch(requestCode) {
case REQUEST_LOCATION:
if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
// 已取得權限,在此做後續處理
mLeDeviceListAdapter.clear();
scanLeDevice(true);
} else {
// 使用者拒絕,顯示對話框告知
new AlertDialog.Builder(this)
.setMessage("要使用BLE功能,必須先允許定位權限")
.setPositiveButton("確定", null)
.show();
}
break;
default:
break;
}
}
private void checkLocationPermission() {
// 動態申請權限
int permission = ActivityCompat.checkSelfPermission(this, ACCESS_FINE_LOCATION);
if (permission != PackageManager.PERMISSION_GRANTED) {
// 未取得權限,詢問使用者是否授與
ActivityCompat.requestPermissions(
this,
new String[]{ACCESS_FINE_LOCATION},
REQUEST_LOCATION);
} else {
// 已取得權限,在此做後續處理
mLeDeviceListAdapter.clear();
scanLeDevice(true);
}
}
private void scanLeDevice(final boolean enable) {
if (enable) {
// 如果掃描時間到,停止掃描
mHandler.postDelayed(new Runnable() {
@Override
public void run() {
isScanning = false;
mBluetoothAdapter.stopLeScan(mLeScanCallback);
invalidateOptionsMenu();
}
}, SCAN_INTERVAL);
isScanning = true;
mBluetoothAdapter.startLeScan(mLeScanCallback);
} else {
isScanning = false;
mBluetoothAdapter.stopLeScan(mLeScanCallback);
}
invalidateOptionsMenu();
}
// 裝置清單 Adapter
private class LeDeviceListAdapter extends BaseAdapter {
private ArrayList<BluetoothDevice> mLeDevices;
private LayoutInflater mInflator;
public LeDeviceListAdapter() {
super();
mLeDevices = new ArrayList<>();
mInflator = ScanActivity.this.getLayoutInflater();
}
public void addDevice(BluetoothDevice device) {
if (!mLeDevices.contains(device)) {
mLeDevices.add(device);
}
}
public BluetoothDevice getDevice(int position) {
return mLeDevices.get(position);
}
public void clear() {
mLeDevices.clear();
}
@Override
public int getCount() {
return mLeDevices.size();
}
@Override
public Object getItem(int i) {
return mLeDevices.get(i);
}
@Override
public long getItemId(int i) {
return i;
}
@Override
public View getView(int i, View view, ViewGroup viewGroup) {
ViewHolder viewHolder;
// General ListView optimization code.
if (view == null) {
view = mInflator.inflate(R.layout.listitem_device, null);
viewHolder = new ViewHolder();
viewHolder.deviceAddress = view.findViewById(R.id.device_mac);
viewHolder.deviceName = view.findViewById(R.id.device_name);
view.setTag(viewHolder);
} else {
viewHolder = (ViewHolder) view.getTag();
}
BluetoothDevice device = mLeDevices.get(i);
final String deviceName = device.getName();
if (deviceName != null && deviceName.length() > 0)
viewHolder.deviceName.setText(deviceName);
else
viewHolder.deviceName.setText("未知的裝置");
viewHolder.deviceAddress.setText(device.getAddress());
return view;
}
}
// 裝置掃描 callback
private BluetoothAdapter.LeScanCallback mLeScanCallback =
new BluetoothAdapter.LeScanCallback() {
@Override
public void onLeScan(final BluetoothDevice device, int rssi, byte[] scanRecord) {
runOnUiThread(new Runnable() {
@Override
public void run() {
mLeDeviceListAdapter.addDevice(device);
mLeDeviceListAdapter.notifyDataSetChanged();
}
});
}
};
static class ViewHolder {
TextView deviceName;
TextView deviceAddress;
}
}
連接裝置並進行 GATT service 探索
搜尋到想要的裝置之後,接著就是連線的部分。我們可以透過點選清單中的裝置,並把該項的裝置資訊用intent傳遞給MainActivity,之後MainActivity便會啟動BluetoothLeService進行連線,如下:
MainActivity
public class MainActivity extends AppCompatActivity {
private final static String TAG = MainActivity.class.getSimpleName();
public static final String EXTRAS_DEVICE_NAME = "DEVICE_NAME";
public static final String EXTRAS_DEVICE_ADDRESS = "DEVICE_ADDRESS";
private String mDeviceName;
private String mDeviceAddress;
private BluetoothLeService mBluetoothLeService;
private final ServiceConnection mServiceConnection = new ServiceConnection() {
@Override
public void onServiceConnected(ComponentName componentName, IBinder service) {
mBluetoothLeService = ((BluetoothLeService.LocalBinder) service).getService();
if (!mBluetoothLeService.initialize()) {
Log.e(TAG, "無法初始化 Bluetooth 元件");
finish();
}
// Automatically connects to the device upon successful start-up initialization.
mBluetoothLeService.connect(mDeviceAddress);
}
@Override
public void onServiceDisconnected(ComponentName componentName) {
mBluetoothLeService = null;
}
};
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
final Intent intent = getIntent();
mDeviceName = intent.getStringExtra(EXTRAS_DEVICE_NAME);
mDeviceAddress = intent.getStringExtra(EXTRAS_DEVICE_ADDRESS);
Intent gattServiceIntent = new Intent(this, BluetoothLeService.class);
bindService(gattServiceIntent, mServiceConnection, BIND_AUTO_CREATE);
}
}
BluetoothLeService
public class BluetoothLeService extends Service {
private final static String TAG = BluetoothLeService.class.getSimpleName();
private BluetoothManager mBluetoothManager;
private BluetoothAdapter mBluetoothAdapter;
private String mBluetoothDeviceAddress;
private BluetoothGatt mBluetoothGatt;
private int mConnectionState = STATE_DISCONNECTED;
private static final int STATE_DISCONNECTED = 0;
private static final int STATE_CONNECTING = 1;
private static final int STATE_CONNECTED = 2;
public final static String ACTION_GATT_CONNECTED =
"com.example.bluetooth.le.ACTION_GATT_CONNECTED";
public final static String ACTION_GATT_DISCONNECTED =
"com.example.bluetooth.le.ACTION_GATT_DISCONNECTED";
public final static String ACTION_GATT_SERVICES_DISCOVERED =
"com.example.bluetooth.le.ACTION_GATT_SERVICES_DISCOVERED";
public final static String ACTION_DATA_AVAILABLE =
"com.example.bluetooth.le.ACTION_DATA_AVAILABLE";
public final static String EXTRA_DATA =
"com.example.bluetooth.le.EXTRA_DATA";
public final static UUID UUID_HEART_RATE_MEASUREMENT =
UUID.fromString(GattAttributes.HEART_RATE_MEASUREMENT);
// 實例化 GATT 事件 callback
private final BluetoothGattCallback mGattCallback = new BluetoothGattCallback() {
@Override
public void onConnectionStateChange(BluetoothGatt gatt, int status, int newState) {
String intentAction;
if (newState == BluetoothProfile.STATE_CONNECTED) {
intentAction = ACTION_GATT_CONNECTED;
mConnectionState = STATE_CONNECTED;
broadcastUpdate(intentAction);
Log.i(TAG, "連線到 BLE 裝置");
// 連線後探索 BLE 裝置提供的服務
Log.i(TAG, "開始探索:" +
mBluetoothGatt.discoverServices());
} else if (newState == BluetoothProfile.STATE_DISCONNECTED) {
intentAction = ACTION_GATT_DISCONNECTED;
mConnectionState = STATE_DISCONNECTED;
Log.i(TAG, "已斷線");
broadcastUpdate(intentAction);
}
}
@Override
public void onServicesDiscovered(BluetoothGatt gatt, int status) {
if (status == BluetoothGatt.GATT_SUCCESS) {
broadcastUpdate(ACTION_GATT_SERVICES_DISCOVERED);
} else {
Log.w(TAG, "已探索到服務: " + status);
}
}
@Override
public void onCharacteristicRead(BluetoothGatt gatt,
BluetoothGattCharacteristic characteristic,
int status) {
if (status == BluetoothGatt.GATT_SUCCESS) {
broadcastUpdate(ACTION_DATA_AVAILABLE, characteristic);
}
}
@Override
public void onCharacteristicChanged(BluetoothGatt gatt,
BluetoothGattCharacteristic characteristic) {
broadcastUpdate(ACTION_DATA_AVAILABLE, characteristic);
}
};
private void broadcastUpdate(final String action) {
final Intent intent = new Intent(action);
sendBroadcast(intent);
}
private void broadcastUpdate(final String action,
final BluetoothGattCharacteristic characteristic) {
final Intent intent = new Intent(action);
final byte[] data = characteristic.getValue();
if (data != null && data.length > 0) {
final StringBuilder stringBuilder = new StringBuilder(data.length);
for(byte byteChar : data)
stringBuilder.append(String.format("%02X ", byteChar));
intent.putExtra(EXTRA_DATA, new String(data) + "\n" + stringBuilder.toString());
}
sendBroadcast(intent);
}
public class LocalBinder extends Binder {
BluetoothLeService getService() {
return BluetoothLeService.this;
}
}
@Override
public IBinder onBind(Intent intent) {
return mBinder;
}
@Override
public boolean onUnbind(Intent intent) {
// 當 Service 與 Activity 解除綁定後,關閉 BLE 連線,以免浪費資源
close();
return super.onUnbind(intent);
}
private final IBinder mBinder = new LocalBinder();
/**
* 初始化 Bluetooth adapter.
*
* @return 如果初始化成功,則回傳 true。
*/
public boolean initialize() {
// For API level 18 and above, get a reference to BluetoothAdapter through
// BluetoothManager.
if (mBluetoothManager == null) {
mBluetoothManager = (BluetoothManager) getSystemService(Context.BLUETOOTH_SERVICE);
if (mBluetoothManager == null) {
Log.e(TAG, "無法初始化 BluetoothManager");
return false;
}
}
mBluetoothAdapter = mBluetoothManager.getAdapter();
if (mBluetoothAdapter == null) {
Log.e(TAG, "無法取得 BluetoothAdapter 元件");
return false;
}
return true;
}
/**
* 連線到 BLE 裝置的 GATT server
*
* @param address 目的地裝置的 mac address
*
* @return 如果連線成功,回傳 true。
* 連線結果透過 {@code BluetoothGattCallback#onConnectionStateChange(android.bluetooth.BluetoothGatt, int, int)}
* callback 回傳。
*/
public boolean connect(final String address) {
if (mBluetoothAdapter == null || address == null) {
Log.w(TAG, "BluetoothAdapter 未初始化或未指定 mac address");
return false;
}
// Previously connected device. Try to reconnect.
if (mBluetoothDeviceAddress != null && address.equals(mBluetoothDeviceAddress)
&& mBluetoothGatt != null) {
Log.d(TAG, "正在嘗試使用現有的 mBluetoothGatt 連線");
if (mBluetoothGatt.connect()) {
mConnectionState = STATE_CONNECTING;
return true;
} else {
return false;
}
}
final BluetoothDevice device = mBluetoothAdapter.getRemoteDevice(address);
if (device == null) {
Log.w(TAG, "找不到裝置,無法連線");
return false;
}
// We want to directly connect to the device, so we are setting the autoConnect
// parameter to false.
mBluetoothGatt = device.connectGatt(this, false, mGattCallback);
Log.d(TAG, "正在嘗試建立新的連線");
mBluetoothDeviceAddress = address;
mConnectionState = STATE_CONNECTING;
return true;
}
/**
* 將現有的連線中斷,斷線結果經由:
* {@code BluetoothGattCallback#onConnectionStateChange(android.bluetooth.BluetoothGatt, int, int)}
* callback 回傳。
*/
public void disconnect() {
if (mBluetoothAdapter == null || mBluetoothGatt == null) {
Log.w(TAG, "BluetoothAdapter 尚未被初始化");
return;
}
mBluetoothGatt.disconnect();
}
/**
* 釋放藍芽資源
*/
public void close() {
if (mBluetoothGatt == null) {
return;
}
mBluetoothGatt.close();
mBluetoothGatt = null;
}
/**
* 讀取特徵屬性的資料
*
* @param characteristic 特徵屬性
*/
public void readCharacteristic(BluetoothGattCharacteristic characteristic) {
if (mBluetoothAdapter == null || mBluetoothGatt == null) {
Log.w(TAG, "BluetoothAdapter 尚未被初始化");
return;
}
mBluetoothGatt.readCharacteristic(characteristic);
}
/**
* 開啟或關閉特徵屬性的通知功能
*
* @param characteristic 特徵屬性
* @param enabled 開啟或關閉
*/
public void setCharacteristicNotification(BluetoothGattCharacteristic characteristic,
boolean enabled) {
if (mBluetoothAdapter == null || mBluetoothGatt == null) {
Log.w(TAG, "BluetoothAdapter 尚未被初始化");
return;
}
mBluetoothGatt.setCharacteristicNotification(characteristic, enabled);
}
/**
* 檢查已連線 BLE 裝置的 GATT service,
* 必須在 {@code BluetoothGatt#discoverServices()} 完成後呼叫本方法
*
* @return 支援的服務清單 {@code List}
*/
public List<BluetoothGattService> getSupportedGattServices() {
if (mBluetoothGatt == null) return null;
return mBluetoothGatt.getServices();
}
}
讀取特徵屬性的資料
public void readCharacteristic(BluetoothGattCharacteristic characteristic) {
if (mBluetoothAdapter == null || mBluetoothGatt == null) {
Log.w(TAG, "BluetoothAdapter 尚未被初始化");
return;
}
mBluetoothGatt.readCharacteristic(characteristic);
}
將資料寫入特徵屬性
public void writeCharacteristic(BluetoothGattCharacteristic characteristic, byte[] data) {
if (mBluetoothAdapter == null || mBluetoothGatt == null) {
Log.w(TAG, "BluetoothAdapter 尚未被初始化");
return;
}
characteristic.setValue(data);
mBluetoothGatt.writeCharacteristic(characteristic);
}
開啟通知
public void setCharacteristicNotification(BluetoothGattCharacteristic characteristic,
boolean enabled) {
if (mBluetoothAdapter == null || mBluetoothGatt == null) {
Log.w(TAG, "BluetoothAdapter 尚未被初始化");
return;
}
mBluetoothGatt.setCharacteristicNotification(characteristic, enabled);
UUID uuid = UUID.fromString(GattAttributes.CLIENT_CHARACTERISTIC_CONFIG);
BluetoothGattDescriptor descriptor = characteristic.getDescriptor(uuid);
descriptor.setValue(BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE);
mBluetoothGatt.writeDescriptor(descriptor);
}