【Android App】低功耗蓝牙中扫描BLE设备的讲解及实战(附源码和演示 超详细)

news2024/11/23 17:12:53

需要源码请点赞关注收藏后评论区留言私信~~~

一、扫描BLE设备

传统蓝牙虽然历史悠久,但它的缺陷也很明显,包括但不限于下列几点:

(1)需要两部设备配对之后才能继续连接,而且连接速度也慢;

(2)连接之后就一直保持传输链路,很消耗电能;

(3)数据传输的有效距离不到10米,导致使用场景受限;

为解决传统蓝牙的上述痛点,蓝牙技术联盟制定了低功耗蓝牙技术(BLE),因为BLE采取非常快速的连接方式,所以平时处于非连接的状态,此时链路两端仅仅只是知晓对方,只有在必要时才开启链路,完成传输后会尽快关闭链路,BLE技术与之前版本的蓝牙标准相比,主要有三个方面的改进,更省电,连接速度更快以及传输距离更远

低功耗蓝牙所有BLE设备遵循统一的通用属性规范(GATT):

(1)BLE从机,又称服务端,它接受GATT指令,并根据指令调整自身行为。

(2)BLE主机,又称客户端,它向服务端发送GATT指令,令其遵照指令行事。

(3)特征值(characteristic),BLE通过参数来传输数据,这种参数被称作特征值。

(4)服务(service),一个设备可拥有多个服务,每个服务也可包含多个特征值,每个特征值又存在多种属性(properties)。

GATT规范的内容框架如下 

扫描与连接BLE设备

调用蓝牙适配器的getBluetoothLeScanner方法,获得BluetoothLeScanner扫描器对象。 扫描器的主要方法说明如下:

startScan方法表示开始扫描BLE设备,

stopScan方法表示停止扫描BLE设备。 调用设备对象的connectGatt方法,连接GATT服务器并获得客户端的GATT对象。 

BLE连接之后的回调

BluetoothGattCallback接口定义了许多方法,常用方法主要有:

onConnectionStateChange:BLE连接的状态发生变化时回调。此时判断如果连接成功,就调用GATT对象的discoverServices方法查找BLE服务。

onServicesDiscovered:发现BLE服务端的服务列表及其特征值时回调。 onCharacteristicChanged:收到BLE服务端的数据变更时回调。该方法会收到服务端送来的消息。

onCharacteristicWrite:收到BLE服务端的数据写入时回调。此时判断执行成功的话,表示服务端已经收到了客户端发给它的消息。

运行测试App效果如下 系统会自动扫描附近的低功耗蓝牙设备

 代码如下

package com.example.iot;

import androidx.appcompat.app.AppCompatActivity;

import android.bluetooth.BluetoothAdapter;
import android.bluetooth.BluetoothDevice;
import android.bluetooth.BluetoothGatt;
import android.bluetooth.BluetoothGattCharacteristic;
import android.bluetooth.BluetoothGattServer;
import android.bluetooth.BluetoothGattServerCallback;
import android.bluetooth.BluetoothGattService;
import android.bluetooth.BluetoothManager;
import android.bluetooth.BluetoothProfile;
import android.bluetooth.le.AdvertiseCallback;
import android.bluetooth.le.AdvertiseData;
import android.bluetooth.le.AdvertiseSettings;
import android.bluetooth.le.BluetoothLeAdvertiser;
import android.content.Context;
import android.content.pm.PackageManager;
import android.os.Bundle;
import android.os.Handler;
import android.os.Looper;
import android.text.TextUtils;
import android.util.Log;
import android.view.View;
import android.view.WindowManager;
import android.widget.EditText;
import android.widget.LinearLayout;
import android.widget.ScrollView;
import android.widget.TextView;
import android.widget.Toast;

import com.example.iot.constant.BleConstant;
import com.example.iot.util.BluetoothUtil;
import com.example.iot.util.ChatUtil;
import com.example.iot.util.DateUtil;
import com.example.iot.util.Utils;
import com.example.iot.util.ViewUtil;

import java.util.List;

public class BleServerActivity extends AppCompatActivity {
    private static final String TAG = "BleServerActivity";
    private TextView tv_hint; // 声明一个文本视图对象
    private ScrollView sv_chat; // 声明一个滚动视图对象
    private LinearLayout ll_show; // 声明一个线性视图对象
    private LinearLayout ll_input; // 声明一个线性视图对象
    private EditText et_input; // 声明一个编辑框对象
    private Handler mHandler = new Handler(Looper.myLooper()); // 声明一个处理器对象
    private int dip_margin; // 每条聊天记录的四周空白距离
    private String mMinute = "00:00";

    private BluetoothManager mBluetoothManager; // 声明一个蓝牙管理器对象
    private BluetoothAdapter mBluetoothAdapter; // 声明一个蓝牙适配器对象
    private BluetoothDevice mRemoteDevice; // 声明一个蓝牙设备对象
    private BluetoothGattServer mGattServer; // 声明一个蓝牙GATT服务器对象
    private BluetoothGattCharacteristic mReadChara; // 客户端读取数据的特征值

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_ble_server);
        getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); // 保持屏幕常亮
        initView(); // 初始化视图
        initBluetooth(); // 初始化蓝牙适配器
        mHandler.postDelayed(mAdvertise, 200); // 延迟200毫秒开启低功耗蓝牙广播任务
    }

    // 初始化视图
    private void initView() {
        dip_margin = Utils.dip2px(this, 5);
        tv_hint = findViewById(R.id.tv_hint);
        sv_chat = findViewById(R.id.sv_chat);
        ll_show = findViewById(R.id.ll_show);
        ll_input = findViewById(R.id.ll_input);
        et_input = findViewById(R.id.et_input);
        findViewById(R.id.btn_send).setOnClickListener(v -> sendMesssage());
    }

    // 初始化蓝牙适配器
    private void initBluetooth() {
        if (!getPackageManager().hasSystemFeature(PackageManager.FEATURE_BLUETOOTH_LE)) {
            Toast.makeText(this, "当前设备不支持低功耗蓝牙", Toast.LENGTH_SHORT).show();
            finish(); // 关闭当前页面
        }
        // 获取蓝牙管理器,并从中得到蓝牙适配器
        mBluetoothManager =(BluetoothManager) getSystemService(Context.BLUETOOTH_SERVICE);
        mBluetoothAdapter = mBluetoothManager.getAdapter(); // 获取蓝牙适配器
        if (!BluetoothUtil.getBlueToothStatus()) { // 还未打开蓝牙
            BluetoothUtil.setBlueToothStatus(true); // 开启蓝牙功能
        }
    }

    // 创建一个低功耗蓝牙广播任务
    private Runnable mAdvertise = new Runnable() {
        @Override
        public void run() {
            if (BluetoothUtil.getBlueToothStatus()) { // 已经打开蓝牙
                stopAdvertise(); // 停止低功耗蓝牙广播
                String server_name = getIntent().getStringExtra("server_name");
                startAdvertise(server_name); // 开始低功耗蓝牙广播
                tv_hint.setText("“"+server_name+"”服务端正在广播,请等候客户端连接");
            } else {
                mHandler.postDelayed(this, 2000);
            }
        }
    };

    // 开始低功耗蓝牙广播
    private void startAdvertise(String ble_name) {
        // 设置广播参数
        AdvertiseSettings settings = new AdvertiseSettings.Builder()
                .setConnectable(true) // 是否允许连接
                .setTimeout(0) // 设置超时时间
                .setTxPowerLevel(AdvertiseSettings.ADVERTISE_TX_POWER_HIGH)
                .setAdvertiseMode(AdvertiseSettings.ADVERTISE_MODE_LOW_LATENCY)
                .build();
        // 设置广播内容
        AdvertiseData advertiseData = new AdvertiseData.Builder()
                .setIncludeDeviceName(true) // 是否把设备名称也广播出去
                .setIncludeTxPowerLevel(true) // 是否把功率电平也广播出去
                .build();
        mBluetoothAdapter.setName(ble_name); // 设置BLE服务端的名称
        // 获取BLE广播器
        BluetoothLeAdvertiser advertiser = mBluetoothAdapter.getBluetoothLeAdvertiser();
        // BLE服务端开始广播,好让别人发现自己
        advertiser.startAdvertising(settings, advertiseData, mAdvertiseCallback);
    }

    // 停止低功耗蓝牙广播
    private void stopAdvertise() {
        if (mBluetoothAdapter != null) {
            // 获取BLE广播器
            BluetoothLeAdvertiser advertiser = mBluetoothAdapter.getBluetoothLeAdvertiser();
            if (advertiser != null) {
                advertiser.stopAdvertising(mAdvertiseCallback); // 停止低功耗蓝牙广播
            }
        }
    }

    // 创建一个低功耗蓝牙广播回调对象
    private AdvertiseCallback mAdvertiseCallback = new AdvertiseCallback() {
        @Override
        public void onStartSuccess(AdvertiseSettings settings) {
            Log.d(TAG, "低功耗蓝牙广播成功:"+settings.toString());
            addService(); // 添加读写服务UUID,特征值等
        }

        @Override
        public void onStartFailure(int errorCode) {
            Log.d(TAG, "低功耗蓝牙广播失败,错误代码为"+errorCode);
        }
    };

    // 添加读写服务UUID,特征值等
    private void addService() {
        BluetoothGattService gattService = new BluetoothGattService(
                BleConstant.UUID_SERVER, BluetoothGattService.SERVICE_TYPE_PRIMARY);
        // 只读的特征值
        mReadChara = new BluetoothGattCharacteristic(BleConstant.UUID_CHAR_READ,
                BluetoothGattCharacteristic.PROPERTY_READ | BluetoothGattCharacteristic.PROPERTY_NOTIFY,
                BluetoothGattCharacteristic.PERMISSION_READ);
        // 只写的特征值
        BluetoothGattCharacteristic charaWrite = new BluetoothGattCharacteristic(BleConstant.UUID_CHAR_WRITE,
                BluetoothGattCharacteristic.PROPERTY_WRITE | BluetoothGattCharacteristic.PROPERTY_NOTIFY,
                BluetoothGattCharacteristic.PERMISSION_WRITE);
        gattService.addCharacteristic(mReadChara); // 将特征值添加到服务里面
        gattService.addCharacteristic(charaWrite); // 将特征值添加到服务里面
        // 开启GATT服务器等待客户端连接
        mGattServer = mBluetoothManager.openGattServer(this, mGattCallback);
        mGattServer.addService(gattService); // 向GATT服务器添加指定服务
    }

    private BluetoothGattServerCallback mGattCallback = new BluetoothGattServerCallback() {
        // BLE连接的状态发生变化时回调
        @Override
        public void onConnectionStateChange(BluetoothDevice device, int status, int newState) {
            super.onConnectionStateChange(device, status, newState);
            Log.d(TAG, "onConnectionStateChange device=" + device.toString() + " status=" + status + " newState=" + newState);
            if (newState == BluetoothProfile.STATE_CONNECTED) {
                mRemoteDevice = device;
                runOnUiThread(() -> {
                    String desc = String.format("已连接BLE客户端,对方名称为“%s”,MAC地址为%s",
                            device.getName(), device.getAddress());
                    tv_hint.setText(desc);
                    ll_input.setVisibility(View.VISIBLE);
                });
            }
        }

        // 收到BLE客户端写入请求时回调
        @Override
        public void onCharacteristicWriteRequest(BluetoothDevice device, int requestId, BluetoothGattCharacteristic chara, boolean preparedWrite, boolean responseNeeded, int offset, byte[] value) {
            super.onCharacteristicWriteRequest(device, requestId, chara, preparedWrite, responseNeeded, offset, value);
            String message = new String(value); // 把客户端发来的数据转成字符串
            Log.d(TAG, "收到了客户端发过来的数据 " + message);
            // 向GATT客户端发送应答,告诉它成功收到了要写入的数据
            mGattServer.sendResponse(device, requestId, BluetoothGatt.GATT_SUCCESS, offset, chara.getValue());
            runOnUiThread(() -> appendChatMsg(message, false)); // 往聊天窗口添加聊天消息
        }
    };

    @Override
    protected void onDestroy() {
        super.onDestroy();
        stopAdvertise(); // 停止低功耗蓝牙广播
        if (mGattServer != null) {
            mGattServer.close(); // 关闭GATT服务器
        }
    }

    // 发送聊天消息
    private void sendMesssage() {
        String message = et_input.getText().toString();
        if (TextUtils.isEmpty(message)) {
            Toast.makeText(this, "请先输入聊天消息", Toast.LENGTH_SHORT).show();
            return;
        }
        et_input.setText("");
        ViewUtil.hideOneInputMethod(this, et_input); // 隐藏软键盘
        List<String> msgList = ChatUtil.splitString(message, 20); // 按照20字节切片
        for (String msg : msgList) {
            mReadChara.setValue(msg); // 设置读特征值
            // 发送本地特征值已更新的通知
            mGattServer.notifyCharacteristicChanged(mRemoteDevice, mReadChara, false);
        }
        appendChatMsg(message, true); // 往聊天窗口添加聊天消息
    }

    // 往聊天窗口添加聊天消息
    private void appendChatMsg(String content, boolean isSelf) {
        appendNowMinute(); // 往聊天窗口添加当前时间
        // 把单条消息的线性布局添加到聊天窗口上
        ll_show.addView(ChatUtil.getChatView(this, content, isSelf));
        // 延迟100毫秒后启动聊天窗口的滚动任务
        new Handler(Looper.myLooper()).postDelayed(() -> {
            sv_chat.fullScroll(ScrollView.FOCUS_DOWN); // 滚动到底部
        }, 100);
    }

    // 往聊天窗口添加当前时间
    private void appendNowMinute() {
        String nowMinute = DateUtil.getNowMinute();
        if (!mMinute.substring(0, 4).equals(nowMinute.substring(0, 4))) {
            mMinute = nowMinute;
            ll_show.addView(ChatUtil.getHintView(this, nowMinute, dip_margin));
        }
    }

}

创作不易  觉得有帮助请点赞关注收藏~~~

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/53765.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

数组与字符串总结

一、数组 基本概念 特点&#xff1a;顺序存储&#xff0c;每个元素大小&#xff0c;类型相同&#xff0c;元素有限 高维数组可以转化为一维数组 高维数组存放次序&#xff1a;按行优先或者按列优先 按行优先的寻址公式&#xff1a; 二维数组a[m] [n]: Loc(a[i] [j]) Loc…

Ajax axios JSON Fastjson

1、概述 AJAX (Asynchronous JavaScript And XML) &#xff1a;异步的JavaScript和XML AJAX工作流程如下: 1.1、作用 AJAX作用有以下两方面&#xff1a; 1&#xff09;与服务器进行数据交换&#xff1a;通过AJAX可以给服务器发送请求&#xff0c;服务器将数据直接响应回浏览…

算法训练Day36 贪心算法系列 - 重叠区间问题 | LeetCode435. 无重叠区间;763. 划字母区间;56.合并区间

前言&#xff1a; 算法训练系列是做《代码随想录》一刷&#xff0c;个人的学习笔记和详细的解题思路&#xff0c;总共会有60篇博客来记录&#xff0c;计划用60天的时间刷完。 内容包括了面试常见的10类题目&#xff0c;分别是&#xff1a;数组&#xff0c;链表&#xff0c;哈…

【Linux】快捷键

Ctrl C&#xff1a;终止当前命令

星环科技数据中台解决方案,助力某政府机构建设新型智慧城市

客户背景 城市&#xff0c;是人们工作生活的栖息地&#xff0c;也是展示发展成果的全景图。某政府机构不仅注重城市“中枢大脑”的建设&#xff0c;而且兼顾“神经末梢”的需求&#xff0c;既有技术进步的“面子”&#xff0c;更有民生保障的“里子”。站在新的起点上&#xff…

Linux计划任务管理

一&#xff0c;计划任务管理&#xff1a; 任务管理很宽泛&#xff0c;这里是指的计划任务管理&#xff0c;在指定的时间执行。 1&#xff0c;at命令 &#xff1a; 由atd守护进程来执行&#xff0c;atd进程会定期检查系统上的 /var/spool/at 目录&#xff0c;获取at命令写入的任…

如何建立你的财务体系?

天下人都想同时实现财务管理民主自由&#xff0c;换言之一下&#xff0c;你躺在毛里求斯的海滩上&#xff0c;吹着南风&#xff0c;晒着月亮&#xff0c;还有总收入源源不绝的流向&#xff0c;阿涅尔&#xff1f; 那么&#xff0c;怎样同时实现&#xff1f;坚信我们都知道投资…

准备大半年,面试频繁受挫,Java岗面试为何越来越难?

作为一名优秀的程序员&#xff0c;技术面试都是不可避免的一个环节&#xff0c;一般技术面试官都会通过自己的方式去考察程序员的技术功底与基础理论知识。 如果你参加过一些大厂面试&#xff0c;肯定会遇到一些这样的问题&#xff1a; 1、看你项目都用的框架&#xff0c;熟悉…

什么是链动2+1模式?链动2+1模式玩法解析

链动21模式玩法解析 模式框架&#xff1a;代理、老板 奖励机制&#xff1a;平级奖、见点奖、平级奖、帮扶基金、分红奖 商业模式玩法&#xff1a;每一个代理晋升为老板的同时&#xff0c;都需要给上级代理留下“两个原始种子用户”&#xff0c;咱们这里就俗称“感恩机制”。…

双十二有哪些数码好物值得入手、双十二必买数码好物清单

双十二马上就到了&#xff0c;相信很多小伙伴已经按耐不住想要入手了吧&#xff1f;但如果目前还没什么头绪&#xff0c;不知道买什么的话&#xff0c;现在就不妨来抄一下作业吧&#xff01;近期我整理了一份双十二数码好物清单&#xff0c;都是我从用户评价、产品亮点、折扣力…

python中base64编码

1. base64编码简介 用记事本打开exe、jpg、pdf这些文件时&#xff0c;我们都会看到一大堆乱码&#xff0c;因为二进制文件包含很多无法显示和打印的字符&#xff0c;所以&#xff0c;如果要让记事本这样的文本处理软件能处理二进制数据&#xff0c;就需要一个二进制到字符串的…

Perl与JS的对比分析(数组、哈希)

一、数组 可以对数组进行增删&#xff0c;插入。与JS不同的是这些函数都是全局的&#xff0c;JS则是挂在Array.prototype上。 1&#xff0c;对数组尾部的操作pop&#xff08;删除最后的元素&#xff09;、push&#xff08;在尾部添加&#xff09; 1 2 3 goods qw/pen penci…

[附源码]JAVA毕业设计婚纱影楼服务管理(系统+LW)

[附源码]JAVA毕业设计婚纱影楼服务管理&#xff08;系统LW&#xff09; 目运行 环境项配置&#xff1a; Jdk1.8 Tomcat8.5 Mysql HBuilderX&#xff08;Webstorm也行&#xff09; Eclispe&#xff08;IntelliJ IDEA,Eclispe,MyEclispe,Sts都支持&#xff09;。 项目技术…

BluePrism里WorkQueue的几种传法和区别

WorkQueue是开发中的好帮手&#xff0c;流程间任务流转非常方便&#xff0c;基本可以取代数据库的场景。 一.循环SourceData单行传入 把Queue的Item Key搭好New Item Data本来为空的collection&#xff0c;每次循环增加一行把Item Key加入New Item Data.Item KeyNew Item Data加…

SpringBoot+Vue实现前后端分离的员工日志管理信息系统

文末获取源码 开发语言&#xff1a;Java 使用框架&#xff1a;spring boot 前端技术&#xff1a;JavaScript、Vue.js 、css3 开发工具&#xff1a;IDEA/MyEclipse/Eclipse、Visual Studio Code 数据库&#xff1a;MySQL 5.7/8.0 数据库管理工具&#xff1a;phpstudy/Navicat JD…

以工程化路径破题,中国系统推动数据要素化

光阴似箭&#xff0c;大数据产业已走过十余载。 如今&#xff0c;人们在各种数字化场景中深刻感受到数据所蕴含的巨大价值&#xff0c;也深深为数据滥用、数据窃取、数据非法交易等各种乱象而担忧。似乎&#xff0c;数据安全与和数据流动、共享与使用已成一对天然的“矛盾”&a…

15.前端笔记-CSS-PS切图

1、图片格式 格式优点jpg同jpeg,产品类图片常用&#xff0c;高清&#xff0c;颜色多gif可以保存成透明背景和动画效果&#xff0c;图片小动画常用&#xff0c;最多存储256色png结合了jpg和gif的优点&#xff0c;保存为背景透明&#xff0c;存储形式丰富psdphotoshop的文件格式…

JMM(Java Memory Model)

Java虚拟机规范中定义了Java内存模型&#xff08;共享内存模型&#xff0c;实现线程与线程之间的通信&#xff0c;其中主内存是逻辑空间&#xff09;&#xff0c;用于屏蔽掉各种硬件和操作系统的内存访问差异&#xff0c;以实现让Java程序在各种平台下都能达到一致的并发效果&a…

30张图 讲清楚Redis Cluster

今天下午和一位同学聊Redis集群&#xff0c;这玩意真没那么简单&#xff0c;内容非常多。 Redis Cluster是Redis官方提供的Redis集群功能。 1.为什么要实现Redis Cluster 1.主从复制不能实现高可用 2.随着公司发展&#xff0c;用户数量增多&#xff0c;并发越来越多&#x…

基于微分段的东西向安全防护,如何提升数据中心运维效率?|社区成长营分享回顾

在社区成长营网络与安全第二期分享中&#xff0c;SmartX 高级产品营销经理张涛介绍了原生于超融合系统的微分段防火墙和其工作模式的特点&#xff08;点击查看第二期图文总结&#xff09;。 网络与安全第三期分享继续由张涛老师提供&#xff0c;以下为内容回顾。 SmartX 社区成…