为了使用串口通信,可以发送文字、图片、声音、文件等,实现了一个小的程序。
程序的框架,决定采用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; } }
接下来
后面待续