1. 程式人生 > >深入淺出VC++串列埠程式設計--簡訊應用開發

深入淺出VC++串列埠程式設計--簡訊應用開發

前面數次連載我們以較長的篇幅講解了串列埠通訊的硬體原理、DOS平臺控制以及基於WIN32 API、控制元件和第三方類的串列埠程式設計。作為本系列文章的最後一次連載,本章將給出一個典型的應用例項:西門子簡訊服務模組TC35的串列埠控制。

  1.簡訊控制終端

  作為簡訊 (Short Message Service,SMS)一族,想必你有這樣的體會:用手機編輯簡訊息十分不便、容易出錯,而且修改費時,若能用計算機來收發簡訊則方便許多。注意,本文所說的用計算機收發簡訊並不是說通過"網易簡訊王"等方式在Internet上收發簡訊,而是直接用計算機控制運行了GSM通訊系統的簡訊終端進行收發,因而其收發簡訊的原理與手機是本質相同的。

  實際上,一大堆的垃圾簡訊也是採用這種簡訊終端發出來的!

  我們來介紹一款GSM模組,它就是西門子公司的TC35,它由GSM基帶處理器、電源專用積體電路、射頻電路和閃速儲存器等部分組成,負責處理GSM蜂窩裝置中的音訊、資料和訊號,內嵌的軟體部分執行應用介面和所有GSM協議棧的功能。TC35支援中文簡訊息,工作在EGSM900和GSM1800雙頻段,電源範圍為3.3~5.5V,可傳輸語音和資料訊號,消耗功率在EGSM900(4類)和GSM1800(1類)分別為2W和1W,通過介面聯結器和天線聯結器分別連線SIM卡讀卡器和天線。TC35的資料介面(CMOS電平)通過AT命令可雙向傳輸指令和資料,可選波特率為300bit/s~115kbit/s,自動波特率為1.2k~115kbit/s。它支援文字和PDU格式的,可通過AT命令或關斷訊號實現重啟和故障恢復。

  我們需要利用以TC35模組為主的硬體組成一個TC35終端裝置,並與電腦通過RS-232C串列埠相連,並自行編制在PC上執行的簡訊息收發軟體,就可以組成一個簡訊收發系統。TC35終端電路如下圖所示:


  TC35的控制主要包含如下幾類指令:

  (1)初始化指令

  設定短訊息傳送格式AT+CMGF=1<CR>,設定1代表PDU模式,<CR>是回車符號,也就是0x0d,指令正確則模組返回<CRLF>OK<CRLF>,<CRLF>是回車換行符號。

  (2)設定/讀取短訊息中心

  短訊息中心號碼由移動運營商提供。

  設定短訊息中心的指令格式為:

  AT+CSCA=″+8613800531500″(短訊息中心)<CR>

  設定正確則模組返回<CRLF>OK<CRLF>。

  讀取短訊息服務中心則使用命令:

AT+CSCA=?<CR>

  TC35模組應該返回:

<CRLF>+CSCA:″8613800531500″<CRLF>。

  (3)設定短訊息到達自動提示

  設定短訊息到達自動提示的指令格式為:

AT+CNMI=1,1,0,0,1<CR>

  設定正確則TC35模組返回:

<CRLF>OK<CRLF>。

  設定此命令可使模組在短訊息到達後向串列埠傳送指令:

<CRLF>+CMTI:″SM″,INDEX(資訊儲存位置)<CRLF>。

  通過TC35傳送短訊息的方法為:

  PC上的控制軟體按照PDU的格式傳送和接收資料,短訊息的內容可以是中文或者其他字元。在PDU模式,如果傳送短訊息,則首先發送短訊息資料的長度:

AT+CMGS=<length><CR>

  等待TC35模組返回ASCII字元">",則可以將PDU資料輸入,PDU資料以<Z>(也就是0x1a)作為結束符。短訊息傳送成功,模組返回:

<CRLF>OK<CRLF>

  通過TC35接收短訊息的方法為:

  短訊息到來後,串列埠上會接收到指令

<CRLF>+CMTI:″SM″,INDEX(資訊儲存位置)<CRLF>

  PC上的控制軟體通過讀取PDU資料的AT命令

AT+CMGR=INDEX<CRLF>

  將TC35模組中PDU格式的短訊息內容讀出。如果用+CMGL代替+CMGR,則可一次性讀出全部短訊息。

  通過TC35刪除短訊息的方法為:

  PC上的控制軟體收到一條短訊息並處理後,需要將其在SIM卡上刪除,以防止SIM卡飽和。刪除短訊息的指令為:

AT+CMGD=INDEX<CR>

  刪除後模組返回

<CRLF>OK<CRLF>

2.程式例項

  由於本文的宗旨在於講解串列埠通訊,因此,我們遮蔽圖形使用者介面的細節,製作一個簡單的簡訊收發軟體,它包含了控制簡訊終端的所有串列埠通訊內容。實際上,一個理想的簡訊收發軟體的介面應類似於Outlook或Foxmail,包含收件箱、發件箱、已傳送簡訊箱等內容,但是這些東西都與我們要介紹的串列埠通訊無關,因此,下面的軟體介面雖"敗絮其外",但仍可稱得上"金玉其中":



  關於介面上控制元件的描述如下:

BEGIN
 EDITTEXT IDC_SMSCONTENT_EDIT,39,61,242,38,ES_AUTOHSCROLL
 PUSHBUTTON "傳送",IDC_SEND_BUTTON,316,80,45,18
 GROUPBOX "接收短訊息",IDC_STATIC,28,124,361,167
 LTEXT "對方手機號",IDC_STATIC,41,35,42,11
 EDITTEXT IDC_PHONENUM_EDIT,88,30,192,17,ES_AUTOHSCROLL
 PUSHBUTTON "清除",IDC_CLEAR_BUTTON,316,30,45,18
 GROUPBOX "傳送短訊息",IDC_STATIC,29,19,361,95
 LISTBOX IDC_RECVSMS_LIST,43,137,331,127,LBS_SORT |
 LBS_NOINTEGRALHEIGHT | WS_VSCROLL | WS_TABSTOP
 PUSHBUTTON "接收",IDC_RECV_BUTTON,77,269,55,16
 PUSHBUTTON "清空",IDC_DELETEALL_BUTTON,273,268,45,14
END

  對話方塊類的訊息對映為:

BEGIN_MESSAGE_MAP(CSMSControlDlg, CDialog)
//{{AFX_MSG_MAP(CSMSControlDlg)
 ON_WM_SYSCOMMAND()
 ON_WM_PAINT()
 ON_WM_QUERYDRAGICON()
 ON_BN_CLICKED(IDC_CLEAR_BUTTON, OnClearButton)
 ON_BN_CLICKED(IDC_SEND_BUTTON, OnSendButton)
 ON_BN_CLICKED(IDC_RECV_BUTTON, OnRecvButton)
 ON_BN_CLICKED(IDC_DELETEALL_BUTTON, OnDeleteallButton)
//}}AFX_MSG_MAP
END_MESSAGE_MAP()

  感謝《通過串列埠收發短訊息》一文的作者bhw98,他為我們編寫了數個獨立於作業系統平臺的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;

  傳送短訊息

  傳送按鈕對應的函式為CSMSControlDlg::OnSendButton,它讀取使用者輸出並根據目標電話號碼和簡訊息內容形成SM_PARAM(源PDU引數)的內容,接著進行傳送:

void CSMSControlDlg::OnSendButton()
{
 // TODO: Add your control notification handler code here
 //獲得使用者輸入
 CString desPhoneNum;
 CString smsContent;
 GetDlgItemText(IDC_PHONENUM_EDIT,desPhoneNum);
 GetDlgItemText(IDC_SMSCONTENT_EDIT,smsContent);

 //填充SM_PARAM結構體內容
 SM_PARAM smParam;
 smParam = CreateSMPARAMStruct(desPhoneNum,smsContent);

 //傳送簡訊息
 gsmSendMessage(smParam);
}

  其中呼叫的gsmSendMessage函式體現了串列埠通訊的核心內容,它按照第1節闡述的GSM模組傳送短訊息的串列埠控制流程進行簡訊的傳送:

BOOL gsmSendMessage(const SM_PARAM *pSrc // pSrc: 源PDU引數指標)
{
 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;
}

  讀取短訊息

  點選"接收"按鈕會通過gsmReadMessage函式的呼叫獲得所有短訊息,最後在列表控制元件中顯示所有簡訊:

void CSMSControlDlg::OnRecvButton()
{
 // TODO: Add your control notification handler code here
 SM_PARAM smParam[100];//簡訊緩衝區
 int smsNum;//簡訊條數
 smsNum = gsmReadMessage(smParam);//讀取簡訊

 //顯示簡訊
 for(int i=0;i<smsNum;i++)
 {
  m_recvlist.AddString(CString(smsNum[i].TPA)+smsNum[i].TP_UD);
 }
}


  其中呼叫的gsmReadMessage函式完成最核心的簡訊接收功能,它按照第1節闡述的GSM模組接收短訊息的串列埠控制流程進行簡訊的接收:


// 引數: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"); // 生成命令,用+CMGL可一次性讀出全部短訊息
                  
 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;
}

刪除短訊息

  我們可以在讀取完所有簡訊息後呼叫gsmDeleteMessage函式在GSM模組上刪除那些已經被接收到PC上的簡訊息,它按照第1節闡述的GSM模組刪除短訊息的串列埠控制流程進行簡訊的刪除:

// 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;
}

  在PC控制軟體的簡訊列表框中刪除所有短訊息的"清空"按鈕函式為:

void CSMSControlDlg::OnDeleteallButton()
{
 // TODO: Add your control notification handler code here
 m_recvlist.ResetContent();
}

  設定/讀/寫串列埠

  在應用程式啟動與退出及gsmSendMessage、gsmReadMessage和gsmDeleteMessage函式中廣泛使用的串列埠相關函式用WIN32 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;
}

編/解碼GSM短訊息

  陷於本文的篇幅,這裡只給出編解碼函式的原型,具體請參看GSM標準及《通過串列埠收發短訊息》一文。

// UCS2編碼 返回: 目標編碼串長度
int gsmEncodeUcs2(const char *pSrc, // 源字串指標
 unsigned char *pDst, // pDst: 目標編碼串指標
 int nSrcLength // nSrcLength: 源字串長度
);

// UCS2解碼 返回: 目標字串長度
int gsmDecodeUcs2(const unsigned char *pSrc, //源編碼串指標
char *pDst, // pDst: 目標字串指標
int nSrcLength // nSrcLength: 源編碼串長度
);

//可列印字串轉換為位元組資料 返回: 目標資料長度
//如:"C8329BFD0E01" --> {0xC8, 0x32, 0x9B, 0xFD, 0x0E, 0x01}
int gsmString2Bytes(const char *pSrc, // pSrc: 源字串指標
unsigned char *pDst, // pDst: 目標資料指標
int nSrcLength // nSrcLength: 源字串長度
);

// 位元組資料轉換為可列印字串 返回: 目標字串長度
// 如:{0xC8, 0x32, 0x9B, 0xFD, 0x0E, 0x01} --> "C8329BFD0E01"
int gsmBytes2String(const unsigned char *pSrc, // pSrc: 源資料指標
char *pDst, // pDst: 目標字串指標
int nSrcLength // nSrcLength: 源資料長度
);

  3.總結

  串列埠程式設計的核心在於串列埠通訊方式(傳送、接收和握手)的控制,而具體的應用領域反而是次要的。掌握了根本的原理,就可以靈活地將其應用於任意領域,綜合例項中的例子"簡訊控制終端"只是冰山一角。