翔子 发表于 2011-11-22 23:47:15

手机接收的PDU串的分析(包含7-bit和UCS2解码,超长短信解释)

网络中这方面的资源还挺多的,特别是发短信的源码。利用AT(attention)命令接口控制SIM卡的活动,虽然我们也许不会去写嵌入式系统,但仍然建议基于串口/USB口通过Modem/手机编写AT高级语言编程的朋友们先找本较新版本的《AT Commands Interface》手册读读。对AT命令接口有个认识之后,再去开发你的解决方案。另外,通过串口连接Modem,还需要对串口的基础知识有一些了解,比如基本的常识:端口名称(COM1,COM2...)、波特率(正确的波特率才能与设备正常通信,在终端看到正确的字符编码,否则会是乱码)、校验位(这里通常为0)、数据位(这里通常为8)、停止位(这里通常为1)、读写超时(比如100ms)、串口缓冲区、握手协议(Handshake,通常不设定,由设备自己控制)、RTS是否启用(这里应该启用)、DTR是否启用等等。

.NET Fromework 2.0之后,我们有两种方法解决与串口的通信问题,第一种方式,利用.NET框架中提供的SerialPort类,它可以很好的通过事件的方式监听串口数据,对缓冲区控制也不错,但是要注意,接收事件是在另一个线程进行的,你可能需要维护发送与接收串口数据的同步。第二种方式,使用网络上流传较多的JustinIO类,很简单的类,实现了读写串口的API方法(Read和Write),比较简单易用,但是功能有限,也许需要二次开发。还要明白串口的收发数据的长度需要从缓冲区里读写,也没有读完了或者写完了的说法,只是有数据就提供,需要由自己编写代码来判断数据的长短,是否读取完整。

关于AT命令接口的问题,你需要了解,每条AT命令以CR结束(我也查到过CRLF结束的),命令的返回信息则是被CRLF首尾括起的字符串。什么时候返回什么样的数据,成功(如<CRLF>OK<CRLF>)或者失败(如<CRLF>ERROR<CRLF>但不仅限于它)的提示都需要有一些了解。对得到的回应数据进行分析取舍,来电时或者来短信时,会自动返回的数据。推荐编程之前使用PComm Terminal软件(在国内的大型软件下载站内能够搜索下载到),向Modem测试AT指令。

当我们收到短信时,可能会收到这样的数据:

+CMTI: "SM",1

+CMTI: "SM",2



然后通过AT+CMGR=1或者AT+CMGR=2读取SIM卡中的短信内容,如果设置了PDU格式,那么会得到PDU数据。如果在之前设定过AT+CNMI=2,1,0,0,0,那么短信息会跳过SIM卡的存储,直接显示出收到的数据。将短信模式设置为PDU模式时(at+cmgf=0),在PDU解码时需要注意编码方式,通常有汉字信息的采用UCS2编码(最多70字符),纯英文信息采用7-bit编码(有7位编码算法,可以将ASCII的数字、大小写字母转换成该编码,但是标点符号就无能为力了,最多160字符),图像和铃声采用8-bit编码(最多140字节)。在7位编码的时候建议用文本模式(at+cmgf=1)去读取短信,就不必进行7-bit解码,而且能够显示正确的标点。
下面是设备收到PDU信息时得到的PDU字串的解码分析,仅供参考:

using System;   
using System.Text;   
using System.Globalization;   
   
class Test   
{   
    static void Main()   
   {   
      string[] pdus =   
         {   
            "0891683108401105F0240D91685149910183F0000890013261604423026D4B",   
            "0891683108401105F0040D91683105706027F50008900162311142230A6D4B8BD5003100320033",   
            "0891683108401105F0040D91683105706027F500009001728033652304D4E2940A",   
            "0891683108401105F0040D91683105706027F50000900172907074230928D58612D9505429",   
         };   
      for (int i = 0; i < pdus.Length; i++)   
         {   
            string pdu = pdus;   
            string number, message;   
             DateTime timestamp;   
             ParseReceivedSms(pdu, out number, out timestamp, out message);   
             Console.WriteLine("TEST.NO.{0}", i + 1);   
             Console.WriteLine("number: {0}\ntimestamp: {1}\nmessage: {2}", number, timestamp, message);   
             Console.WriteLine("message length: " + message.Length + "\n");   
         }   
   
         Console.Write("Press any key to EXIT...");   
         Console.ReadKey(true);   
   }   
   
    public static void ParseReceivedSms(string pduEncoded, out string number, out DateTime timestamp, out string message)   
   {   
      //分析电话号码   
      char[] numberChars = new char;   
      for (int i = 26, j = 0; i < 26 + 12; i += 2, j += 2)   
         {   
             numberChars = pduEncoded;   
             numberChars = pduEncoded;   
         }   
      //numberChars='\0';   
         number = new string(numberChars, 0, 11);   
   
      //分析接收时间   
      string timestampString = string.Format("{0}{1}/{2}{3}/{4}{5} {6}{7}:{8}{9}:{10}{11}",   
                                              pduEncoded, pduEncoded,   
                                              pduEncoded, pduEncoded,   
                                              pduEncoded, pduEncoded,   
                                              pduEncoded, pduEncoded,   
                                              pduEncoded, pduEncoded,   
                                              pduEncoded, pduEncoded   
                                             );   
         timestamp = Convert.ToDateTime(timestampString);   
   
      //分析短信   
      //int msgCountByByte=(pduEncoded.Length-58)/2;   
      int msgCountByByte = Byte.Parse(pduEncoded.Substring(56, 2), NumberStyles.HexNumber);   
      if (msgCountByByte <= 0)   
         {   
             message = string.Empty;   
         }   
      else   
         {   
            byte[] bytes = new byte;   
            int msgType = Byte.Parse(pduEncoded.Substring(40, 2), NumberStyles.HexNumber); //取编码方式   
            switch (msgType)   
             {   
                case 0: //bit7   
                     message = Gsm7bitDecoding(pduEncoded.Substring(58));   
                  break;   
                case 8: //UCS2   
                  for (int i = 58, j = 0; j < msgCountByByte; i += 2, j++)   
                     {   
                         bytes = Byte.Parse(pduEncoded.Substring(i, 2), NumberStyles.HexNumber);   
                     }   
                     message = Encoding.BigEndianUnicode.GetString(bytes);   
                  break;   
                case 4: //bit8   
                default:   
                  for (int i = 58, j = 0; j < msgCountByByte; i += 2, j++)   
                     {   
                         bytes = Byte.Parse(pduEncoded.Substring(i, 2), NumberStyles.HexNumber);   
                     }   
                     message = Encoding.ASCII.GetString(bytes);   
                  break;   
             }   
         }   
   }   
   
    public static string Gsm7bitEncoding(string text)   
   {   
      byte[] textBytes = Encoding.ASCII.GetBytes(text);   
      //int gsm7bitEncodingLength = textBytes.Length * 7;   
      //gsm7bitEncodingLength = gsm7bitEncodingLength % 8 != 0 ? gsm7bitEncodingLength / 8 + 1 : gsm7bitEncodingLength / 8;   
      int textBytesLength = textBytes.Length;   
      int gsm7bitEncodingLength = (int)Math.Ceiling((textBytesLength * 7) / 8.0);   
      byte[] gsm7bitEncodingBytes = new byte;   
      int a, b, k;   
      for (a = 0, b = 0; b < textBytesLength; a++, b++)   
         {   
             k = b % 8;   
            if (k == 7)   
             {   
               a--;   
                if (b < (textBytesLength - 1))   
               {   
                     gsm7bitEncodingBytes |= (byte)((textBytes & 0x7f) << 1);   
               }   
             }   
            else   
             {   
                if (b < (textBytesLength - 1))   
               {   
                     gsm7bitEncodingBytes = (byte)(((textBytes & 0x7f) >> k) | ((textBytes & 0x7f) << (7 - k)));   
               }   
                else   
               {   
                     gsm7bitEncodingBytes = (byte)(((textBytes & 0x7f) >> k));   
               }   
             }   
         }   
      return BytesToHexString(gsm7bitEncodingBytes);   
   }   
   
    public static string Gsm7bitDecoding(string textEncoded)   
   {   
      byte[] src = HexStringToBytes(textEncoded);   
      if (src.Length == 0)   
         {   
            return string.Empty;   
         }   
      int srcLength = src.Length;   
      int dstLength = srcLength * 8 / 7;   
      byte[] dst = new byte;   
      int a, b, k;   
      for (a = 0, b = 0; b < srcLength ; a++, b++)   
         {   
             k = a % 8;   
            if (a > 0)   
             {   
               dst = (byte)(((src << k) & 0x7f) | (src >> 8 - k));   
             }   
            else   
             {   
               dst = (byte)(src & 0x7f);   
             }   
            if (k == 7 && a > 0)   
             {   
               dst[++a] = (byte)(src & 0x7f);   
             }   
         }   
      return Encoding.ASCII.GetString(dst);   
   }   
   
    static string BytesToHexString(byte[] data)   
   {   
         StringBuilder sb = new StringBuilder();   
      for (int i = 0; i < data.Length; i++)   
         {   
             sb.Append(data.ToString("X2"));   
         }   
      return sb.ToString();   
   }   
   
    static byte[] HexStringToBytes(string hexString)   
   {   
      int hexStringLength = hexString.Length;   
      if (hexStringLength < 2 || hexStringLength % 2 != 0)   
         {   
            return new byte;   
         }   
      byte[] data = new byte;   
      for (int i = 0, j = 0; i < hexStringLength; i += 2, j++)   
         {   
             data = Byte.Parse(hexString.Substring(i, 2), NumberStyles.HexNumber);   
         }   
      return data;   
   }   
}

直接用CSC编译,编码的分析看代码最后的注释部分,测试结果如下:


TEST.NO.1
number: 15941910380
timestamp: 2009-10-23 16:06:44
message: 测
message length: 1

TEST.NO.2
number: 13500706725
timestamp: 2009-10-26 13:11:24
message: 测试123
message length: 5

TEST.NO.3
number: 13500706725
timestamp: 2009-10-27 8:33:56
message: TEST
message length: 4

TEST.NO.4
number: 13500706725
timestamp: 2009-10-27 9:07:47
message: (*   *)
message length: 9

Press any key to EXIT...
为了便于对齐,接收PDU串分析注释的等宽字体如下(您可能需要Firefox浏览器查看):

+CMGL: 1,1,,22
0891683108401105F0240D91685149910183F0000890013261604423026D4B
+CMGL: 3,1,,30
0891683108401105F0040D91683105706027F50008900162311142230A6D4B8BD5003100320033
+CMGL: 6,1,,24
0891683108401105F0040D91683105706027F500009001728033652304D4E2940A
+CMGL: 7,1,,28
0891683108401105F0040D91683105706027F50000900172907074230928D58612D9505429

OK

手机接收的PDU串分析
0891683108401105F0240D91685149910183F0000890013261604423026D4B
08 +8613800411500F0D +8615941910380F    091023160644326D4B
91            2491            00                02
                                        08
08   - 短信中心号码/地址长度(指后面有8位字节)
91   - 国际格式号码(在前面加"+")
+8613800411500F - 短信中心号码/地址:13800411500
24   - SMS_DELIVER的第一个8位: 参考http://www.dreamfabric.com/sms/submit_fo.html
0D   - 发送方号码/地址长度(指后面的号码是13位,与前面的短信中心号码表示方法不同)
91   - 国际格式号码(在前面加"+")
+8615941910380F - 发送方号码/地址:15941910380
00   - 协议标识TP-PID:普通GSM,点到点方式
08   - 编码方式TP-DCS:三种:00表示7-bit编码(英文)、04表示8-bit编码(图片和铃声)、08表示UCS2编码(汉字)(注意:见17楼回复)
09102316064423- 时间戳TP-SCTS:09/10/23 16:06:44 32(+32时区)
02   - 短信内容字节长度(此处是16进制数,若是7-bit表示解码后的字节长度)
6D4B    - 短信内容(0x6D4B表示汉字“测”)


--------------------------------------------------------------------------------

2009年11月3日补充:关于收到超长短信的问题
上面代码中并没有处理超长短信,但是处理方法很简单,请阅读下面的内容。
一条短信的长度是有限的,不能超过140个Octs,因此,当我们发送超长短信的时候,需要被分割成若干条。那么,在接收PDU串的解码中,会占用掉前6个Octs用于记录超长短信的报文标志——所以你会发现,即使用手机发送超长短信的时候,分割后的汉字字符是按70-3=67个来计算的,即每67个汉字会被分成一条消息(不必奇怪,手机提示比较人性化,第1页提示最多输入70个汉字,第2页提示最多输入64个汉字,以后每页提示最多输入67个汉字)。总之,最终要保证一条短信长度不超过140个Octs。

这6个Octs的超长短信报文标志会放在短信内容的最前6个Octs当中,也就是前面“手机接收的PDU串分析”中紧随“短信内容字节长度”之后。这6位代表的意义如下:(参见这里的1.4超长短信)

假设短信内容前6位是:
0500037E0201
050302
007E01

05 - 协议长度(后面占5位)
00 - 表示拆分短信
03 - 拆分数据的长度(后面的3位)
7E - 唯一标识(用于把多条短信合并)
02 - 共被拆分2条短信
01 - 序号,这是其中的第1条短信

那么,第2条短信的头6位数据就应该是:
0500037E0202

2010年3月8日补充:关于收到超长短信的问题2
感谢565730166的讨论!

超长短信(Long SMS)官方叫法是Concatenated SMS(CSMS),大家可以到维基百科中查阅资料。上面关于超长短信的描述并不完整,实际上,针对存储地址类型不同,它可以分为两种类型:8位表示法和16位表示法。8位表示法,就是前面描述的办法。此次补充16位表示法。为了把CSMS表达清楚,下面引用了国外一篇CSMS解释的文章中的图片。

简单描述一下:SMS短信的内容部分共140个字节长,CSMS把这140个字节分成两个部分,第一部分叫做UDH(User Data Header,用户数据头),第二部分是短信内容的信息部分。现在关注的就是UDH怎么解释。UDH又分成三块(按图来的,似乎不太合理,大家明白了就好),第一块(UDHL),一个字节,表示其后UDH占用的字节长度;第二块,三个字节或四个字节,它们分别叫作IEI(Information Elements Identifier,信息元素标识位)、IEDL(Information Elements Data Length,信息元素数据长度)和IED(Information Elements Data,信息元素数据)的引用部分;第三块,IED的剩余部分,信息分隔数和当前信息编号组成。

IEI有两种标识,00和08,00是单字节(8位)信息地址引用号码表示方式,08是双字节(16信)信息地址引用号码表示方式。前面超长信息的解释,只解释的IEI为00的情况。所以,当采用08标识时,整个UDH部分会多出一个字节,用来表示16位的引用号码。

假设短信内容UDH部分(此例为前7位)是:
060804F42E0201
0604    02
08F42E01

06 - UDHL
08 - IEI 表示用16位表示引用方式
04 - IEDL
F42E - 唯一标识(用于把多条短信合并)
02 - 共被拆分2条短信
01 - 序号,这是其中的第1条短信

那么,第2条短信的UDH应该是:
060804F42E0202


--------------------------------------------------------------------------------

2009年12月14日补充:关于SMS字母表的问题
SMS字母表与ASCII的字母表不同,因此,使用前面算法转换符号编码时并不会得到原始的输入符号。见讨论7和讨论34、36。请参考:官方的GSM 03.38转换Unicode映射表格

页: [1]
查看完整版本: 手机接收的PDU串的分析(包含7-bit和UCS2解码,超长短信解释)