Unity3D安卓端串口通信

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

插件助手链接

  • 7
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 11
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 11
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值