早期 GSM Modem/手機透過序列埠 (COM Port) 以 AT 指令集發送簡訊,是嵌入式系統、工業監控、早期企業通訊系統的常見方案。本文解析一個完整的 WinForms 專案,涵蓋 MSComm 控制項封裝、PDU 模式編碼、中文 Unicode 處理、非同步收發流程。
[UI 層] FrmMain
├─ 通訊設定區 (COM Port, Baud Rate, 連線/斷開)
├─ 簡訊發送區 (簡訊中心號碼, 手機號, 有效期, 內容)
└─ 狀態顯示區 (原始回應, 發送狀態)
↓
[通訊核心] AxMSComm (ActiveX Wrapper)
├─ PortOpen / Settings / Output / Input
└─ OnComm 事件 (資料到達通知)
↓
[協定層] AT 指令 + PDU 編碼
├─ AT 指令流程控制 (連線測試、設定 PDU 模式、發送)
└─ PDU 編碼器 (UCS2 中文支援、號碼半位元組交換、時間戳)
// 設計器自動產生的初始化 (關鍵屬性)
this.MSCOMM.Settings = "19200,n,8,1"; // Baud, Parity, DataBits, StopBits
this.MSCOMM.PortOpen = true; // 開啟埠口
this.MSCOMM.RThreshold = 1; // 收到 1 位元組觸發 OnComm
this.MSCOMM.InputLen = 0; // 讀取緩衝區所有資料
// 連線測試流程 (btnCnn_Click)
private void TestConnection()
{
MSCOMM.Output = "AT\r"; // 發送測試指令
// 等待 "OK\r\n" 回應,逾時 2.5 秒
string buffer = "";
int start = Environment.TickCount;
while (Environment.TickCount - start < 2500)
{
Application.DoEvents(); // 保持 UI 回應
buffer += MSCOMM.Input; // 累積接收資料
if (buffer.Contains("OK\r\n")) break;
}
// 判斷成功/失敗並更新 UI 狀態
}
PDU (Protocol Description Unit) 是 GSM 規定的二進位封裝格式。發送中文簡訊需使用 UCS2 編碼 (16-bit Unicode)。
private string BuildPduString(string centerNumber, string mobile, string message, string validityPeriod)
{
// 1. 簡訊中心號碼編碼 (BCD 半位元組交換 + 長度 + TON/NPI)
// 範例: +886932123456 → 長度 08, TON/NPI 91, 號碼 868392214365F0
string pdu = "00"; // SMSC 由網路提供,這裡填 00 表示使用預設
// 2. 第一個位元組: TP-MTI(2bit) + TP-VPF(2bit) + TP-RP + TP-UDHI + TP-SRR
// 0x11 = 0001 0001 → SMS-SUBMIT, 有效期相對格式, 不要求狀態報告
pdu += "11";
// 3. TP-MR (訊息參考號) 00 讓網路自動指派
pdu += "00";
// 4. 目的號碼: 長度(BCD) + TON/NPI(91=國際號碼) + 號碼半位元組交換
pdu += mobile.Length.ToString("X2"); // 長度
pdu += "91"; // 國際格式
pdu += SwapNibbles(mobile); // 0912345678 → 9021436587
// 5. TP-PID (協定識別碼) 00 = 隱含
pdu += "00";
// 6. TP-DCS (資料編碼方案) 08 = UCS2 (16-bit Unicode)
pdu += "08";
// 7. TP-VP (有效期) 範例: A7 = 1 天
pdu += validityPeriod;
// 8. TP-UDL (用戶資料長度) = 位元組數 (UCS2 每字 2 位元組)
byte[] msgBytes = Encoding.BigEndianUnicode.GetBytes(message); // UCS2 Big Endian
pdu += msgBytes.Length.ToString("X2");
// 9. TP-UD (用戶資料) - 直接附加 UCS2 位元組的 Hex 字串
pdu += BitConverter.ToString(msgBytes).Replace("-", "");
// 10. 總長度 (不含前兩位 SMSC 長度) 用於 AT+CMGS 長度參數
int pduLength = (pdu.Length / 2) - 1; // 減去 SMSC 長度位元組
return (pdu, pduLength);
}
// 半位元組交換輔助函式
private string SwapNibbles(string input)
{
string s = input + "F"; // 奇數長度補 F
var sb = new StringBuilder();
for (int i = 0; i < s.Length; i += 2)
sb.Append(s[i+1]).Append(s[i]);
return sb.ToString();
}
private void SendSms(string pdu, int pduLength)
{
// 1. 設定 PDU 模式
MSCOMM.Output = "AT+CMGF=0\r";
WaitFor("OK", 2000);
// 2. 發送 CMGS 指令,長度 = PDU 位元組數
MSCOMM.Output = $"AT+CMGS={pduLength}\r";
WaitFor("> ", 2000); // 等待提示符
// 3. 發送 PDU 內容 + Ctrl+Z (0x1A) 結束
MSCOMM.Output = pdu + (char)26;
// 4. 等待最終回應 (最長 5.5 秒)
string result = WaitFor("OK", 5500);
if (result.Contains("OK")) ShowSuccess();
else ShowFailure();
}
private string WaitFor(string expected, int timeoutMs)
{
string buffer = "";
int start = Environment.TickCount;
while (Environment.TickCount - start < timeoutMs)
{
Application.DoEvents();
buffer += MSCOMM.Input;
if (buffer.Contains(expected)) return buffer;
}
return buffer; // 逾時返回已收內容
}
KeyPress 事件限制簡訊中心號碼只能輸入 +0-9、手機號碼只能輸入 0-9SmsSender/ ├── Models/ │ ├── PduMessage.cs # PDU 封裝類別 │ ├── SmsSubmitPdu.cs # 發送 PDU 建構器 │ └── SmsDeliverPdu.cs # 接收 PDU 解析器 (擴充) ├── Services/ │ ├── SerialPortWrapper.cs # 封裝 SerialPort (替代 MSComm) │ ├── AtCommandExecutor.cs # AT 指令流程控制 │ └── SmsService.cs # 高階發送/接收 API ├── UI/ │ └── MainForm.cs # WinForms 介面 └── Program.cs
System.IO.Ports.SerialPort (標準 .NET 類別,無 ActiveX 相依)Task.Run + async/await 避免 Application.DoEvents() 造成重入問題| 現象 | 可能原因 | 解決方向 |
|---|---|---|
| AT 指令無回應 | 接線錯誤、Baud Rate 不符、Modem 未就緒 | 檢查硬體、嘗試 9600/115200、加啟動延遲 |
| 發送失敗 CMS ERROR | 號碼格式錯、中心號碼錯、SIM 卡欠費/鎖定 | 查閱 GSM 07.05 錯誤碼表、確認 PDU 編碼正確性 |
| 中文顯示亂碼 | DCS 未設 0x08 (UCS2)、編碼非 Big Endian | 強制使用 Encoding.BigEndianUnicode |
| 收到亂碼/不完整 | 讀取時機過早、緩衝區殘留 | OnComm 累積緩衝、以 \r\n 或 > 為界定符解析 |
雖然現代簡訊發送多已轉向 HTTP API/SMPP,但序列埠 AT 指令模式在離線環境、工業現場、成本敏感專案仍具價值。深入理解 PDU 編碼、AT 指令流程、序列埠非同步處理,不僅能維護既有系統,更能培養底層通訊協定實作的核心功力。