Unity在Windows平台使用串口

关于Unity在Windows平台使用串口,基础内容百度一下,看一下别人的博客就能学习到,这里说几点,避免自己忘记:

1、Unity2019要使用串口的类SerialPort,需要将Api Copatibility Level选项换成.NET 4.x 。

2、Unity编辑器里面可以直接调试串口,不用导出exe,如果串口打开失败,看看此串口是否可用,以及有没有被占用。

3、如果电脑硬件上没有串口,可以使用串口模拟工具模拟出两个串口,让Unity程序与串口调试助手各连接一个,实现互通。

我的相关代码(有参考别人的代码):(完整工程下载地址在最下面)

以下假设下位机发送来的一次完整消息是以"START"开头,以"END"结尾。(我这是给自己挖坑了,有点不太好处理,不如直接用"\r\n"或者'\0'作为消息结束的标志,这样还好处理一些)。

水平有限,代码写得比较乱(尤其是接收数据那一块),凑合着看吧。

using UnityEngine;
using System.Collections;
using System.IO.Ports;
using System;
using System.Collections.Generic;
using System.Threading;
using System.Text;

/// <summary>
/// 控制串口接收和发送消息
/// </summary>
public class SerialPortControl : MonoBehaviour
{
    //定义基本信息
    [SerializeField] private string _portName = "COM1";//串口名
    [SerializeField] private int _baudRate = 9600;//波特率
    [SerializeField] private Parity _parity = Parity.None;//效验位
    [SerializeField] private int _dataBits = 8;//数据位
    [SerializeField] private StopBits _stopBits = StopBits.One;//停止位
    [SerializeField] private Handshake _handshake = Handshake.None;//握手协议
    [SerializeField] private bool _dtrEnable = false;
    [SerializeField] private bool _rtsEnable = false;

    private string START_FLAG = "START";//一条完整消息的起始标志
    private string END_FLAG = "END";//一条完整消息的结束标志

    /// <summary>
    /// 接收数据后的回调,参数是对方传来的消息
    /// </summary>
    private Action<string> _onRecv;
    private SerialPort _serialPort = null;
    private Thread recvThread;//数据接收线程
    private Thread sendThread;//数据发送线程
    private bool _bRunning = false;//线程是否需要继续运行

    /// <summary>
    /// 消息发送队列
    /// </summary>
    private Queue<string> _msgSendQueue = new Queue<string>();
    /// <summary>
    /// 消息接收队列
    /// </summary>
    private Queue<string> _msgRecvQueue = new Queue<string>();

    public void Init(SerialPortConfig config)
    {
        Init(config.portName, config.baudRate, config.parity, config.dataBits, config.stopBits, config.handshake, config.dtrEnable, config.rtsEnable);
    }

    /// <summary>
    /// 如果不调用初始化,就默认使用Inspector面板中的设置。
    /// </summary>
    /// <param name="portName"></param>
    /// <param name="baudRate"></param>
    /// <param name="parity"></param>
    /// <param name="dataBits"></param>
    /// <param name="stopBits"></param>
    /// <param name="handshake"></param>
    /// <param name="dtrEnable"></param>
    /// <param name="rtsEnable"></param>
    public void Init(string portName, int baudRate, Parity parity, int dataBits, StopBits stopBits, Handshake handshake = Handshake.None, bool dtrEnable = false, bool rtsEnable = false)
    {
        this._portName = portName;
        this._baudRate = baudRate;
        this._parity = parity;
        this._dataBits = dataBits;
        this._stopBits = stopBits;
        this._handshake = handshake;
        this._dtrEnable = dtrEnable;
        this._rtsEnable = rtsEnable;
    }

    /// <summary>
    /// 打开串口以及开启读写数据的线程。返回值表示是否成功。
    /// </summary>
    /// <returns></returns>
    public bool Open()
    {
        if (OpenPort())
        {
            _msgSendQueue.Clear();
            _msgRecvQueue.Clear();
            _bRunning = true;
            recvThread = new Thread(new ThreadStart(ReceiveDataLoop));
            recvThread.Start();
            sendThread = new Thread(new ThreadStart(SendDataLoop));
            sendThread.Start();
            return true;
        }
        return false;
    }

    public void Close()
    {
        if (IsOpen())
        {
            ClosePort();
        }
        _bRunning = false;//让线程结束循环,自动结束线程
    }

    public bool IsOpen()
    {
        if (_serialPort != null)
        {
            return _serialPort.IsOpen;
        }
        return false;
    }

    /// <summary>
    /// 打开串口,返回值表示是否打开成功
    /// </summary>
    /// <returns></returns>
    private bool OpenPort()
    {
        //创建串口
        _serialPort = new SerialPort();
        _serialPort.PortName = _portName;
        _serialPort.BaudRate = _baudRate;
        _serialPort.Parity = _parity;
        _serialPort.DataBits = _dataBits;
        _serialPort.StopBits = _stopBits;
        _serialPort.Handshake = _handshake;
        _serialPort.DtrEnable = _dtrEnable;
        _serialPort.RtsEnable = _rtsEnable;

        //设置读写超时,单位毫秒
        _serialPort.ReadTimeout = 500;
        _serialPort.WriteTimeout = 500;

        //下位机数据一般是ASCII
        _serialPort.Encoding = Encoding.ASCII;//其实默认就是这个,不设置也行。ASCII就是只能使用英文字符,数值为0-127里面的字符。

        bool success = true;
        try
        {
            _serialPort.Open();
            Debug.Log($"打开串口成功:{_portName}");
        }
        catch (Exception ex)
        {
            success = false;
            Debug.LogError("打开串口失败,原因:" + ex.Message);
        }
        return success;
    }

    private bool ClosePort()
    {
        bool success = true;
        try
        {
            _serialPort.Close();
            _serialPort = null;
            Debug.Log($"关闭串口成功: {_portName}");
        }
        catch (Exception ex)
        {
            success = false;
            Debug.LogError("关闭串口失败,原因:" + ex.Message);
        }
        return success;
    }

    public void AddReceiveListener(Action<string> listener)
    {
        _onRecv -= listener;
        _onRecv += listener;
    }

    public void RemoveReceiveListener(Action<string> listener)
    {
        _onRecv -= listener;
    }

    private void Update()
    {
        //这样,接收数据后就在Unity主线程中处理,如果涉及UI显示,就不会出问题了。
        HandleReceivedData();
    }

    /// <summary>
    /// 处理已经接收的数据
    /// </summary>
    private void HandleReceivedData()
    {
        List<string> msgList = null;

        lock (_msgRecvQueue)
        {
            if (_msgRecvQueue.Count > 0)
            {
                msgList = new List<string>();
                while (_msgRecvQueue.Count > 0)
                {
                    msgList.Add(_msgRecvQueue.Dequeue());
                }
            }
        }

        if (msgList != null && msgList.Count > 0)
        {
            for (int i = 0; i < msgList.Count; i++)
            {
                _onRecv?.Invoke(msgList[i]);
            }
        }
    }

    /// <summary>
    /// 接收数据的线程
    /// </summary>
    private void ReceiveDataLoop()
    {
        Debug.Log("进入串口接收数据线程");

        byte[] recvBuffer = new byte[8 * 1024];
        int bytesToRead = 0;//需要读取多少字节
        int countTemp = 0;
        int totalCount = 0;//已经读取多少字节
        bool maybeNoMoreData = false;//“也许”,串口接收缓存区里已经没有数据了
        bool noMoreData = false;//确实,串口接收缓存区里已经没有数据了        
        int sleepTime_Waiting = 30;//没有数据的时候的睡眠时间,单位毫秒
        int sleepTime_Receiving = 10;//接收数据的时候的睡眠时间,单位毫秒
        int sleepTime = sleepTime_Waiting;
        bool receiving = false;//是否接收中

        //没有数据时以5毫秒为间隔查询,有数据时以2毫秒为间隔查询。比特率为9600的情况下,大概1点几毫秒传输1个字节,
        //当间隔5毫秒的时间没有接到新数据的时候,则说明此次的数据传输完成并读取完成了。
        //这种方式,如果下位机发送数据的间隔低于5毫秒,可能会出现连续两次或多次的数据被合在一起作为一次读取,因此就需要规定好数据之间的间隔符或起始结束标志,
        //处理数据的时候,按照这些标志将数据隔开。同时也可以将查询间隔缩短。但是,如果下位机一直高频发送数据,并且巧妙地导致上位机一直判断到有数据而没有捕捉到
        //数据发送的间隔时间,会导致上位机一直没判断到接收结束,因此导致问题。所以我就在接收数据的中途就处理数据,判断起始/结束标志,将完整消息分离出来。
        //以上5毫秒、2毫秒是本来这样想的,但是后来想想也没必要这么短间隔,而且间隔越短可能越容易出事,不该认为上位机代码里面接收到数据的时序天衣无缝,
        //尤其使用虚拟串口调试的时候更容易时序对不上,于是改成了几十毫秒这样子,时间上留点余裕。
        while (_bRunning)
        {
            if (IsOpen())
            {
                try
                {
                    bytesToRead = _serialPort.BytesToRead;
                    if (bytesToRead > 0)
                    {
                        //如果缓存空间不够,则扩充缓存空间并将数据Copy过去。
                        //其实串口默认的接收缓存区大小是4K,更多的数据不接收了,设置_serialPort.ReadBufferSize的值可改变接收缓存区的大小。
                        if (bytesToRead > recvBuffer.Length - totalCount)
                        {
                            bool extend = true;//是否扩大buffer
                            if (recvBuffer.Length >= 128 * 1024)
                            {
                                totalCount = 0;//清空现存数据,避免无限扩大buffer。能运行到这,说明下位机一直高频率发送上位机处理不了的数据,或者就是串口电平受到持续扰动。
                                extend = bytesToRead > recvBuffer.Length;//_serialPort.ReadBufferSize默认是4k,所以bytesToRead实际上不会超过4k。
                                Debug.LogError($"串口接收buffer的尺寸过大!!! recvBuffer.Length:{recvBuffer.Length}。自动清空现存数据!");
                            }
                            if (extend)
                            {
                                byte[] newBuffer = new byte[totalCount + bytesToRead];
                                recvBuffer.CopyTo(newBuffer, 0);
                                Debug.Log($"串口接收buffer大小扩展:{recvBuffer.Length} -> {newBuffer.Length}");
                                recvBuffer = newBuffer;
                            }
                        }
                        countTemp = _serialPort.Read(recvBuffer, totalCount, bytesToRead);//从串口接收缓存区取出一些数据

                        Debug.Log($"串口接收数量:{countTemp}");

                        totalCount += countTemp;

                        //对数据进行处理。正常情况下,在这里就能把数据处理完了。除非数据出错,或者出现了奇怪的扰动。
                        HandleRecvData(recvBuffer, totalCount, out int restCount, reserveRest: true);
                        totalCount = restCount;

                        maybeNoMoreData = false;//也许还有数据可以读
                        noMoreData = false;
                        sleepTime = sleepTime_Receiving;//抓紧查询
                        receiving = true;
                    }
                    else
                    {
                        if (receiving)
                        {
                            if (maybeNoMoreData == true)//连续第二次判断到缓存里面没数据了(第一次与第二次之间睡眠了5毫秒),因此可以确认接收完成了。
                            {
                                noMoreData = true;
                                receiving = false;
                            }

                            if (maybeNoMoreData == false)//第一次判断到缓存里面没数据了,说明2毫秒之前就把数据读完了,之后睡眠3毫秒再看,如果还没数据,说明确实没了。
                            {
                                maybeNoMoreData = true;
                                sleepTime = sleepTime_Waiting - sleepTime_Receiving;
                            }
                        }
                    }

                    if (noMoreData)
                    {
                        if (totalCount > 0)
                        {
                            string dataStr = _serialPort.Encoding.GetString(recvBuffer, 0, totalCount);
                            Debug.Log($"处理剩余数据:{dataStr}");

                            //如果数据出错,或者出现了奇怪的扰动,就可能执行到这里,把数据清空。
                            HandleRecvData(recvBuffer, totalCount, out int restCount, reserveRest: false);
                        }
                        totalCount = 0;
                        maybeNoMoreData = false;
                        noMoreData = false;
                        sleepTime = sleepTime_Waiting;
                        receiving = false;
                    }
                }
                catch (Exception e)
                {
                    totalCount = 0;//舍弃已经读取的数据
                    maybeNoMoreData = false;
                    noMoreData = false;
                    sleepTime = sleepTime_Waiting;
                    receiving = false;
                    Debug.LogError("串口数据接收异常:" + e.Message + "  StackTrace: " + e.StackTrace);
                }
            }
            Thread.Sleep(sleepTime);
        }

        Debug.Log("退出串口接收数据线程");
    }

    /// <summary>
    /// 处理接收到的数据,从其中获取消息放进接收到的消息处理队列,并将剩下的数据放回buffer(也可以选择不放回,从而清空buffer).
    /// 返回值表示是否添加了一个或多个消息进队列。
    /// </summary>
    /// <param name="buffer"></param>
    /// <param name="totalCount">buffer里面的数据的字节数</param>
    /// <param name="restCount">剩下的数据的字节数,其实代表方法返回后buffer中留存的数据的字节数</param>
    /// <param name="reserveRest">剩下的数据是否放回buffer开头</param>
    /// <returns></returns>
    private bool HandleRecvData(byte[] buffer, int totalCount, out int restCount, bool reserveRest)
    {
        if (SeparateRecvData(buffer, totalCount, out restCount, out List<string> msgList, reserveRest))
        {
            //放进接收的消息处理队列
            lock (_msgRecvQueue)
            {
                for (int i = 0; i < msgList.Count; i++)
                {
                    _msgRecvQueue.Enqueue(msgList[i]);
                    Debug.Log("串口接收到数据:" + msgList[i]);
                }
            }
            return true;
        }
        return false;
    }

    /// <summary>
    /// 从接收到的数据中分离出单条消息存到列表中。并将剩余的数据重新放回到数组中。返回值表示是否分离出一条或多条数据。
    /// </summary>
    /// <param name="buffer">保存接收的数据的数组</param>
    /// <param name="totalCount">数组中保存的数据的字节数</param>
    /// <param name="restCount">分离后,剩余的字节数,其实代表方法返回后buffer中留存的数据的字节数</param>
    /// <param name="msgList">保存消息的列表</param>
    /// <param name="reserveRest">是否保留剩余的数据(指后面的暂时分析不出来的数据,可能数据还没接收完全)。true的话,就把剩余的数据拷贝到buffer开头,false的话就直接丢弃(在确认数据接收完了,暂时(几毫秒内)没有新数据的时候,应该使用false,以清空buffer,以免错误数据干扰)。</param>
    /// <returns></returns>
    private bool SeparateRecvData(byte[] buffer, int totalCount, out int restCount, out List<string> msgList, bool reserveRest)
    {
        msgList = new List<string>();

        if (buffer == null || buffer.Length == 0 || totalCount == 0)
        {
            restCount = 0;
            return false;
        }

        //以下假设单条数据是以字符串 START_FLAG 开始,以字符串 END_FLAG 结束。可以自己根据情况更改。
        //这种以字符串 START_FLAG 开始,以字符串 END_FLAG 结束的方式,处理起来比较复杂,不如直接使用"\r\n"或者使用'\0'等单个字符隔开的方式处理起来更加简单。        

        string dataStr = _serialPort.Encoding.GetString(buffer, 0, totalCount);

        if (!string.IsNullOrEmpty(dataStr))
        {
            string fullMsgStr = null, restStr = null;
            SeparateRecvData2FullAndRest(dataStr, out fullMsgStr, out restStr);

            if (!string.IsNullOrEmpty(fullMsgStr))
            {
                SeparateMsgsFromFullMsgStr(fullMsgStr, out msgList);
            }

            if (reserveRest && !string.IsNullOrEmpty(restStr))
            {
                byte[] restBytes = _serialPort.Encoding.GetBytes(restStr);
                restBytes.CopyTo(buffer, 0);
                restCount = restBytes.Length;
            }
            else
            {
                restCount = 0;
            }
        }
        else
        {
            restCount = 0;
        }

        return msgList.Count > 0;
    }

    /// <summary>
    /// 将接收到的数据拆分成具有一个或多个完整消息的字符串(每个消息以起始标志开头,以结束标志结尾,因此fullMsgStr也以起始标志开头,以结束标志结尾),
    /// 以及剩余的部分(最后一个结束标志后面的部分)。
    /// 方法返回后,fullMsgStr的状态是:"START????????????????END",问号表示不知道是什么,中间的问号中也许也有START、END标志。这个字符串中包含一个或多个完整的消息。
    /// 或者fullMsgStr的状态是:null。
    /// restStr的状态是"???????????"或者null。
    /// </summary>
    /// <param name="buffer"></param>
    /// <param name="fullMsgStr"></param>
    /// <param name="restStr"></param>
    private void SeparateRecvData2FullAndRest(string buffer, out string fullMsgStr, out string restStr)
    {
        if (string.IsNullOrEmpty(buffer))
        {
            fullMsgStr = null;
            restStr = null;
        }
        else
        {
            int lastEndIndex = buffer.LastIndexOf(END_FLAG);
            if (lastEndIndex != -1)//有最后一个结束标志,说明“可能”有完整的消息,"STARTxxxxENDSTARTxx"有完整消息,"xxENDSTARTxx"则没有完整消息。xxx表示消息具体内容
            {
                string inFrontOfRest = null;//在剩余的部分的前面,也就是除去了Rest部分的数据后所剩下的数据,也就是前面的数据,应该是这样"?????????END"
                int restIndex = lastEndIndex + END_FLAG.Length;//剩余的部分的索引
                if (restIndex < buffer.Length)//最后一个结束标志后还有数据,也就是说有剩余的部分
                {
                    inFrontOfRest = buffer.Substring(0, restIndex);
                    restStr = buffer.Substring(restIndex);
                }
                else//最后一个结束标志后没有数据了,说明没有剩余的部分
                {
                    inFrontOfRest = buffer;
                    restStr = null;
                }

                //到这里,inFrontOfRest的状态是:"???????????????????END"                

                int firstStartIndex = inFrontOfRest.IndexOf(START_FLAG);//找到第一个开始标志
                if (firstStartIndex != -1)//有第一个开始标志,说明有完整消息
                {
                    fullMsgStr = inFrontOfRest.Substring(firstStartIndex);//firstStartIndex之前的数据丢弃。
                }
                else
                {
                    fullMsgStr = null;//没有第一个开始标志,说明前面数据不完整、或者START标志损坏,那么直接丢弃掉就行了。(正常处理情况下不会出现这种情况)
                }

                //到这里,fullMsgStr的状态是:"START????????????????END",问号表示不知道是什么,中间的问号中也许也有START、END标志。这个字符串中包含一个或多个完整的消息。
                //或者fullMsgStr的状态是:null
                //此时,restStr的状态是"???????????"或者null
            }
            else//没有最后一个结束标志,说明没有完整的消息。或者结束标志损坏(这种情况也没事,因为在以后处理的时候,会将损坏的剔除)
            {
                fullMsgStr = null;
                restStr = buffer;
            }
        }
    }

    /// <summary>
    /// 从具有完整消息的字符串中分离出一个或多个完整消息。
    /// 参数要保证,fullMsgStr要以起始标志开头,以结束标志结尾。
    /// 返回值表示是否分离出了一个有效的消息,即msgList中是否有元素。
    /// msgList中保存的将是一条一条的单独的完整消息,如果传输的数据的起始结束标志不出错的话,确实是这样。如果出错,则只能保证元素以 起始标志 开始,以 结束标志 结束,
    /// 只是以后在处理这里面的元素的时候,要注意处理数据可能出错的情况。
    /// </summary>
    /// <param name="fullMsgStr"></param>
    /// <param name="msgList"></param>
    /// <returns></returns>
    private bool SeparateMsgsFromFullMsgStr(string fullMsgStr, out List<string> msgList)
    {
        msgList = new List<string>();
        string restFullMsgStr = fullMsgStr;
        while (!string.IsNullOrEmpty(restFullMsgStr))
        {
            int firstStartIndex = restFullMsgStr.IndexOf(START_FLAG);
            if (firstStartIndex == -1)
            {
                break;
            }
            else
            {
                //如果前一条消息的起始标志或结束标志损坏了,希望不影响下一条消息。

                int firstEndIndex = restFullMsgStr.IndexOf(END_FLAG);//这个值一定不会是-1,因为方法参数里面已经保证fullMsgStr的最后是结束标志。
                if (firstEndIndex < firstStartIndex)//说明位于firstStartIndex之前的起始标志损坏了,则此时firstStartIndex是下一条消息的起始标志。
                {
                    restFullMsgStr = restFullMsgStr.Substring(firstStartIndex);//舍弃掉前面的消息
                }
                else
                {
                    int secondStartIndex = restFullMsgStr.IndexOf(START_FLAG, firstStartIndex + START_FLAG.Length);
                    if (secondStartIndex != -1 && secondStartIndex < firstEndIndex)//这种情况说明此条消息的结束标志损坏了。
                    {
                        restFullMsgStr = restFullMsgStr.Substring(secondStartIndex);//因为此条消息的结束标志损坏了,所以将这条消息直接舍弃,而留下剩下的。
                    }
                    else
                    {
                        int firstMsgLength = firstEndIndex + END_FLAG.Length - firstStartIndex;
                        string msg = restFullMsgStr.Substring(firstStartIndex, firstMsgLength);
                        msgList.Add(msg);
                        //如果secondStartIndex为-1,则表示没有下一条消息了,或者后面都是无效消息。
                        restFullMsgStr = secondStartIndex == -1 ? string.Empty : restFullMsgStr.Substring(firstEndIndex + END_FLAG.Length);
                    }
                }
            }
        }

        return msgList.Count > 0;
    }

    /// <summary>
    /// 发送数据。其实只是将数据放进消息队列里面,发送线程会将消息队列里的消息一个个发送出去。
    /// </summary>
    /// <param name="msg"></param>
    public void Send(string msg)
    {
        if (!string.IsNullOrEmpty(msg))
        {
            lock (_msgSendQueue)
            {
                _msgSendQueue.Enqueue(msg);
            }
        }
    }

    /// <summary>
    /// 发送数据的线程
    /// </summary>
    private void SendDataLoop()
    {
        Debug.Log("进入串口发送数据线程");

        while (_bRunning)
        {
            if (IsOpen())
            {
                if (_serialPort.BytesToWrite <= 0)//输出缓存区里面没有数据了,等几毫秒再将数据写进去,保证两次数据之间时间上是隔断的
                {
                    Thread.Sleep(10);

                    lock (_msgSendQueue)
                    {
                        if (_msgSendQueue.Count > 0)
                        {
                            string msg = _msgSendQueue.Peek();
                            if (!string.IsNullOrEmpty(msg))
                            {
                                try
                                {
                                    _serialPort.Write(msg);
                                    _msgSendQueue.Dequeue();
                                }
                                catch (Exception e)
                                {
                                    Debug.LogError("串口数据发送异常:" + e.Message);
                                }
                            }
                            else
                            {
                                _msgSendQueue.Dequeue();
                            }
                        }
                    }
                }
            }
        }

        Debug.Log("退出串口发送数据线程");
    }

}

完整工程下载:https://download.csdn.net/download/Thechuang/21487378

使用Unity版本:2019.4.15f1c1

工程是东拼西凑的,所以有一些用不到的代码。水平有限,请多担待。

  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值