Android Ble藍芽入門
Android BLE藍芽入門
一、什麼是BLE藍芽
google官方對BLE藍芽的解釋
簡述:API級別:Android 4.3(API 級別 18)引入。低功耗藍芽區別於“經典藍芽”。
侷限:最多隻支援20個資料(後面會展示)。
低功耗藍芽優勢:1.低功耗,使用鈕釦電池就可執行數月至數年;2.小體積、低成本;3.與現有的大部分手機、平板電腦和計算機相容。(百度百科)
二、硬體準備工作
1.藍芽開發模組(如果有現成的模組可以直接進行除錯)
2.串列埠除錯工具(文章末尾會給出軟體的下載方式)
3.支援BLE藍芽的Android手機。
三、準備開發
1.許可權新增
<uses-permission android:name="android.permission.BLUETOOTH" />
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN" />
<!-- 僅支援低耗藍芽 -->
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<uses-feature
android:name="android.hardware.bluetooth_le"
android:required="true" />
2.檢查開關和許可權
搜尋裝置準備:
1.當前裝置是否支援BLE藍芽功能
2.裝置的藍芽功能是否處於開啟狀態
3.判斷裝置的api是否需要開啟定位許可權
(PS:至於為什麼要開啟定位許可權,這你得問Google了)
3.1GPS是否開啟了
3.2是否擁有GPS許可權,需要使用GPS才能使用藍芽裝置
這裡的藍芽,定位開關狀態,許可權獲取比較麻煩但是不復雜
程式碼:
package com.my.mwble;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.app.AppCompatActivity;
import androidx.core.app.ActivityCompat;
import android.Manifest;
import android.app.Activity;
import android.app.AlertDialog;
import android.bluetooth.BluetoothAdapter;
import android.bluetooth.BluetoothManager;
import android.content.Context;
import android.content.DialogInterface;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.provider.Settings;
import android.view.View;
import android.widget.Toast;
import com.my.mwble.util.BleUtil;
import com.my.mwble.util.GpsUtil;
import com.my.mwble.util.LogUtil;
import com.my.mwble.util.ToastUtil;
/**
* Created by Android Studio.
* User: mwb
* Date: 2020/10/24 0024
* Time: 上午 11:32
* Describe:BLE藍芽基礎
*/
public class MainActivity extends AppCompatActivity implements View.OnClickListener {
private BluetoothAdapter bluetoothAdapter;
private static int REQUEST_ENABLE_BT = 1; // 開啟藍芽頁面請求程式碼
private static final int REQUEST_CODE_ACCESS_COARSE_LOCATION = 1; // 位置許可權
private static final int SET_GPS_OPEN_STATE = 2; // 設定GPS是否開啟了
private static final int REQUEST_STORY_CODE = 3; // 檔案讀取許可權
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
initView();
initDevice();
}
private void initView() {
findViewById(R.id.btn_seach).setOnClickListener(this);
}
private void initDevice() {
BluetoothManager bluetoothManager = (BluetoothManager) getSystemService(Context.BLUETOOTH_SERVICE);
bluetoothAdapter = bluetoothManager.getAdapter();
}
@Override
public void onClick(View v) {
switch (v.getId()) {
case R.id.btn_seach: // 搜尋藍芽裝置
seach();
break;
}
}
/**
* 搜尋裝置
* 1.當前裝置是否支援BLE藍芽功能
* 2.裝置的藍芽功能是否處於開啟狀態
* 2.1 沒有開啟則去開啟
* 3.判斷裝置的api是否需要開啟定位許可權
* (PS:至於為什麼要開啟定位許可權,這你得問Google了)
* 3.1GPS是否開啟了
* 3.2是否擁有GPS許可權,需要使用GPS才能使用藍芽裝置
*/
private void seach() {
// 當前的系統版本 < Android 4.3 API=18,目前市面大部分系統都在6.0了...這個判斷幾乎可以不用寫了。可省略
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.ICE_CREAM_SANDWICH) {
ToastUtil.show(this, "當前裝置系統版本不支援BLE藍芽功能!請升級系統版本到4.3以上");
return;
}
//1. 當前裝置是否支援BLE藍芽裝置
if (BleUtil.checkDeviceSupportBleBlueTooth(this)) {
// 2.判斷藍芽裝置是否開啟了
if (checkBlueIsOpen()) {
// 3.斷裝置的api是否需要開啟定位許可權
checkGPS();
} else { // 沒有開啟,跳轉到系統藍芽頁面
Intent enableBtIntent = new Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE);
startActivityForResult(enableBtIntent, REQUEST_ENABLE_BT);
}
} else {
ToastUtil.show(this, "當前裝置不支援BLE藍芽功能!");
}
}
/**
* GPS是否開啟了
*/
private void checkGPS() {
// 3.1GPS是否開啟了
if (GpsUtil.isOPen(this)) { // GPS已經開啟了
checkGpsPermission();
} else {
// 3.2是否擁有GPS許可權,需要使用GPS才能使用藍芽裝置
tipGPSSetting();
}
}
/**
* 藍芽是否開啟了
*
* @return true 開啟了,false 沒有開啟
*/
private boolean checkBlueIsOpen() {
if (bluetoothAdapter == null || !bluetoothAdapter.isEnabled()) {
return false;
} else {
return true;
}
}
/**
* 搜尋藍芽裝置
*/
private void seachBlueTooth() {
ToastUtil.show(this, "開始搜尋藍芽裝置");
}
/**
* 提示需要開啟藍芽
*/
private void tipGPSSetting() {
AlertDialog.Builder builder = new AlertDialog.Builder(MainActivity.this);
builder.setTitle("提示");
builder.setMessage("安卓6.0以後使用藍芽需要開啟定位功能,但本應用不會使用到您的位置資訊,開始定位只是為了掃描到藍芽裝置。是否確定開啟");
builder.setPositiveButton("確定", new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
GpsUtil.openGPS(MainActivity.this, SET_GPS_OPEN_STATE);
}
});
builder.setNegativeButton("取消", new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
ToastUtil.show(MainActivity.this, "您無法使用此功能");
}
});
builder.show();
}
/**
* 藍芽需要的定位許可權
*/
private void checkGpsPermission() {
if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.P) { // 如果當前版本是9.0(包含)以下的版本
if (ActivityCompat.checkSelfPermission(this,
Manifest.permission.ACCESS_FINE_LOCATION) != PackageManager.PERMISSION_GRANTED
|| ActivityCompat.checkSelfPermission(this,
Manifest.permission.ACCESS_COARSE_LOCATION) != PackageManager.PERMISSION_GRANTED) {
String[] strings =
{Manifest.permission.ACCESS_FINE_LOCATION, Manifest.permission.ACCESS_COARSE_LOCATION};
ActivityCompat.requestPermissions(this, strings, REQUEST_CODE_ACCESS_COARSE_LOCATION);
} else {
seachBlueTooth();
}
} else {
// 10.0系統
if (ActivityCompat.checkSelfPermission(this,
Manifest.permission.ACCESS_FINE_LOCATION) != PackageManager.PERMISSION_GRANTED
|| ActivityCompat.checkSelfPermission(this,
Manifest.permission.ACCESS_COARSE_LOCATION) != PackageManager.PERMISSION_GRANTED
|| ActivityCompat.checkSelfPermission(this,
"android.permission.ACCESS_BACKGROUND_LOCATION") != PackageManager.PERMISSION_GRANTED) {
String[] strings = {android.Manifest.permission.ACCESS_FINE_LOCATION,
android.Manifest.permission.ACCESS_COARSE_LOCATION,
"android.permission.ACCESS_BACKGROUND_LOCATION"};
ActivityCompat.requestPermissions(this, strings, REQUEST_CODE_ACCESS_COARSE_LOCATION);
} else {
seachBlueTooth();
}
}
}
@Override
protected void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) {
super.onActivityResult(requestCode, resultCode, data);
if (requestCode == REQUEST_ENABLE_BT) { // 從藍芽頁面返回了,在檢查一次是否開啟了
if (checkBlueIsOpen()) {
// 藍芽開啟了
seach();
} else {
AlertDialog.Builder builder = new AlertDialog.Builder(MainActivity.this);
builder.setTitle("提示");
builder.setMessage("藍芽沒有開啟將無法使用此功能,是否確定開啟");
builder.setPositiveButton("確定", new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
seach(); // 再次執行搜尋
}
});
builder.setNegativeButton("取消", new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
ToastUtil.show(MainActivity.this, "您無法使用此功能");
}
});
builder.setCancelable(false);
builder.show();
}
}else if (requestCode == SET_GPS_OPEN_STATE) { // GPS是否開啟了
if (GpsUtil.isOPen(this)) { // GPS開啟了
checkGpsPermission();
} else {
tipGPSSetting();
}
}
}
@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
switch (requestCode) {
case REQUEST_CODE_ACCESS_COARSE_LOCATION:
if (grantResults.length > 0
&& grantResults[0] == PackageManager.PERMISSION_GRANTED) { // 得到了許可權
seachBlueTooth();
} else {
AlertDialog.Builder builder = new AlertDialog.Builder(MainActivity.this);
builder.setTitle("提示");
builder.setMessage("安卓6.0以後使用藍芽需要開啟定位功能,但本應用不會使用到您的位置資訊,開啟定位只是為了掃描到藍芽裝置。是否確定開啟");
builder.setPositiveButton("確定", new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
launchAppDetailsSettings(MainActivity.this);
}
});
builder.setNegativeButton("取消", new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
ToastUtil.show(MainActivity.this, "您無法使用此功能");
}
});
builder.setCancelable(false);
builder.show();
}
break;
default:
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
break;
}
}
/**
* 跳轉許可權Activity
*/
public void launchAppDetailsSettings(Activity activity) {
Intent intent = new Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS);
intent.setData(Uri.parse("package:" + activity.getPackageName()));
if (!isIntentAvailable(this, intent)) {
ToastUtil.show(this, "請手動跳轉到許可權頁面,給予許可權!");
return;
}
activity.startActivity(intent);
}
/**
* 意圖是否可用
*
* @param intent The intent.
* @return {@code true}: yes<br>{@code false}: no
*/
public boolean isIntentAvailable(Activity activity, Intent intent) {
return activity
.getPackageManager()
.queryIntentActivities(intent, PackageManager.MATCH_DEFAULT_ONLY)
.size() > 0;
}
}
看一下效果:GIF太大了,就截幾個圖吧…
3.搜尋裝置
注意:搜尋到的裝置可能會多次出現需要我們自己進行篩選
關鍵程式碼:
/**
* 搜尋藍芽裝置
* 建立搜尋callback 返回掃描到的資訊
* 建立定時任務,在指定時間內結束藍芽掃描,藍芽掃描是一個很耗電的操作!
*/
private void seachBlueTooth() {
ToastUtil.show(this, "開始搜尋藍芽裝置");
mBluetoothLeScanner = bluetoothAdapter.getBluetoothLeScanner();
mBluetoothLeScanner.startScan(null, createScanSetting(), scanCallback);
bluetoothAdapter.startDiscovery();
handler.postDelayed(new Runnable() { // 指定時間內停止藍芽搜尋
@Override
public void run() {
closeSeach();
}
}, SCAN_PERIOD);
deviceData.clear();
}
/**
* 回撥
*/
private ScanCallback scanCallback = new ScanCallback() {
@Override
public void onScanResult(int callbackType, ScanResult result) {
BluetoothDevice device = result.getDevice();
LogUtil.i("name:" + result.getDevice().getName() + ";強度:" + result.getRssi());
if (device != null) {
if (deviceData.size() > 0) {
if (!deviceData.contains(device)) { // 掃描到會有很多重複的資料,剔除,只新增第一次掃描到的裝置
deviceData.add(device);
}
} else {
deviceData.add(device);
}
adapter.setData(deviceData);
}
}
@Override
public void onBatchScanResults(List<ScanResult> results) {
super.onBatchScanResults(results);
}
@Override
public void onScanFailed(int errorCode) {
super.onScanFailed(errorCode);
}
};
效果圖:
至此我們就得到了掃描到了裝置資訊了。
4.連線BLE藍芽裝置
概述:每個BLE藍芽裝置都會包含幾個服務Service
而每個Service中還包含了多個Characteristics(特徵)
他們的關係如下圖:
開啟通訊我們還需要繫結指定Service中的Characteristics(特徵)。
至於使用哪個Service或者哪個Characteristics(特徵)需要跟你們的硬體開發人員進行溝通。
獲取當前裝置的Service UUID,和特徵的UUID
關鍵程式碼:
/**
* 繫結藍芽
*
* @param device
*/
private void bindBlueTooth(BluetoothDevice device) {
//連線裝置
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
mBluetoothGatt = device.connectGatt(this,
false, mGattCallback, BluetoothDevice.TRANSPORT_LE);
} else {
mBluetoothGatt = device.connectGatt(this, false, mGattCallback);
}
}
//定義藍芽Gatt回撥類
public class mWBluetoothGattCallback extends BluetoothGattCallback {
//連線狀態回撥
@Override
public void onConnectionStateChange(BluetoothGatt gatt, final int status, final int newState) {
super.onConnectionStateChange(gatt, status, newState);
// status 用於返回操作是否成功,會返回異常碼。
// newState 返回連線狀態,如BluetoothProfile#STATE_DISCONNECTED、BluetoothProfile#STATE_CONNECTED
runOnUiThread(new Runnable() {
@Override
public void run() {
//操作成功的情況下
if (status == BluetoothGatt.GATT_SUCCESS) {
//判斷是否連線碼
if (newState == BluetoothProfile.STATE_CONNECTED) {
runOnUiThread(new Runnable() {
@Override
public void run() {
ToastUtil.show(MainActivity.this, "藍芽已連線");
LogUtil.i("裝置已連線上,開始掃描服務");
// 發現服務
mBluetoothGatt.discoverServices();
}
});
} else if (newState == BluetoothProfile.STATE_DISCONNECTED) {
//判斷是否斷開連線碼
ToastUtil.show(MainActivity.this, "連線已斷開");
}
} else {
//異常碼
// 重連次數不大於最大重連次數
if (reConnectionNum < maxConnectionNum) {
// 重連次數自增
reConnectionNum++;
LogUtil.i("重新連線:" + reConnectionNum);
// 連線裝置
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
mBluetoothGatt = mBluetoothDevice.connectGatt(MainActivity.this,
false, mGattCallback, BluetoothDevice.TRANSPORT_LE);
} else {
mBluetoothGatt = mBluetoothDevice.connectGatt(MainActivity.this, false, mGattCallback);
}
} else {
// 斷開連線,失敗回撥
ToastUtil.show(MainActivity.this, "藍芽連線失敗,建議重啟APP,或者重啟藍芽,或重啟裝置");
closeBLE();
}
}
}
});
}
//服務發現回撥
@Override
public void onServicesDiscovered(final BluetoothGatt gatt, int status) {
super.onServicesDiscovered(gatt, status);
if (status == BluetoothGatt.GATT_SUCCESS) {
runOnUiThread(new Runnable() {
@Override
public void run() {
LogUtil.i("mmmm:" + mBluetoothGatt.getServices().size());
for (int i = 0; i < mBluetoothGatt.getServices().size(); i++) {
LogUtil.i("mmmm service:" + mBluetoothGatt.getServices().get(i).getUuid());
for (int k = 0; k < mBluetoothGatt.getServices().get(i).getCharacteristics().size(); k++) {
LogUtil.i("mmmm Characteristic:" + mBluetoothGatt.getServices().get(i).getCharacteristics().get(k).getUuid());
}
}
}
});
}
}
@Override
public void onCharacteristicRead(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic, int status) {
super.onCharacteristicRead(gatt, characteristic, status);
}
//特徵寫入回撥
@Override
public void onCharacteristicWrite(BluetoothGatt gatt, final BluetoothGattCharacteristic characteristic, final int status) {
super.onCharacteristicWrite(gatt, characteristic, status);
}
//外設特徵值改變回撥
@Override
public void onCharacteristicChanged(BluetoothGatt gatt, final BluetoothGattCharacteristic characteristic) {
super.onCharacteristicChanged(gatt, characteristic);
}
//描述寫入回撥
@Override
public void onDescriptorWrite(BluetoothGatt gatt, BluetoothGattDescriptor descriptor, int status) {
super.onDescriptorWrite(gatt, descriptor, status);
LogUtil.i("開啟監聽成功");
}
}
Service的Uuid,和Characteristics(特徵)的Uuid
配置Uuid連線裝置:
修改 onServicesDiscovered中的程式碼:
//服務發現回撥
@Override
public void onServicesDiscovered(final BluetoothGatt gatt, int status) {
super.onServicesDiscovered(gatt, status);
if (status == BluetoothGatt.GATT_SUCCESS) {
runOnUiThread(new Runnable() {
@Override
public void run() {
// LogUtil.i("mmmm:" + mBluetoothGatt.getServices().size());
// for (int i = 0; i < mBluetoothGatt.getServices().size(); i++) {
// LogUtil.i("mmmm service:" + mBluetoothGatt.getServices().get(i).getUuid());
//
//
// for (int k = 0; k < mBluetoothGatt.getServices().get(i).getCharacteristics().size(); k++) {
// LogUtil.i("mmmm Characteristic:" + mBluetoothGatt.getServices().get(i).getCharacteristics().get(k).getUuid());
// }
//
// }
//獲取指定uuid的service
BluetoothGattService gattService = mBluetoothGatt.getService(UUID.fromString(UUDI_1));
// bluetoothGattServiceList.add(gattService);
//獲取到特定的服務不為空
if (gattService != null) {
LogUtil.i("獲取服務成功!");
BluetoothGattCharacteristic gattCharacteristic =
gattService.getCharacteristic(UUID.fromString(CHARACTERISTIC_UUID_1));
mGattCharacteristic = gattCharacteristic;
if (gattCharacteristic != null) {
LogUtil.i("獲取特徵成功!");
boolean isEnableNotification = mBluetoothGatt.setCharacteristicNotification(gattCharacteristic, true);
if (isEnableNotification) {
LogUtil.i("開啟通知成功!");
//通過GATt實體類將,特徵值寫入到外設中。
mBluetoothGatt.writeCharacteristic(gattCharacteristic);
//如果只是需要讀取外設的特徵值:
//通過Gatt物件讀取特定特徵(Characteristic)的特徵值
mBluetoothGatt.readCharacteristic(gattCharacteristic);
} else {
LogUtil.i("開啟通知失敗!");
}
} else {
LogUtil.i("獲取特徵失敗!");
}
} else {
//獲取特定服務失敗
LogUtil.i("獲取服務失敗!");
}
}
});
}
}
修改onCharacteristicChanged中的程式碼
//外設特徵值改變回撥
@Override
public void onCharacteristicChanged(BluetoothGatt gatt, final BluetoothGattCharacteristic characteristic) {
super.onCharacteristicChanged(gatt, characteristic);
final byte[] value = characteristic.getValue();
runOnUiThread(new Runnable() {
@Override
public void run() {
// value為裝置傳送的資料,根據資料協議進行解析
LogUtil.i("原始資料:" + new String(characteristic.getValue()));
LogUtil.i("裝置傳送資料:" + DigitalTrans.byte2hex(value)); // 這是一個byte轉16進位制的工具類,後面會給完整的程式碼,所以現在不用糾結
}
});
}
再次連線裝置,連線成功後我們來進行測試
資料接收成功。需要注意的地方是,接收到的資料是byte型別的,使用的時候需要自己進行轉化。
至此接收資料完成。
下面我們來看看傳送資料怎麼完成
修改程式碼:
/**
* 傳送資料
* 將輸入的16進位制轉化為byte傳送
*/
private void sendMsg(String msg) {
if (null == mGattCharacteristic || null == mBluetoothGatt) {
ToastUtil.show(MainActivity.this, "請先連線藍芽裝置");
return;
}
mGattCharacteristic.setValue(NumUtil.hexString2Bytes(msg));
mBluetoothGatt.writeCharacteristic(mGattCharacteristic);
}
關閉藍芽:
/**
* 關閉BLE藍芽連線
*/
public void closeBLE() {
if (mBluetoothGatt == null) {
return;
}
mBluetoothGatt.disconnect();
mBluetoothGatt.close();
mBluetoothGatt = null;
ToastUtil.show(this, "藍芽已斷開");
}
至此藍芽的接收和傳送已全部完成。
問題
藍芽傳送資料大於20個的問題:
這明明是40個資料才拆分了啊,你這是不是欺負老實人嗎?
請聽我狡辯:
從XCOM串列埠工具中我勾選了16進位制傳送,所以兩個才是一個16進位制的數值。不知道16進位制的請自行百度,這裡不再贅述。在實際的開發中我們用到的也會是16進位制根據規定的協議進行溝通。
可以看到資料被拆分了,如果資料大於20個需要進行拼包操作。
如果有時間的話我以後會發拼包的功能實現。
作者能力有限,如果有什麼問題,請留言進行溝通。
程式碼和工具類奉上:
XCOM串列埠除錯工具:連結:https://pan.baidu.com/s/1i-9W31CjXd-551mqphi_hg
提取碼:95vv
程式碼:
相關文章
- android藍芽BLE(一) —— 掃描Android藍芽
- android藍芽BLE(三) —— 廣播Android藍芽
- android藍芽BLE(二) —— 通訊Android藍芽
- Android BLE藍芽詳細解讀Android藍芽
- BLE低功耗藍芽藍芽
- Android BLE 藍芽開發——掃碼槍基於BLESSEDAndroid藍芽
- React Native 藍芽4.0 BLE開發React Native藍芽
- Android藍芽使用詳解(普通藍芽)Android藍芽
- ESP32:藍芽BLE控制M3508電機藍芽
- Android開發--藍芽操作Android藍芽
- Android-藍芽聊天demoAndroid藍芽
- Android 傳統藍芽開發Android藍芽
- iOS之BLE藍芽SDK開發個人總結(進階篇)iOS藍芽
- iOS之BLE藍芽SDK開發個人總結(基礎篇)iOS藍芽
- Android藍芽開發流程實踐Android藍芽
- Android Studio 藍芽 示例程式碼(轉)Android藍芽
- Android藍芽讀取簡訊調研Android藍芽
- Android:藍芽實現一對一聊天Android藍芽
- ios微信小程式 BLE藍芽通訊開發介面UI卡頓問題iOS微信小程式藍芽UI
- 藍芽藍芽
- 深入瞭解Android藍芽Bluetooth——《基礎篇》Android藍芽
- iOS藍芽開發 Bluetooth藍芽CoreBluetooth 藍芽中心裝置的實現 藍芽外設的實現 有DemoiOS藍芽
- ESP32藍芽學習--藍芽概念學習藍芽
- Android BLE 快速上手指南Android
- java+藍芽Java藍芽
- 藍芽模組藍芽模組
- MASA MAUI Plugin IOS藍芽低功耗(三)藍芽掃描UIPluginiOS藍芽
- 平板藍芽鍵盤怎麼切換輸入法 ipad藍芽鍵盤切換中英文藍芽iPad
- Android藍芽子系統"BlueFrag"漏洞分析(CVE-2020-0022)Android藍芽
- MASA MAUI Plugin 安卓藍芽低功耗(一)藍芽掃描UIPlugin安卓藍芽
- iOS藍芽開發iOS藍芽
- 白色藍芽耳機藍芽
- Android入門教程 | RecyclerView使用入門AndroidView
- Android Bluetooth 入門Android
- android BLE Peripheral 手機模擬裝置發出BLE廣播 BluetoothLeAdvertiserAndroid
- iOS BLE藍芽開發資料傳輸協議詳解 常用演算法(AES加密,HMAC_hash,PRF)iOS藍芽協議演算法加密Mac
- Android:手機如何控制BLE裝置?Android
- Android入門教程 | Kotlin協程入門AndroidKotlin