安卓蓝牙开发与CH582M实现串口透传

您所在的位置:网站首页 安卓修改uuid 安卓蓝牙开发与CH582M实现串口透传

安卓蓝牙开发与CH582M实现串口透传

2023-09-29 21:07| 来源: 网络整理| 查看: 265

最是人间(西湖美景)三月天,春雨如酒柳如烟。已经三月底了,再过几天就入职两周年了。好快啊! 发现好好的写文章,真的很费时间。可能还是自己太水了,很简单的东西写出来还是要花很长时间。

前言 * 公司的一款设备使用了单片机+串口蓝牙模块,主机采用安卓端,主从设备双方开发人员对于蓝牙自动重连有不同的理解,提出当前的蓝牙模块无法实现自动重连,决定重新更换能够自动重连的蓝牙模块。因此需要我查找对应的硬件设备或是查找对应的解决方案。 * 以前做毕设及刚毕业那会,为了学习使用NRF51822/52830,了解和使用过一段时间的蓝牙,也是用安卓实现了简单的数据对传,但是由于不是工作关系,所以探究的不是很深,后来就放弃了。但是记忆中,蓝牙重连的实现好像不是很复杂,或者说,不是像他们说的,必须要更换特定的硬件才能实现,我认为在逻辑层就能。 * 现在重新开始走一遍安卓和蓝牙的连接流程,争取实现安卓与硬件设备间的蓝牙透传功能,然后探究一下重连机制的实现,验证一下我的想法,同时查找相关资料。 一:硬件说明 因为是测试安卓的开发流程,所以主要还是安卓端。硬件端我使用的是CH582M蓝牙芯片。 理论上来说,程序支持任意的蓝牙4.0以上的模块或是完整的程序的相关蓝牙芯片(对于经典蓝牙可能需要稍作修改,但是我们不用经典蓝牙,所以此处不做关心),UUID那里使用获取到的即可,但实际上我没有测其他的蓝牙模块。 1:CH582M芯片

芯片简介:

CH583 是集成 BLE 无线通讯的 32 位 RISC 微控制器。片上集成 2Mbps 低功耗蓝牙 BLE 通讯模块、2 个全速 USB 主机和设备控制器及收发器、2 个 SPI、4 个串口、ADC、触摸按键检测模块、RTC 等丰富的外设资源。 芯片手册 沁恒官网 程序编写环境:需要使用MounRiverStudio软件:MounRiverStudio 烧写软件:WCHISPTool 芯片使用教程:教程

实际上,我使用的是如下教程中的资料:

CH582M开发教程 开发板也是这家的,自带串口 其开发资料下载链接 LearnCH581-2-3 ,里面有我们所需要的的demo程序。下载Git中的资料,有详细的入门步骤和开发例程以及需要的安卓测试软件。可以按照教程打开【BLE_UART】例子进行编译和下载尝试。 windows端使用串口调试助手【UartAssist】,或是下载包中的串口助手。 下载后的文件夹如下: image 2:安卓端

使用的软件环境:

使用【AndroidStudio】环境。 使用沁恒电子提供的【ble调试助手】测试软件,程序为上面下载的Git文件包中的【工具助手】文件夹下的BLEAssist.apk。 蓝牙使用【FastBle开源库】。

FastBle 简介:

FastBle是安卓端关于蓝牙的集成封装库 支持与外围BLE设备进行扫描、连接、读、写、通知订阅与取消等基本操作 支持获取信号强度、设置最大传输单元 支持自定义扫描规则 支持多设备连接 支持重连机制 支持配置超时机制

FastBle中文Git网址

FastBle参考网址:

FastBle 的操作说明 Android BLE开发详解和FastBle源码解析 二:蓝牙 1:简介 蓝牙技术是一种无线数据和语音通信开放的全球规范,它是基于低成本的近距离无线连接,为固定和移动设备建立通信环境的一种特殊的近距离无线技术连接。 蓝牙是基于2.4Ghz频段的近距离无线连接技术,目前分为经典蓝牙和低功耗蓝牙。蓝牙版本已经迭代到最新的5.4版本。 2:蓝牙相关资料

蓝牙技术联盟中文网:是蓝牙标准联盟的官网,里面有丰富详尽的资料。 另外,也可以通过其他中文网站进行简单学习,如 总览蓝牙协议

3:需要了解的相关知识

以下是需要了解的安卓蓝牙开发的相关知识和蓝牙操作流程,不熟悉的可以自行查找相关资料。

经典蓝牙与低功耗蓝牙 蓝牙协议栈 蓝牙的主从模式及对应支持的广播、扫描、连接、数据收发等方式 蓝牙的UUID,以及 【服务UUID】 和 【特性UUID】 【特性UUID】的属性,如【订阅通知】、【读】、【写】等属性 蓝牙的连接和操作流程图: image 三:开发过程 step1:CH582M程序下载和连接测试:

注意:关于CH582M的入门使用可以参考下载文件夹中的教程,不再赘述。

由于是验证蓝牙的使用,所以用官方提供的透传例程就可以。此处选择 【BLE_UART】demo,位于上面下载的Git文件包的 【LearnCH582M\单片机开发相关\CH583EVT\EVT\EXAM\BLE\BLE_UART】文件夹下。

首先,编译此demo,然后使用【WCHISPTool】软件烧写到芯片上。

将MicroUSB线插到 【串口1】一端,打开【UartAssist】并正确连接到CH582M开发板上,波特率选115200,其他默认。

打开安卓端【ble调试助手】,点击搜索。

搜索到CH582M的蓝牙,名称为【ch583_ble_uart】,可自行在程序中按照规则更改。点击右侧的【CONNECT】进行连接: image

连接后就能看到当前连接设备的所有【服务UUID】,点击对应的列表,会显示当前服务UUID下的【特性UUID】: image

上图中划线处可以看到,服务UUID为 【0000fff0】,其下属的可读属性的特性UUID为【0000fff1】,可写属性的UUID为【0000fff2】。

在 CH582M程序的【APP->ble_uart_service_16bit.c】中的 【ble_uart GATT Profile Service UUID】部分可以看到: image 对应的正是上面的三个UUID。

点击安卓的 可写属性 图标,跳出写数据界面: image

我们在文本框中随便输入几个字符,选择单次发送,点击【发送】,在windows端的串口调试软件上可以看到有数据输出: image

实际上,默认程序只会输出第一行,即收到了几个数据,收到的数据是什么没有打印。我们可以在CH582M的程序的【peripheral.c】的on_bleuartServiceEvt()函数的 BLE_UART_EVT_BLE_DATA_RECIEVED 查看,发现它只有输出长度的提示。我们可以在里面加入输出蓝牙发送的数据的程序。程序增加后如下:

case BLE_UART_EVT_BLE_DATA_RECIEVED: PRINT("BLE RX DATA len:%d\r\n",p_evt->data.length); uint8_t sendbuf[50] ={0}; for(int i = 0;idata.length;i++){//复制p_evt->data.length规定的长度避免后面的值造成乱码 sendbuf[i]=p_evt->data.p_data[i]; } PRINT(sendbuf);//输出显示 PRINT("\r\n"); 于是,我们实现了使用已有程序的整个连接测试过程(目前只实现【安卓端的写入->蓝牙发送给CH582M->CH582M蓝牙接收->串口发送给win】这一流程,反向还没有实现,实际上测试发现,串口发送数据给CH582M,安卓不论是读取还是通知都没有响应,推测是CH582M的串口读取然后发送给蓝牙写出 这一步骤的问题。可以理解的是,只要实现了安卓蓝牙写入,那么安卓实现数据读取从而实现蓝牙透传也是没问题的。但由于时间关系,没有详细探究,后面有时间再研究一下)。 因此,总结一下,步骤就和上面的安卓连接流程图一样,【打开蓝牙->搜索蓝牙->连接目标设备->对特定的UUID进行读写实现通讯->完成关闭蓝牙】 这么几步。当然,本文主要用于探索重连,即在连接目标设备与主动断开之间进行重连判断。 step2:开始编写安卓程序 说明 对于想要入门安卓开发的,可以查找相关教程资源进行学习,比如: 安卓入门开发教程 此处对于安卓开发的基础环境不再赘述。(java是一门入门相对简单的编程语言,以前我还写过安卓小游戏。跟着任意一个教程走一遍,基本的功能就能写出来,后面可以根据兴趣爱好分流。) 另外,程序是之前写好的,写的时候命名和逻辑都很随意,功能也很简单,也可能有不少BUG,此处不再修改。 开始

以下程序已放置在 Gitee 上,可以自行下载编译。

0:权限和库等

安卓6.0以上使用蓝牙需要很多权限,所以需要是授权。 在【AndroidManifest.xml】中添加如下的代码来获取权限:

添加 Fastble库: 在 【build.gradle】的 dependencies{}中添加FastBle的包并等待其下载完毕

implementation 'com.github.Jasonchenlijian:FastBle:2.4.0' 1:定义一下需要的功能: * 一个用于显示扫描到的蓝牙的【ListView】控件 * 一个用于显示文本信息的【TextView】控件 * 一个用于输入传输文本信息的【EditText】控件 * 几个按钮【Button】,分别用于开启和关闭蓝牙扫描、连接和断开蓝牙、发送数据。几个相反状态的按钮可以集成在一个上,此处为了演示,每个功能一个按钮。 2:绘图,在 AndroidStudio中新建一个空白程序,在【active_main.xml】中拖拽上述几个控件。如图:

image 对其分别命名。关于控件的位置,可以自行处理,比如使用【layout】或是其他定位方法,使位置保证在合适的地方。 完成后,【active_main.xml】文件内容如下:

3:绑定上述控件

在【MainActivity】中定义并绑定这几个控件。 一开始,我们可以将除了【扫描】之外的按钮控件都设为失能状态。

buttonXXX.setEnabled(false);

然后,创建这几个按键的点击回调函数。

buttonXXX.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { } });

后面,就要根据需要创建不同的类了

4:创建【ListView】的显示处理类

因为listView使用上较按钮等控件更加复杂一些,所以我们将对它的处理集成在一个类中,方便调试和使用。 定义为 MyListViewShowClass 类,实现如下几个方法:

初始化方法,用于传递所需的上下文和其他必要资源 【添加】和【清除】方法 其他根据需要添加 最终代码如下: class MyListViewShowClass{ private ArrayAdapter arrayAdapter; private ListView MylistView; private Context context; private List listdata = new ArrayList();//用来给ListView显示使用,顺序需要和bleDeviceListData一致 public List bleDeviceListData = new ArrayList();//用来存储蓝牙扫描结果,要和listdata顺序保持一致,方便实用 private TextView textView; public int ListViewTouchedNum = 0; public int ListVierTouchedFlg = 0; public int ListViewShowUUIDFlg = 0; public void ListViewInit( Context context1, ListView listView1,TextView textView1){ //初始化,将需要的控件传递进来并绑定到本地 context = context1; MylistView = listView1; textView = textView1; arrayAdapter = new ArrayAdapter (context1, R.layout.support_simple_spinner_dropdown_item, listdata);//实例化一个适配器 MylistView.setAdapter(arrayAdapter);//将适配器绑定给listView控件 MylistView.setOnItemClickListener(new AdapterView.OnItemClickListener() { //listVier 点击回调事件 @Override public void onItemClick(AdapterView adapterView, View view, int i, long l) { //listView的点击回调函数 ListViewTouchedNum = i;//用于记录点击了第几个listV结尾,供【连接】按钮使用 ListVierTouchedFlg= 1;//用于记录是否点击过。因为是测试程序,所以就用简单的方法实现。实际上可以传递【连接】按键进来,进行更有逻辑的配置 if(ListViewShowUUIDFlg == 0) { textView.setText("touch:" + "i" + "\n"); textView.append(listdata.get(i) + "\n" + bleDeviceListData.get(i).getName() + "\n" + bleDeviceListData.get(i).getMac()); char atest[] = listdata.get(i).toCharArray();//返回点击了第几行 System.out.println("ListTouch:" + i + " nameLength:" + atest.length); }else{ textView.setText("UUID View: "+ i); } } }); } public void clean(){ //清空listView bleDeviceListData.clear(); listdata.clear(); MylistView.setAdapter(arrayAdapter); } public void add(BleDevice bleDevice){ //添加显示内容到listView,传递值为扫描得到的bleDevice String nameString = bleDevice.getName(); String macString = bleDevice.getMac(); String keyString = bleDevice.getKey(); for(int i=0;i19)data.substring(0,19);//大于20字节要分包或是设置MTU,此处测试,只截取,不做其他处理, byte sendData[] =data.getBytes(); //ble写函数,实质上就是对连接上的 bleDevice 设备的 服务UUID 下的 写特征UUID 写入 sendData 数据,所以最关键的就是获取到对应的UUID。因为我们主机和从机 //是可以事先定好UUID的,所以我们此处的UUID是事先写好的,可以根据程序需要自行获取对应设备的UUID,然后根据Property属性来判断是否可以读写 BleManager.getInstance().write( bleDevice, ServiceUUID, CharacteristicUUID_Write, sendData, new BleWriteCallback() { @Override public void onWriteSuccess(int current, int total, byte[] justWrite) { textView.setText("Write OK!:"+data); System.out.println("Write OK!:"+data); } @Override public void onWriteFailure(BleException exception) { textView.setText("onWriteFailure!"); System.out.println("onWriteFailure!"); } } ); } public void bleNotify(){ //notify 订阅属性,即蓝牙收到的主动通知的回调函数 //此处没调通,应该是是CH582M那边的问题 BleManager.getInstance().notify( bleDevice, ServiceUUID, CharacteristicUUID_Read, new BleNotifyCallback() { @Override public void onNotifySuccess() { System.out.println("onNotifySuccess"); textView.setText("onNotifySuccess"); } @Override public void onNotifyFailure(BleException exception) { System.out.println("onNotifyFailure"); textView.setText("onNotifyFailure"); } @Override public void onCharacteristicChanged(byte[] data) { System.out.println(data); //textView.setText("GetDaata:"+data); } } ); } public void bleRead(){ //ble主动读操作 //此处没调通,应该是是CH582M那边的问题 BleManager.getInstance().read( bleDevice, ServiceUUID, CharacteristicUUID_Read, new BleReadCallback() { @Override public void onReadSuccess(byte[] data) { System.out.println("onReadSuccess!Get datalength:"+data.length); textView.setText("onReadSuccess!Get datalength:"+data.length); } @Override public void onReadFailure(BleException exception) { System.out.println("onReadFailure"); textView.setText("onReadFailure"); } } ); } public void bleDisconnectedScanTimer(){ //定义一个用于自动重连的扫描定时器 //只测试了一主一从的方式,一主多从没有测试。 task = new TimerTask() { @Override public void run() { //BleManager.getInstance().cancelScan();//不要随随便便的取消扫描,会导致崩溃! System.out.println("Count:"+TimerCount); textView.setText("Count:"+TimerCount); //扫描配置,配置为之前连上的bleDevice的参数,这样就可以只扫描是否有目标设备了,不会返回其他的 //下面的可选,就是可以将不需要的注释掉,需要的填上所需的值 BleScanRuleConfig scanRuleConfig = new BleScanRuleConfig.Builder() //.setServiceUuids(bleDevice.) // 只扫描指定的服务的设备,可选 .setDeviceName(true, bleDevice.getName()) // 只扫描指定广播名的设备,可选 .setDeviceMac(bleDevice.getMac()) // 只扫描指定mac的设备,可选 .setAutoConnect(true) // 连接时的autoConnect参数,可选,默认false .setScanTimeOut(10000) // 扫描超时时间,可选,默认10秒;小于等于0表示不限制扫描时间 .build(); BleManager.getInstance().initScanRule(scanRuleConfig); BleManager.getInstance().scanAndConnect(new BleScanAndConnectCallback() { //scanAndConnect:此方法就是扫描到符合的设备然后自动连接,不用主动调用connect方法。 //另外,这个连接后的断连回调在本方法中,不会渠道上面connect的断连方法中 @Override public void onScanFinished(BleDevice scanResult) { //scanAndConnect方法扫描成功的回调函数 if(BleManager.getInstance().isConnected(bleDevice)) { //此处主动判断一次是否连接成功,不成功就再扫描。其实后面发现下面有个onConnectSuccess回调... System.out.println("onTimerScanFinished"); textView.setText("onTimerScanFinished"); buttonConnect.setEnabled(false); buttonDisConnect.setEnabled(true); buttonSendOneTime.setEnabled(true); }else{ System.out.println("onTimerScanFinished ButNotConnected"); textView.setText("onTimerScanFinished ButNotConnected"); if(bleDisconnecteType == false){//不是主动点击按钮断联的 //开始扫描定时器操作 if(bleDevice.getName() != null){//不为空 bleDisconnectedScanTimer(); bleTimerStart(10000);//加长重连时间 } } } } @Override public void onStartConnect() { System.out.println("onTimerStartConnect"); textView.setText("onTimerStartConnect"); } @Override public void onConnectFail(BleDevice bleDevice, BleException exception) { System.out.println("onTimerConnectFail"); textView.setText("onTimerConnectFail"); } @Override public void onConnectSuccess(BleDevice bleDevice, BluetoothGatt gatt, int status) { System.out.println("onTimerConnectSuccess"); textView.setText("onTimerConnectSuccess"); buttonConnect.setEnabled(false); buttonDisConnect.setEnabled(true); buttonSendOneTime.setEnabled(true); } @Override public void onDisConnected(boolean isActiveDisConnected, BleDevice device, BluetoothGatt gatt, int status) { System.out.println("onTimerDisConnected"); textView.setText("onTimerDisConnected"); buttonConnect.setEnabled(true); buttonDisConnect.setEnabled(false); buttonSendOneTime.setEnabled(false); if(bleDisconnecteType == false){//不是主动点击按钮断联的 //开始扫描定时器操作 if(bleDevice.getName() != null){//不为空 bleDisconnectedScanTimer(); bleTimerStart(5000); } } } @Override public void onScanStarted(boolean success) { System.out.println("onTimerScanStarted"); textView.setText("onTimerScanStarted"); } @Override public void onScanning(BleDevice bleDevice) { System.out.println("onTimerScanning"); textView.setText("onTimerScanning"); } }); } }; } public void bleTimerStart(int delaytime){ timer.schedule(task, delaytime);//只填一个参数就是只执行一次 } public void bleTimerStop(){ //timer.cancel(); } public void bleTimerClean(){ timer.purge(); } } 6:完善主程序的运行

首先是按照蓝牙流程,判断是否支持蓝牙,和蓝牙是否已经打开。然后就是完善各个按键的功能和状态:

public class MainActivity extends AppCompatActivity { private Button buttonSearch ,buttonConnect,buttonDisConnect,buttonStopScarch,buttonSendOneTime; private TextView text1InforShow; private ListView listView1; private EditText editText; final MyListViewShowClass myListViewShowClass = new MyListViewShowClass();//创建新的 MyListViewShowClass()实例 final BleDealClass bleDealClass = new BleDealClass();//创建新的 BleDealClass()实例 @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); buttonSearch=(Button)findViewById(R.id.button1); buttonStopScarch = (Button)findViewById(R.id.button); buttonConnect=(Button)findViewById(R.id.button2); buttonDisConnect=(Button)findViewById(R.id.button4); text1InforShow=(TextView)findViewById(R.id.text1); listView1 = (ListView)findViewById(R.id.listview); buttonSendOneTime = (Button)findViewById(R.id.button3); editText = (EditText)findViewById(R.id.editText); //先将几个按键设为失能 buttonSearch.setEnabled(false); buttonStopScarch.setEnabled(false); buttonConnect.setEnabled(false); buttonDisConnect.setEnabled(false); buttonSendOneTime.setEnabled(false); myListViewShowClass.ListViewInit(MainActivity.this,listView1,text1InforShow);//初始化绑定控件 bleDealClass.bleButtonInit(buttonSearch ,buttonConnect,buttonDisConnect,buttonStopScarch,buttonSendOneTime); bleDealClass.bleInit(getApplication(),myListViewShowClass); //初始化蓝牙 if(!bleDealClass.isSupportBle()){ //判断是否支持BLE text1InforShow.setText("Device Not Suppot Ble!\n"); bleDealClass.bleIsSupport = false; }else{ bleDealClass.bleIsSupport = true; if(!bleDealClass.isBlueEnable()){//判断是否开启BLE text1InforShow.setText("Device's Ble Not Opend!\n"); bleDealClass.bleIsEnable = false; }else{ bleDealClass.bleIsEnable = true; } } if(bleDealClass.bleIsEnable == true && bleDealClass.bleIsSupport == true){ //若是支持蓝牙且蓝牙已开启,则打开扫描按键,允许扫描 buttonSearch.setEnabled(true); text1InforShow.append("Device's Support Ble And it had Opend!\n"); }else{ if(bleDealClass.bleIsEnable == false && bleDealClass.bleIsSupport == true){ text1InforShow.setText("Device's Ble Not Opend! Now will Open the Ble!\n"); bleDealClass.enableBluetooth(); } } buttonSearch.setOnClickListener(new View.OnClickListener() { //搜索按钮的点击事件 @Override public void onClick(View v) { if(bleDealClass.bleIsScaning == true){//已经开启扫描了,先关闭 bleDealClass.cancelScan(); } myListViewShowClass.clean();//清空列表 text1InforShow.append("Now Start Scan Ble!"); bleDealClass.bleScan(text1InforShow); buttonStopScarch.setEnabled(true); buttonSearch.setEnabled(false); bleDealClass.bleDisconnecteType=false;//清除标志位 } }); buttonStopScarch.setOnClickListener(new View.OnClickListener() { //取消扫描的点击事件 @Override public void onClick(View view) { if(bleDealClass.bleIsScaning == true){ text1InforShow.setText("Now Stop Scan Ble!"); bleDealClass.cancelScan(); bleDealClass.bleIsScaning = false; buttonStopScarch.setEnabled(false); buttonSearch.setEnabled(true); buttonDisConnect.setEnabled(false); buttonSendOneTime.setEnabled(false); } } }); buttonConnect.setOnClickListener(new View.OnClickListener() { //连接的点击事件 @Override public void onClick(View view) { if(myListViewShowClass.ListVierTouchedFlg == 1){ text1InforShow.setText("开始连接蓝牙 "+myListViewShowClass.ListViewTouchedNum+"\n"); text1InforShow.append("Name: "+myListViewShowClass.bleDeviceListData.get(myListViewShowClass.ListViewTouchedNum).getName()+"\n"+ "Mac: "+myListViewShowClass.bleDeviceListData.get(myListViewShowClass.ListViewTouchedNum).getMac()+"\n"+ "Key: "+myListViewShowClass.bleDeviceListData.get(myListViewShowClass.ListViewTouchedNum).getKey()+"\n"+ "Device: "+myListViewShowClass.bleDeviceListData.get(myListViewShowClass.ListViewTouchedNum).getDevice()+"\n"+ "Rssi: "+myListViewShowClass.bleDeviceListData.get(myListViewShowClass.ListViewTouchedNum).getRssi()+"\n"+ "ScanRecord: "+myListViewShowClass.bleDeviceListData.get(myListViewShowClass.ListViewTouchedNum).getScanRecord() ); bleDealClass.cancelScan();//取消继续扫描 bleDealClass.bleConnect(myListViewShowClass.bleDeviceListData.get(myListViewShowClass.ListViewTouchedNum));//开始连接 }else{ text1InforShow.setText("Please slect a bleDevice From ListView!"); } } }); buttonDisConnect.setOnClickListener(new View.OnClickListener() { //主动断开连接 @Override public void onClick(View view) { bleDealClass.bleDisconnecte(); bleDealClass.bleDisconnecteType=true;//是主动断开的 myListViewShowClass.ListVierTouchedFlg = 0; } }); buttonSendOneTime.setOnClickListener(new View.OnClickListener() { //单次发送按键点击事件 @Override public void onClick(View view) { String SendData; SendData=editText.getText().toString();//获取字符串 if(SendData.length()


【本文地址】


今日新闻


推荐新闻


CopyRight 2018-2019 办公设备维修网 版权所有 豫ICP备15022753号-3