为了使用串口通信,可以发送文字、图片、声音、文件等,实现了一个小的程序。
程序的框架,决定采用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; |
下面是发送帧的部分程序。
01 | public static void SendBytesBySerialPort( byte [] Buf, byte tag) |
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; |
10 | for ( int i = 0; i < sendTimes_1; i++) |
14 | for ( int j = 0; j < LengthOfDataFrame; j++) |
16 | outBuf[2 + j] = Buf[i * LengthOfDataFrame + j]; |
18 | if (lastTimesLength == 0 && i == sendTimes_1 - 1) |
19 | outBuf[1] = LengthOfDataFrame; |
20 | proxySerialPort.Write(outBuf, 0, LengthOfFrame); |
24 | if (lastTimesLength > 0) |
27 | outBuf[1] = ( byte )lastTimesLength; |
30 | for ( int j = 0; j < lastTimesLength; j++) |
32 | outBuf[j + 2] = Buf[sendTimes_1 * LengthOfDataFrame + j]; |
34 | proxySerialPort.Write(outBuf, 0, LengthOfFrame); |
上面的程序就算是从发送部分解决问题。
下面从数据接收部分解决问题,接收出现的最多问题是,DataReceived调用的时候,不是每次都有恰好那么多个字节,这个就需要多次接收,堆积在一起。接着出现,有数据来了,系统不调用这个DataReceived,更是无语,只好采用一直调用DataReceived的方法了。具体代码如下:
1 | new Thread( new ThreadStart(StartDataReceived)).Start(); |
1 | private static void StartDataReceived() |
5 | proxySerialPort_DataReceived( null , null ); |
01 | static void proxySerialPort_DataReceived( object sender, SerialDataReceivedEventArgs e) |
05 | if (proxySerialPort.IsOpen) |
07 | int arrivalNum = proxySerialPort.BytesToRead; |
08 | if (arrivalNum >= LengthOfFrame) |
10 | byte [] inBuf = new byte [LengthOfFrame]; |
11 | proxySerialPort.Read(inBuf, 0, LengthOfFrame); |
13 | dealNormalFrame(inBuf); |
17 | System.Threading.Thread.Sleep(10); |
22 | System.Threading.Thread.Sleep(1000); |
27 | System.Threading.Thread.Sleep(1000); |
二、处理接收到的数据
上面的dealNormalFrame(inBuf)函数,是分类处理这些帧的,如果有错误也在这个函数里面实现。他的具体形式如下:
01 | private static void dealNormalFrame( byte [] inBuf) |
04 | if (inBuf[0] == DataTag) |
06 | dealNormalDataFrame(inBuf); |
08 | else if (inBuf[0] == CMDTag) |
10 | dealNormalCMDFrame(inBuf); |
12 | else if (inBuf[0] == FileTag) |
14 | dealNormalFileFrame(inBuf); |
16 | else if (inBuf[0] == VoiceTag) |
18 | dealNormalVoiceFrame(inBuf); |
22 | dealErrorFormatFrame(inBuf); |
上面处理异常帧,主要是处理错位的帧的方法,就是向下搜索直到搜索到一个正确的标识为止,然后跳到处理正常帧那里。代码如下:
01 | private static void dealErrorFormatFrame( byte [] inBuf) |
03 | int needNumberBytes = 0; |
04 | for (; needNumberBytes < inBuf.Length; needNumberBytes++) |
06 | switch (inBuf[needNumberBytes]) |
21 | int currentArrivalNum = proxySerialPort.BytesToRead; |
22 | byte [] outBuf = new byte [LengthOfFrame]; |
23 | if (needNumberBytes <= currentArrivalNum) |
25 | proxySerialPort.Read(inBuf, 0, needNumberBytes); |
26 | for ( int j = needNumberBytes; j < LengthOfFrame; j++) |
28 | outBuf[j - needNumberBytes] = inBuf[j]; |
30 | for ( int j = 0; j < needNumberBytes; j++) |
32 | outBuf[LengthOfFrame - needNumberBytes + j] = inBuf[j]; |
34 | dealNormalFrame(outBuf); |
三、发送文件功能的实现
发送文件的大致流程是,先告诉对方我要发送某个文件,然后把该文件视作字符流,分成若干帧发送过去,然后告诉对方发送完毕,或者告诉对方我不想发送了。为了简化本程序没有做类似TCP的可靠传输,而是类似UDP的尽最大努力的发送文件,不考虑安全和丢包等。
下面的程序是发送文件请求的程序,前台的发送文件按钮。
01 | private void buttonSendFile_Click( object sender, EventArgs e) |
05 | if (buttonSendFile.Text == "发送文件" ) |
07 | if (!proxySerialDataPort.IsOpen) |
09 | buttonOpen_Click(sender, e); |
11 | Communication.SendFile(); |
15 | WriteTipToRichTextBox( "好啊,您停止了发送文件!" , "我马上就告诉对方!" ); |
16 | Communication.StopSendFile(); |
上面实现的发送文件请求和终止发送文件在另外一个类class Communication里面实现。
下面的代码是发送接收文件需要使用的全局变量
1 | static string filePathReceived= "" ; |
2 | static string filePathSend = "" ; |
3 | static Thread threadSendFile; |
下面的代码是发送文件请求的代码,提供了两种方式的请求方式。
01 | public static void SendFile( string filePath) |
03 | filePathSend = filePath; |
04 | string [] files = filePath.Split( new char [] { '/' , '\\' }); |
07 | + files[files.Length-1]); |
09 | public static void SendFile() |
11 | OpenFileDialog ofd = new OpenFileDialog(); |
12 | if (ofd.ShowDialog() == DialogResult.OK) |
17 | filePathSend = ofd.FileName; |
如果对方同意接收文件,则可以发送文件了。由于发送文件,占用时间比较长,所以最后采用开辟新的线程的做法。开辟新线程出现了一些问题,经过上网搜寻,但是忘记记录从哪里搜到的资料了。发送文件的程序如下。
01 | private static void SendFileBySerialPort() |
03 | if (!File.Exists(filePathSend)) |
05 | MessageBox.Show( "文件:" + filePathSend + "不存在,请核实后再操作!" , |
08 | MessageBoxIcon.Error); |
15 | using (FileStream fs = File.OpenRead(filePathSend)) |
17 | byte [] outBuf = new byte [LengthOfFrame]; |
18 | outBuf[0] = ( byte )FileTag; |
22 | while ((length = fs.Read(outBuf, 2, LengthOfDataFrame)) == LengthOfDataFrame) |
26 | if (fs.Position == fs.Length) |
28 | proxySerialPort.Write(outBuf, 0, LengthOfFrame); |
36 | outBuf[1] = ( byte )length; |
37 | proxySerialPort.Write(outBuf, 0, LengthOfFrame); |
41 | DisplayToForm.SetAllowSendFile( false ); |
47 | private static void SendFileBySerialPortAsy() |
49 | threadSendFile = new Thread( new ThreadStart(SendFileBySerialPort)); |
50 | threadSendFile.IsBackground = true ; |
51 | threadSendFile.SetApartmentState(ApartmentState.STA); |
52 | threadSendFile.Start(); |
如果对方拒绝接收文件,则清空filePathSend="";如果我想终止发送文件,则如下停止发送文件。没有提供接收方终止发送文件的功能。利用关闭串口产生的异常来停止发送文件,从而实现停止发送的功能。因为使用直接停掉线程的做法没有成功,只好用这种方式实现了。下面是终止发送文件的程序。
01 | public static void StopSendFile() |
03 | SendCMDBySerialPort( "stopSendFile" ); |
07 | if (proxySerialPort.IsOpen) |
09 | proxySerialPort.Close(); |
16 | proxySerialPort.Open(); |
18 | DisplayToForm.SetAllowSendFile( false ); |
四、接收命令的处理方式
由于本程序帧的定义是有两个字节的头部,第一个字节是标识字节,第二个字节是长度字节。对于命令帧可以任意长度,不受最长帧长的限制,所以使用起来很方便。上面发送文件或者语音通信等的握手信号都可以使用命令帧来实现。
下面的代码实现接收一个完整的命令,并解析该命令。
01 | static string CMDString = "" ; |
02 | private static void dealNormalCMDFrame( byte [] inBuf) |
04 | if (inBuf[1] > 0 && inBuf[1] <= LengthOfDataFrame) |
06 | CMDString += MyEncoding.GetString(inBuf, 2, inBuf[1]); |
07 | DisplayToForm.WriteToRichTextBoxFromThat( "Sound/tweet.wav" , "" , false ); |
15 | CMDString += MyEncoding.GetString(inBuf, 2, LengthOfDataFrame); |
上面CMD_Parse(string cmd)函数实现对一个完成命令的解析,并作出相应的响应。存在一个很大的问题是这里的命令没有保存到一个具体的数组里面,或者使用enum类型,容易出错,这里仅仅是一个例子,不再追求完美。具体的代码如下。
02 | private static void CMD_Parse( string CMD) |
04 | if (CMD == "" ) return ; |
05 | string [] cmdLines = CMD.Split( new char [] { '/' }); |
08 | case "requestSendFile" : |
10 | Thread t = new Thread( new ParameterizedThreadStart(WhenGotRequestSendFile)); |
11 | t.IsBackground = true ; |
12 | t.SetApartmentState(ApartmentState.STA); |
16 | DisplayToForm.SetAllowSendFile( true ); |
17 | SendFileBySerialPortAsy(); |
18 | DisplayToForm.WriteTipToRichTextBox( "恭喜您" , "对方同意接收文件!" ); |
19 | DisplayToForm.WriteTipToRichTextBox( |
21 | "正在发送\n“" + filePathSend |
24 | case "refuseSendFile" : |
25 | DisplayToForm.SetAllowSendFile( false ); |
26 | DisplayToForm.WriteTipToRichTextBox( "很不幸" , "对方拒绝接收您的文件!" ); |
30 | DisplayToForm.SetAllowSendFile( false ); |
31 | DisplayToForm.WriteTipToRichTextBox( "很不幸" , "对方停止发送文件了!" ); |
36 | DisplayToForm.SetAllowSendFile( false ); |
37 | DisplayToForm.WriteTipToRichTextBox( "恭喜您" , "文件发送完毕!" ); |
41 | DisplayToForm.WriteTipToRichTextBox( "呵呵" , "对方给您发送了一个窗口抖动。" ); |
42 | DisplayToForm.JitterWindow(); |
45 | WhenGotRequestVoice(); |
48 | System.Threading.Thread.Sleep(40); |
49 | DisplayToForm.SetAllowVoice( true ); |
50 | DisplayToForm.WriteTipToRichTextBox( "恭喜你" , "对方同意了您的通话请求" ); |
53 | DisplayToForm.SetAllowVoice( false ); |
54 | DisplayToForm.WriteTipToRichTextBox( "语音聊天" , "对方拒绝了您的通话请求。" ); |
57 | DisplayToForm.SetAllowVoice( false ); |
58 | DisplayToForm.WriteTipToRichTextBox( "语音聊天" , "对方停止了通话。" ); |
62 | VoiceSpeakAndListen.Stop(); |
63 | DisplayToForm.WriteTipToRichTextBox( "消息提醒" , "有人在说话,您先听着吧。" ); |
67 | VoiceSpeakAndListen.Stop(); |
68 | DisplayToForm.WriteTipToRichTextBox( "消息提醒" , "现在没人说话,您可以点击说话,开始说话了。" ); |
|