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,14END  對話方塊類的訊息對映為: 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_MAPEND_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, intnStopBits){ 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.總結