Unity与Android的蓝牙通信

平台

  Windows 10
  Unity 2020.3
  Android Studio 2020.3

安装模块

Unity Android模块

  打开Unity Hub,安装Android Build Support模块:
安装Android模块

Android环境配置

  首先需要在Anroid Studio上编写好与Unity通信的java包,然后将生成的文件放到Unity中,最后Build生成apk文件,运行在相应的安卓设备上。

新建Android Studio工程

  打开Android Studio,新建工程:
新建工程
  这里名字可以任意选择,注意记住这里的SDK版本号。

新建Android Library

  这里我们需要导出的是jar文件,所以需要新建一个Android库。选择File->New->New Module,选择Android Library新建:
新建Android库

添加UnityAndroid jar

  切换至Project文件目录,选择刚才新建的Android Libray文件夹下面的libs文件夹,将UnityAndroid jar包文件(C:\Program Files\Unity\Hub\Editor\2020.3.17f1c1\Editor\Data\PlaybackEngines\AndroidPlayer\Variations\il2cpp\Release\Classes\classes.jar)复制到此文件夹:
复制Unity jar包文件
  并右击选择Add as Library将其设置为库文件:
设置为库文件

新建MainActivity文件

  在myapplication->src->main->java->com.example.myapplocation文件夹右击选择New->Activity->Empty Activity新建空白Activity
新建Activity
  注意这里不要选择Generate a Layout File生成布局文件。

添加Unity3D UnityPlayerActivity.java文件

  由于新版Unity(2018版后),UnityPlayerActivity类没有放到刚才的classes.jar中,因此需要将此文件单独放置到Android的包文件夹中。
  新建包com.unity3d.player,将UnityPlayerActivity.java文件(C:\Program Files\Unity\Hub\Editor\2020.3.17f1c1\Editor\Data\PlaybackEngines\AndroidPlayer\Source\com\unity3d\player\UnityPlayerActivity.java)复制到此包文件夹中:
复制UnityPlayerActivity文件

更改AndroidManifest.xml

  将android工程的app模块(参见上文的文件目录)中的app->src->main->res->values->strings.xmlapp->src->main->AndroidManifest.xml文件一并复制替换到myapplication库模块的相应位置。
  并更改AndroidManifest.xml中的内容为:
更改AndroidManifest文件
  附:

1
<meta-data android:name="unityplayer.UnityActivity" android:value="true" />

生成jar包文件

  点击编译文件工具,即可在build文件夹下看到生成的jar包文件:
编译生成jar包文件

Android端蓝牙通讯模块

BluetoothService

  在刚才新建的Android模块中新建MainActivity.javaBluetoothService.java文件:
  MainActivity.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;

import android.annotation.SuppressLint;
import android.bluetooth.BluetoothDevice;
import android.bluetooth.BluetoothAdapter;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.os.Handler;
import android.os.Looper;
import android.os.Message;
import android.widget.Toast;

import androidx.annotation.RequiresPermission;

import com.unity3d.player.UnityPlayer;
import com.unity3d.player.UnityPlayerActivity;

public class MainActivity extends UnityPlayerActivity {

public static final int MESSAGE_STATE_CHANGE = 1;
public static final int MESSAGE_READ = 2;
public static final int MESSAGE_WRITE = 3;
public static final int MESSAGE_DEVICE_NAME = 4;
public static final int MESSAGE_TOAST = 5;

private static final int REQUEST_CONNECT_DEVICE = 1;
private static final int REQUEST_ENABLE_BT = 2;

private static final String TAG = "BluetoothPlugin";
private static final String TARGET = "BluetoothModel";

private boolean IsScan = false;

private String mConnectedDeviceName = null;

private StringBuffer mOutStringBuffer;
private BluetoothAdapter mBtAdapter = null;
private BluetoothService mBtService = null;

private ArrayList<String> singleAddress = new ArrayList();

private String pairedDevicesName = "";

private Map<String, String> pairedDevicesMap = new HashMap<String,String>();

private String readMessage = "";

private String connectStatus = "false";

// 处理消息模块Handle
private final Handler mHandler = new Handler() {
public void handleMessage(Message msg) {
switch(msg.what) {
case MESSAGE_STATE_CHANGE:
UnityPlayer.UnitySendMessage(TARGET, "OnStateChanged", String.valueOf(msg.arg1));
break;
case MESSAGE_READ:
// Log.d(TAG, msg.obj.toString());
//byte[] readBuf = (byte[])msg.obj;
//String readMessage = new String(readBuf, 0, msg.arg1);
// UnityPlayer.UnitySendMessage(TARGET, "OnReadMessage", readMessage);
//Toast.makeText(MainActivity.this.getApplicationContext(), readMessage, Toast.LENGTH_SHORT).show();
setReadMessage(msg.obj.toString());
break;
case MESSAGE_WRITE:
byte[] writeBuf = (byte[])msg.obj;
String writeMessage = new String(writeBuf);
// UnityPlayer.UnitySendMessage(TARGET, "OnSendMessage", writeMessage);
break;
case MESSAGE_DEVICE_NAME:
MainActivity.this.mConnectedDeviceName = msg.getData().getString("device_name");
setConnectStatus("true");
Toast.makeText(MainActivity.this.getApplicationContext(), "Connected to " + MainActivity.this.mConnectedDeviceName, Toast.LENGTH_SHORT).show();
break;
case MESSAGE_TOAST:
Toast.makeText(MainActivity.this.getApplicationContext(), msg.getData().getString("toast"), Toast.LENGTH_SHORT).show();
break;
}

}
};

private final BroadcastReceiver mReceiver = new BroadcastReceiver() {
@RequiresPermission("android.permission.BLUETOOTH")
public void onReceive(Context context, Intent intent) {
String action = intent.getAction();
if("android.bluetooth.device.action.FOUND".equals(action)) {
BluetoothDevice device = intent.getParcelableExtra("android.bluetooth.device.extra.DEVICE");
MainActivity.this.singleAddress.add(device.getName() + "\n" + device.getAddress());
UnityPlayer.UnitySendMessage(TARGET, "OnFoundDevice", device.getName() + ",\n" + device.getAddress());

} else if("android.bluetooth.adapter.action.DISCOVERY_FINISHED".equals(action)) {
if(MainActivity.this.IsScan) {
UnityPlayer.UnitySendMessage(TARGET, "OnScanFinish", "");
}

if(MainActivity.this.singleAddress.size() == 0) {
UnityPlayer.UnitySendMessage(TARGET, "OnFoundNoDevice", "");
}
}

}
};

// 1. Starting Point in Unity Script
// 开启蓝牙设备
@RequiresPermission("android.permission.BLUETOOTH")
public void StartPlugin() {
if(Looper.myLooper() == null) {
Looper.prepare();
}

this.SetupPlugin();
}


// 2. Setup Plugin
// Get Default Bluetooth Adapter and start Service
@RequiresPermission("android.permission.BLUETOOTH")
public void SetupPlugin() {
// Bluetooth Adapter
this.mBtAdapter = BluetoothAdapter.getDefaultAdapter();

// if Bluettoth Adapter is avaibale, start Service
if(this.mBtAdapter == null) {
Toast.makeText(MainActivity.this, "Bluetooth is not available", Toast.LENGTH_SHORT).show();
} else {
if (!this.mBtAdapter.isEnabled()) {
this.mBtAdapter.enable();
if (this.mBtService == null) {
this.startService();
}
Toast.makeText(MainActivity.this, "Open bluetooth success", Toast.LENGTH_SHORT).show();
}else {
if (this.mBtService == null) {
this.startService();
}
Toast.makeText(MainActivity.this, "Open bluetooth success", Toast.LENGTH_SHORT).show();
}
// Log.d(TAG, "SetupPlugin SUCCESS");
}
}


// 3. Setup and Start Bluetooth Service
private void startService() {
// Log.d(TAG, "setupService()");
this.mBtService = new BluetoothService(this, this.mHandler);
this.mOutStringBuffer = new StringBuffer("");
}

public String DeviceName() {
return this.mBtAdapter.getName();
}

@RequiresPermission("android.permission.BLUETOOTH")
public String GetDeviceConnectedName() {
return !this.mBtAdapter.isEnabled()?"You Must Enable The BlueTooth":(this.mBtService.getState() != 3?"Not Connected":this.mConnectedDeviceName);
}

@RequiresPermission("android.permission.BLUETOOTH")
public boolean IsEnabled() {
return this.mBtAdapter.isEnabled();
}

public boolean IsConnected() {
return this.mBtService.getState() == 3;
}

@RequiresPermission("android.permission.BLUETOOTH")
public void stopThread() {
if(this.mBtService != null) {
this.mBtService.stop();
this.mBtService = null;
}

if(this.mBtAdapter != null) {
this.mBtAdapter = null;
}

this.SetupPlugin();
}

// 扫描蓝牙设备
@RequiresPermission(allOf = {"android.permission.BLUETOOTH", "android.permission.BLUETOOTH_ADMIN"})
public String ScanDevice() {
// Toast.makeText(MainActivity.this, "Start - ScanDevice()", Toast.LENGTH_SHORT).show();
// Log.d(TAG, "Start - ScanDevice()");
if(this.mBtAdapter == null || !this.mBtAdapter.isEnabled()) {
Toast.makeText(MainActivity.this, "You Must Enable The BlueTooth", Toast.LENGTH_SHORT).show();
// Log.d(TAG, "You Must Enable The BlueTooth");
// return "You Must Enable The BlueTooth";
} else {
this.IsScan = true;
this.singleAddress.clear();
IntentFilter filter = new IntentFilter("android.bluetooth.device.action.FOUND");
this.registerReceiver(this.mReceiver, filter);
filter = new IntentFilter("android.bluetooth.adapter.action.DISCOVERY_FINISHED");
this.registerReceiver(this.mReceiver, filter);

// 获取扫描到的蓝牙设备名称和ID
this.mBtAdapter = BluetoothAdapter.getDefaultAdapter();
Set<BluetoothDevice> pairedDevices = this.mBtAdapter.getBondedDevices();
if(pairedDevices.size() > 0) {
pairedDevicesName = "";
for (BluetoothDevice device : pairedDevices) {
pairedDevicesName = pairedDevicesName + device.getName() + ",";
pairedDevicesMap.put(device.getName(), device.getAddress());
}
}

this.doDiscovery();
Toast.makeText(MainActivity.this, "ScanDevice SUCCESS", Toast.LENGTH_SHORT).show();
// Log.d(TAG, "ScanDevice SUCCESS");
// return "SUCCESS";
}
// Toast.makeText(MainActivity.this, pairedDevicesName.toString(), Toast.LENGTH_SHORT).show();
// Log.d(TAG, pairedDevicesName);
return pairedDevicesName;
}

// 根据MAC地址连接蓝牙设备
@RequiresPermission(allOf = {"android.permission.BLUETOOTH", "android.permission.BLUETOOTH_ADMIN"})
public void Connect(String TheAddress) {
if(this.mBtAdapter == null || !this.mBtAdapter.isEnabled()) {
Toast.makeText(MainActivity.this, "You Must Enable The BlueTooth", Toast.LENGTH_SHORT).show();
}else {
if(this.mBtAdapter.isDiscovering()) {
this.mBtAdapter.cancelDiscovery();
}

this.IsScan = false;
String address = TheAddress.substring(TheAddress.length() - 17);
this.mConnectedDeviceName = TheAddress.split(",")[0];
BluetoothDevice device = this.mBtAdapter.getRemoteDevice(address);

this.mBtService.connect(device);
// Toast.makeText(MainActivity.this, "Connect SUCCESS", Toast.LENGTH_SHORT).show();
}
}

// 关闭蓝牙连接
@RequiresPermission("android.permission.BLUETOOTH")
public void disConnect() {
if(this.mBtAdapter == null || !this.mBtAdapter.isEnabled()) {
Toast.makeText(MainActivity.this, "You Must Enable The BlueTooth", Toast.LENGTH_SHORT).show();
// Log.d(TAG, "You Must Enable The BlueTooth");

} else if(this.mBtService.getState() != 3) {
Toast.makeText(MainActivity.this, "Not Connected", Toast.LENGTH_SHORT).show();
// Log.d(TAG, "Not Connected");

} else {
this.mBtService.close();

Toast.makeText(MainActivity.this, "DisConnect SUCCESS", Toast.LENGTH_SHORT).show();
// Log.d(TAG, "sendMessage SUCCESS");
}
}

@RequiresPermission(allOf = {"android.permission.BLUETOOTH", "android.permission.BLUETOOTH_ADMIN"})
private void doDiscovery() {
if(this.mBtAdapter.isDiscovering()) {
this.mBtAdapter.cancelDiscovery();
}

this.mBtAdapter.startDiscovery();
}

@RequiresPermission(allOf = {"android.permission.BLUETOOTH", "android.permission.BLUETOOTH_ADMIN"})
String BluetoothSetName(String name) {
if(!this.mBtAdapter.isEnabled()) {
return "You Must Enable The BlueTooth";
} else if(this.mBtService.getState() != 3) {
return "Not Connected";
} else {
this.mBtAdapter.setName(name);
return "SUCCESS";
}
}

// 关闭蓝牙设备
@RequiresPermission(allOf = {"android.permission.BLUETOOTH", "android.permission.BLUETOOTH_ADMIN"})
public void DisableBluetooth() {
if(this.mBtAdapter == null || !this.mBtAdapter.isEnabled()) {
Toast.makeText(MainActivity.this, "You Must Enable The BlueTooth", Toast.LENGTH_SHORT).show();
} else {
if(this.mBtAdapter != null) {
this.mBtAdapter.cancelDiscovery();
}

if(this.mBtAdapter.isEnabled()) {
this.mBtAdapter.disable();
}

Toast.makeText(MainActivity.this, "DisableBluetooth SUCCESS", Toast.LENGTH_SHORT).show();
}
}

@RequiresPermission("android.permission.BLUETOOTH")
public String BluetoothEnable() {
try {
if(!this.mBtAdapter.isEnabled()) {
Intent e = new Intent("android.bluetooth.adapter.action.REQUEST_ENABLE");
this.startActivityForResult(e, 2);
}

return "SUCCESS";

} catch (Exception e) {
return "Faild";
}
}

public void showMessage(final String message) {
this.runOnUiThread(new Runnable() {
public void run() {
Toast.makeText(MainActivity.this, message, Toast.LENGTH_SHORT).show();
}
});
}

@SuppressLint("WrongConstant")
@RequiresPermission("android.permission.BLUETOOTH")
public String ensureDiscoverable() {
if(!this.mBtAdapter.isEnabled()) {
return "You Must Enable The BlueTooth";
} else {
if(this.mBtAdapter.getScanMode() != 23) {
Intent discoverableIntent = new Intent("android.bluetooth.adapter.action.REQUEST_DISCOVERABLE");
discoverableIntent.putExtra("android.bluetooth.adapter.extra.DISCOVERABLE_DURATION", 300);
this.startActivity(discoverableIntent);
}

return "SUCCESS";
}
}

// 发送蓝牙数据
@RequiresPermission("android.permission.BLUETOOTH")
public void sendMessage(String message) {
if(this.mBtAdapter == null || !this.mBtAdapter.isEnabled()) {
Toast.makeText(MainActivity.this, "You Must Enable The BlueTooth", Toast.LENGTH_SHORT).show();
// Log.d(TAG, "You Must Enable The BlueTooth");

} else if(this.mBtService.getState() != 3) {
Toast.makeText(MainActivity.this, "Not Connected", Toast.LENGTH_SHORT).show();
// Log.d(TAG, "Not Connected");

} else {
if(message.length() > 0) {
byte[] send = message.getBytes();
this.mBtService.write(send);
this.mOutStringBuffer.setLength(0);
}

// Toast.makeText(MainActivity.this, "sendMessage SUCCESS", Toast.LENGTH_SHORT).show();
// Log.d(TAG, "sendMessage SUCCESS");
}
}

// 与Unity的接口
public void onOpen(){
StartPlugin();
}

public String onScan() {
return ScanDevice();
}

public String onGetDevicesAddress(String deviceName) {
return pairedDevicesMap.get((deviceName));
}

public void onConnect(String deviceName) {
String deviceAddress = onGetDevicesAddress(deviceName);
Connect(deviceAddress);
}

public String getConnectStatus() {
return connectStatus;
}

public void setConnectStatus(String connectStatus) {
this.connectStatus = connectStatus;
}

public void onSendMessage(String message) {
sendMessage(message);
}

public String getReadMessage() {
return readMessage;
}

public void setReadMessage(String readMessage) {
this.readMessage = readMessage;
}

public String onReadMessage(){
return getReadMessage();
}

public void onDisconnect() {
disConnect();
}
}

  蓝牙连接的大致步骤可以分为打开蓝牙->扫描蓝牙设备->连接蓝牙设备->发送信息->关闭蓝牙设备

  BluetoothService.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
package com.example.myapplication;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.nio.charset.StandardCharsets;
import java.util.UUID;

import android.bluetooth.BluetoothAdapter;
import android.bluetooth.BluetoothDevice;
import android.bluetooth.BluetoothSocket;
import android.bluetooth.BluetoothServerSocket;
import android.content.Context;
import android.os.Bundle;
import android.os.Handler;
import android.os.Message;
import android.util.Log;

import androidx.annotation.RequiresPermission;

public class BluetoothService {
// Debugging Tag
private static final String TAG = "BluetoothService";

// RFCOMM Protocol
// 蓝牙固定的uuid
private static final UUID MY_UUID = UUID
.fromString("00001101-0000-1000-8000-00805F9B34FB");

private final BluetoothAdapter mBtAdapter;
private final Handler mHandler;

// Threads
// 3个子线程 连接,连接后,接受
private BluetoothService.ConnectThread mConnectThread;
private BluetoothService.ConnectedThread mConnectedThread;
private BluetoothService.AcceptThread mAcceptThread;
private int mState;

// Connection State
// 连接状态标志位
private static final int STATE_NONE = 0; // we're doing nothing
private static final int STATE_LISTEN = 1; // now listening for incoming
private static final int STATE_CONNECTING = 2; // now initiating an outgoing
private static final int STATE_CONNECTED = 3; // now connected to a remote

public static final String DEVICE_NAME = "device_name";

// Constructors
public BluetoothService(Context ct, Handler h) {
this.mHandler = h;
mBtAdapter = BluetoothAdapter.getDefaultAdapter();
}

public boolean getDeviceState() {
if (mBtAdapter == null) {
return false;
} else {
return true;
}
}

private synchronized void setState(int state) {
this.mState = state;
this.mHandler.obtainMessage(MainActivity.MESSAGE_STATE_CHANGE, state, -1).sendToTarget();
mState = state;
}

public synchronized int getState() {
return mState;
}

@RequiresPermission("android.permission.BLUETOOTH")
public synchronized void start() {
// Cancel any thread attempting to make a connection
if (mConnectThread != null) {
mConnectThread.cancel();
mConnectThread = null;
}

// Cancel any thread currently running a connection
if (mConnectedThread != null) {
mConnectedThread.cancel();
mConnectedThread = null;
}

// If Accept Tread is null, create and start
if (mAcceptThread != null) {

}
else {
this.mAcceptThread = new BluetoothService.AcceptThread();
this.mAcceptThread.start();
}

this.setState(STATE_LISTEN);
}

@RequiresPermission("android.permission.BLUETOOTH")
public synchronized void connect(BluetoothDevice device) {
// Cancel any thread attempting to make a connection
if (mState == STATE_CONNECTING) {
if (mConnectThread == null) {

} else {
mConnectThread.cancel();
mConnectThread = null;
}
}

// Cancel any thread currently running a connection
if (mConnectedThread == null) {

} else {
mConnectedThread.cancel();
mConnectedThread = null;
}

// Start the thread to connect with the given device
// 新建ConnectThread线程
mConnectThread = new BluetoothService.ConnectThread(device);
mConnectThread.start();

setState(STATE_CONNECTING);
}

@RequiresPermission("android.permission.BLUETOOTH")
public synchronized void connected(BluetoothSocket socket,
BluetoothDevice device) {
// Cancel the thread that completed the connection
if (this.mConnectThread != null) {
this.mConnectThread.cancel();
this.mConnectThread = null;
}

// Cancel any thread currently running a connection
if (this.mConnectedThread != null) {
this.mConnectedThread.cancel();
this.mConnectedThread = null;
}

// Cancel
if(this.mAcceptThread != null) {
this.mAcceptThread.cancel();
this.mAcceptThread = null;
}

// Start the thread to manage the connection and perform transmissions
// 开始ConnectedThread线程
mConnectedThread = new BluetoothService.ConnectedThread(socket);
mConnectedThread.start();
Message msg = this.mHandler.obtainMessage(MainActivity.MESSAGE_DEVICE_NAME);
Bundle bundle = new Bundle();
bundle.putString(DEVICE_NAME, device.getName());
msg.setData(bundle);
this.mHandler.sendMessage(msg);
setState(STATE_CONNECTED);
}

public synchronized void stop() {
if (mConnectThread != null) {
mConnectThread.cancel();
mConnectThread = null;
}

if (mConnectedThread != null) {
mConnectedThread.cancel();
mConnectedThread = null;
}

if(this.mAcceptThread != null) {
this.mAcceptThread.cancel();
this.mAcceptThread = null;
}

setState(STATE_NONE);
}

// 写入功能
public void write(byte[] out) { // Create temporary object
ConnectedThread r; // Synchronize a copy of the ConnectedThread
synchronized (this) {
if (mState != STATE_CONNECTED)
return;
r = mConnectedThread;
r.write(out);
}
}

// 连接失败处理
private void connectionFailed() {
setState(STATE_LISTEN);
Message msg = this.mHandler.obtainMessage(MainActivity.MESSAGE_TOAST);
Bundle bundle = new Bundle();
bundle.putString("toast", "Unable to connect device");
msg.setData(bundle);
this.mHandler.sendMessage(msg);
}

// 连接丢失处理
private void connectionLost() {
setState(STATE_LISTEN);
Message msg = this.mHandler.obtainMessage(MainActivity.MESSAGE_TOAST);
Bundle bundle = new Bundle();
bundle.putString("toast", "Device connection was lost");
msg.setData(bundle);
this.mHandler.sendMessage(msg);
}


// 关闭蓝牙
public void close() { // Create temporary object
ConnectedThread r; // Synchronize a copy of the ConnectedThread
synchronized (this) {
if (mState != STATE_CONNECTED)
return;
r = mConnectedThread;
r.cancel();
}
}

private class AcceptThread extends Thread {
private final BluetoothServerSocket mmServerSocket;

public AcceptThread() {
BluetoothServerSocket tmp = null;

try {
tmp = BluetoothService.this.mBtAdapter.listenUsingRfcommWithServiceRecord("BluetoothPlugin", BluetoothService.MY_UUID);
} catch (IOException ignored) {
}

this.mmServerSocket = tmp;
}

@RequiresPermission("android.permission.BLUETOOTH")
public void run() {
this.setName("AcceptThread");
BluetoothSocket socket = null;

while(BluetoothService.this.mState != STATE_CONNECTED) {
try {
socket = this.mmServerSocket.accept();
} catch (IOException e1) {
break;
}

if(socket != null) {
BluetoothService e = BluetoothService.this;
synchronized(BluetoothService.this) {
switch(BluetoothService.this.mState) {
case STATE_NONE:
case STATE_CONNECTED:
try {
socket.close();
} catch (IOException ignored) {
}
break;
case STATE_LISTEN:
case STATE_CONNECTING:
BluetoothService.this.connected(socket, socket.getRemoteDevice());
}
}
}
}
}

public void cancel() {
try {
this.mmServerSocket.close();
} catch (IOException ignored) {
}

}
}

private class ConnectThread extends Thread {
private final BluetoothSocket mmSocket;
private final BluetoothDevice mmDevice;

public ConnectThread(BluetoothDevice device) {
this.mmDevice = device;
BluetoothSocket tmp = null;

try {
// 验证 Android SPP协议的UUID
tmp = device.createRfcommSocketToServiceRecord(MY_UUID);
} catch (IOException ignored) {
}
mmSocket = tmp;
}

public void run() {
this.setName("ConnectThread");
BluetoothService.this.mBtAdapter.cancelDiscovery();

try {
// 连接蓝牙
this.mmSocket.connect();

} catch (IOException e) {
// 连接失败
connectionFailed();

try {
// 关闭蓝牙连接
mmSocket.close();
} catch (IOException ignored) {
}

BluetoothService.this.start();
return;
}

synchronized (BluetoothService.this) {
mConnectThread = null;
}

// 连接后的操作(收发功能)
connected(mmSocket, mmDevice);
}

public void cancel() {
try {
mmSocket.close();
} catch (IOException ignored) {
}
}
}

private class ConnectedThread extends Thread {
private final BluetoothSocket mmSocket;
private final InputStream mmInStream;
private final OutputStream mmOutStream;

private BufferedReader bufferedReader;

public ConnectedThread(BluetoothSocket socket) {
mmSocket = socket;
InputStream tmpIn = null;
OutputStream tmpOut = null;

try {
// 建立Input Output流
tmpIn = socket.getInputStream();
tmpOut = socket.getOutputStream();
} catch (IOException ignored) {
}

mmInStream = tmpIn;
mmOutStream = tmpOut;
}

public void run() {
char[] charBuffer = new char[128];
int bytesRead = -1;

// 循环等待Input流
while (true) {
try {
// 有新的Input流进入,这里需要注意字符串格式,需要和发送端保持一致,这里为UTF_8
bufferedReader = new BufferedReader(new InputStreamReader(mmSocket.getInputStream(), StandardCharsets.UTF_8));
bytesRead = bufferedReader.read(charBuffer);
// 将字符数组转为字符串
String readMessage = new String(charBuffer, 0, bytesRead);
BluetoothService.this.mHandler.obtainMessage(MainActivity.MESSAGE_READ, readMessage).sendToTarget();
} catch (IOException e) {
// IO错误则视为连接失败,即已经断开连接了
connectionLost();
break;
}
}
}

public void write(byte[] buffer) {
try {
// 写入Output流
mmOutStream.write(buffer);
} catch (IOException ignored) {
}
}

public void cancel() {
try {
mmSocket.close();
} catch (IOException ignored) {
}
}
}

}

  这里新建了3个线程类ConnectThreadConnectedThreadAcceptThread分别处理相应的线程事件。

添加蓝牙权限

  在AndroidManifest.xml文件中,增加蓝牙权限。

1
2
3
4
5
6
7
8
9
10
11
<uses-permission android:name="android.permission.BLUETOOTH_CONNECT" />
<uses-permission android:name="android.permission.BLUETOOTH_SCAN" />

<uses-permission android:name="android.permission.BLUETOOTH" />
<uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE"/>
<uses-permission android:name="android.permission.WAKE_LOCK"/>
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN"/>
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION"/>
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/>

Unity蓝牙Mobile端模块

Scene布局

Unity发送端Scene
  首先新建一个Canvas,然后放置相应的按钮控件,最后挂载一个TestBluetoothSend.cs脚本文件。

Unity与Android交互模块

  TestBluetoothSend.cs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
using System;
using System.Collections;
using System.Collections.Generic;
using System.Text;
using UnityEngine;
using UnityEngine.UI;

public class TestBluetoothSend : MonoBehaviour
{
private AndroidJavaClass jc;
private AndroidJavaObject jo;
private string deviceNameStr;

private Dropdown dpn;

private InputField bluetoothText;

// Start is called before the first frame update
void Start()
{
//获得com.unity3d.player.UnityPlayer 下的类,对于扩展的Activity 是一个固定的写法。只要记住就行了
jc = new AndroidJavaClass("com.unity3d.player.UnityPlayer");
//获得 jc 类中的 currentActivity 对象,也是一种固定的写法
jo = jc.GetStatic<AndroidJavaObject>("currentActivity");

// 添加按钮对象
List<string> btnsName = new List<string>();
btnsName.Add("BtnOpen");
btnsName.Add("BtnDisconnect");

btnsName.Add("BtnSend");
btnsName.Add("BtnReceive");

// 输入控件
bluetoothText = GameObject.Find("Canvas/BluetoothText").GetComponent<InputField>();

foreach (string btnName in btnsName)
{
GameObject btnObj = GameObject.Find(btnName);
Button btn = btnObj.GetComponent<Button>();
btn.onClick.AddListener(delegate () {
this.OnClick(btnObj);
});
}

// 添加下拉菜单选项
GameObject dpnObj = GameObject.Find("DpnDeviceSelect");
dpn = dpnObj.GetComponent<Dropdown>();
dpn.ClearOptions();
dpn.onValueChanged.AddListener(DpnDeviceSelect);//监听点击

}

// 响应按钮点击事件
public void OnClick(GameObject sender)
{
switch (sender.name)
{
case "BtnOpen":
BtnOpen();
break;
case "BtnDisconnect":
jo.Call("onDisconnect");
break;
case "BtnSend":
BtnSend();
break;
case "BtnReceive":
BtnReceice();
break;
default:
break;
}
}

public void BtnOpen()
{
jo.Call("onOpen");
deviceNameStr = jo.Call<string>("onScan");

dpn.ClearOptions();
dpn.options.Clear();

string[] deviceNameList = deviceNameStr.Split(',');
for (int i = 0; i < deviceNameList.Length - 1; i++)
{
Dropdown.OptionData data = new Dropdown.OptionData();
data.text = deviceNameList[i].ToString();
dpn.options.Add(data);
}
}

public void DpnDeviceSelect(int n)
{
jo.Call("onConnect", dpn.captionText.text);
}

public void BtnSend()
{
string value = bluetoothText.text;
jo.Call("onSendMessage", value);
}

public void BtnReceice()
{
string readMessage = jo.Call<string>("onReadMessage");

// 和发送端保持一致
UTF8Encoding utf8 = new UTF8Encoding();
Byte[] encodedBytes = utf8.GetBytes(readMessage);
String decodedString = utf8.GetString(encodedBytes);

bluetoothText.text = decodedString;
}
}

  AndroidJavaObject.Call说明:

1
2
JavaScript => function Call (methodName : string, params args : object[]) : void
C# => void Call(string methodName, params object[] args);

  参考文档:AndroidJavaClassAndroidJavaObjectUnity5 中文 API 手册

  打开蓝牙后,在选项框中选择相应的蓝牙设备,正确连接中,在输入控件输入相应的发送内容,点击发送即可向PC端设备发送内容。
  当有PC端设备发送内容时,点击接收即可在输入控件中看到发送的内容。

添加Android Jar包文件

  在Plugins文件夹中新建Android文件夹,将Android工程中编译出来的AndroidMainifest.xmlclass.jar文件放到此文件夹中。
编译生成jar包文件
添加Android文件
  注:需要将AndroidMainifest.xml文件中的

1
2
3
<uses-sdk
android:minSdkVersion="21"
android:targetSdkVersion="31" />

  部分删除。

编译与Build

  选择File->Build Settings,选择Android设备,点击Player Settings中的Other Settings选项更改相应的包名和安卓API版本号。
buildApk

Unity蓝牙PC端模块

Scene布局

Unity接收端Scene
  同样的,在接收端也建立一些简单的布局控件。

蓝牙接收模块

  TestBluetoothReceive.cs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
using System.Collections.Generic;
using UnityEngine;
using System.IO.Ports;
using System.Threading;
using UnityEngine.UI;
using System.Text;

public class TestBluetoothReceive : MonoBehaviour
{
// 串口设置
private string portName = "COM5";
private int baudRate = 9600;
private Parity parity = Parity.Odd;
private int dataBits = 8;
private StopBits stopBits = StopBits.One;

// 串口对象
private SerialPort sp = null;
// 读取数据线程对象
private Thread dataReceiveThread = null;
// 是否接收数据
private bool canRecieveMsg = true;
// 接收到的数据
string strReceived;
private bool IsOpenSerial = false;

private InputField bluetoothText;


void Awake()
{
strReceived = string.Empty;
}

// Use this for initialization
void Start()
{
List<string> btnsName = new List<string>();
btnsName.Add("BtnOpenSerial");
btnsName.Add("BtnCloseSerial");
btnsName.Add("BtnSend");

// 输入控件
bluetoothText = GameObject.Find("Canvas/BluetoothText").GetComponent<InputField>();

foreach (string btnName in btnsName)
{
GameObject btnObj = GameObject.Find(btnName);
Button btn = btnObj.GetComponent<Button>();
btn.onClick.AddListener(delegate () {
this.OnClick(btnObj);
});
}

// 实例化对象
sp = new SerialPort(portName, baudRate, parity, dataBits, stopBits);
// OpenPort();
}

public void OnClick(GameObject sender)
{
switch (sender.name)
{
case "BtnOpenSerial":
OpenPort();
break;
case "BtnCloseSerial":
ClosePort();
break;
case "BtnSend":
BtnSend();
break;
default:
break;
}
}

public void OpenPort()
{
// 读取时间
sp.ReadTimeout = 100;
try
{
sp.Open();
Debug.Log("open success");

// 实例化读取数据线程
this.dataReceiveThread = new Thread(new ThreadStart(DataReceiveFunction));
this.dataReceiveThread.IsBackground = true;
this.dataReceiveThread.Start();
IsOpenSerial = true;

}
catch (System.Exception ex)
{
Debug.Log(ex.Message);
}


}

public void ClosePort()
{
try
{
sp.Close();
Debug.Log("close success");
dataReceiveThread.Abort();

}
catch (System.Exception ex)
{
Debug.Log(ex.Message);
}
}

/// <summary>
/// 可以读取多个字符,即字节数组;但是需要的平台的是.net 4.6
/// 将 Scripting Runing Vision 和Api Compatibility都改为.Net 4.6
/// </summary>
void DataReceiveFunction()
{
try
{
while (canRecieveMsg)
{
// 设定读取间隔
Thread.Sleep(25);
if (!sp.IsOpen)
return;
int datalength = sp.BytesToRead;
if (datalength == 0)
{
continue;
}

byte[] bytes = new byte[datalength];
sp.Read(bytes, 0, datalength);
strReceived = System.Text.Encoding.Default.GetString(bytes);

}
}
catch (System.Exception ex)
{
if (ex.GetType() != typeof(ThreadAbortException))
{
}
Debug.Log(ex);
}
}

public void handleReceivedData(string str)
{
if (str != "")
{
bluetoothText.text = str;
}
}


private void Update()
{
if (IsOpenSerial)
{
handleReceivedData(strReceived);
strReceived = string.Empty;
}
}

void OnApplicationQuit()
{
canRecieveMsg = false;
ClosePort();
}

public void BtnSend()
{
byte[] bytes = Encoding.GetEncoding("utf-8").GetBytes(bluetoothText.text);
sp.Write(bytes, 0, bytes.Length);
}
}

  首先配置串口信息,然后实例化串口对象,最后单独开启一个线程处理接收到的串口数据。
  这里的COM端口号信息可以在系统->蓝牙设置中查看:
蓝牙COM信息
  也可以使用串口调试工具进行简单的测试。

编译与build

  这里编译生成选择PC即可。
  运行后,首先打开蓝牙串口,如果此时Mobile端有数据发送过来,就会在输入控件中显示。
  在输入控件中输入相应内容,并点击发送,此时在Mobile端点击接收即可在Mobile端的输入控件中看到发送的内容。

常见问题整理

  1. Android最新的添加蓝牙注解包为import androidx.annotation.RequiresPermission;
  2. 每次编辑完Anroid工程要重新编译打包。
  3. Unity打包andorid jar包时出现报错:提示 \unityLibrary\src\main\java\com\unity3d\player\UnityPlayerActivity.java使用或覆盖了已过时的 API。,将Unity中的UnityPlayerActivity.java文件(C:\Program Files\Unity\Hub\Editor\2020.3.17f1c1\Editor\Data\PlaybackEngines\AndroidPlayer\Source\com\unity3d\player\UnityPlayerActivity.java)删除。
  4. 使用蓝牙串口时,注意Unity.net的版本号,本项目采用的是.net 4.0版本。
谢谢老板!
-------------本文结束感谢您的阅读给个五星好评吧~~-------------