首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式碼之家 >> 編程語言 >  >> .NET 網頁程式設計 >> C# >> 關於C#

C# 串口通訊實現 GSM Modem 發送簡訊 (AT 指令與 PDU 編碼)

     編輯:關於C#

前言

透過 GSM Modem (或支援 AT 指令的手機) 經由串口發送簡訊,適用於無網路環境、低成本、自主可控的簡訊應用。本文涵蓋:串口通訊類庫封裝、AT 指令流程、PDU 編碼/解碼 (支援中文 UCS2)、完整發送流程與錯誤處理。

硬體與環境準備

  • GSM Modem (如 SIMCom SIM800, Quectel M66) 或支援 AT 指令的手機 (西門子 C35, 諾基亞舊款)
  • USB 轉序列埠纜線 (建議原廠晶片 FTDI/CP2102/PL2303)
  • 有效 SIM 卡 (已設定簡訊中心號碼)
  • .NET 6+ / .NET Framework 4.7.2+

核心類別:串口通訊封裝

使用 System.IO.Ports.SerialPort 封裝非同步讀寫:


using System;
using System.IO.Ports;
using System.Text;
using System.Threading.Tasks;

public class GsmModem : IDisposable
{
    private readonly SerialPort _port;
    private readonly StringBuilder _responseBuffer = new();
    private readonly TaskCompletionSource _readTcs = new();
    private readonly object _lock = new();

    public GsmModem(string portName, int baudRate = 115200)
    {
        _port = new SerialPort(portName, baudRate, Parity.None, 8, StopBits.One)
        {
            ReadTimeout = 2000,
            WriteTimeout = 2000,
            NewLine = "\r\n",
            Encoding = Encoding.ASCII
        };
        _port.DataReceived += Port_DataReceived;
    }

    public async Task OpenAsync()
    {
        if (!_port.IsOpen) _port.Open();
        await TestConnectionAsync();
    }

    private async Task TestConnectionAsync()
    {
        var resp = await SendCommandAsync("AT");
        if (!resp.Contains("OK")) throw new Exception("Modem 無回應");
    }

    private void Port_DataReceived(object sender, SerialDataReceivedEventArgs e)
    {
        string data = _port.ReadExisting();
        lock (_lock)
        {
            _responseBuffer.Append(data);
            if (_responseBuffer.ToString().Contains("OK") || _responseBuffer.ToString().Contains("ERROR") || _responseBuffer.ToString().Contains("> "))
            {
                _readTcs.TrySetResult(_responseBuffer.ToString());
            }
        }
    }

    public async Task SendCommandAsync(string command, int timeoutMs = 5000)
    {
        lock (_lock)
        {
            _responseBuffer.Clear();
            _readTcs = new TaskCompletionSource();
            _port.WriteLine(command);
        }

        var completed = await Task.WhenAny(_readTcs.Task, Task.Delay(timeoutMs));
        if (completed == _readTcs.Task)
            return _readTcs.Task.Result;
        throw new TimeoutException($"指令逾時: {command}");
    }

    public void Dispose() => _port?.Dispose();
}

PDU 編碼核心實作 (支援中文 UCS2)


public static class PduEncoder
{
    // 發送簡訊 PDU 編碼
    public static string EncodeSubmit(string sca, string phoneNumber, string message, bool unicode = true)
    {
        // 1. 簡訊中心號碼 (SCA)
        string pdu = EncodeAddress(sca, 0x91); // 國際格式

        // 2. PDU Type (SMS-SUBMIT)
        pdu += "11"; // TP-MTI=01 (SUBMIT), TP-VPF=00, TP-RD=0, TP-SRR=0, TP-UDHI=0, TP-RP=0

        // 3. TP-MR (訊息參考) = 00
        pdu += "00";

        // 4. 目的地址 (DA)
        pdu += EncodeAddress(phoneNumber, 0x91);

        // 5. TP-PID (協定識別碼) = 00
        pdu += "00";

        // 6. TP-DCS (資料編碼方案)
        pdu += unicode ? "08" : "00"; // 08=UCS2(16-bit), 00=GSM 7-bit

        // 7. TP-VP (有效期) = 00 (相對格式,無有效期)
        pdu += "00";

        // 8. TP-UDL (用戶資料長度) + TP-UD (用戶資料)
        if (unicode)
        {
            byte[] ud = Encoding.BigEndianUnicode.GetBytes(message); // UCS2 Big Endian
            pdu += ud.Length.ToString("X2"); // 長度單位:八位元組
            pdu += BitConverter.ToString(ud).Replace("-", "");
        }
        else
        {
            // GSM 7-bit 編碼較複雜,此處略過,建議統一用 UCS2
            throw new NotSupportedException("建議使用 Unicode 編碼");
        }

        return pdu;
    }

    private static string EncodeAddress(string number, byte tonNpi)
    {
        // 移除 + 號
        if (number.StartsWith("+")) number = number.Substring(1);
        // 長度 (半位元組數)
        int len = number.Length;
        string lenHex = len.ToString("X2");
        // TON/NPI
        string tonHex = tonNpi.ToString("X2");
        // 號碼內容:兩兩交換
        string numHex = "";
        for (int i = 0; i < number.Length; i += 2)
        {
            char c1 = number[i];
            char c2 = (i + 1 < number.Length) ? number[i + 1] : 'F';
            numHex += $"{c2}{c1}";
        }
        return $"{lenHex}{tonHex}{numHex}";
    }
}

完整發送流程整合


public class SmsSender
{
    private readonly GsmModem _modem;

    public SmsSender(GsmModem modem) => _modem = modem;

    public async Task SendSmsAsync(string phoneNumber, string message, string sca = null)
    {
        try
        {
            // 1. 取得簡訊中心號碼 (若未提供)
            if (string.IsNullOrEmpty(sca))
            {
                var resp = await _modem.SendCommandAsync("AT+CSCA?");
                // 解析 +CSCA: "+886932123456",145
                var match = System.Text.RegularExpressions.Regex.Match(resp, @"\+CSCA:\s*""([^""]+)""");
                if (match.Success) sca = match.Groups[1].Value;
                else throw new Exception("無法取得簡訊中心號碼");
            }

            // 2. 設定 PDU 模式
            await _modem.SendCommandAsync("AT+CMGF=0"); // 0=PDU, 1=Text

            // 3. 編碼 PDU
            string pdu = PduEncoder.EncodeSubmit(sca, phoneNumber, message, unicode: true);
            int pduLength = (pdu.Length - 2) / 2; // 扣除 SCA 長度欄位後的八位元組數

            // 4. 發送指令
            string cmd = $"AT+CMGS={pduLength}";
            string resp = await _modem.SendCommandAsync(cmd);
            if (!resp.Contains("> ")) throw new Exception("Modem 未進入資料輸入模式");

            // 5. 發送 PDU 資料 + Ctrl+Z (0x1A)
            _modem._port.Write(pdu + "\x1A"); // 直接寫入 SerialPort

            // 6. 等待最終回應
            string finalResp = await _modem.SendCommandAsync("", 10000); // 空指令只等回應
            return finalResp.Contains("+CMGS:") || finalResp.Contains("OK");
        }
        catch (Exception ex)
        {
            Console.WriteLine($"發送失敗: {ex.Message}");
            return false;
        }
    }
}

UI 範例:簡訊發送工具


public class SmsToolForm : Form
{
    private GsmModem _modem;
    private SmsSender _sender;
    private ComboBox _cmbPort;
    private TextBox _txtPhone, _txtMsg, _txtLog;
    private Button _btnConnect, _btnSend;

    public SmsToolForm()
    {
        InitializeComponent();
        LoadPorts();
    }

    private void InitializeComponent()
    {
        _cmbPort = new ComboBox { Location = new Point(10, 10), Width = 100 };
        _btnConnect = new Button { Text = "連接", Location = new Point(120, 10), Width = 80 };
        _btnConnect.Click += BtnConnect_Click;

        _txtPhone = new TextBox { Location = new Point(10, 50), Width = 200, PlaceholderText = "接收號碼 (含國碼)" };
        _txtMsg = new TextBox { Location = new Point(10, 90), Width = 300, Height = 80, Multiline = true, PlaceholderText = "簡訊內容 (支援中文)" };
        _btnSend = new Button { Text = "發送", Location = new Point(10, 180), Width = 80, Enabled = false };
        _btnSend.Click += async (s, e) => await SendAsync();

        _txtLog = new TextBox { Location = new Point(10, 220), Width = 460, Height = 200, Multiline = true, ReadOnly = true, ScrollBars = ScrollBars.Vertical };

        this.Controls.AddRange(new Control[] { _cmbPort, _btnConnect, _txtPhone, _txtMsg, _btnSend, _txtLog });
        this.Text = "GSM Modem 簡訊發送工具";
        this.ClientSize = new Size(500, 450);
    }

    private void LoadPorts() => _cmbPort.Items.AddRange(SerialPort.GetPortNames());

    private async void BtnConnect_Click(object sender, EventArgs e)
    {
        if (_cmbPort.SelectedItem == null) return;
        try
        {
            _modem = new GsmModem(_cmbPort.SelectedItem.ToString());
            await _modem.OpenAsync();
            _sender = new SmsSender(_modem);
            _btnSend.Enabled = true;
            Log("連接成功");
        }
        catch (Exception ex)
        {
            Log($"連接失敗: {ex.Message}");
        }
    }

    private async Task SendAsync()
    {
        if (_sender == null) return;
        _btnSend.Enabled = false;
        Log($"發送中... {_txtPhone.Text}");
        bool ok = await _sender.SendSmsAsync(_txtPhone.Text, _txtMsg.Text);
        Log(ok ? "發送成功!" : "發送失敗!");
        _btnSend.Enabled = true;
    }

    private void Log(string msg) => _txtLog.AppendText($"[{DateTime.Now:HH:mm:ss}] {msg}\r\n");

    protected override void OnFormClosing(FormClosingEventArgs e)
    {
        _modem?.Dispose();
        base.OnFormClosing(e);
    }
}

常見問題排查

現象可能原因解決
AT 無回應纜線、驅動程式、COM 埠錯誤、波特率不符檢查裝置管理員、嘗試 9600/115200、更換原廠纜線
+CMS ERROR: 304簡訊中心號碼錯誤AT+CSCA? 確認、手動設定 AT+CSCA="+886932123456"
中文亂碼編碼不一致強制使用 UCS2 (TP-DCS=08),確認 BigEndianUnicode
發送超時訊號弱、SIM 卡未就緒、Modem 狀態異常AT+CSQ 查訊號、AT+CPIN? 查 SIM 狀態、重啟 Modem

總結

本文提供完整的 C# GSM Modem 簡訊發送方案:非同步串口封裝、標準 PDU 編碼 (支援中文 UCS2)、AT 指令流程控制、錯誤處理與 UI 整合。關鍵成功因素:穩定的硬體連接、正確的簡訊中心號碼、統一使用 UCS2 編碼、完善的超時與重試機制。現代專案亦可考慮使用 SerialPortStream (NuGet) 獲得更健壯的串口操作。

  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved