短信开发系列目录:
短信开发系列(一):GSM手机短信开发初探
短信开发系列(二):GSM手机短信开发之短信解码
短信开发系列(三):短信接收引擎
这两天需要实现一个短信的报警平台,便看了一下相关的资料。
其实短信的收发还是比较简单的。
首先,我们需要有个短信模块,也就是短信猫。
其次,需要熟悉一下AT命令,这个到google了解一下即可。
接着,需要了解串口编程。串口的编程,其实也是比较简单的一块。
最后,往串口中写入相关的命令即可。
过程是简单的,但是在实践中还是遇到一些麻烦,主要是调试硬件和调试AT命令,如果没有一定的经验,可能会不知所措。本文着重于介绍其功能的实现和协议的解析
下面是部分代码的解析
首先是打开串口
2 {
3 WriteTimeout = 500,
4 ReadTimeout = 500,
5 RtsEnable = true,
6 DtrEnable = true
7 };
8 serialPort.Open();
其中波特率为115200,RtsEnable设置为true是为了能够在发送完命令后能够及时接收到返回值。每个AT命令发送后都应该会接收到相应的返回值。
刚开始的时候,没有设置这个属性,结果在读取接收缓冲区时,没能读取到任何信息。
然后就是发送一个设置发送格式的命令,确定发送的格式
2 /// 生成设置信息发送格式的发送命令串
3 /// </summary>
4 /// <param name="format"> The format. </param>
5 /// <returns></returns>
6 public static string GenSendFormatCmd(SendFormat format = SendFormat.Text)
7 {
8 return string.Format( " AT+CMGF={0}\r ", ( int)format);
9 }
发送完之后,返回也会存在OK字样,通过该返回串可以判断是否发送成功。
最后,也是最主要的部分,就是构建发送内容了。
根据平常我们使用的情况,手机的信息发送,包括两部分,目的地址和信息内容。
实际的发送时,手机信息会被编码为PDU格式(当然还有其他格式,暂时没有研究),整个报文发出。
PDU格式的报文可以简单解析如下:
报文头+编码的手机号码+信息内容编码方案+编码的信息内容
具体的格式可以上网查找,下面我用代码来说明如果对手机号码和信息内容进行编码
2 /// 生成短信息发送串
3 /// </summary>
4 /// <param name="phoneNo"> The phone no. </param>
5 /// <param name="message"> The message. </param>
6 /// <returns></returns>
7 public static string GenSendCmd( string phoneNo, string message)
8 {
9 if (phoneNo.StartsWith( " 86 ")) phoneNo = " + " + phoneNo;
10 else if (!phoneNo.StartsWith( " +86 ")) phoneNo = " +86 " + phoneNo;
11
12 phoneNo = EncodePhone(phoneNo);
13 message = EncodeMessage(message);
14
15 return string.Format( " AT+CMGS={0}\r{1}{2}{3} ", (phoneNo.Length + message.Length) / 2 - 1, phoneNo, message, ( char) 26);
16 }
2 /// 对电话号码进行编码
3 /// </summary>
4 /// <param name="phone"> The phone. </param>
5 /// <returns></returns>
6 private static string EncodePhone( string phone)
7 {
8 var result = new StringBuilder();
9
10 /* *构建协议头部* */
11 result.Append( " 00 "); // Length of SMSC information. Here the length is 0, which means that the SMSC stored in the phone should be used. Note: This octet is optional. On some phones this octet should be omitted! (Using the SMSC stored in phone is thus implicit)
12 result.Append( " 11 "); // PDU type (forst octet)文件头字节,一般为11或者01,10为乱码
13 result.Append( " 00 "); // 信息类型(TP-Message-Reference),一般为00
14
15 /* *构建被叫号码地址(目的地址(TP-Destination-Address)* */
16 var isInternational = phone.StartsWith( " + ");
17 if (isInternational) phone = phone.Remove( 0, 1); // 去除前面的+
18 var header = (phone.Length << 8) + 0x81 | (isInternational ? 0x10 : 0x20);
19 result.Append(Convert.ToString(header, 16).PadLeft( 4, ' 0 ')); // 被叫号码长度+被叫号码类型
20
21 if (phone.Length % 2 == 1) phone = phone + " F "; // 个数为奇数,则在后面补F凑成偶数
22 phone = SwapOddEven(phone);
23 result.Append(phone); // 互换了奇偶位的电话号码
24
25 /* *构建协议尾部* */
26 result.Append( " 00 "); // 协议标识TP-PID,这里一般为00
27 result.Append( " 08 "); // 数据编码方案TP-DCS(TP-Data-Coding-Scheme),采用前面说的USC2(16bit)数据编码
28 result.Append( " 00 "); // 有效期TP-VP(TP-Valid-Period)
29 // if (_validityPeriodFormat != ValidityPeriodFormat.FieldNotPresent)
30 // result.Append("00"); // 有效期TP-VP(TP-Valid-Period)
31 // result.Append("A7"); // ?
32
33 return result.ToString();
34 }
2 /// 对信息内容进行编码
3 /// </summary>
4 /// <param name="message"> The message. </param>
5 /// <returns></returns>
6 private static string EncodeMessage( string message)
7 {
8 var len = Encoding.BigEndianUnicode.GetByteCount(message);
9
10 var result = new StringBuilder();
11 // 信息内容长度,一个字节两个16进制表示
12 // result.Append(Convert.ToString(len, 16).PadLeft(2, '0'));
13 result.AppendFormat( " {0:X2} ", len);
14 // Unicode 两个字节,4个16进制数表示
15 // foreach (byte b in messageBytes)
16 // result.AppendFormat(Convert.ToString(b, 16).PadLeft(2, '0'));
17 foreach ( var m in message)
18 {
19 result.AppendFormat( " {0:X4} ", ( int)m);
20 }
21
22 return result.ToString();
23 }
2 /// 互换奇偶位
3 /// </summary>
4 /// <param name="source"> 原字符串,如1234567890 </param>
5 /// <returns> 返回互换了奇偶位的字符串,如:2143658709 </returns>
6 private static string SwapOddEven( string source)
7 {
8 var result = string.Empty;
9
10 for ( var i = 0; i < source.Length; i++)
11 result = result.Insert(i % 2 == 0 ? i : i - 1, source[i].ToString());
12
13 return result;
14 }
构建好发送串之后,就可以通过串口将内容发送出去。
这里有一点要注意的,发送完每个AT命令之后,一定要睡眠一段时间,否则是不能发送出去的。这个可能是连续多条命令会被当成一条命令使用吧,求大神解答。
2 serialPort.Write(buffer, 0, buffer.Length);
3 Thread.Sleep( 100);
4 len = serialPort.BytesToRead;
5 if (len > 0)
6 {
7 buffer = new byte[len];
8 serialPort.Read(buffer, 0, len);
9 Console.WriteLine(Encoding.ASCII.GetString(buffer));
10 }
发送后,返回串会是如下的内容(返回报文头+经过编码的发送内容)
比如我发送的内容为:
string.Format("hello,marvin,now time is {0}", DateTime.Now)
AT+CMGS=99
00110
> 00d91683115509050F00008005400680065006C006C006F002C006D0061007200760069006E002C006E006F0077002000740069006D006500200069007300200032003000310032002F0037002F0034002000310031003A00320033003A00340035
因此根据返回内容是否包含\r\n>来判断是否已成功发送出去
完整的发送函数如下:
2 {
3 var serialPort = new SerialPort( " COM6 ", 115200, Parity.None, 8, StopBits.One)
4 {
5 WriteTimeout = 500,
6 ReadTimeout = 500,
7 RtsEnable = true,
8 DtrEnable = true
9 };
10 serialPort.Open();
11
12 var buffer = Encoding.ASCII.GetBytes(SMSUtil.GenSendFormatCmd()); // 设置发送格式
13 serialPort.Write(buffer, 0, buffer.Length);
14 Thread.Sleep( 100);
15 var len = serialPort.BytesToRead;
16 if (len > 0)
17 {
18 buffer = new byte[len];
19 serialPort.Read(buffer, 0, len);
20 Console.WriteLine(Encoding.ASCII.GetString(buffer));
21 }
22
23 buffer = Encoding.ASCII.GetBytes(SMSUtil.GenSendCmd(phone, message));
24 serialPort.Write(buffer, 0, buffer.Length);
25 Thread.Sleep( 100);
26 len = serialPort.BytesToRead;
27 if (len > 0)
28 {
29 buffer = new byte[len];
30 serialPort.Read(buffer, 0, len);
31 Console.WriteLine(Encoding.ASCII.GetString(buffer));
32 }
33
34 serialPort.Close();
35 }