SerialPort串口传输文件、语音等

为了使用串口通信,可以发送文字、图片、声音、文件等,实现了一个小的程序。

程序的框架,决定采用window form的界面,参考QQ聊天界面和声音等,实现双工低速率4kbps的通信。界面主要实现一些按钮和必要的建立连接的装置。后面其他的代码实现相应的功能,可以说是采用了某种模型吧。

一、串口出现的问题

1、不能按照我们的目的接收bytes
解决方案是:采用有固定格式的帧,采用的帧长 LengthOfFrame=180,这个纯属自己的爱好,以后所有的帧都是这么长。然后是定义帧的形式。采用帧头长LengthOfHeader=2,剩下的部分长度LengthOfDataFrame=180-2=178,第一个数据表示该帧的类型,第二个数据表示该帧的实际长度,由于帧长是180<,buffer[0]=(byte)tag,buffer[1]=(byte)LengthOfThisFrame,考虑可能有过长的帧,我们要特殊处理一下。如果后面还有该帧的后续帧,则该帧的长度buffer[1]=0x00,设为一个标记,表示本帧的长度是LengthOfDataFrame,还有下一帧也是本帧的一部分。具体的程序,见下面的程序。

#region const parameters
        const int LengthOfFrame = 180;
        const int LengthOfHeaderDataFrame = 2;
        const int LengthOfDataFrame = LengthOfFrame - LengthOfHeaderDataFrame;
        const byte DataTag = (byte)'d';
        const byte CMDTag = (byte)'c';
        const byte FileTag = (byte)'f';
        const byte VoiceTag = (byte)'v';
        static Encoding MyEncoding = Encoding.Unicode;
        // here we use utf16,that is two bytes,which can express Chinese and English
        #endregion
下面是发送帧的部分程序。
public static void SendBytesBySerialPort(byte[] Buf, byte tag)
        {
            if (Buf == null) return;
            int length = Buf.Length;
            byte[] outBuf = new byte[LengthOfFrame];
            int sendTimes_1 = length / LengthOfDataFrame;
            int lastTimesLength = length % LengthOfDataFrame;

            // if Buf is too long, then divided into dataLength
            for (int i = 0; i < sendTimes_1; i++)
            {
                outBuf[0] = tag;
                outBuf[1] = 0x00;
                for (int j = 0; j < LengthOfDataFrame; j++)
                {
                    outBuf[2 + j] = Buf[i * LengthOfDataFrame + j];
                }
                if (lastTimesLength == 0 && i == sendTimes_1 - 1)
                    outBuf[1] = LengthOfDataFrame;
                proxySerialPort.Write(outBuf, 0, LengthOfFrame);
            }

            // send the last information
            if (lastTimesLength > 0)
            {
                outBuf[0] = tag;
                outBuf[1] = (byte)lastTimesLength;


                for (int j = 0; j < lastTimesLength; j++)
                {
                    outBuf[j + 2] = Buf[sendTimes_1 * LengthOfDataFrame + j];
                }
                proxySerialPort.Write(outBuf, 0, LengthOfFrame);
            }
        }
上面的程序就算是从发送部分解决问题。

下面从数据接收部分解决问题,接收出现的最多问题是,DataReceived调用的时候,不是每次都有恰好那么多个字节,这个就需要多次接收,堆积在一起。接着出现,有数据来了,系统不调用这个DataReceived,更是无语,只好采用一直调用DataReceived的方法了。具体代码如下:

            new Thread(new ThreadStart(StartDataReceived)).Start();// start the dataReceived
//delete .DataReceived+=new EventHandle(proxySerialPort_DataReceived);
        private static void StartDataReceived()
        {
            while (true)
            {
                proxySerialPort_DataReceived(null, null);
            }
        }
        static void proxySerialPort_DataReceived(object sender, SerialDataReceivedEventArgs e)
        {
            try
            {
                if (proxySerialPort.IsOpen)
                {
                    int arrivalNum = proxySerialPort.BytesToRead;
                    if (arrivalNum >= LengthOfFrame)
                    {
                        byte[] inBuf = new byte[LengthOfFrame];
                        proxySerialPort.Read(inBuf, 0, LengthOfFrame);
                        //new Thread(new ParameterizedThreadStart(dealNormalFrame)).Start(inBuf);
                        dealNormalFrame(inBuf);
                    }
                    else
                    {
                        System.Threading.Thread.Sleep(10);
                    }
                }
                else
                {
                    System.Threading.Thread.Sleep(1000);
                }
            }
            catch 
            {
                System.Threading.Thread.Sleep(1000);
            }
        }

二、处理接收到的数据

上面的dealNormalFrame(inBuf)函数,是分类处理这些帧的,如果有错误也在这个函数里面实现。他的具体形式如下:

private static void dealNormalFrame(byte[] inBuf)
        {

            if (inBuf[0] == DataTag)
            {
                dealNormalDataFrame(inBuf);
            }
            else if (inBuf[0] == CMDTag)
            {
                dealNormalCMDFrame(inBuf);
            }
            else if (inBuf[0] == FileTag)
            {
                dealNormalFileFrame(inBuf);
            }
            else if (inBuf[0] == VoiceTag)
            {
                dealNormalVoiceFrame(inBuf);
            }
            else//here received error-format frame,we assume that it is data-frame with a great probablity.
            {
                dealErrorFormatFrame(inBuf);
            }
        }
上面处理异常帧,主要是处理错位的帧的方法,就是向下搜索直到搜索到一个正确的标识为止,然后跳到处理正常帧那里。代码如下:
private static void dealErrorFormatFrame(byte[] inBuf)
        {
            int needNumberBytes = 0;
            for (; needNumberBytes < inBuf.Length; needNumberBytes++)
            {
                switch (inBuf[needNumberBytes])
                {
                    case CMDTag:
                        break;
                    case DataTag:
                        break;
                    case FileTag:
                        break;
                    case VoiceTag:
                        break;
                    default:
                        continue;
                }
                break;
            }
            int currentArrivalNum = proxySerialPort.BytesToRead;
            byte[] outBuf = new byte[LengthOfFrame];
            if (needNumberBytes <= currentArrivalNum)
            {
                proxySerialPort.Read(inBuf, 0, needNumberBytes);
                for (int j = needNumberBytes; j < LengthOfFrame; j++)
                {
                    outBuf[j - needNumberBytes] = inBuf[j];
                }
                for (int j = 0; j < needNumberBytes; j++)
                {
                    outBuf[LengthOfFrame - needNumberBytes + j] = inBuf[j];
                }
                dealNormalFrame(outBuf);
            }
        }

三、发送文件功能的实现

发送文件的大致流程是,先告诉对方我要发送某个文件,然后把该文件视作字符流,分成若干帧发送过去,然后告诉对方发送完毕,或者告诉对方我不想发送了。为了简化本程序没有做类似TCP的可靠传输,而是类似UDP的尽最大努力的发送文件,不考虑安全和丢包等。

下面的程序是发送文件请求的程序,前台的发送文件按钮。

        private void buttonSendFile_Click(object sender, EventArgs e)
        {
            try
            {
                if (buttonSendFile.Text == "发送文件")
                {
                    if (!proxySerialDataPort.IsOpen)
                    {
                        buttonOpen_Click(sender, e);
                    }
                    Communication.SendFile();
                }
                else
                {
                    WriteTipToRichTextBox("好啊,您停止了发送文件!", "我马上就告诉对方!");
                    Communication.StopSendFile();
                }
            }
            catch { }
        }
上面实现的发送文件请求和终止发送文件在另外一个类class Communication里面实现。
下面的代码是发送接收文件需要使用的全局变量
        static string filePathReceived="";
        static string filePathSend = "";
        static Thread threadSendFile;
下面的代码是发送文件请求的代码,提供了两种方式的请求方式。
        public static void SendFile(string filePath)
        {
            filePathSend = filePath;
            string[] files = filePath.Split(new char[] { '/','\\'});
            SendCMDBySerialPort(
                    "requestSendFile/"
                    + files[files.Length-1]);
        }
        public static void SendFile()
        {
            OpenFileDialog ofd = new OpenFileDialog();
            if (ofd.ShowDialog() == DialogResult.OK)
            {
                SendCMDBySerialPort(
                    "requestSendFile/"
                    + ofd.SafeFileName);
                filePathSend = ofd.FileName;
            }
        }

如果对方同意接收文件,则可以发送文件了。由于发送文件,占用时间比较长,所以最后采用开辟新的线程的做法。开辟新线程出现了一些问题,经过上网搜寻,但是忘记记录从哪里搜到的资料了。发送文件的程序如下。

        private static void SendFileBySerialPort()
        {
            if (!File.Exists(filePathSend))
            {
                MessageBox.Show("文件:" + filePathSend + "不存在,请核实后再操作!",
                    "文件错误",
                    MessageBoxButtons.OK,
                    MessageBoxIcon.Error);
                return;
            }

            //Open the stream and read it back. 
            try
            {
                using (FileStream fs = File.OpenRead(filePathSend))
                {
                    byte[] outBuf = new byte[LengthOfFrame];
                    outBuf[0] = (byte)FileTag;
                    outBuf[1] = 0x00;//we assume that this is not end;
                    int length = 0;

                    while ((length = fs.Read(outBuf, 2, LengthOfDataFrame)) == LengthOfDataFrame)
                    {
                        // code from http://msdn.microsoft.com/en-us/library/system.io.filestream.position.aspx
                        // show that next line is the end of file.
                        if (fs.Position == fs.Length)
                            break;
                        proxySerialPort.Write(outBuf, 0, LengthOfFrame);

                    }

                    // if there are N*LengthOfDataFrame,length=0,return;
                    // else we should send the last bytes;
                    if (length > 0)
                    {
                        outBuf[1] = (byte)length;
                        proxySerialPort.Write(outBuf, 0, LengthOfFrame);
                    }

                    filePathSend = "";
                    DisplayToForm.SetAllowSendFile(false);
                }
            }
            catch { }
        }

        private static void SendFileBySerialPortAsy()
        {
            threadSendFile = new Thread(new ThreadStart(SendFileBySerialPort));
            threadSendFile.IsBackground = true;
            threadSendFile.SetApartmentState(ApartmentState.STA);
            threadSendFile.Start();
        }
如果对方拒绝接收文件,则清空filePathSend="";如果我想终止发送文件,则如下停止发送文件。没有提供接收方终止发送文件的功能。利用关闭串口产生的异常来停止发送文件,从而实现停止发送的功能。因为使用直接停掉线程的做法没有成功,只好用这种方式实现了。下面是终止发送文件的程序。
        public static void StopSendFile()
        {
            SendCMDBySerialPort("stopSendFile");

            try
            {
                if (proxySerialPort.IsOpen)
                {
                    proxySerialPort.Close();
                }
            }
            catch
            {

            }
            proxySerialPort.Open();
            filePathSend = "";
            DisplayToForm.SetAllowSendFile(false);
        }

四、接收命令的处理方式

由于本程序帧的定义是有两个字节的头部,第一个字节是标识字节,第二个字节是长度字节。对于命令帧可以任意长度,不受最长帧长的限制,所以使用起来很方便。上面发送文件或者语音通信等的握手信号都可以使用命令帧来实现。

下面的代码实现接收一个完整的命令,并解析该命令。

        static string CMDString = "";
        private static void dealNormalCMDFrame(byte[] inBuf)
        {
            if (inBuf[1] > 0 && inBuf[1] <= LengthOfDataFrame)
            {
                CMDString += MyEncoding.GetString(inBuf, 2, inBuf[1]);
                DisplayToForm.WriteToRichTextBoxFromThat("Sound/tweet.wav", "", false);

                CMD_Parse(CMDString);
                CMDString = "";

            }
            else
            {
                CMDString += MyEncoding.GetString(inBuf, 2, LengthOfDataFrame);
            }
        }
上面CMD_Parse(string cmd)函数实现对一个完成命令的解析,并作出相应的响应。存在一个很大的问题是这里的命令没有保存到一个具体的数组里面,或者使用enum类型,容易出错,这里仅仅是一个例子,不再追求完美。具体的代码如下。
        [STAThread]
        private static void CMD_Parse(string CMD)
        {
            if (CMD == "") return;
            string[] cmdLines = CMD.Split(new char[] { '/' });
            switch (cmdLines[0])
            {
                case "requestSendFile":
                    //follow reason: http://social.msdn.microsoft.com/Forums/zh-CN/winforms/thread/2411f889-8e30-4a6d-9e28-8a46e66c0fdb
                    Thread t = new Thread(new ParameterizedThreadStart(WhenGotRequestSendFile));
                    t.IsBackground = true;
                    t.SetApartmentState(ApartmentState.STA);
                    t.Start(cmdLines);
                    break;
                case "ackSendFile":
                    DisplayToForm.SetAllowSendFile(true);
                    SendFileBySerialPortAsy();
                    DisplayToForm.WriteTipToRichTextBox("恭喜您", "对方同意接收文件!");
                    DisplayToForm.WriteTipToRichTextBox(
                    "发送文件提示",
                    "正在发送\n“" + filePathSend
                    + "”");
                    break;
                case "refuseSendFile":
                    DisplayToForm.SetAllowSendFile(false);
                    DisplayToForm.WriteTipToRichTextBox("很不幸", "对方拒绝接收您的文件!");
                    filePathSend = "";
                    break;
                case "stopSendFile":
                    DisplayToForm.SetAllowSendFile(false);
                    DisplayToForm.WriteTipToRichTextBox("很不幸", "对方停止发送文件了!");
                    filePathSend = "";
                    isNewFile = true;
                    break;
                case "fileSendOver":
                    DisplayToForm.SetAllowSendFile(false);
                    DisplayToForm.WriteTipToRichTextBox("恭喜您", "文件发送完毕!");
                    filePathSend = "";
                    break;
                case "jitter":
                    DisplayToForm.WriteTipToRichTextBox("呵呵", "对方给您发送了一个窗口抖动。");
                    DisplayToForm.JitterWindow();
                    break;
                case "requestVoice":
                    WhenGotRequestVoice();
                    break;
                case "ackVoice":
                    System.Threading.Thread.Sleep(40);
                    DisplayToForm.SetAllowVoice(true);
                    DisplayToForm.WriteTipToRichTextBox("恭喜你", "对方同意了您的通话请求");
                    break;
                case "refuseVoice":
                    DisplayToForm.SetAllowVoice(false);
                    DisplayToForm.WriteTipToRichTextBox("语音聊天", "对方拒绝了您的通话请求。");
                    break;
                case "stopVoice":           
                    DisplayToForm.SetAllowVoice(false);
                    DisplayToForm.WriteTipToRichTextBox("语音聊天", "对方停止了通话。");
                    break;
                case "iSpeak":
                    //if I listen other say iSpeak, i will shut up;
                    VoiceSpeakAndListen.Stop();
                    DisplayToForm.WriteTipToRichTextBox("消息提醒", "有人在说话,您先听着吧。");
                    break;
                case "iShutUp":
                    //if I listen other say iSpeak, i will shut up;
                    VoiceSpeakAndListen.Stop();
                    DisplayToForm.WriteTipToRichTextBox("消息提醒", "现在没人说话,您可以点击说话,开始说话了。");
                    break;
            }
        }

接下来

后面待续












你可能感兴趣的:(错位,串口错误,串口传输)