首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
MCP广场
社区首页 >专栏 >Android 低功耗蓝牙开发(扫描、连接、数据交互)Kotlin版

Android 低功耗蓝牙开发(扫描、连接、数据交互)Kotlin版

作者头像
晨曦_LLW
发布于 2021-09-23 07:20:25
发布于 2021-09-23 07:20:25
3.3K00
代码可运行
举报
运行总次数:0
代码可运行

低功耗蓝牙开发(扫描、连接、数据交互)Kotlin版

前言

  写这篇文章是因为有读者想看看Kotlin中怎么操作低功耗蓝牙,再加上我也想写一些关于Kotlin的内容,对于低功耗蓝牙的Java版的,我写了两篇,一个是扫描、连接,另一篇就是数据交互,而这篇Kotlin文章我会减少讲解的环节,更多的注重业务逻辑和UI以及Kotlin的语法。

运行效果图

正文

创建项目

一、配置项目

  配置项目常规来说两个环节,AndroidManifest.xml和build.gradle。

AndroidManifest.xml

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
    <uses-permission android:name="android.permission.BLUETOOTH" />
    <uses-permission android:name="android.permission.BLUETOOTH_ADMIN" />
    
    <uses-feature
        android:name="android.hardware.bluetooth_le"
        android:required="true" />
    
    <uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
    <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />

然后是build.gradle,这个地方有两处,一个是项目的build.gradle,增加如下代码:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
	maven { url "https://jitpack.io"}

然后是app模块下的build.gradle,增加如下代码:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
	//使用viewBinding
    viewBinding {
        enabled = true
    }
代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
	//蓝牙扫描库
    implementation 'no.nordicsemi.android.support.v18:scanner:1.5.0'
    //权限请求 支持Androidx
    implementation 'com.permissionx.guolindev:permissionx:1.4.0'
    //让你的适配器一目了然,告别代码冗余
    implementation 'com.github.CymChad:BaseRecyclerViewAdapterHelper:3.0.4'

两处代码:

这里注意一点,viewBinding的开启代码是在android{}闭包中的,不要放错地方了,然后点击Sync或者Sync Now。当程序编译完成之后,运行到自己手机上,先确保项目配置这一步没有问题。

二、页面设计

  首先改一下主题的颜色,列如标题,改成绿色。在colors.xml中增加:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
	<color name="green">#2b9247color>
    <color name="green_light">#00cd66color>

然后修改style中的样式:

修改activity_main.xml

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    tools:context=".MainActivity">

    <ProgressBar
        android:id="@+id/progress_bar"
        style="@style/Widget.AppCompat.ProgressBar.Horizontal"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:indeterminate="true"
        android:visibility="invisible" />

    <androidx.recyclerview.widget.RecyclerView
        android:id="@+id/rv_device"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:layout_below="@+id/progress_bar"
        android:overScrollMode="never"
        android:scrollbars="none" />

    <LinearLayout
        android:id="@+id/lay_no_equipment"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:layout_below="@+id/progress_bar"
        android:gravity="center"
        android:orientation="vertical">

        <ImageView
            android:layout_width="60dp"
            android:layout_height="60dp"
            android:layout_marginBottom="12dp"
            android:src="@drawable/ic_widgets" />

        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="空空如也~" />
    LinearLayout>

    <com.google.android.material.floatingactionbutton.ExtendedFloatingActionButton
        android:id="@+id/fab_add"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_alignParentEnd="true"
        android:layout_alignParentBottom="true"
        android:layout_margin="20dp"
        android:text="Scanning"
        android:textColor="@color/white"
        app:elevation="6dp"
        app:fabSize="normal"
        app:icon="@drawable/ic_add_white"
        app:iconTint="@color/white" />

RelativeLayout>

这里面有两个图标,代码如下: ic_widget.xml

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
<vector xmlns:android="http://schemas.android.com/apk/res/android"
    android:width="24dp"
    android:height="24dp"
    android:alpha="0.6"
    android:viewportWidth="24.0"
    android:viewportHeight="24.0">
    <path
        android:fillColor="#FF000000"
        android:pathData="M13,13v8h8v-8h-8zM3,21h8v-8L3,13v8zM3,3v8h8L11,3L3,3zM16.66,1.69L11,7.34 16.66,13l5.66,-5.66 -5.66,-5.65z" />
vector>

ic_add.xml

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
<vector xmlns:android="http://schemas.android.com/apk/res/android"
        android:width="24dp"
        android:height="24dp"
        android:viewportWidth="24.0"
        android:viewportHeight="24.0">
    <path
        android:fillColor="#FFFFFFFF"
        android:pathData="M19,13h-6v6h-2v-6H5v-2h6V5h2v6h6v2z"/>
vector>

下面写扫描到的列表适配器布局文件,在layout下新建一个item_bluetooth.xml,里面的代码如下:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:background="@color/white"
    android:foreground="?attr/selectableItemBackground"
    android:orientation="vertical">

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:gravity="center_vertical"
        android:padding="16dp">

        <ImageView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:src="@drawable/ic_bluetooth" />

        <LinearLayout
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:layout_weight="1"
            android:orientation="vertical"
            android:paddingStart="12dp">

            <TextView
                android:id="@+id/tv_device_name"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:ellipsize="end"
                android:singleLine="true"
                android:text="设备名称"
                android:textColor="@color/black"
                android:textSize="16sp" />

            <TextView
                android:id="@+id/tv_mac_address"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_marginTop="8dp"
                android:ellipsize="end"
                android:singleLine="true"
                android:text="Mac地址" />
        LinearLayout>

        <TextView
            android:id="@+id/tv_rssi"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="信号强度" />
    LinearLayout>

    <View
        android:layout_width="match_parent"
        android:layout_height="0.5dp"
        android:background="#EEE" />
LinearLayout>

这里也有一个蓝牙图标的ic_bluetooth.xml,如下:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
<vector xmlns:android="http://schemas.android.com/apk/res/android"
    android:width="36dp"
    android:height="36dp"
    android:autoMirrored="true"
    android:tint="@color/green"
    android:viewportWidth="24.0"
    android:viewportHeight="24.0">

    <path
        android:fillColor="@android:color/white"
        android:pathData="M14.58,12.36l1.38,1.38c0.28,0.28 0.75,0.14 0.84,-0.24c0.12,-0.48 0.18,-0.99 0.18,-1.5c0,-0.51 -0.06,-1.01 -0.18,-1.48c-0.09,-0.38 -0.56,-0.52 -0.84,-0.24l-1.39,1.38C14.39,11.85 14.39,12.17 14.58,12.36zM18.72,7.51l-0.05,0.05c-0.25,0.25 -0.3,0.62 -0.16,0.94c0.47,1.07 0.73,2.25 0.73,3.49c0,1.24 -0.26,2.42 -0.73,3.49c-0.14,0.32 -0.09,0.69 0.16,0.94l0,0c0.41,0.41 1.1,0.29 1.35,-0.23c0.63,-1.3 0.98,-2.76 0.98,-4.3c-0.01,-1.48 -0.34,-2.89 -0.93,-4.16C19.83,7.22 19.13,7.1 18.72,7.51zM15,7l-4.79,-4.79C10.07,2.07 9.89,2 9.71,2h0C9.32,2 9,2.32 9,2.71v6.88L5.12,5.7c-0.39,-0.39 -1.02,-0.39 -1.41,0l0,0c-0.39,0.39 -0.39,1.02 0,1.41L8.59,12l-4.89,4.89c-0.39,0.39 -0.39,1.02 0,1.41h0c0.39,0.39 1.02,0.39 1.41,0L9,14.41v6.88C9,21.68 9.32,22 9.71,22h0c0.19,0 0.37,-0.07 0.5,-0.21L15,17c0.39,-0.39 0.39,-1.02 0,-1.42L11.41,12L15,8.42C15.39,8.03 15.39,7.39 15,7zM11,5.83l1.88,1.88L11,9.59V5.83zM12.88,16.29L11,18.17v-3.76L12.88,16.29z" />

vector>

设备扫描页面就差不多了,下面进行这个页面的代码编写。

三、扫描设备

  首先想清楚扫描之前要做什么,扫描之后要做什么。扫描之前要判断Android版本,6.0及以上需要动态请求权限,请求之后要判断蓝牙是否打开,蓝牙打开权限也有了就可以点击扫描蓝牙开始扫描了,扫描时显示加载条表示正在扫描,扫描到设备后添加到列表中,页面上渲染出来。当点击一个设备时连接这个设备,然后就是连接设备后的数据交互了,先写现在的业务逻辑。

① 绑定视图

先进行视图绑定,activity_main.xml 对应的就是ActivityMainBinding。由ViewBinding根据布局生成的

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
	//视图绑定
    private lateinit var binding: ActivityMainBinding

然后在onCreate中进行绑定

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
	override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        binding = ActivityMainBinding.inflate(layoutInflater)
        setContentView(binding.root)
    }

② 检查Android版本

当进入页面是检查版本

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
	/**
     * Android版本
     */
    private fun checkAndroidVersion() =
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) requestPermission() else openBluetooth()

这里的语法就是Kotlin的语法,等价于Java中的如下代码。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
	private fun checkAndroidVersion() {
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
            requestPermission()
        } else {
            openBluetooth()
        }
    }

当你用Kotlin时间越久你就越觉得Kotlin设计的好,非常的简洁。当然最主要的是多使用Kotlin,作为弱类型语言,代码的阅读需要有一定的Kotlin基础才可以,高阶的写法可读性很差,但是效率很高代码也很简洁。后面我就直接写Kotlin代码,不熟悉的可以留言提问,事先声明我的Kotlin很菜,所以可读性相对来说高一些。

从上面的方法中可以知道逻辑就是Android6.0以上就请求权限,以下就打开蓝牙。这两个方法现在还都没有的,先写打开蓝牙的方法。

③ 打开蓝牙

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
	//默认蓝牙适配器
    private var defaultAdapter = BluetoothAdapter.getDefaultAdapter()
	/**
     * 打开蓝牙
     */
    private fun openBluetooth() = defaultAdapter.let {
            if (it.isEnabled) showMsg("蓝牙已打开,可以开始扫描设备了") else activityResult.launch(Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE))
        }

这个方法中主要就是当蓝牙开发未打开的时候,通过Intent去打开系统蓝牙,注意这一行代码:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
	activityResult.launch(Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE))

在Android高版本中弃用了startActivityForResult,改用registerForActivityResult。使用此方法需要在onCreate之前进行初始化。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
	//注册开启蓝牙  注意在onCreate之前注册
    private val activityResult =
        registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
            if (it.resultCode == Activity.RESULT_OK) showMsg(if (defaultAdapter.isEnabled) "蓝牙已打开" else "蓝牙未打开")
        }

这里的showMsg代码如下:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
	/**
     * Toast提示
     */
    private fun showMsg(msg: String = "权限未通过") = Toast.makeText(this, msg, Toast.LENGTH_SHORT).show()

④ 请求权限

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
	/**
     * 请求权限
     */
    private fun requestPermission() =
        PermissionX.init(this).permissions(Manifest.permission.ACCESS_FINE_LOCATION)
            .request { allGranted, _, _ -> if (allGranted) openBluetooth() else showMsg() }

初始阶段完成了,最终在onCreate方法中调用

当权限同意之后就打开蓝牙,如果都打开了就可以开始进行扫描蓝牙的操作了,在扫描之后先要确定蓝牙设备需要什么信息。

⑤ 扫描结果

现在前期的准备工作就做好了,那么下面就是点击扫描按钮进行蓝牙设备的扫描了。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
	//扫描结果回调
    private val scanCallback = object : ScanCallback() {
        override fun onScanResult(callbackType: Int, result: ScanResult) {
            
        }
    }

看这段代码相对于Java的区别还是很大的,不过返回的结果值是一样的,然后就是触发回调的地方,这里容我一会儿再写这个开始扫描和停止扫描的方法,因为这两个方法牵扯到的内容比较多,需要控制数据、视图、业务逻辑。因此等先把数据展示出来再去进行这个扫描的开始和结束的操作方法的编写。

⑥ 设备适配器编写

首先我们要定义一个设备类,用来存放扫描到的结果,在Kotlin中有一个数据类,来做这个事情,新建一个BleDevice,代码如下:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
data class BleDevice(var device:BluetoothDevice, var rssi:Int, var name:String?)

扫描毫无疑问肯定要展示数据在页面上的。然后就需要一个视图来显示数据,之前创建了item的xml文件,现在我们需要写一个适配器去配合这个item的xm去渲染列表数据。

BaseQuickAdapter的使用,之前我是没有通过ViewBinding去进行布局绑定的,都是通过R.layout.item布局文件进行的,那么换成了ViewBinding要怎么操作呢?BaseQuickAdapter的源码中没有提到ViewBinding,倒是提到了DataBinding,很明显这是两回事,因此我们需要自己扩展一下,让BaseQuickAdapter中可以使用ViewBinding,看下面这一段代码:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
class BleDeviceBaseAdapter(layoutResId: Int, data: MutableList<BleDevice>?) :
    BaseQuickAdapter<BleDevice, BaseViewHolder>(layoutResId, data) {
    
    override fun convert(holder: BaseViewHolder, item: BleDevice) {
        
    }
}

这是常规的写法,只要传入数据和布局文件的id就可以了,但是现在布局id变成了ViewBinding,因此就需要对这个BaseViewHolder进行一个覆写,这个方式我也是参考了网上博客的内容,

新建一个adapter包,包下新建一个ViewBindingHolder类,里面的代码如下:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
class ViewBindingHolder<VB : ViewBinding>(val vb: VB, view: View) : BaseViewHolder(view)

这里我们自定义了一个ViewBindingHolder,这个类继承了BaseViewHolder,同时构造这个类的时候传入了一个VIewBinding,这说明支持任何ViewBinding,然后就是构造参数vb,view。

在这个ViewBindingHolder类中 新增一个抽象类ViewBindingAdapter,代码如下:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
abstract class ViewBindingAdapter<VB : ViewBinding, T>(data: MutableList<T>? = null) :
    BaseQuickAdapter<T, ViewBindingHolder<VB>>(0, data) {
    //重写返回自定义 ViewHolder
    override fun onCreateDefViewHolder(parent: ViewGroup, viewType: Int): ViewBindingHolder<VB> {
        //这里为了使用简洁性,使用反射来实例ViewBinding
        val viewBindingClass: Class<VB> =
            (javaClass.genericSuperclass as ParameterizedType).actualTypeArguments[0] as Class<VB>
        val inflate = viewBindingClass.getDeclaredMethod(
            "inflate",
            LayoutInflater::class.java,
            ViewGroup::class.java,
            Boolean::class.java
        )
        val mBinding = inflate.invoke(null, LayoutInflater.from(parent.context), parent, false) as VB
        return ViewBindingHolder(mBinding, mBinding.root)
    }
}

ViewBindingAdapter中传入的参数是VB和T,也就是ViewBinding和Data,然后继承BaseQuickAdapter,传入T和ViewBindingHolder,因为ViewBindingHolder继承了BaseViewHolder,因此是可以这么写的。

然后看下面的这个构造方法。onCreateDefViewHolder,创建默认到的ViewHolder,然后就是根据这个传进来的VB进行一个相应的编译类寻找,因为ViewBinding使用了编译时技术,会在布局完成时构建一个编译类,这个类对应一个xml文件,因此通过这个ViewBinding去反射拿到对应的类,再通过这个类名的中infalte,infalte相信你不会默认,因为在MainActivity中也用到了这个,然后通过infalte去获取mBinding,这个就等价于

然后mBinding.root对应的就是具体的View,也就是ViewBindingHolder中的View。

刚才的一系列操作就是通过ViewBinding去获取View,一句话说起来很简单,但是你要实践起来很复杂。

现在这个类就写好了,下面在adapter包下新建一个BleDeviceAdapter类,代码如下:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
class BleDeviceAdapter(data: MutableList<BleDevice>? = null) :
    ViewBindingAdapter<ItemBluetoothBinding, BleDevice>(data) {
    @SuppressLint("SetTextI18n")
    override fun convert(holder: ViewBindingHolder<ItemBluetoothBinding>, item: BleDevice) {
        val binding = holder.vb
        binding.tvDeviceName.text = item.name
        binding.tvMacAddress.text = item.device.address
        binding.tvRssi.text = "${item.rssi} dBm"
    }
}

我相信经过了上面的代码之后,你现在很好理解现在的这种方式,唯一的区别就是从之前的layoutId变成了ViewBinding。ItemBluetoothBinding对应的就是之前的item_bluetooth.xml文件。

⑦ 数据渲染

适配器编写好了,下面就是使用了。先定义一些变量

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
	//低功耗蓝牙适配器
    private lateinit var bleAdapter: BleDeviceAdapter
    //蓝牙列表
    private var mList: MutableList<BleDevice> = ArrayList()
    //地址列表
    private var addressList: MutableList<String> = ArrayList()
    //当前是否扫描
    private var isScanning = false

然后新增一个页面初始化的方法initView(),代码如下:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
	private fun initView() {
        bleAdapter = BleDeviceAdapter(mList).apply {
            setOnItemClickListener { _, _, position ->
                stopScan()
                val device = mList[position].device
                //跳转页面
            }
            animationEnable = true
            setAnimationWithDefault(AnimationType.SlideInRight)
        }
        binding.rvDevice.apply {
            layoutManager = LinearLayoutManager(this@MainActivity)
            adapter = bleAdapter
        }
        //扫描蓝牙
        binding.fabAdd.setOnClickListener { if (isScanning) stopScan() else scan() }
    }

在这个方法中我配置了适配器和RecyclerView,最后是浮动按钮的点击事件,用于控制扫描的开始和停止。然后在onCreate中调用这个initView方法。

然后就是扫描后的数据处理,之前里面可是啥也没有的。增加代码如下图所示:

当扫描到设备时添加到获取设备地址和设备名称,如果设备名称为null则赋值为Unkown。然后根据地址列表的size去进行数据处理,为空直接添加,不为空则检查地址列表中是否存在之前设备地址,因为一个设备是可以被重复扫描到的,因此这是为了避免重复添加数据。这里的addDeviceList方法,代码如下:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
	private fun addDeviceList(bleDevice: BleDevice) {
        mList.add(bleDevice)
        //无设备UI展示
        binding.layNoEquipment.visibility = if (mList.size > 0) View.GONE else View.VISIBLE
        //刷新列表适配器
        bleAdapter.notifyDataSetChanged()
    }

下面只要扫描设备就可以了,现在写这两个方法,scan和stopScan。

⑧ 开始和停止扫描

开始扫描

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
	 /**
     * 扫描蓝牙
     */
    private fun scan() {
        if (!defaultAdapter.isEnabled) {
            showMsg("蓝牙未打开");return
        }
        if (isScanning) {
            showMsg("正在扫描中...");return
        }
        isScanning = true
        addressList.clear()
        mList.clear()
        BluetoothLeScannerCompat.getScanner().startScan(scanCallback)
        binding.progressBar.visibility = View.VISIBLE
        binding.fabAdd.text = "扫描中"
    }

首先判断手机蓝牙是否打开,没打开直接return,然后是判断是否正在扫描中,是直接return,然后设置isScanning = true,下一次点击就会return掉,之后就是清掉之前的设备数据。然后启动扫描,显示加载进度条表示当前正在扫描设备,最后修改浮动按钮的文字。

停止扫描

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
	/**
     * 停止扫描
     */
    private fun stopScan() {
        if (!defaultAdapter.isEnabled) {
            showMsg("蓝牙未打开");return
        }
        if (isScanning) {
            isScanning = false
            BluetoothLeScannerCompat.getScanner().stopScan(scanCallback)
            binding.progressBar.visibility = View.INVISIBLE
            binding.fabAdd.text = "开始扫描"
        }
    }

这个方法就不用解释了,你明白对不对。你现在可以运行一下,不过我打算一气呵成,写完再运行。

四、连接和数据交互

  这里的连接自然还是Gatt连接,同样的新建一个Activity,去哪里进行连接和数据交互操作。新建一个DataExchangeActivity,对应的布局activity_data_exchange.xml。生成了ActivityDataExchangeBinding,然后在onCreate中,进行配置。

① 绑定视图

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
private lateinit var binding: ActivityDataExchangeBinding

② 初始化连接

从MainActivity中传递点击的Device过来。回到MainActivity中,添加如下图中所选处代码。

然后回到DataExchangeActivity中新建一个initView方法,用于页面视图的初始化,同时也要接收传递过来的device。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
	//Gatt
    private lateinit var gatt: BluetoothGatt

	private fun initView() {
        supportActionBar?.apply {
            title = "Data Exchange"
            setDisplayHomeAsUpEnabled(true)
        }
        val device = intent.getParcelableExtra<BluetoothDevice>("device")
        //gatt连接
        gatt = device!!.connectGatt(this, false, bleCallback)
    }

③ Ble回调

这里有一个bleCallback,所以你的代码会报红,这很正常,只不过我们现在没有这个类,新建一个callback包,包下我们新建一个BleCallback类来管理回调,代码如下:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
class BleCallback : BluetoothGattCallback() {
    private val TAG = BleCallback::class.java.simpleName
    private lateinit var uiCallback: UiCallback

    fun setUiCallback(uiCallback: UiCallback) {
        this.uiCallback = uiCallback
    }

    /**
     * 连接状态回调
     */
    override fun onConnectionStateChange(gatt: BluetoothGatt, status: Int, newState: Int) {
        if (status != BluetoothGatt.GATT_SUCCESS) {
            Log.e(TAG, "onConnectionStateChange: $status")
            return
        }
        uiCallback.state(
            when (newState) {
                BluetoothProfile.STATE_CONNECTED -> {
                    //获取MtuSize
                    gatt.requestMtu(512)
                    "连接成功"
                }
                BluetoothProfile.STATE_DISCONNECTED -> "断开连接"
                else -> "onConnectionStateChange: $status"
            }
        )
    }

    /**
     * 获取MtuSize回调
     */
    override fun onMtuChanged(gatt: BluetoothGatt, mtu: Int, status: Int) {
        uiCallback.state("获取到MtuSize:$mtu")
        //发现服务
        gatt.discoverServices()
    }

    /**
     * 发现服务回调
     */
    override fun onServicesDiscovered(gatt: BluetoothGatt, status: Int) {
        uiCallback.state(if (!BleHelper.enableIndicateNotification(gatt)) { gatt.disconnect()
            "开启通知属性异常"
        } else "发现了服务")
    }

    /**
     * 特性改变回调
     */
    override fun onCharacteristicChanged(gatt: BluetoothGatt, characteristic: BluetoothGattCharacteristic) {
        val content = ByteUtils.bytesToHexString(characteristic.value)
        uiCallback.state("特性改变: 收到内容:$content")
    }

    /**
     * 特性写入回调
     */
    override fun onCharacteristicWrite(gatt: BluetoothGatt, characteristic: BluetoothGattCharacteristic, status: Int) {
        val command = ByteUtils.bytesToHexString(characteristic.value)
        uiCallback.state("特性写入: ${if (status == BluetoothGatt.GATT_SUCCESS) "写入成功:" else "写入失败:"}$command")
    }

    /**
     * 描述符写入回调
     */
    override fun onDescriptorWrite(gatt: BluetoothGatt, descriptor: BluetoothGattDescriptor, status: Int) {
        if (BleConstant.DESCRIPTOR_UUID == descriptor.uuid.toString().lowercase(Locale.getDefault())) {
            uiCallback.state(if (status == BluetoothGatt.GATT_SUCCESS) {
                gatt.apply {
                    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) readPhy()
                    readDescriptor(descriptor)
                    readRemoteRssi() }
                "通知开启成功"
            } else "通知开启失败")
        }
    }

    /**
     * 读取远程设备的信号强度回调
     */
    override fun onReadRemoteRssi(gatt: BluetoothGatt?, rssi: Int, status: Int) = uiCallback.state("onReadRemoteRssi: rssi: $rssi")

    /**
     * UI回调
     */
    interface UiCallback {
        /**
         * 当前Ble状态信息
         */
        fun state(state: String)
    }
}

④ 帮助类

这个类里面会用到一些常量,新建一个utils包,包下新建一个BleConstant类,代码如下:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
class BleConstant {

    companion object BleConstant {
        /**
         * 服务 UUID
         */
        const val SERVICE_UUID = "5833ff01-9b8b-5191-6142-22a4536ef123"
        /**
         * 描述 UUID
         */
        const val DESCRIPTOR_UUID = "00002902-0000-1000-8000-00805f9b34fb"

        /**
         * 特征(特性)写入 UUID
         */
        const val CHARACTERISTIC_WRITE_UUID = "5833ff02-9b8b-5191-6142-22a4536ef123"

        /**
         * 特征(特性)表示 UUID
         */
        const val CHARACTERISTIC_INDICATE_UUID = "5833ff03-9b8b-5191-6142-22a4536ef123"
    }
}

同时也把工具类给写一下,utils包下再新建ByteUtils类,代码如下:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
object ByteUtils {

    /**
     * Convert hex string to byte[]
     *
     * @param hexString the hex string
     * @return byte[]
     */
    fun hexStringToBytes(hexString: String): ByteArray {
        val hexString = hexString.uppercase(Locale.getDefault())
        val length = hexString.length / 2
        val hexChars = hexString.toCharArray()
        val byteArrayResult = ByteArray(length)
        for (i in 0 until length) {
            val pos = i * 2
            byteArrayResult[i] = (charToByte(hexChars[pos]).toInt().shl(4) or charToByte(hexChars[pos + 1]).toInt()).toByte()
        }
        return byteArrayResult
    }

    /**
     * Convert byte[] to string
     */
    fun bytesToHexString(src: ByteArray): String {
        val stringBuilder = StringBuilder("")
        for (i in src.indices) {
            val v = (src[i].toInt() and 0xFF)
            val hv = Integer.toHexString(v)
            stringBuilder.append(if (hv.length < 2) 0 else hv)
        }
        return stringBuilder.toString()
    }

    /**
     * Convert char to byte
     *
     * @param c char
     * @return byte
     */
    private fun charToByte(c: Char): Byte = "0123456789ABCDEF".indexOf(c).toByte()

}

注意一下Kotlin中的位运算只能是Int和Long,而不能通过Byte进行,其次就是运算符号和Java中不同,可以去了解一下。

最后在utils包下新建一个BleHelper类,代码如下:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
object BleHelper {

    /**
     * 启用指令通知
     */
    fun enableIndicateNotification(gatt: BluetoothGatt): Boolean =
        setCharacteristicNotification(gatt, gatt.getService(UUID.fromString(BleConstant.SERVICE_UUID))
                .getCharacteristic(UUID.fromString(BleConstant.CHARACTERISTIC_INDICATE_UUID)))

    /**
     * 设置特征通知
     * return true, if the write operation was initiated successfully
     */
    private fun setCharacteristicNotification(gatt: BluetoothGatt, gattCharacteristic: BluetoothGattCharacteristic): Boolean =
        if (gatt.setCharacteristicNotification(gattCharacteristic, true))
            gatt.writeDescriptor(gattCharacteristic.getDescriptor(UUID.fromString(BleConstant.DESCRIPTOR_UUID))
                    .apply {
                        value = BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE
                    }) else false
    /**
     * 发送指令
     * @param gatt gatt
     * @param command 指令
     * @param isResponse 是否响应
     */
    fun sendCommand(gatt: BluetoothGatt, command: String, isResponse: Boolean): Boolean =
        gatt.writeCharacteristic(gatt.getService(UUID.fromString(BleConstant.SERVICE_UUID))
            .getCharacteristic(UUID.fromString(BleConstant.CHARACTERISTIC_WRITE_UUID)).apply {
            writeType = if (isResponse) WRITE_TYPE_DEFAULT else WRITE_TYPE_NO_RESPONSE
            value = ByteUtils.hexStringToBytes(command) })


}

⑤ UI回调

首先在DataExchangeActivity中创建变量:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
	//Ble回调
    private val bleCallback = BleCallback()

现在你的bleCallback 就不会报红了,下面需要实现BleCallback中的UiCallback接口,注意在Kotlin中继承和实现都是 : 。

然后实现

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
	//状态缓存
    private var stringBuffer = StringBuffer()
	
	override fun state(state: String) = runOnUiThread {
        stringBuffer.append(state).append("\n")
        binding.tvState.text = stringBuffer.toString()
    }

下面丰富一下initView方法,

最后再写一个页面返回的方法

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
	//页面返回
    override fun onOptionsItemSelected(item: MenuItem): Boolean =
        if (item.itemId == android.R.id.home)  { onBackPressed();true } else false

OK,可以运行了。

五、源码

GitHub: BleDemo-Kotlin

如果对你有所帮助,欢迎Star 和Fork。我是初学者-Study,山高水长,后会有期~

本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2021/09/16 ,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
暂无评论
推荐阅读
NLP入门 | 通俗讲解Subword Models
之前介绍的模型都是基于词向量的, 那么能不能换一个角度来表示语言。说英文的时候, 每个单词都是由音节构成的, 而人们听到了连续的音节就可以理解其中的含义, 而音节显然比词粒度更细。
Datawhale
2020/07/23
1.7K0
NLP入门 | 通俗讲解Subword Models
【Embedding】fastText:极快的文本分类工具
今天我们来看 Mikolov 大佬 2016 年的另一大巨作——fastText。2013 年大佬在 Google 开源了 Word2Vec,2016 年刚就职于 FaceBook 就开源了 fastText,全都掀起了轩然大波。
beyondGuo
2020/06/12
2.8K0
NLP中的词向量对比:word2vec/glove/fastText/elmo/GPT/bert
一、文本表示和各词向量间的对比 1、文本表示哪些方法? 2、怎么从语言模型理解词向量?怎么理解分布式假设? 3、传统的词向量有什么问题?怎么解决?各种词向量的特点是什么? 4、word2vec和NNLM对比有什么区别?(word2vec vs NNLM) 5、word2vec和fastText对比有什么区别?(word2vec vs fastText) 6、glove和word2vec、 LSA对比有什么区别?(word2vec vs glove vs LSA) 7、 elmo、GPT、bert三者之间有什么区别?(elmo vs GPT vs bert)
zenRRan
2019/06/14
3.9K0
NLP中的词向量对比:word2vec/glove/fastText/elmo/GPT/bert
无所不能的Embedding 2. FastText词向量&文本分类
Fasttext是FaceBook开源的文本分类和词向量训练库。最初看其他教程看的我十分迷惑,咋的一会ngram是字符一会ngram又变成了单词,最后发现其实是两个模型,一个是文本分类模型[Ref2],表现不是最好的但胜在结构简单高效,另一个用于词向量训练[Ref1],创新在于把单词分解成字符结构,可以infer训练集外的单词。这里拿quora的词分类数据集尝试了下Fasttext在文本分类的效果, 代码详见 https://github.com/DSXiangLi/Embedding
风雨中的小七
2020/09/08
1.8K0
无所不能的Embedding 2. FastText词向量&文本分类
Python3 使用fastText进行文本分类 新闻分类
这篇论文的模型非常之简单,之前了解过word2vec的同学可以发现这跟CBOW的模型框架非常相似。
大鹅
2021/06/15
3.2K0
技术干货丨fastText原理及实践
fastText是Facebook于2016年开源的一个词向量计算和文本分类工具,在学术上并没有太大创新。但是它的优点也非常明显,在文本分类任务中,fastText(浅层网络)往往能取得和深度网络相媲美的精度,却在训练时间上比深度网络快许多数量级。在标准的多核CPU上, 能够训练10亿词级别语料库的词向量在10分钟之内,能够分类有着30万多类别的50多万句子在1分钟之内。 本文首先会介绍一些预备知识,比如softmax、ngram等,然后简单介绍word2vec原理,之后来讲解fastText的原理,并
达观数据
2018/04/02
4K0
技术干货丨fastText原理及实践
NLP系列文章:子词嵌入(fastText)的理解!(附代码)
版权声明:本文为博主原创文章,遵循 CC 4.0 by-sa 版权协议,转载请附上原文出处链接和本声明。
mantch
2019/08/29
2.3K0
NLP系列文章:子词嵌入(fastText)的理解!(附代码)
基于深度学习的文本分类应用!
在基于机器学习的文本分类中,我们介绍了几种常见的文本表示方法:One-hot、Bags of Words、N-gram、TF-IDF。这些方法存在两个共同的问题:一是转换得到的向量维度很高,需要较长的训练实践;二是没有考虑到单词与单词之间的关系,只是进行了统计。
Datawhale
2020/08/17
6730
斯坦福NLP课程 | 第12讲 - NLP子词模型
教程地址:http://www.showmeai.tech/tutorials/36
ShowMeAI
2022/05/16
8180
斯坦福NLP课程 | 第12讲 - NLP子词模型
NLP︱高级词向量表达(二)——FastText(简述、学习笔记)
本文介绍了fastText这款基于子词(subword)的文本分类模型,该模型在文本分类任务上表现优异,具有较快的训练速度,并且支持多种语言。fastText采用子词建模,将文本拆分成子词,然后利用这些子词来训练模型。相较于word2vec和BERT等模型,fastText具有更高的训练效率和更好的性能。同时,fastText还可以用于多语言文本分类,并且不需要额外的预处理或数据标注。
悟乙己
2018/01/02
4K1
NLP︱高级词向量表达(二)——FastText(简述、学习笔记)
Fasttext 总结
这两个改变都是为了做分类,第一个不需要解释,第二个做文本分类只需要考虑一次整个句子的特征就行,所以不使用滑动窗口,这也是为了降低计算复杂度。
Steve Wang
2020/09/14
9560
机器学习|7种经典预训练模型原理解析
目前无论在CV领域还是NLP领域,预训练都是一个很普遍和普适的方法。我们都知道深度学习的模型越庞大,模型参数越多,为了避免过拟合就需要相应大规模的数据集,但对于很多任务而言,样本标注的成本昂贵。相反,大规模无标签数据库相对容易建立,为了充分利用这些无标记数据,我们可以先使用它们在其他一些任务上学习一个好的特征表示,再用于训练目标任务。
智能生信
2021/02/04
5.7K0
fastText细节及实践
fastText模型是类似CBOW的三层结构,关于这个结构的介绍,很多博客都讲了,这里我不多赘述,我主要叙述一下其中的部分细节
mathor
2020/06/18
1.3K1
fastText细节及实践
NLP︱高级词向量表达(二)——FastText(简述、学习笔记)「建议收藏」
1、NLP︱高级词向量表达(一)——GloVe(理论、相关测评结果、R&python实现、相关应用) 2、NLP︱高级词向量表达(二)——FastText(简述、学习笔记) 3、NLP︱高级词向量表达(三)——WordRank(简述) 4、其他NLP词表示方法paper:从符号到分布式表示NLP中词各种表示方法综述
全栈程序员站长
2022/08/10
1.5K0
NLP︱高级词向量表达(二)——FastText(简述、学习笔记)「建议收藏」
【关于 NLP】百问百答
作者:杨夕、芙蕖、李玲、陈海顺、twilight、LeoLRH、JimmyDU、艾春辉、张永泰、金金金
杨夕
2021/03/11
1.1K0
【关于 NLP】百问百答
深度 | 比深度学习快几个数量级,详解Facebook最新开源工具——fastText
导读:Facebook声称fastText比其他学习方法要快得多,能够训练模型“在使用标准多核CPU的情况下10分钟内处理超过10亿个词汇”,特别是与深度模型对比,fastText能将训练时间由数天缩
AI科技评论
2018/03/07
1.1K0
深度 | 比深度学习快几个数量级,详解Facebook最新开源工具——fastText
词向量(1)--从Word2Vec到ELMo
若你是做NLP的,一定对词向量很亲切,若你是做推荐的,对词向量也一定不会陌生,以词向量为代表的序列向量化方法已经成为机器学习中必不可少的实战利器。
流川枫
2020/04/24
9810
word2vec原理(二) 基于Hierarchical Softmax的模型
    word2vec原理(二) 基于Hierarchical Softmax的模型
刘建平Pinard
2018/08/07
1.3K0
word2vec原理(二) 基于Hierarchical Softmax的模型
NLP: Word Embedding 词嵌入(Part2: fastText)
word2vec, n-gram 等 word-embedding 方法选择用vector表示single word 而不考虑词根词缀之间的关系
JiahuiZhu1998
2022/12/03
7700
【特征提取+分类模型】4种常见的NLP实践思路
越来越多的人选择参加算法赛事,为了提升项目实践能力,同时也希望能拿到好的成绩增加履历的丰富度。期望如此美好,现实却是:看完赛题,一点思路都木有。那么,当我们拿到一个算法赛题后,如何破题,如何找到可能的解题思路呢。
昱良
2020/08/28
3.4K0
【特征提取+分类模型】4种常见的NLP实践思路
推荐阅读
相关推荐
NLP入门 | 通俗讲解Subword Models
更多 >
LV.0
这个人很懒,什么都没有留下~
目录
  • 低功耗蓝牙开发(扫描、连接、数据交互)Kotlin版
  • 前言
  • 正文
    • 一、配置项目
    • 二、页面设计
    • 三、扫描设备
      • ① 绑定视图
      • ② 检查Android版本
      • ③ 打开蓝牙
      • ④ 请求权限
      • ⑤ 扫描结果
      • ⑥ 设备适配器编写
      • ⑦ 数据渲染
      • ⑧ 开始和停止扫描
    • 四、连接和数据交互
      • ① 绑定视图
      • ② 初始化连接
      • ③ Ble回调
      • ④ 帮助类
      • ⑤ UI回调
    • 五、源码
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档