分享

SerialPort串口传输文件、语音等

 BeautymengRoom 2013-10-10

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

程序的框架,决定采用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,还有下一帧也是本帧的一部分。具体的程序,见下面的程序。

01#region const parameters
02        const int LengthOfFrame = 180;
03        const int LengthOfHeaderDataFrame = 2;
04        const int LengthOfDataFrame = LengthOfFrame - LengthOfHeaderDataFrame;
05        const byte DataTag = (byte)'d';
06        const byte CMDTag = (byte)'c';
07        const byte FileTag = (byte)'f';
08        const byte VoiceTag = (byte)'v';
09        static Encoding MyEncoding = Encoding.Unicode;
10        // here we use utf16,that is two bytes,which can express Chinese and English
11        #endregion
下面是发送帧的部分程序。
01public static void SendBytesBySerialPort(byte[] Buf, byte tag)
02        {
03            if (Buf == null) return;
04            int length = Buf.Length;
05            byte[] outBuf = new byte[LengthOfFrame];
06            int sendTimes_1 = length / LengthOfDataFrame;
07            int lastTimesLength = length % LengthOfDataFrame;
08 
09            // if Buf is too long, then divided into dataLength
10            for (int i = 0; i < sendTimes_1; i++)
11            {
12                outBuf[0] = tag;
13                outBuf[1] = 0x00;
14                for (int j = 0; j < LengthOfDataFrame; j++)
15                {
16                    outBuf[2 + j] = Buf[i * LengthOfDataFrame + j];
17                }
18                if (lastTimesLength == 0 && i == sendTimes_1 - 1)
19                    outBuf[1] = LengthOfDataFrame;
20                proxySerialPort.Write(outBuf, 0, LengthOfFrame);
21            }
22 
23            // send the last information
24            if (lastTimesLength > 0)
25            {
26                outBuf[0] = tag;
27                outBuf[1] = (byte)lastTimesLength;
28 
29 
30                for (int j = 0; j < lastTimesLength; j++)
31                {
32                    outBuf[j + 2] = Buf[sendTimes_1 * LengthOfDataFrame + j];
33                }
34                proxySerialPort.Write(outBuf, 0, LengthOfFrame);
35            }
36        }
上面的程序就算是从发送部分解决问题。

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

1            new Thread(new ThreadStart(StartDataReceived)).Start();// start the dataReceived
2//delete .DataReceived+=new EventHandle(proxySerialPort_DataReceived);
1        private static void StartDataReceived()
2        {
3            while (true)
4            {
5                proxySerialPort_DataReceived(null, null);
6            }
7        }
01        static void proxySerialPort_DataReceived(object sender, SerialDataReceivedEventArgs e)
02        {
03            try
04            {
05                if (proxySerialPort.IsOpen)
06                {
07                    int arrivalNum = proxySerialPort.BytesToRead;
08                    if (arrivalNum >= LengthOfFrame)
09                    {
10                        byte[] inBuf = new byte[LengthOfFrame];
11                        proxySerialPort.Read(inBuf, 0, LengthOfFrame);
12                        //new Thread(new ParameterizedThreadStart(dealNormalFrame)).Start(inBuf);
13                        dealNormalFrame(inBuf);
14                    }
15                    else
16                    {
17                        System.Threading.Thread.Sleep(10);
18                    }
19                }
20                else
21                {
22                    System.Threading.Thread.Sleep(1000);
23                }
24            }
25            catch 
26            {
27                System.Threading.Thread.Sleep(1000);
28            }
29        }

二、处理接收到的数据

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

01private static void dealNormalFrame(byte[] inBuf)
02        {
03 
04            if (inBuf[0] == DataTag)
05            {
06                dealNormalDataFrame(inBuf);
07            }
08            else if (inBuf[0] == CMDTag)
09            {
10                dealNormalCMDFrame(inBuf);
11            }
12            else if (inBuf[0] == FileTag)
13            {
14                dealNormalFileFrame(inBuf);
15            }
16            else if (inBuf[0] == VoiceTag)
17            {
18                dealNormalVoiceFrame(inBuf);
19            }
20            else//here received error-format frame,we assume that it is data-frame with a great probablity.
21            {
22                dealErrorFormatFrame(inBuf);
23            }
24        }
上面处理异常帧,主要是处理错位的帧的方法,就是向下搜索直到搜索到一个正确的标识为止,然后跳到处理正常帧那里。代码如下:
01private static void dealErrorFormatFrame(byte[] inBuf)
02        {
03            int needNumberBytes = 0;
04            for (; needNumberBytes < inBuf.Length; needNumberBytes++)
05            {
06                switch (inBuf[needNumberBytes])
07                {
08                    case CMDTag:
09                        break;
10                    case DataTag:
11                        break;
12                    case FileTag:
13                        break;
14                    case VoiceTag:
15                        break;
16                    default:
17                        continue;
18                }
19                break;
20            }
21            int currentArrivalNum = proxySerialPort.BytesToRead;
22            byte[] outBuf = new byte[LengthOfFrame];
23            if (needNumberBytes <= currentArrivalNum)
24            {
25                proxySerialPort.Read(inBuf, 0, needNumberBytes);
26                for (int j = needNumberBytes; j < LengthOfFrame; j++)
27                {
28                    outBuf[j - needNumberBytes] = inBuf[j];
29                }
30                for (int j = 0; j < needNumberBytes; j++)
31                {
32                    outBuf[LengthOfFrame - needNumberBytes + j] = inBuf[j];
33                }
34                dealNormalFrame(outBuf);
35            }
36        }

三、发送文件功能的实现

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

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

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

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

01private static void SendFileBySerialPort()
02{
03    if (!File.Exists(filePathSend))
04    {
05        MessageBox.Show("文件:" + filePathSend + "不存在,请核实后再操作!",
06            "文件错误",
07            MessageBoxButtons.OK,
08            MessageBoxIcon.Error);
09        return;
10    }
11 
12    //Open the stream and read it back.
13    try
14    {
15        using (FileStream fs = File.OpenRead(filePathSend))
16        {
17            byte[] outBuf = new byte[LengthOfFrame];
18            outBuf[0] = (byte)FileTag;
19            outBuf[1] = 0x00;//we assume that this is not end;
20            int length = 0;
21 
22            while ((length = fs.Read(outBuf, 2, LengthOfDataFrame)) == LengthOfDataFrame)
23            {
24                // code from http://msdn.microsoft.com/en-us/library/system.io.filestream.position.aspx
25                // show that next line is the end of file.
26                if (fs.Position == fs.Length)
27                    break;
28                proxySerialPort.Write(outBuf, 0, LengthOfFrame);
29 
30            }
31 
32            // if there are N*LengthOfDataFrame,length=0,return;
33            // else we should send the last bytes;
34            if (length > 0)
35            {
36                outBuf[1] = (byte)length;
37                proxySerialPort.Write(outBuf, 0, LengthOfFrame);
38            }
39 
40            filePathSend = "";
41            DisplayToForm.SetAllowSendFile(false);
42        }
43    }
44    catch { }
45}
46 
47private static void SendFileBySerialPortAsy()
48{
49    threadSendFile = new Thread(new ThreadStart(SendFileBySerialPort));
50    threadSendFile.IsBackground = true;
51    threadSendFile.SetApartmentState(ApartmentState.STA);
52    threadSendFile.Start();
53}
如果对方拒绝接收文件,则清空filePathSend="";如果我想终止发送文件,则如下停止发送文件。没有提供接收方终止发送文件的功能。利用关闭串口产生的异常来停止发送文件,从而实现停止发送的功能。因为使用直接停掉线程的做法没有成功,只好用这种方式实现了。下面是终止发送文件的程序。
01public static void StopSendFile()
02{
03    SendCMDBySerialPort("stopSendFile");
04 
05    try
06    {
07        if (proxySerialPort.IsOpen)
08        {
09            proxySerialPort.Close();
10        }
11    }
12    catch
13    {
14 
15    }
16    proxySerialPort.Open();
17    filePathSend = "";
18    DisplayToForm.SetAllowSendFile(false);
19}

四、接收命令的处理方式

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

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

01static string CMDString = "";
02private static void dealNormalCMDFrame(byte[] inBuf)
03{
04    if (inBuf[1] > 0 && inBuf[1] <= LengthOfDataFrame)
05    {
06        CMDString += MyEncoding.GetString(inBuf, 2, inBuf[1]);
07        DisplayToForm.WriteToRichTextBoxFromThat("Sound/tweet.wav", "", false);
08 
09        CMD_Parse(CMDString);
10        CMDString = "";
11 
12    }
13    else
14    {
15        CMDString += MyEncoding.GetString(inBuf, 2, LengthOfDataFrame);
16    }
17}
上面CMD_Parse(string cmd)函数实现对一个完成命令的解析,并作出相应的响应。存在一个很大的问题是这里的命令没有保存到一个具体的数组里面,或者使用enum类型,容易出错,这里仅仅是一个例子,不再追求完美。具体的代码如下。
01[STAThread]
02private static void CMD_Parse(string CMD)
03{
04    if (CMD == "") return;
05    string[] cmdLines = CMD.Split(new char[] { '/' });
06    switch (cmdLines[0])
07    {
08        case "requestSendFile":
09            //follow reason: http://social.msdn.microsoft.com/Forums/zh-CN/winforms/thread/2411f889-8e30-4a6d-9e28-8a46e66c0fdb
10            Thread t = new Thread(new ParameterizedThreadStart(WhenGotRequestSendFile));
11            t.IsBackground = true;
12            t.SetApartmentState(ApartmentState.STA);
13            t.Start(cmdLines);
14            break;
15        case "ackSendFile":
16            DisplayToForm.SetAllowSendFile(true);
17            SendFileBySerialPortAsy();
18            DisplayToForm.WriteTipToRichTextBox("恭喜您", "对方同意接收文件!");
19            DisplayToForm.WriteTipToRichTextBox(
20            "发送文件提示",
21            "正在发送\n“" + filePathSend
22            + "”");
23            break;
24        case "refuseSendFile":
25            DisplayToForm.SetAllowSendFile(false);
26            DisplayToForm.WriteTipToRichTextBox("很不幸", "对方拒绝接收您的文件!");
27            filePathSend = "";
28            break;
29        case "stopSendFile":
30            DisplayToForm.SetAllowSendFile(false);
31            DisplayToForm.WriteTipToRichTextBox("很不幸", "对方停止发送文件了!");
32            filePathSend = "";
33            isNewFile = true;
34            break;
35        case "fileSendOver":
36            DisplayToForm.SetAllowSendFile(false);
37            DisplayToForm.WriteTipToRichTextBox("恭喜您", "文件发送完毕!");
38            filePathSend = "";
39            break;
40        case "jitter":
41            DisplayToForm.WriteTipToRichTextBox("呵呵", "对方给您发送了一个窗口抖动。");
42            DisplayToForm.JitterWindow();
43            break;
44        case "requestVoice":
45            WhenGotRequestVoice();
46            break;
47        case "ackVoice":
48            System.Threading.Thread.Sleep(40);
49            DisplayToForm.SetAllowVoice(true);
50            DisplayToForm.WriteTipToRichTextBox("恭喜你", "对方同意了您的通话请求");
51            break;
52        case "refuseVoice":
53            DisplayToForm.SetAllowVoice(false);
54            DisplayToForm.WriteTipToRichTextBox("语音聊天", "对方拒绝了您的通话请求。");
55            break;
56        case "stopVoice":          
57            DisplayToForm.SetAllowVoice(false);
58            DisplayToForm.WriteTipToRichTextBox("语音聊天", "对方停止了通话。");
59            break;
60        case "iSpeak":
61            //if I listen other say iSpeak, i will shut up;
62            VoiceSpeakAndListen.Stop();
63            DisplayToForm.WriteTipToRichTextBox("消息提醒", "有人在说话,您先听着吧。");
64            break;
65        case "iShutUp":
66            //if I listen other say iSpeak, i will shut up;
67            VoiceSpeakAndListen.Stop();
68            DisplayToForm.WriteTipToRichTextBox("消息提醒", "现在没人说话,您可以点击说话,开始说话了。");
69            break;
70    }
71}

    本站是提供个人知识管理的网络存储空间,所有内容均由用户发布,不代表本站观点。请注意甄别内容中的联系方式、诱导购买等信息,谨防诈骗。如发现有害或侵权内容,请点击一键举报。
    转藏 分享 献花(0

    0条评论

    发表

    请遵守用户 评论公约

    类似文章 更多