短信開發系列目錄:
短信開發系列(一):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 }