Unity3D安卓端串口通信
在读本文章之前要对串口通信有一定基础的了解 例如:
波特率:这是一个衡量符号传输速率的参数。
数据位:这是衡量通信中实际数据位的参数。
停止位:用于表示单个包的最后一位。
奇偶校验:在串口通信中一种简单的检错方式。
以上内容可以自己查找资料熟悉
本文章内容是针对 U3D 安卓端环境的串口通信 类似RS485,非USB(当然看过后USB通信也可以实现)
文章分2大模块 1、是喜欢自己折腾的 自己编译.so库 给U3D调用通信。 2、是利用插件。 两种方法各有利弊, 自己编译呢,会了解整个通信过程 缺点就是麻烦。利用插件 方便快捷 就是接收数据的速率可能会降低。
文章开始之前 建议大家先下载一个安卓端的串口助手插件 本人喜欢用这个 (SerialPortHelperV1.0.1) 稍后我会提供下载链接。
好了 开始第一种方法
相信各位在查询U3D安卓环境下串口通信已经看了不少文章 大部分是讲两个方法来实现,
1、 用java调用serial_port库,然后编写jni供Unity3D调用。
2,C#引用C或者C++语言动态链接库函数(android的动态库扩展名是.so)
写jni和U3D交互太麻烦 所以我们自己选择调用.so文件
先下载ndk 有ndk的开始配置ndk环境
这里注意自己的路径NDK路径
在打开这个新建一个 %NDK_HOME% 确认就好
然后再CMD里输入ndk-build看看自己有没有配置好
好了,ndk装好了,然后编译文件
新建一个文件,名为Android.mk,代码如下:
Copyright 2009 Cedric Priscal
Licensed under the Apache License, Version 2.0 (the “License”);
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an “AS IS” BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
LOCAL_PATH := $(call my-dir)
include $(CLEAR_VARS)
NDK_TOOLCHAIN_VERSION := 4.9
LOCAL_C_INCLUDES := C:/android-ndk-r15b/sources/cxx-stl/gnu-libstdc++/4.9/include
LOCAL_C_INCLUDES +=C:\android-ndk-r15b\sources\cxx-stl\gnu-libstdc++\4.9\libs\armeabi\include
TARGET_PLATFORM := android-19
LOCAL_MODULE := serialport
LOCAL_SRC_FILES := serial.cpp
LOCAL_LDLIBS := -llog
LOCAL_CPPFLAGS := -std=c++11 -D __cplusplus=201103L
APP_STL := gnustl_shared
APP_CPPFLAGS := -std=c++11 -frtti -fexceptions
APP_CPPFLAGS += -std=gun++11 -lpthread
include $(BUILD_SHARED_LIBRARY)
再新建一个文件,名为:Application.mk,
APP_ABI := armeabi-v7a x86
最后新建一个文件,名为:serial.cpp,代码如下:
//包含头文件
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#ifndef _WIN32
#include <sys/ioctl.h>
#include <unistd.h>
#include <termios.h>
#else
#include <io.h>
#pragma warning (disable:4996)
#endif
#include <errno.h>
#include <string.h>
#include <time.h>
int fd_ = -1;
extern "C" {
int set_opt(int fd, int nSpeed, int nBits, char nEvent, int nStop, int min_btye);
}
long long getMilisec()
{
#ifndef _WIN32
struct timeval now;
gettimeofday(&now, NULL);
long long t_mili = ((long long)now.tv_sec) * 1000 + now.tv_usec / 1000;
return t_mili;
#else
return 0;
#endif
}
//打印
//#include "android/log.h"
//static const char *TAG = "HelloWorldScene";
//#define LOGI(fmt, args...) __android_log_print(ANDROID_LOG_INFO, TAG, fmt, ##args)
//#define LOGD(fmt, args...) __android_log_print(ANDROID_LOG_DEBUG, TAG, fmt, ##args)
//#define LOGE(fmt, args...) __android_log_print(ANDROID_LOG_ERROR, TAG, fmt, ##args)
extern "C" {
bool Serial_Open()
{
if (fd_ > 0)//already open
return true;
#ifndef _WIN32
//char *dev[] = { "/dev/ttySAC0", "/dev/ttySAC1", "/dev/ttySAC2", "/dev/ttySAC3" };
//long vdisable;
fd_ = open("/dev/ttyS1", O_RDWR | O_NOCTTY | O_NDELAY);
if (fd_ == -1) {
return false;
}
//LOGE("Open Serial Port %s\n",dev[comport]);
//恢复串口为阻塞状态
if (fcntl(fd_, F_SETFL, 0) < 0) {
return false;
}
/*测试是否为终端设备*/
if (isatty(STDIN_FILENO) == 0) {
//LOGE("standard input is not a terminal device\n");
}
set_opt(fd_, 115200, 8, 'N', 1, 200);
#else
fd_ = open("/dev/ttyS1", O_RDWR);
if (fd_ == -1) {
return false;
}
#endif
return fd_ >= 0;
}
void Serial_Close()
{
if (fd_ > 0)
{
close(fd_);
fd_ = -1;
}
}
int Serial_SendData(const unsigned char* data, int size)
{
if (fd_ < 0)//not open
return -1;
int ret = write(fd_, data, size);
return ret;
}
int Serial_RecvData(unsigned char* buff, int len)
{
if (fd_ < 0)//not open
return -1;
memset(buff, 0, len);
int readSize = read(fd_, buff, len);
return readSize;
}
/*****************************
* 功能:设置串口函数
* 入口:(fd,波特率,数据位,奇偶校验,停止位)
*****************************/
int set_opt(int fd, int nSpeed, int nBits, char nEvent, int nStop, int min_btye)
{
#ifndef _WIN32
struct termios newtio, oldtio;
/*保存测试现有串口参数设置,在这里如果串口号出错,会有相关的出错信息*/
if (tcgetattr(fd, &oldtio) != 0) {
// LOGE("SetupSerial 1");
return -1;
}
bzero(&newtio, sizeof(newtio));
/*步骤一:设置字符大小*/
newtio.c_cflag |= CLOCAL | CREAD;
newtio.c_cflag &= ~CSIZE;
/*设置停止位*/
switch (nBits) {
case 7: newtio.c_cflag |= CS7;
break;
case 8: newtio.c_cflag |= CS8;
break;
}
/*设置奇偶校验位*/
switch (nEvent) {
case 'O': //奇数
newtio.c_cflag |= PARENB;
newtio.c_cflag |= PARODD;
newtio.c_iflag |= (INPCK | ISTRIP);
break;
case 'E': //偶数
newtio.c_iflag |= (INPCK | ISTRIP);
newtio.c_cflag |= PARENB;
newtio.c_cflag &= ~PARODD;
break;
case 'N': //无奇偶校验位
newtio.c_cflag &= ~PARENB;
break;
}
/*设置波特率*/
switch (nSpeed)
{
case 2400:
cfsetispeed(&newtio, B2400);
cfsetospeed(&newtio, B2400);
break;
case 4800:
cfsetispeed(&newtio, B4800);
cfsetospeed(&newtio, B4800);
break;
case 9600:
cfsetispeed(&newtio, B9600);
cfsetospeed(&newtio, B9600);
break;
case 19200:
cfsetispeed(&newtio, B19200);
cfsetospeed(&newtio, B19200);
break;
case 115200:
cfsetispeed(&newtio, B115200);
cfsetospeed(&newtio, B115200);
break;
case 460800:
cfsetispeed(&newtio, B460800);
cfsetospeed(&newtio, B460800);
break;
default:
cfsetispeed(&newtio, B115200);
cfsetospeed(&newtio, B115200);
break;
}
/*设置停止位*/
if (nStop == 1) {
newtio.c_cflag &= ~CSTOPB;
}
else if (nStop == 2) {
newtio.c_cflag |= CSTOPB;
}
/*设置等待时间和最小接收字符*/
newtio.c_cc[VTIME] = 1;
newtio.c_cc[VMIN] = min_btye;
/*处理未接收字符*/
tcflush(fd, TCIFLUSH);
/*激活新配置*/
if ((tcsetattr(fd, TCSANOW, &newtio)) != 0) {
// LOGE("COM set error\n");
return -1;
}
#endif
return 0;
}
}
有个地方要注意:
在bool Serial_Open()函数中,有一句
这里的双引号中的/dev/ttyS1是串口的名字,ttyS1一般表示第二个串口,ttyS0表示第一个… 但是每个公司可能有自己的串口名字不一样 建议在串口助手查看或者问自己的硬件工程师
还有一个地方:set_opt(fd_, 115200, 8, ‘N’, 1, 200);
这行代码就是设置波特率,数据位,奇偶校验停止位什么的,我的是波特率115200
好了,该注意的都说了,接下来编译。
将以上3个文件放在一个名为jni的文件夹内,然后cmd运行,进到这个目录,
然后输入ndk-build 编译
我的是将这个新建的jni文件夹放在一个test2文件夹中,路径为E:\test2\jni
先 进这个文件夹 然后输入命令
如图编译好后,会在test2文件夹中生成一个libs和一个obj文件夹
要使用的文件夹就在libs中
将E:\test2\libs\armeabi\libserialport.so文件拷贝到你的Unity工程下的Plugins\Android\中
然后在你的一个脚本中引入,就可以使用啦,如下:
[DllImport(“serialport”)]
private static extern bool Serial_Open ();
[DllImport(“serialport”)]
private static extern void Serial_Close ();
[DllImport(“serialport”)]
private static extern int Serial_SendData (byte[] com_data, int size);
[DllImport(“serialport”)]
private static extern int Serial_RecvData (byte[] com_data, int size);
就这4个函,看名字就知道用处啦
这里接收和数据处理,我是放在两个线程中的,和使用PC串口是一样的
贴上我的代码:
using System;
using System.Collections;
using System.Collections.Generic;
using System.Runtime.InteropServices;
using System.Threading;
using UnityEngine;
using UnityEngine.UI;
public class NewBehaviourScript : MonoBehaviour
{
[DllImport("serialport")]
private static extern bool Serial_Open();
[DllImport("serialport")]
private static extern void Serial_Close();
[DllImport("serialport")]
private static extern int Serial_SendData(byte[] com_data, int size);
[DllImport("serialport")]
private static extern int Serial_RecvData(byte[] com_data, int size);
Thread portRev, portDeal;
ArrayList recvBuf;
private void Awake()
{
}
public void sendbutton()
{
//byte[] binaryData = new byte[] {
// 0xFA, 0xFA, 0x00, 0x00, 0x01, 0x00, 0x00, 0x30, 0x11, 0x00, 0x00, 0x01, 0x01, 0x01, 0x01, 0x01,
// 0x01, 0xFF, 0xFF, 0xFF, 0xFF, 0x0A, 0x05, 0x0A, 0x0A, 0x00, 0x69, 0x2B, 0x55, 0xAA
//};
string dataStr = "FAFA00000100003011000001010101010101FFFFFFFF0A050A0A00692B55AA";
byte[] dataBytes = new byte[dataStr.Length / 2];
for (int i = 0; i < dataBytes.Length; i++)
{
string byteStr = dataStr.Substring(i * 2, 2);
byte byteValue = byte.Parse(byteStr, System.Globalization.NumberStyles.HexNumber);
dataBytes[i] = byteValue;
}
SendData(dataBytes);
}
void SendData(byte[] msg)
{
Serial_SendData(msg, msg.Length);
}
// Start is called before the first frame update
void Start()
{
if (Serial_Open())
{
Debug.Log("串口打开成功!");
//text.text = "success!";
portRev = new Thread(PortReceivedThread);
portRev.Start();
//portDeal = new Thread(DealDataThread);
//portDeal.Start();
}
else
{
Debug.Log("串口打开失败!");
//text.text = "failed!";
}
}
public Int32 port = -1;
void PortReceivedThread()
{
byte[] buf =new byte[10];
Debug.Log(buf[1]);
int n = 0;
n = Serial_RecvData(buf, 1);
Debug.Log(n.ToString());
if (n == 1)
{
recvBuf.Add(buf[0]);
// Debug.Log(n.ToString());
}
}
// Update is called once per frame
void Update()
{
//if (!portRev.IsAlive)
//{
// portRev = new Thread(PortReceivedThread);
// portRev.IsBackground = true;
// portRev.Start();
//}
}
void OnApplicationQuit()
{
Debug.Log("loading, 程序退出");
if (portRev != null)
{
if (portRev.IsAlive)
{
portRev.Abort();
}
}
if (portDeal != null)
{
if (portDeal.IsAlive)
{
portDeal.Abort();
}
}
Serial_Close();
}
void OnDestroy()
{
//Debug.Log("scene destroy!");
//if (portRev.IsAlive)
//{
// portRev.Abort();
//}
//if (portDeal.IsAlive)
//{
// portDeal.Abort();
//}
//Serial_Close();
}
}
其中sendbutton() 方法是发送数据 然后根据自己公司的协议写自己的数据就行
好了,这是第一种方法的比较方便的一种
另外,编译好的libserialport.so这个文件一定要在android环境中才能识别,就是要在安卓虚拟机或者真机上才能跑,在电脑上直接用Unity跑会识别不出来的,切记!
还有第一种方法比较正规的做法 是用Android Studio 编译.so文件 和上面方法出来结果是一样的 但是是正确流程 就是麻烦 先安装Android Studio 然后各种环境配置 自己查找资料 然后查看以下文章
https://blog.csdn.net/weixin_41733225/article/details/131521373
然后按照资料 修改 CMake工具里的参数 和 粘贴刚才C++代码到工程里就行
开始第二种方法 就是上插件
SerialPortUtilityPro
这个插件 在U3D商店里有 不过可惜的是 现在最新版本不知道为什么有错误 所以只能用2.51版本 我试过2个版本 只有2.51的可以用
导入插件后 然后添加的他的组件
这4个分别是 串口方式 串口名称 波特率 和接收消息的方法
下面是接收方法
/// <summary>
/// 串口读取二进制流数据
/// </summary>
/// <param name="data"></param>
public void ReadStreamingBinary(object data)
{
//byte[] binaryData = ObjectToBinary(data);
//string binaryString = BitConverter.ToString(binaryData); // 将二进制数据转换为字符串
//Debug.Log(binaryString);
var bin = data as byte[];
tempDataQueue = new Queue<byte>();
for (int i = 0; i < bin.Length; i++)
{
tempDataQueue.Enqueue(bin[i]);
}
PrintFrame(tempDataQueue);
}
static void PrintFrame(Queue<byte> dataQueue)
{
bool frameStart = false;
List<byte> frameData = new List<byte>();
while (dataQueue.Count > 0)
{
byte data = dataQueue.Dequeue();
if (!frameStart && data == 0xFA)
{
frameStart = true;
frameData.Add(data);
}
else if (frameStart)
{
frameData.Add(data);
if (frameData.Count >= 4 && frameData[frameData.Count - 2] == 0x55 && frameData[frameData.Count - 1] == 0xAA)
{
int startingAddress = baseAddress + offsetAddress;
// Debug.Log(BitConverter.ToString(frameData.ToArray()));
Debug.Log(BitConverter.ToString(frameData.ToArray()));
// 打印一帧数据
break;
}
}
}
}
接收后 自己怎么处理逻辑看你们项目需求 但是这个地方要主要到粘包各种问题 自行查找资料
然后就是发送消息
public void PortData()
{
// Debug.Log(SerialPortData.SendIdData());
SerialPortUtilityPro spup = FindObjectOfType<SerialPortUtilityPro>();
if (!spup.IsConnected()) return;
spup.Write(SerialPortData.SendIdData());
}
public static class SerialPortData
{
public static byte[] SendIdData()
{
string dataStr = "FAFA00000100003011000001010101010101FFFFFFFF0A050A0A00692B55AA";
byte[] dataBytes = new byte[dataStr.Length / 2];
for (int i = 0; i < dataBytes.Length; i++)
{
string byteStr = dataStr.Substring(i * 2, 2);
byte byteValue = byte.Parse(byteStr, System.Globalization.NumberStyles.HexNumber);
dataBytes[i] = byteValue;
}
return dataBytes;
}
}
注意吧dataStr 换成自己的数据。
好了这个时候 文章就算结束了, 插件是好用 但是缺少了流程原理的理解,这个插件基本原理也是作者自己编了。so库然后调用
链接: https://pan.baidu.com/s/1DgPrwdGK4q1s3HPYoqD0FQ 提取码: 6yjc
插件助手链接