前言:好久没更新这博客了,接下来有时间记录下前阵子做的东西,当作是复习之余的回顾往事和时间消遣,哈哈。首先是这个蓝牙遥控APP,这个APP是之前做一个小比赛的时候尝试做来玩玩的,主要内容是使用这个APP来与HC05设备进行数据交互然后控制一个灯光云台,虽然最后拿了个小奖,但是毕竟术业有专攻,做的不是很好,也过去一段时间了,下面记录一下主要的实现过程。先有一个整个工程的概念逻辑,逐步完善即是perfect。
GitHub仓库地址:https://github.com/linzs-online/Bluetooth_App.git
环境:Android Studio,Android系统
语言:Android、Java
主要实现功能:启动手机蓝牙、搜索蓝牙、蓝牙数据传输
主要技术指标:局限布局、列表、弹出窗口、启动蓝牙……
正文:
这里把整个工程分成两个部分:APP控件布局+主函数code
一、APP控件布局
因为这里使用的是Android Studio,它有一种比较合适小白新手入门的布局方式constraintlayout,这种方式可以直接拖拽空间进入布局界面然后设置限位即可完成布局非常方便,当然有时候它表现得也不是很好(可能是我不会使用),在多控件的时候这个布局会出现一些错位,需要手动调整合适的限位才能正确显示控件位置。
主要的控件有:SeekBar、TextView、Button、Switch
这个布局比较入门没啥好记录的,注意的就是在控件多起来的时候需要通过code来调整位置。还有就是那个SeekBar控件要设置一些初值,如果想要改变颜色的话也可以在交互界面调整,这个AndroidStudio个人感觉做得真的非常好,很人性化小白入门也很快。下面给出布局的code
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout 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"
tools:context=".MainActivity"
tools:layout_editor_absoluteY="81dp">
<SeekBar
android:id="@+id/Yaw"
android:layout_width="202dp"
android:layout_height="21dp"
android:background="#EFF1F3"
android:max="355"
android:progress="0"
android:secondaryProgress="15"
app:layout_constraintBottom_toBottomOf="@+id/Yaw_view"
app:layout_constraintStart_toEndOf="@+id/Yaw_view" />
<TextView
android:id="@+id/Yaw_view"
android:layout_width="129dp"
android:layout_height="21dp"
android:layout_marginStart="16dp"
android:layout_marginLeft="16dp"
android:layout_marginBottom="28dp"
android:background="#FFFFFF"
android:text="@string/yaw"
app:layout_constraintBottom_toTopOf="@+id/light_view"
app:layout_constraintStart_toStartOf="@+id/light_view" />
<Button
android:id="@+id/ADD_BLUETOOTH"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="60dp"
android:layout_marginRight="60dp"
android:layout_marginBottom="60dp"
android:text="@string/Add_bluetooth"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent" />
<Button
android:id="@+id/Scene_1"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/Sence1"
app:layout_constraintBottom_toBottomOf="@+id/Scene_2"
app:layout_constraintEnd_toStartOf="@+id/WorkView"
app:layout_constraintTop_toTopOf="@+id/Scene_2" />
<Button
android:id="@+id/Scene_4"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="28dp"
android:text="@string/Sence4"
app:layout_constraintEnd_toEndOf="@+id/Scene_2"
app:layout_constraintHorizontal_bias="0.0"
app:layout_constraintStart_toStartOf="@+id/Scene_2"
app:layout_constraintTop_toBottomOf="@+id/Scene_2" />
<Button
android:id="@+id/Scene_2"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="20dp"
android:text="@string/Scene2"
app:layout_constraintStart_toEndOf="@+id/WorkView"
app:layout_constraintTop_toBottomOf="@+id/WorkView" />
<Button
android:id="@+id/Scene_3"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/Sence3"
app:layout_constraintBottom_toBottomOf="@+id/Scene_4"
app:layout_constraintEnd_toEndOf="@+id/Scene_1"
app:layout_constraintStart_toStartOf="@+id/Scene_1"
app:layout_constraintTop_toTopOf="@+id/Scene_4" />
<TextView
android:id="@+id/Pitch_view"
android:layout_width="129dp"
android:layout_height="21dp"
android:background="#FFFFFF"
android:text="@string/BrightneesResult"
app:layout_constraintBottom_toBottomOf="@+id/Pitch"
app:layout_constraintEnd_toEndOf="@+id/Yaw_view"
app:layout_constraintHorizontal_bias="0.0"
app:layout_constraintStart_toStartOf="@+id/Yaw_view"
app:layout_constraintTop_toTopOf="@+id/Pitch"
app:layout_constraintVertical_bias="0.0" />
<SeekBar
android:id="@+id/Pitch"
android:layout_width="204dp"
android:layout_height="19dp"
android:layout_marginBottom="24dp"
android:background="#EFF1F3"
android:max="60"
android:progress="0"
android:secondaryProgress="30"
app:layout_constraintBottom_toTopOf="@+id/Yaw"
app:layout_constraintEnd_toEndOf="@+id/Yaw" />
<Switch
android:id="@+id/LED_Switch"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="76dp"
android:layout_marginLeft="76dp"
android:layout_marginBottom="60dp"
android:text="@string/Switch"
app:layout_constraintBottom_toBottomOf="@+id/ADD_BLUETOOTH"
app:layout_constraintEnd_toStartOf="@+id/ADD_BLUETOOTH"
app:layout_constraintHorizontal_bias="0.0"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="@+id/ADD_BLUETOOTH"
app:layout_constraintVertical_bias="0.0" />
<TextView
android:id="@+id/WorkView"
android:layout_width="155dp"
android:layout_height="105dp"
android:text="@string/workview"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.512"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_bias="0.002" />
<SeekBar
android:id="@+id/light"
android:layout_width="287dp"
android:layout_height="17dp"
android:layout_marginEnd="24dp"
android:layout_marginRight="24dp"
android:layout_marginBottom="32dp"
android:max="100"
android:progress="0"
app:layout_constraintBottom_toTopOf="@+id/ADD_BLUETOOTH"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="@+id/LED_Switch" />
<TextView
android:id="@+id/light_view"
android:layout_width="69dp"
android:layout_height="19dp"
android:text="@string/light_view"
app:layout_constraintBottom_toBottomOf="@+id/light"
app:layout_constraintEnd_toStartOf="@+id/light"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="@+id/light" />
</androidx.constraintlayout.widget.ConstraintLayout>
二、主函数code
主要有两个Activity、一个是主界面的另一个是连接蓝牙弹窗的listview使用的
1、主界面code
这里主要是对一些控件的初始化,然后申请调用系统的蓝牙,对蓝牙进行一些配置
1)Button等基本控件这里简单一带而过
主要是先定义这个控件
private Button ADD_BLUETOOTH;
private Button Scene_1;
private Button Scene_2;
private Button Scene_3;
private Button Scene_4;
private BluetoothAdapter mBluetoothAdapter;
private BluetoothManager mBluetoothManager;
private SeekBar Yaw;
private TextView Yaw_view;
private TextView Pitch_view;
private SeekBar Pitch;
private SeekBar light;
private TextView light_view;
private View bluetooth_view;
private AlertDialog mAlertDialog;
private ListView listView;
private Switch LED_Switch;
private TextView WorkView;
然后找到版面的对应控件,对控件进行初始化配置
private void initView() {
ADD_BLUETOOTH = (Button) findViewById(R.id.ADD_BLUETOOTH);
ADD_BLUETOOTH.setOnClickListener(this);
Scene_1 = (Button) findViewById(R.id.Scene_1);
Scene_1.setOnClickListener(this);
Scene_2 = (Button) findViewById(R.id.Scene_2);
Scene_2.setOnClickListener(this);
Scene_3 = (Button) findViewById(R.id.Scene_3);
Scene_3.setOnClickListener(this);
Scene_4 = (Button) findViewById(R.id.Scene_4);
Scene_4.setOnClickListener(this);
LED_Switch = (Switch) findViewById(R.id.LED_Switch);
LED_Switch.setOnClickListener(this);
LED_Switch.setOnCheckedChangeListener(this);
WorkView = (TextView) findViewById(R.id.WorkView);
WorkView.setOnClickListener(this);
}
然后设置一些点击事件执行函数
@Override
public void onClick(View v) {
switch (v.getId()) {
case R.id.ADD_BLUETOOTH:
Add_bluetooth();
break;
case R.id.Scene_1:
Work_Scene_1();
break;
case R.id.Scene_2:
Work_Scene_2();
break;
case R.id.Scene_3:
Work_Scene_3();
break;
case R.id.Scene_4:
Work_Scene_4();
break;
}
}
在系统初始化控件之后就会进入这个点击事件检测,如果触发点击事件就执行相应的Scene函数,触发事件可以直接写入到初始化那个函数里面,这里是使用这种switch case的方式,个人感觉这种结构清晰明了。
除了Bottom控件之外SeekBar控件也要设置相应的触发事件,因为我这里功能定义是要无级调光和调整云台角度,所以我这里的触发事件是拖动条进度改变时调用的,如下:
light = (SeekBar) findViewById(R.id.light);
light.setOnClickListener(this);
light.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() {
/*拖动条停止拖动时调用 */
@Override
public void onStopTrackingTouch(SeekBar seekBar) {
}
/*拖动条开始拖动时调用*/
@Override
public void onStartTrackingTouch(SeekBar seekBar) {
}
/* 拖动条进度改变时调用*/
@Override
public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
light_view.setText("亮度:" + progress );
msg_buffer[3]=(byte)((progress)>>0*8);
msg_buffer[4]=(byte)((progress)>>1*8);
msg_buffer[5]=(byte)((progress)>>2*8);
msg_buffer[6]=(byte)((progress)>>3*8);
Write(msg_buffer);
}
});
light_view = (TextView) findViewById(R.id.light_view);
light_view.setOnClickListener(this);
light_view.setText("亮度:"+light.getProgress());
除此之外,还可以给控件设定一些初始值,在控件初始化之后调用一次即可
light.setProgress(50);
Yaw.setProgress(180);
Pitch.setProgress(30);
还要初始化弹窗控件AlertDialog,弹窗用于点击添加设备之后弹窗设备搜寻列表展示搜寻设备结果提供联机对象列表
bluetooth_view = getLayoutInflater().inflate(R.layout.activity_bluetooth_link, null);
listView = bluetooth_view.findViewById(R.id.blue_list);
mAlertDialog = new AlertDialog.Builder(this).setTitle("蓝牙设备列表\n")
.setIcon(R.mipmap.ic_launcher)
.setView(bluetooth_view)
.setPositiveButton("取消", new DialogInterface.OnClickListener() {
public void onClick(DialogInterface paramAnonymousDialogInterface,
int paramAnonymousInt) {
mBluetoothAdapter.cancelDiscovery();//取消蓝牙扫描
}
}).create();
另外一个Activity和简单,它的作用就是当进入搜索设备准备联机模式时候开启的,主要是一个ListView控件
package com.example.bluetoothsdkapp;
import android.os.Bundle;
import android.widget.ListView;
import androidx.appcompat.app.AppCompatActivity;
public class bluetooth_link extends AppCompatActivity {
private ListView blue_list;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_bluetooth_link);
initView();
}
private void initView() {
blue_list = (ListView) findViewById(R.id.blue_list);
}
}
2)蓝牙配置
首先要定义两个列表,分别用来存蓝牙名称和设备的地址。
//定义一个列表,存蓝牙名称的地址。
public ArrayList<String> deviceName = new ArrayList<>();
//定义一个列表,存蓝牙设备的地址。
private List<BluetoothDevice> arrayList = new ArrayList<>();
还有配置一些协议和定义客户端模式
//服务和特征值
private UUID write_UUID_service;
private UUID write_UUID_chara;
private UUID read_UUID_service;
private UUID read_UUID_chara;
private UUID notify_UUID_service;
private UUID notify_UUID_chara;
private UUID indicate_UUID_service;
private UUID indicate_UUID_chara;
private final UUID MY_UUID = UUID.fromString("00001101-0000-1000-8000-00805F9B34FB");
private BluetoothDevice selectDevice;
// 获取到选中设备的客户端串口,全局变量,否则连接在方法执行完就结束了
private BluetoothSocket clientSocket;
// 获取到向设备写的输出流,全局变量,否则连接在方法执行完就结束了
private OutputStream os;
// 连接对象的名称
private final String Name = "HC05";
// 服务端利用线程不断接受客户端信息
private AcceptThread thread;
然后是蓝牙的初始化函数,申请调用系统蓝牙,然后配置蓝牙相关参数
public void init_bluetooth() {
mBluetoothAdapter = BluetoothAdapter.getDefaultAdapter();//获取蓝牙适配器
if (mBluetoothAdapter == null) {//表示手机不支持蓝牙
finish();
return;
}
if (mBluetoothAdapter.getState() == mBluetoothAdapter.STATE_OFF) { //打开蓝牙
mBluetoothAdapter.enable();
}
IntentFilter intentFilter = new IntentFilter();//注册广播接收信号
intentFilter.addAction(BluetoothDevice.ACTION_FOUND);
intentFilter.addAction(BluetoothAdapter.ACTION_REQUEST_DISCOVERABLE);
intentFilter.addAction(BluetoothDevice.ACTION_BOND_STATE_CHANGED);
registerReceiver(bluetoothReceiver, intentFilter);//注册,当一个设备被发现时调用bluetoothReceiver
Toast.makeText(getApplicationContext(), "蓝牙配置初始化成功!", Toast.LENGTH_SHORT).show();
}
接下来是广播机制接收信号的code
private final BroadcastReceiver bluetoothReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
String action = intent.getAction();
switch (action) {
case BluetoothDevice.ACTION_FOUND: //找到一个设备之后响应
BluetoothDevice device = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE);
//添加名字和地址到list中
if (device.getName() != null && arrayList.indexOf(device) == -1) {//防止重复添加
deviceName.add("\n" + "设备名:" + device.getName() + "\n" + "设备地址:" + device.getAddress() + "\n");
arrayList.add(device);//将搜索到的蓝牙地址添加到列表
adapter.notifyDataSetChanged();//更新
}
break;
case BluetoothDevice.ACTION_BOND_STATE_CHANGED:
BluetoothDevice bluetoothDevice = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE);
int state = bluetoothDevice.getBondState();
if (state == BluetoothDevice.BOND_BONDED) {
Log.e("###", "连接成功");
}
break;
}
}
};
然后是配置list以及扫描蓝牙设备,添加蓝牙设备到列表中,
public void Add_bluetooth() {
mAlertDialog.show();//以弹窗的形式显示蓝牙列表
if (mBluetoothAdapter.isDiscovering()) {
//判断蓝牙是否正在扫描,如果是调用取消扫描方法;如果不是,则开始扫描
mBluetoothAdapter.cancelDiscovery();
} else {
mBluetoothAdapter.startDiscovery();
new Handler().postDelayed(new Runnable() {
@Override
public void run() {
mBluetoothAdapter.cancelDiscovery();
}
}, 8000);
}
adapter = new ArrayAdapter<String>(MainActivity.this, android.R.layout.simple_list_item_1, deviceName);
listView.setAdapter(adapter);
listView.setOnItemClickListener(new AdapterView.OnItemClickListener() {
@Override
public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
// 判断当前是否还是正在搜索周边设备,如果是则暂停搜索
if (mBluetoothAdapter.isDiscovering()) {
mBluetoothAdapter.cancelDiscovery();
}
// 如果选择设备为空则代表还没有选择设备
if (selectDevice == null) {
selectDevice = arrayList.get(position);
}
if (selectDevice.getBondState() == BluetoothDevice.BOND_NONE) {
try {
Method method = BluetoothDevice.class.getMethod("createBond");//配对
Log.d("TAG", "开始配对");
method.invoke(selectDevice);
} catch (Exception e) {
e.printStackTrace();
}//配对完成
}
// 实例接收客户端传过来的数据线程
thread = new AcceptThread();
// 接收线程开始
thread.start();
try {
// 判断客户端接口是否为空
if (clientSocket == null) {
// 获取到客户端接口
clientSocket = selectDevice.createRfcommSocketToServiceRecord(MY_UUID);
// 向服务端发送连接
clientSocket.connect();
Toast.makeText(getApplicationContext(), "蓝牙连接成功!", Toast.LENGTH_SHORT).show();
// 获取到输出流,向外写数据
os = clientSocket.getOutputStream();
//关闭弹窗
mAlertDialog.dismiss();
}
} catch (IOException e) {
e.printStackTrace();
Toast.makeText(getApplicationContext(), "蓝牙连接失败!", Toast.LENGTH_SHORT).show();
}
}
});
}
之后就是配置一下数据流模式,以及做一套自定义的数据传送协议即可,我这里是用帧头校验的方式来实现数据交流
/**
* 传输数据
*
* @param message
*/
public void Write(byte[] message) {
try {
if (os != null) {
msg_buffer[0]=0x30;
//String msgString = new String(msgArray);//把Char型转成String发送
//os.write(msgString.getBytes("utf-8"));
os.write(message);
}
Log.e("OS", "write:" + message);
} catch (IOException e) {
e.printStackTrace();
Toast.makeText(getApplicationContext(), "写数据失败!", Toast.LENGTH_SHORT).show();
}
// //关闭流
// try {
// os.close();
// } catch (IOException e) {
// e.printStackTrace();
// }
}
之后另开一个线程做数据传送的
// 服务端,需要监听客户端的线程类
private Handler handler = new Handler() {
public void handleMessage(Message msg) {
super.handleMessage(msg);
}
};
// 线程服务类
public class AcceptThread extends Thread {
private BluetoothServerSocket serverSocket;
private BluetoothSocket socket;
// 输入 输出流
private OutputStream os;
private InputStream is;
public AcceptThread() {
try {
serverSocket = mBluetoothAdapter.listenUsingRfcommWithServiceRecord(Name, MY_UUID);
} catch (IOException e) {
e.printStackTrace();
}
}
@Override
public void run() {
// 截获客户端的蓝牙消息
try {
// 接收其客户端的接口
socket = serverSocket.accept();
// 获取到输入流
is = socket.getInputStream();
// 获取到输出流
os = socket.getOutputStream();
// 循环来接收数据
while (true) {
// 创建一个128字节的缓冲
byte[] buffer = new byte[128];
// 每次读取128字节,并保存其读取的角标
int count = is.read(buffer);
// 创建Message类,向handler发送数据
Message msg = new Message();
// 发送一个String的数据,让他向上转型为obj类型
msg.obj = new String(buffer, 0, count, "utf-8");
// 发送数据
handler.sendMessage(msg);
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
然后不要忘记了注销Activity的时候关闭蓝牙
@Override
protected void onDestroy() {
super.onDestroy();//解除注册
unregisterReceiver(bluetoothReceiver);
mBluetoothAdapter.cancelDiscovery();
mBluetoothAdapter.disable();
}
三、效果
第一次接触Android也没认真学习过Java这门语言,做得不是很好,参考了很多前辈的经验,踩了很多坑,算是能够实现自定义蓝牙数据交互APP吧。因为云台拆卸了,没法展示遥控部分,下面是这个APP的一些截图。