網上看到的一篇關於串口收發短信的文章(收藏一下 vc版)

用串口連接GSM手機發送和接收短消息,在應用程序中如何編程實現?

Q 我們打算開發一個基於GSM短消息方式的GPS系統,如何利用SMS進行數據通信?

A 首先,我們要對由ESTI制訂的SMS規範有所瞭解。與我們討論的短消息收發有關的規範主要包括GSM 03.38、GSM 03.40和GSM 07.05。前二者着重描述SMS的技術實現(含編碼方式),後者則規定了SMS的DTE-DCE接口標準(AT命令集)。
一共有三種方式來發送和接收SMS信息:Block Mode, Text Mode和PDU Mode。Block Mode已是昔日黃花,目前很少用了。Text Mode是純文本方式,可使用不同的字符集,從技術上說也可用於發送中文短消息,但國內手機基本上不支持,主要用於歐美地區。PDU Mode被所有手機支持,可以使用任何字符集,這也是手機默認的編碼方式。Text Mode比較簡單,而且不適合做自定義數據傳輸,我們就不討論了。下面介紹的內容,是在PDU Mode下發送和接收短消息的實現方法。
PDU串表面上是一串ASCII碼,由‘0’-‘9’、 ‘A’-‘F’這些數字和字母組成。它們是8位字節的十六進制數,或者BCD碼十進制數。PDU串不僅包含可顯示的消息本身,還包含很多其它信息,如SMS服務中心號碼、目標號碼、回覆號碼、編碼方式和服務時間等。發送和接收的PDU串,結構是不完全相同的。我們先用兩個實際的例子說明PDU串的結構和編排方式。

例1 發送:SMSC號碼是+8613800250500,對方號碼是13851872468,消息內容是“Hello!”。從手機發出的PDU串可以是
08 91 68 31 08 20 05 05 F0 11 00 0D 91 68 31 58 81 27 64 F8 00 00 00 06 C8 32 9B FD 0E 01
對照規範,具體分析:


分段 含義 說明
08 SMSC地址信息的長度 共8個八位字節(包括91)
91 SMSC地址格式(TON/NPI) 用國際格式號碼(在前面加‘+’)
68 31 08 20 05 05 F0 SMSC地址 8613800250500,補‘F’湊成偶數個
11 基本參數(TP-MTI/VFP) 發送,TP-VP用相對格式
00 消息基準值(TP-MR) 0
0D 目標地址數字個數 共13個十進制數(不包括91和‘F’)
91 目標地址格式(TON/NPI) 用國際格式號碼(在前面加‘+’)
68 31 58 81 27 64 F8 目標地址(TP-DA) 8613851872468,補‘F’湊成偶數個
00 協議標識(TP-PID) 是普通GSM類型,點到點方式
00 用戶信息編碼方式(TP-DCS) 7-bit編碼
00 有效期(TP-VP) 5分鐘
06 用戶信息長度(TP-UDL) 實際長度6個字節
C8 32 9B FD 0E 01 用戶信息(TP-UD) “Hello!”

例2 接收:SMSC號碼是+8613800250500,對方號碼是13851872468,消息內容是“你好!”。手機接收到的PDU串可以是
08 91 68 31 08 20 05 05 F0 84 0D 91 68 31 58 81 27 64 F8 00 08 30 30 21 80 63 54 80 06 4F 60 59 7D 00 21
對照規範,具體分析:

分段 含義 說明
08 地址信息的長度 個八位字節(包括91)
91 SMSC地址格式(TON/NPI) 用國際格式號碼(在前面加‘+’)
68 31 08 20 05 05 F0 SMSC地址 8613800250500,補‘F’湊成偶數個
84 基本參數(TP-MTI/MMS/RP) 接收,無更多消息,有回覆地址
0D 回覆地址數字個數 共13個十進制數(不包括91和‘F’)
91 回覆地址格式(TON/NPI) 用國際格式號碼(在前面加‘+’)
68 31 58 81 27 64 F8 回覆地址(TP-RA) 8613851872468,補‘F’湊成偶數個
00 協議標識(TP-PID) 是普通GSM類型,點到點方式
08 用戶信息編碼方式(TP-DCS) UCS2編碼
30 30 21 80 63 54 80 時間戳(TP-SCTS) 2003-3-12 08:36:45  +8時區
06 用戶信息長度(TP-UDL) 實際長度6鱟紙?
4F 60 59 7D 00 21 用戶信息(TP-UD) “你好!”

若基本參數的最高位(TP-RP)爲0,則沒有回覆地址的三個段。從Internet上發出的短消息常常是這種情形。
注意號碼和時間的表示方法,不是按正常順序順着來的,而且要以‘F’將奇數補成偶數。


Q 上面兩例中已經出現了7-bit和UCS2編碼,請詳細介紹一下這些編碼方式?

A 在PDU Mode中,可以採用三種編碼方式來對發送的內容進行編碼,它們是7-bit、8-bit和UCS2編碼。7-bit編碼用於發送普通的ASCII字符,它將一串7-bit的字符(最高位爲0)編碼成8-bit的數據,每8個字符可“壓縮”成7個;8-bit編碼通常用於發送數據消息,比如圖片和鈴聲等;而UCS2編碼用於發送Unicode字符。PDU串的用戶信息(TP-UD)段最大容量是140字節,所以在這三種編碼方式下,可以發送的短消息的最大字符數分別是160、140和70。這裏,將一個英文字母、一個漢字和一個數據字節都視爲一個字符。
需要注意的是,PDU串的用戶信息長度(TP-UDL),在各種編碼方式下意義有所不同。7-bit編碼時,指原始短消息的字符個數,而不是編碼後的字節數。8-bit編碼時,就是字節數。UCS2編碼時,也是字節數,等於原始短消息的字符數的兩倍。如果用戶信息(TP-UD)中存在一個頭(基本參數的TP-UDHI爲1),在所有編碼方式下,用戶信息長度(TP-UDL)都等於頭長度與編碼後字節數之和。如果採用GSM 03.42所建議的壓縮算法(TP-DCS的高3位爲001),則該長度也是壓縮編碼後字節數或頭長度與壓縮編碼後字節數之和。

下面以一個具體的例子說明7-bit編碼的過程。我們對英文短信“Hello!”進行編碼:


將源串每8個字符分爲一組(這個例子中不滿8個)進行編碼,在組內字符間壓縮,但每組之間是沒有什麼聯繫的。

用C實現7-bit編碼和解碼的算法如下:

// 7-bit編碼
// pSrc: 源字符串指針
// pDst: 目標編碼串指針
// nSrcLength: 源字符串長度
// 返回: 目標編碼串長度
int gsmEncode7bit(const char* pSrc, unsigned char* pDst, int nSrcLength)
{
    int nSrc;        // 源字符串的計數值
    int nDst;        // 目標編碼串的計數值
    int nChar;      // 當前正在處理的組內字符字節的序號,範圍是0-7
    unsigned char nLeft;    // 上一字節殘餘的數據
   
    // 計數值初始化
    nSrc = 0;
    nDst = 0;
   
    // 將源串每8個字節分爲一組,壓縮成7個字節
    // 循環該處理過程,直至源串被處理完
    // 如果分組不到8字節,也能正確處理
    while(nSrc<nSrcLength)
    {
        // 取源字符串的計數值的最低3位
        nChar = nSrc & 7;
   
        // 處理源串的每個字節
        if(nChar == 0)
        {
            // 組內第一個字節,只是保存起來,待處理下一個字節時使用
            nLeft = *pSrc;
        }
        else
        {
            // 組內其它字節,將其右邊部分與殘餘數據相加,得到一個目標編碼字節
            *pDst = (*pSrc << (8-nChar)) | nLeft;
   
            // 將該字節剩下的左邊部分,作爲殘餘數據保存起來
            nLeft = *pSrc >> nChar;
            // 修改目標串的指針和計數值 pDst++;
            nDst++;
        }
       
        // 修改源串的指針和計數值
        pSrc++; nSrc++;
    }
   
    // 返回目標串長度
    return nDst;
}
   
// 7-bit解碼
// pSrc: 源編碼串指針
// pDst: 目標字符串指針
// nSrcLength: 源編碼串長度
// 返回: 目標字符串長度
int gsmDecode7bit(const unsigned char* pSrc, char* pDst, int nSrcLength)
{
    int nSrc;        // 源字符串的計數值
    int nDst;        // 目標解碼串的計數值
    int nByte;      // 當前正在處理的組內字節的序號,範圍是0-6
    unsigned char nLeft;    // 上一字節殘餘的數據
   
    // 計數值初始化
    nSrc = 0;
    nDst = 0;
   
    // 組內字節序號和殘餘數據初始化
    nByte = 0;
    nLeft = 0;
   
    // 將源數據每7個字節分爲一組,解壓縮成8個字節
    // 循環該處理過程,直至源數據被處理完
    // 如果分組不到7字節,也能正確處理
    while(nSrc<nSrcLength)
    {
        // 將源字節右邊部分與殘餘數據相加,去掉最高位,得到一個目標解碼字節
        *pDst = ((*pSrc << nByte) | nLeft) & 0x7f;
        // 將該字節剩下的左邊部分,作爲殘餘數據保存起來
        nLeft = *pSrc >> (7-nByte);
   
        // 修改目標串的指針和計數值
        pDst++;
        nDst++;
   
        // 修改字節計數值
        nByte++;
   
        // 到了一組的最後一個字節
        if(nByte == 7)
        {
            // 額外得到一個目標解碼字節
            *pDst = nLeft;
   
            // 修改目標串的指針和計數值
            pDst++;
            nDst++;
   
            // 組內字節序號和殘餘數據初始化
            nByte = 0;
            nLeft = 0;
        }
   
        // 修改源串的指針和計數值
        pSrc++;
        nSrc++;
    }
   
    *pDst = 0;
   
    // 返回目標串長度
    return nDst;
}

需要指出的是,7-bit的字符集與ANSI標準字符集不完全一致,在0x20以下也排布了一些可打印字符,但英文字母、阿拉伯數字和常用符號的位置兩者是一樣的。用上面介紹的算法收發純英文短消息,一般情況應該是夠用了。如果是法語、德語、西班牙語等,含有 “å”、 “é”這一類字符,則要按上面編碼的輸出去查表,請參閱GSM 03.38的規定。

8-bit編碼其實沒有規定什麼具體的算法,不需要介紹。

UCS2編碼是將每個字符(1-2個字節)按照ISO/IEC10646的規定,轉變爲16位的Unicode寬字符。在Windows系統中,特別是在2000/XP中,可以簡單地調用API 函數實現編碼和解碼。如果沒有系統的支持,比如用單片機控制手機模塊收發短消息,只好用查表法解決了。

Windows環境下,用C實現UCS2編碼和解碼的算法如下:

// UCS2編碼
// pSrc: 源字符串指針
// pDst: 目標編碼串指針
// nSrcLength: 源字符串長度
// 返回: 目標編碼串長度
int gsmEncodeUcs2(const char* pSrc, unsigned char* pDst, int nSrcLength)
{
    int nDstLength;        // UNICODE寬字符數目
    WCHAR wchar[128];      // UNICODE串緩衝區
   
    // 字符串-->UNICODE串
    nDstLength = ::MultiByteToWideChar(CP_ACP, 0, pSrc, nSrcLength, wchar, 128);
   
    // 高低字節對調,輸出
    for(int i=0; i<nDstLength; i++)
    {
        // 先輸出高位字節
        *pDst++ = wchar[i] >> 8;
        // 後輸出低位字節
        *pDst++ = wchar[i] & 0xff;
    }
   
    // 返回目標編碼串長度
    return nDstLength * 2;
}
   
// UCS2解碼
// pSrc: 源編碼串指針
// pDst: 目標字符串指針
// nSrcLength: 源編碼串長度
// 返回: 目標字符串長度
int gsmDecodeUcs2(const unsigned char* pSrc, char* pDst, int nSrcLength)
{
    int nDstLength;        // UNICODE寬字符數目
    WCHAR wchar[128];      // UNICODE串緩衝區
   
    // 高低字節對調,拼成UNICODE
    for(int i=0; i<nSrcLength/2; i++)
    {
        // 先高位字節
        wchar[i] = *pSrc++ << 8;
   
        // 後低位字節
        wchar[i] |= *pSrc++;
    }
   
    // UNICODE串-->字符串
    nDstLength = ::WideCharToMultiByte(CP_ACP, 0, wchar, nSrcLength/2, pDst, 160, NULL, NULL);
   
    // 輸出字符串加個結束符   
    pDst[nDstLength] = '/0';   
   
    // 返回目標字符串長度
    return nDstLength;
}

用以上編碼和解碼模塊,還不能將短消息字符串編碼爲PDU串需要的格式,也不能直接將PDU串中的用戶信息解碼爲短消息字符串,因爲還差一個在可打印字符串和字節數據之間相互轉換的環節。可以循環調用sscanf和sprintf函數實現這種變換。下面提供不用這些函數的算法,它們也適用於單片機、DSP編程環境。

// 可打印字符串轉換爲字節數據
// 如:"C8329BFD0E01" --> {0xC8, 0x32, 0x9B, 0xFD, 0x0E, 0x01}
// pSrc: 源字符串指針
// pDst: 目標數據指針
// nSrcLength: 源字符串長度
// 返回: 目標數據長度
int gsmString2Bytes(const char* pSrc, unsigned char* pDst, int nSrcLength)
{
    for(int i=0; i<nSrcLength; i+=2)
    {
        // 輸出高4位
        if(*pSrc>='0' && *pSrc<='9')
        {
            *pDst = (*pSrc - '0') << 4;
        }
        else
        {
            *pDst = (*pSrc - 'A' + 10) << 4;
        }
   
        pSrc++;
   
        // 輸出低4位
        if(*pSrc>='0' && *pSrc<='9')
        {
            *pDst |= *pSrc - '0';
        }
        else
        {
            *pDst |= *pSrc - 'A' + 10;
        }
        pSrc++;
        pDst++;
    }
   
    // 返回目標數據長度
    returnnSrcLength / 2;
}
   
// 字節數據轉換爲可打印字符串
// 如:{0xC8, 0x32, 0x9B, 0xFD, 0x0E, 0x01} --> "C8329BFD0E01"
// pSrc: 源數據指針
// pDst: 目標字符串指針
// nSrcLength: 源數據長度
// 返回: 目標字符串長度
int gsmBytes2String(const unsigned char* pSrc, char* pDst, int nSrcLength)
{
    const char tab[]="0123456789ABCDEF";    // 0x0-0xf的字符查找表
   
    for(int i=0; i<nSrcLength; i++)
    {
        // 輸出低4位
        *pDst++ = tab[*pSrc >> 4];
   
        // 輸出高4位
        *pDst++ = tab[*pSrc & 0x0f];
   
        pSrc++;
    }
   
    // 輸出字符串加個結束符
    *pDst = '/0';
   
    // 返回目標字符串長度
    return nSrcLength * 2;
}

關於GSM 03.42中的壓縮算法,至今還沒有發現哪裏用過,這裏我們就不討論了。有興趣的話,可深入研究一下。
Q PDU的核心編碼方式已經清楚了,如何實現用AT命令收發短消息呢?

A  在上篇中,我們已經討論了7-bit, 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) = 'F';    // 補'F'
        nDstLength++;        // 目標串長度加1
    }
   
    // 輸出字符串加個結束符
    *pDst = '/0';
   
    // 返回目標字符串長度
    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;        // 複製先出現的字符
    }
   
    // 最後的字符是'F'嗎?
    if(*(pDst-1) == 'F')
    {
        pDst--;
        nDstLength--;        // 目標字符串長度減1
    }
   
    // 輸出字符串加個結束符
    *pDst = '/0';
   
    // 返回目標字符串長度
    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);    // 讀取序號
            TRACE("  index=%d/n",pMsg->index);
   
            ptr = strstr(ptr, "/r/n";    // 找下一行
            ptr += 2;        // 跳過"/r/n"
               
            gsmDecodePdu(ptr, pMsg);    // PDU串解碼
            pMsg++;        // 準備讀下一條短消息
            nMsg++;        // 短消息計數加1
        }
    }
   
    return nMsg;
}
   
// 刪除短消息
// index: 短消息序號,從1開始
BOOL gsmDeleteMessage(const 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",以區分自定義數據和普通短消息
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章