通过串口收发短消息(下)

GSM PDU 编码与 AT 命令
本文介绍 GSM PDU 的核心编码方式,并给出 7-bit、8-bit 和 UCS2 编码的实现代码。此外,还详细讲解了如何使用 AT 命令发送、接收和管理短消息。
作者:bhw98

下载本文示例源代码

原文出处:http://www.kernelstudio.com/getitem.asp?id=14  

Q   PDU的核心编码方式已经清楚了,如何实现用AT命令收发短消息呢?  

A   在上篇中,我们已经讨论了7bit,   8bit和UCS2这几种PDU用户信息的编码方式,并且给出了实现代码。现在,重点描述PDU全串的编码和解码过程,以及GSM   07.05的AT命令实现方法。这些是底层的核心代码,为了保证代码的可移植性,我们尽可能不用MFC的类,必要时用ANSI   C标准库函数。  

首先,定义如下常量和结构:  

//   用户信息编码方式
#define   GSM_7BIT                 0
#define   GSM_8BIT                 4
#define   GSM_UCS2                 8
   
//   短消息参数结构,编码/解码共用
//   其中,字符串以 ' '/0 ' '结尾
typedef   struct   {
        char   SCA[16];               //   短消息服务中心号码(SMSC地址)
        char   TPA[16];               //   目标号码或回复号码(TP-DA或TP-RA)
        char   TP_PID;                 //   用户信息协议标识(TP-PID)
        char   TP_DCS;                 //   用户信息编码方式(TP-DCS)
        char   TP_SCTS[16];       //   服务时间戳字符串(TP_SCTS),   接收时用到
        char   TP_UD[161];         //   原始用户信息(编码前或解码后的TP-UD)
        char   index;                   //   短消息序号,在读取时用到
}   SM_PARAM;


大家已经注意到PDU串中的号码和时间,都是两两颠倒的字符串。利用下面两个函数可进行正反变换:  
//   正常顺序的字符串转换为两两颠倒的字符串,若长度为奇数,补 ' 'F ' '凑成偶数
//   如: "8613851872468 "   -- >   "683158812764F8 "
//   pSrc:   源字符串指针
//   pDst:   目标字符串指针
//   nSrcLength:   源字符串长度
//   返回:   目标字符串长度
int   gsmInvertNumbers(const   char*   pSrc,   char*   pDst,   int   nSrcLength)
{
        int   nDstLength;       //   目标字符串长度
        char   ch;                     //   用于保存一个字符
   
        //   复制串长度
        nDstLength   =   nSrcLength;
   
        //   两两颠倒
        for   (int   i   =   0;   i   <   nSrcLength;   i   +=   2)
        {
                ch   =   *pSrc++;                 //   保存先出现的字符
                *pDst++   =   *pSrc++;       //   复制后出现的字符
                *pDst++   =   ch;                 //   复制先出现的字符
        }
   
        //   源串长度是奇数吗?
        if   (nSrcLength   &   1)
        {
                *(pDst-2)   =   &apos; &apos;F &apos; &apos;;           //   补 &apos; &apos;F &apos; &apos;
                nDstLength++;                 //   目标串长度加1
        }
   
        //   输出字符串加个结束符
        *pDst   =   &apos; &apos;/0 &apos; &apos;;
   
        //   返回目标字符串长度
        return   nDstLength;
}
   
//   两两颠倒的字符串转换为正常顺序的字符串
//   如: "683158812764F8 "   -- >   "8613851872468 "
//   pSrc:   源字符串指针
//   pDst:   目标字符串指针
//   nSrcLength:   源字符串长度
//   返回:   目标字符串长度
int   gsmSerializeNumbers(const   char*   pSrc,   char*   pDst,   int   nSrcLength)
{
        int   nDstLength;       //   目标字符串长度
        char   ch;                     //   用于保存一个字符
   
        //   复制串长度
        nDstLength   =   nSrcLength;
   
        //   两两颠倒
        for   (int   i   =   0;   i   <   nSrcLength;   i   +=   2)
        {
                ch   =   *pSrc++;                 //   保存先出现的字符
                *pDst++   =   *pSrc++;       //   复制后出现的字符
                *pDst++   =   ch;                 //   复制先出现的字符
        }
   
        //   最后的字符是 &apos; &apos;F &apos; &apos;吗?
        if   (*(pDst-1)   ==   &apos; &apos;F &apos; &apos;)
        {
                pDst--;
                nDstLength--;                 //   目标字符串长度减1
        }
   
        //   输出字符串加个结束符
        *pDst   =   &apos; &apos;/0 &apos; &apos;;
   
        //   返回目标字符串长度
        return   nDstLength;
}

以下是PDU全串的编解码模块。为简化编程,有些字段用了固定值。  
//   PDU编码,用于编制、发送短消息
//   pSrc:   源PDU参数指针
//   pDst:   目标PDU串指针
//   返回:   目标PDU串长度
int   gsmEncodePdu(const   SM_PARAM*   pSrc,   char*   pDst)
{
        int   nLength;                           //   内部用的串长度
        int   nDstLength;                     //   目标PDU串长度
        unsigned   char   buf[256];     //   内部用的缓冲区
   
        //   SMSC地址信息段
        nLength   =   strlen(pSrc- >SCA);         //   SMSC地址字符串的长度
        buf[0]   =   (char)((nLength   &   1)   ==   0   ?   nLength   :   nLength   +   1)   /   2   +   1;         //   SMSC地址信息长度
        buf[1]   =   0x91;                 //   固定:   用国际格式号码
        nDstLength   =   gsmBytes2String(buf,   pDst,   2);                 //   转换2个字节到目标PDU串
        nDstLength   +=   gsmInvertNumbers(pSrc- >SCA,   &pDst[nDstLength],   nLength);         //   转换SMSC到目标PDU串
   
        //   TPDU段基本参数、目标地址等
        nLength   =   strlen(pSrc- >TPA);         //   TP-DA地址字符串的长度
        buf[0]   =   0x11;                         //   是发送短信(TP-MTI=01),TP-VP用相对格式(TP-VPF=10)
        buf[1]   =   0;                               //   TP-MR=0
        buf[2]   =   (char)nLength;       //   目标地址数字个数(TP-DA地址字符串真实长度)
        buf[3]   =   0x91;                         //   固定:   用国际格式号码
        nDstLength   +=   gsmBytes2String(buf,   &pDst[nDstLength],   4);     //   转换4个字节到目标PDU串
        nDstLength   +=   gsmInvertNumbers(pSrc- >TPA,   &pDst[nDstLength],   nLength);   //   转换TP-DA到目标PDU串
   
        //   TPDU段协议标识、编码方式、用户信息等
        nLength   =   strlen(pSrc- >TP_UD);         //   用户信息字符串的长度
        buf[0]   =   pSrc- >TP_PID;                 //   协议标识(TP-PID)
        buf[1]   =   pSrc- >TP_DCS;                 //   用户信息编码方式(TP-DCS)
        buf[2]   =   0;                         //   有效期(TP-VP)为5分钟
        if   (pSrc- >TP_DCS   ==   GSM_7BIT)
        {
                //   7-bit编码方式
                buf[3]   =   nLength;                         //   编码前长度
                nLength   =   gsmEncode7bit(pSrc- >TP_UD,   &buf[4],   nLength+1)   +   4;         //   转换TP-DA到目标PDU串
        }
        else   if   (pSrc- >TP_DCS   ==   GSM_UCS2)
        {
                //   UCS2编码方式
                buf[3]   =   gsmEncodeUcs2(pSrc- >TP_UD,   &buf[4],   nLength);         //   转换TP-DA到目标PDU串
                nLength   =   buf[3]   +   4;                 //   nLength等于该段数据长度
        }
        else
        {
                //   8-bit编码方式
                buf[3]   =   gsmEncode8bit(pSrc- >TP_UD,   &buf[4],   nLength);         //   转换TP-DA到目标PDU串
                nLength   =   buf[3]   +   4;                 //   nLength等于该段数据长度
        }
        nDstLength   +=   gsmBytes2String(buf,   &pDst[nDstLength],   nLength);                 //   转换该段数据到目标PDU串
   
        //   返回目标字符串长度
        return   nDstLength;
}
   
//   PDU解码,用于接收、阅读短消息
//   pSrc:   源PDU串指针
//   pDst:   目标PDU参数指针
//   返回:   用户信息串长度
int   gsmDecodePdu(const   char*   pSrc,   SM_PARAM*   pDst)
{
        int   nDstLength;                     //   目标PDU串长度
        unsigned   char   tmp;               //   内部用的临时字节变量
        unsigned   char   buf[256];     //   内部用的缓冲区
   
        //   SMSC地址信息段
        gsmString2Bytes(pSrc,   &tmp,   2);         //   取长度
        tmp   =   (tmp   -   1)   *   2;         //   SMSC号码串长度
        pSrc   +=   4;                             //   指针后移
        gsmSerializeNumbers(pSrc,   pDst- >SCA,   tmp);         //   转换SMSC号码到目标PDU串
        pSrc   +=   tmp;                 //   指针后移
   
        //   TPDU段基本参数、回复地址等
        gsmString2Bytes(pSrc,   &tmp,   2);         //   取基本参数
        pSrc   +=   2;                 //   指针后移
        if   (tmp   &   0x80)
        {
                //   包含回复地址,取回复地址信息
                gsmString2Bytes(pSrc,   &tmp,   2);         //   取长度
                if   (tmp   &   1)   tmp   +=   1;         //   调整奇偶性
                pSrc   +=   4;                     //   指针后移
                gsmSerializeNumbers(pSrc,   pDst- >TPA,   tmp);         //   取TP-RA号码
                pSrc   +=   tmp;                 //   指针后移
        }
   
        //   TPDU段协议标识、编码方式、用户信息等
        gsmString2Bytes(pSrc,   (unsigned   char*)&pDst- >TP_PID,   2);         //   取协议标识(TP-PID)
        pSrc   +=   2;                 //   指针后移
        gsmString2Bytes(pSrc,   (unsigned   char*)&pDst- >TP_DCS,   2);         //   取编码方式(TP-DCS)
        pSrc   +=   2;                 //   指针后移
        gsmSerializeNumbers(pSrc,   pDst- >TP_SCTS,   14);                 //   服务时间戳字符串(TP_SCTS)
        pSrc   +=   14;               //   指针后移
        gsmString2Bytes(pSrc,   &tmp,   2);         //   用户信息长度(TP-UDL)
        pSrc   +=   2;                 //   指针后移
        if   (pDst- >TP_DCS   ==   GSM_7BIT)
        {
                //   7-bit解码
                nDstLength   =   gsmString2Bytes(pSrc,   buf,   tmp   &   7   ?   (int)tmp   *   7   /   4   +   2   :   (int)tmp   *   7   /   4);     //   格式转换
                gsmDecode7bit(buf,   pDst- >TP_UD,   nDstLength);         //   转换到TP-DU
                nDstLength   =   tmp;
        }
        else   if   (pDst- >TP_DCS   ==   GSM_UCS2)
        {
                //   UCS2解码
                nDstLength   =   gsmString2Bytes(pSrc,   buf,   tmp   *   2);                 //   格式转换
                nDstLength   =   gsmDecodeUcs2(buf,   pDst- >TP_UD,   nDstLength);         //   转换到TP-DU
        }
        else
        {
                //   8-bit解码
                nDstLength   =   gsmString2Bytes(pSrc,   buf,   tmp   *   2);                 //   格式转换
                nDstLength   =   gsmDecode8bit(buf,   pDst- >TP_UD,   nDstLength);         //   转换到TP-DU
        }
   
        //   返回目标字符串长度
        return   nDstLength;

 依照GSM   07.05,发送短消息用AT+CMGS命令,阅读短消息用AT+CMGR命令,列出短消息用AT+CMGL命令,删除短消息用AT+CMGD命令。但AT+CMGL命令能够读出所有的短消息,所以我们用它实现阅读短消息功能,而没用AT+CMGR。下面是发送、读取和删除短消息的实现代码:  
//   发送短消息
//   pSrc:   源PDU参数指针
BOOL   gsmSendMessage(const   SM_PARAM*   pSrc)
{
        int   nPduLength;                 //   PDU串长度
        unsigned   char   nSmscLength;         //   SMSC串长度
        int   nLength;                       //   串口收到的数据长度
        char   cmd[16];                     //   命令串
        char   pdu[512];                   //   PDU串
        char   ans[128];                   //   应答串
   
        nPduLength   =   gsmEncodePdu(pSrc,   pdu);         //   根据PDU参数,编码PDU串
        strcat(pdu,   "/x01a ");                 //   以Ctrl-Z结束
   
        gsmString2Bytes(pdu,   &nSmscLength,   2);         //   取PDU串中的SMSC信息长度
        nSmscLength++;                 //   加上长度字节本身
   
        //   命令中的长度,不包括SMSC信息长度,以数据字节计
        sprintf(cmd,   "AT+CMGS=%d/r ",   nPduLength   /   2   -   nSmscLength);         //   生成命令
   
        WriteComm(cmd,   strlen(cmd));         //   先输出命令串
   
        nLength   =   ReadComm(ans,   128);       //   读应答数据
   
        //   根据能否找到 "/r/n >   "决定成功与否
        if   (nLength   ==   4   &&   strncmp(ans,   "/r/n >   ",   4)   ==   0)
        {
                WriteComm(pdu,   strlen(pdu));                 //   得到肯定回答,继续输出PDU串
   
                nLength   =   ReadComm(ans,   128);               //   读应答数据
   
                //   根据能否找到 "+CMS   ERROR "决定成功与否
                if   (nLength   >   0   &&   strncmp(ans,   "+CMS   ERROR ",   10)   !=   0)
                {
                        return   TRUE;
                }
        }
   
        return   FALSE;
}
   
//   读取短消息
//   用+CMGL代替+CMGR,可一次性读出全部短消息
//   pMsg:   短消息缓冲区,必须足够大
//   返回:   短消息条数
int   gsmReadMessage(SM_PARAM*   pMsg)
{
        int   nLength;                 //   串口收到的数据长度
        int   nMsg;                       //   短消息计数值
        char*   ptr;                     //   内部用的数据指针
        char   cmd[16];               //   命令串
        char   ans[1024];           //   应答串
   
        nMsg   =   0;
        ptr   =   ans;
   
        sprintf(cmd,   "AT+CMGL/r ");         //   生成命令
   
        WriteComm(cmd,   strlen(cmd));         //   输出命令串

        nLength   =   ReadComm(ans,   1024);         //   读应答数据

        //   根据能否找到 "+CMS   ERROR "决定成功与否
        if   (nLength   >   0   &&   strncmp(ans,   "+CMS   ERROR ",   10)   !=   0)
        {
                //   循环读取每一条短消息,   以 "+CMGL: "开头
                while   ((ptr   =   strstr(ptr,   "+CMGL: "))   !=   NULL)
                {
                        ptr   +=   6;                 //   跳过 "+CMGL: "
                        sscanf(ptr,   "%d ",   &pMsg- >index);         //   读取序号
   
                        ptr   =   strstr(ptr,   "/r/n ");         //   找下一行
                        ptr   +=   2;                 //   跳过 "/r/n "
   
                        gsmDecodePdu(ptr,   pMsg);         //   PDU串解码

                        pMsg++;                 //   准备读下一条短消息
                        nMsg++;                 //   短消息计数加1
                }
        }
   
        return   nMsg;
}
   
//   删除短消息
//   index:   短消息序号,从1开始
BOOL   gsmDeleteMessage(int   index)
{
        int   nLength;                     //   串口收到的数据长度
        char   cmd[16];                   //   命令串
        char   ans[128];                 //   应答串
   
        sprintf(cmd,   "AT+CMGD=%d/r ",   index);         //   生成命令
   
        //   输出命令串
        WriteComm(cmd,   strlen(cmd));
   
        //   读应答数据
        nLength   =   ReadComm(ans,   128);
   
        //   根据能否找到 "+CMS   ERROR "决定成功与否
        if   (nLength   >   0   &&   strncmp(ans,   "+CMS   ERROR ",   10)   !=   0)
        {
                return   TRUE;
        }
   
        return   FALSE;
}


以上发送AT命令过程中用到了WriteComm和ReadComm函数,它们是用来读写串口的,依赖于具体的操作系统。在Windows环境下,除了用MSComm控件,以及某些现成的串口通信类之外,也可以简单地调用一些Windows   API用实现。以下是利用API实现的主要代码,注意我们用的是超时控制的同步(阻塞)模式。  
//   串口设备句柄
HANDLE   hComm;
   
//   打开串口
//   pPort:   串口名称或设备路径,可用 "COM1 "或 "//./COM1 "两种方式,建议用后者
//   nBaudRate:   波特率
//   nParity:   奇偶校验
//   nByteSize:   数据字节宽度
//   nStopBits:   停止位
BOOL   OpenComm(const   char*   pPort,   int   nBaudRate,   int   nParity,   int   nByteSize,   int   nStopBits)
{
        DCB   dcb;                 //   串口控制块
        COMMTIMEOUTS   timeouts   =   {         //   串口超时控制参数
                100,                 //   读字符间隔超时时间:   100   ms
                1,                     //   读操作时每字符的时间:   1   ms   (n个字符总共为n   ms)
                500,                 //   基本的(额外的)读超时时间:   500   ms
                1,                     //   写操作时每字符的时间:   1   ms   (n个字符总共为n   ms)
                100};               //   基本的(额外的)写超时时间:   100   ms
   
        hComm   =   CreateFile(pPort,         //   串口名称或设备路径
                        GENERIC_READ   ¦   GENERIC_WRITE,         //   读写方式
                        0,                               //   共享方式:独占
                        NULL,                         //   默认的安全描述符
                        OPEN_EXISTING,       //   创建方式
                        0,                               //   不需设置文件属性
                        NULL);                       //   不需参照模板文件
   
        if   (hComm   ==   INVALID_HANDLE_VALUE)   return   FALSE;                 //   打开串口失败
   
        GetCommState(hComm,   &dcb);                 //   取DCB
   
        dcb.BaudRate   =   nBaudRate;
        dcb.ByteSize   =   nByteSize;
        dcb.Parity   =   nParity;
        dcb.StopBits   =   nStopBits;
   
        SetCommState(hComm,   &dcb);                 //   设置DCB
   
        SetupComm(hComm,   4096,   1024);           //   设置输入输出缓冲区大小
   
        SetCommTimeouts(hComm,   &timeouts);         //   设置超时
   
        return   TRUE;
}
   
//   关闭串口
BOOL   CloseComm()
{
        return   CloseHandle(hComm);
}
   
//   写串口
//   pData:   待写的数据缓冲区指针
//   nLength:   待写的数据长度
void   WriteComm(void*   pData,   int   nLength)
{
        DWORD   dwNumWrite;         //   串口发出的数据长度
   
        WriteFile(hComm,   pData,   (DWORD)nLength,   &dwNumWrite,   NULL);
}
   
//   读串口
//   pData:   待读的数据缓冲区指针
//   nLength:   待读的最大数据长度
//   返回:   实际读入的数据长度
int   ReadComm(void*   pData,   int   nLength)
{
        DWORD   dwNumRead;         //   串口收到的数据长度
   
        ReadFile(hComm,   pData,   (DWORD)nLength,   &dwNumRead,   NULL);
   
        return   (int)dwNumRead;
}


Q   在用AT命令同手机通信时,需要注意哪些问题?  

A   任何一个AT命令发给手机,都可能返回成功或失败。例如,用AT+CMGS命令发送短消息时,如果此时正好手机处于振铃或通话状态,就会返回一个 "+CMS   ERROR "。所以,应当在发送命令后,检测手机的响应,失败后重发。而且,因为只有一个通信端口,发送和接收不可能同时进行。  

如果串口通信用超时控制的同步(阻塞)模式,一般做法是专门将发送/接收处理封装在一个工作子线程内。因为代码较多,这里就不详细介绍了。所附的Demo中,包含了完整的子线程和发送/接收应用程序界面的源码。  

Q   以上AT命令,是不是所有厂家的手机都支持?  

A   ETSI   GSM   07.05规范直到1998年才形成最终Release版本(Ver   7.0.1),在这之前及之后一段时间内,不排除各厂商在DTE-DCE的短消息AT命令有所不同的可能性。我们用到的几个PDU模式下的AT命令,是基本的命令,从原则上讲,各厂家的手机以及GSM模块应该都支持,但可能有细微差别。  

Q   用户信息(TP-UD)内除了一般意义上的短消息,还可以是图片和声音数据。关于手机铃声和图片格式方面,有什么规范吗?  

A   为统一手机铃声、图片格式,Motorola和Ericsson,   Siemens,   Alcatel等共同开发了EMS(Enhanced   Messaging   Service)标准,并于2002年2月份公布。这些厂商格式相同。但另一手机巨头Nokia未参加标准的制定,手机铃声、图片格式与它们不同。所以没有形成统一的规范。EMS其实并没有超越GSM   07.05,只是TP-UD数据部分包含一定格式而已。各厂家的手机铃声、图片格式资料,可以查阅相关网站。  

Q   用户信息(TP-UD)其实可以是任何的自定义数据,是吗?  

A   是的,尽管手机上会显示乱码。这种情况下,编码方式已经没有任何意义。但注意仍然要遵守规范。比如,若指定7-bit编码方式,TP-UDL应等于实际数据长度的8/7(用进一法,而不是四舍五入)。在利用SMS进行点对点或多点对一点的数据通信的应用中,可以传输各种自定义数据,如GPS信息,环境监测信息,加密的个人信息,等等。  

如果在传输自定义数据的同时还要收发普通短消息,最简单的办法是在数据前面额外加个识别标志,比如 "FFFF ",以区分自定义数据和普通短消息。  

相关资源:
ETSI官方网站:http://www.etsi.org  
3GPP官方网站:http://www.3gpp.org
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值