ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

C#实现PDU编码短信发送:上位机本地GSM告警方案

C#实现PDU编码短信发送:上位机本地GSM告警方案 简介C#短信发送程序PDU_class是一份面向C#开发者的短信发送功能示例工程重点演示如何通过PDU协议编码并提交短信。资源围绕PDUClass、Form1等模块展开涵盖7位/16位短信编码、Submit PDU构建、SMSC号码封装、长短信分段等关键知识点适合需要对接GSM模块或学习短信底层通信逻辑的中级C#程序员。压缩包共45个文件以10个cs源码文件为主另含6个可执行程序、6个资源文件、5个配置文件及调试符号等整包仅484KB便于快速下载与本地运行。目前已有155人浏览学习说明该示例在同类资源中有一定参考价值。通过阅读源码、调试程序并结合配置文件可较快掌握PDU模式发送短信的完整流程并据此扩展实现状态报告、编码转换及异常处理等功能。 做上位机开发的兄弟十有八九都遇到过这种需求设备故障了系统能自动发一条短信告警门禁有人闯入要通知值班手机或者站点地处偏远、没有公网需要一个纯本地的通知通道。第一反应都是找云短信平台但有些场景根本不具备公网条件。所以我干脆用C#写了一个本地短信发送程序核心就是标题里这个pdu_class——一个专门负责PDUProtocol Data Unit编码的类配合串口接上GSM模块插一张普通SIM卡就能发短信。这个方案的好处是不依赖云平台、不依赖外网硬件成本几十块钱SIM卡月租也便宜特别适合无人值守机房、工业设备报警、农业大棚监控这类场景。这篇文章把这套程序从编码原理到串口实操完整拆一遍包括PDU串每个字节怎么来的、7bit编码怎么压、AT指令怎么发、我踩过的坑有哪些。准备入门C#串口通信、或者在做设备告警系统的同学可以直接照着抄。1. 方案选型为什么坚持用PDU而不是Text模式1.1 Text模式看着简单坑却不少市面上很多教程会先教你用ATCMGF1切到Text模式然后一句话就发出去像这样serialPort.WriteLine(ATCMGS\13800138000\\r); serialPort.Write(告警内容); serialPort.Write(\x1A);Text模式确实简单模块内部帮你完成了编码调试期拿来验证模块是否正常工作很方便。但问题是模块在Text模式下到底用什么字符集完全取决于内部配置。ATCSCS设置成GSM还是UCS2或者干脆保持出厂默认不同厂商的处理方式都不太一样。我头一回做的时候图省事用Text模式发中文华为模块上一切正常换到SIMCom模块上直接乱码排查了一下午最后定位到是默认字符集不一样。从那以后凡是测试通过要上线的代码我统一切回PDU模式。1.2 PDU模式为什么稳PDU模式是GSM协议栈的原生格式所有数据都以十六进制字符串发送模块不做任何字符集转换编码完全由主机端决定。这意味着与模块型号无关同一套代码在华为、移远、SIMCom等模块上表现一致中英文混发、特殊字符都能精确控制不存在字符集串台问题后续要扩展长短信、状态报告、Flash短信PDU都能直接支持。代价是编码逻辑稍微复杂一点但这点复杂度完全可以封装在pdu_class里对上层业务透明。我的原则是把不稳定的因素尽量收拢到主机端模块只做它最擅长的事——把封装好的PDU送到基站。1.3 pdu_class的职责边界pdu_class我坚持只做三件事把目标号码、短信内容、短信中心号码编码成标准SMS-SUBMIT格式的PDU十六进制串算出ATCMGS需要的TPDU长度提供7bit和UCS2两种编码方法自动选择。串口发送逻辑我单独放在SmsSender类里。pdu_class保持纯字符串运算不直接引用SerialPort这样单元测试不用插硬件就能跑换通信方式比如通过TCP转发给远程串口服务器也不用改编码类。分层清楚后面维护省心很多。2. PDU编码核心细节解析2.1 先拆一只麻雀一条完整PDU串直接看例子给13800138000发英文hello短信中心用SIM卡里默认的SCA字段写00。最终拼出来的PDU十六进制串是00 11 00 0D 68 31 08 10 83 00 F0 00 00 00 05 E8 32 9B FD 46我习惯把这条串按字段拆开看每一段都有固定含义字段长度本例取值含义SCA1字节00短信中心地址长度0表示用SIM卡内置号码TP-MTI1字节11PDU类型SMS-SUBMIT含VP字段TP-MR1字节00消息参考号可随便写TP-DA17字节0D 68 31 08 10 83 00 F0目标地址长度和编码后的号码TP-PID1字节00协议标识普通短信固定00TP-DCS1字节00编码方案7bit默认字母表TP-VP1字节00有效期默认值TP-UDL1字节05用户数据长度5TP-UD5字节E8 32 9B FD 46hello的7bit压缩编码第一字节00是短信中心地址长度表示SCA数据部分为0字节模块自动从SIM卡读取短信中心。从第二字节开始到结尾就是TPDU部分。发送时ATCMGS后面的数字就是这个TPDU的总字节数数一下上面从11到46一共19个字节所以发送指令是ATCMGS19。2.2 电话号码的半字节反转编码目标号码13800138000手机号在国内直接在前面拼86变成8613800138000一共13位数字。GSM协议里电话号码是BCD编码每半字节存一位数字还要把相邻两位反转顺序存储。原因是GSM规范里地址的半字节遵循低地址优先也就是第一个数字放在低4位。实现代码其实不复杂private static string EncodePhoneNumber(string number) { number number.Replace(, ).Replace( , ); if (number.StartsWith(0)) { number 86 number.Substring(1); } else if (!number.StartsWith(86)) { number 86 number; } if (number.Length % 2 ! 0) { number F; // 奇数位补F } StringBuilder sb new StringBuilder(); for (int i 0; i number.Length; i 2) { sb.Append(number[i 1]); sb.Append(number[i]); } return sb.ToString(); }以8613800138000为例补F后变成8613800138000F两两反转后就是6831081000F0不对按每两位再拼接86反转得到6813反转得到3180反转得到0801反转得到1038反转得到8300反转得到000F反转得到F0所以结果是683108108300F0我上面表格写的是68 31 08 10 83 00 F0而这里算出来是68 31 08 10 83 00 F0对上了。注意函数返回的是纯十六进制字符串683108108300F0实际PDU串里前面还要加一个DA长度字节0D。2.3 GSM 7bit编码到底在干什么GSM默认字母表里每个字符占7bit短信最长160字符就是基于这个。但计算机存储的最小单位是8bit所以需要把一串7bit数据“压缩”成8bit字节流。规则是每个字符取低7位从字节流的低位开始依次填入填满8位就生成一个新字节。以hello为例h、e、l、l、o的7位ASCII码分别是0x68、0x65、0x6C、0x6C、0x6F。从h的低7位开始填第一个字节填满h的7位和e的第1位得到0xE8第二个字节填e剩余6位和l的前2位得到0x32以此类推最后得到E8 32 9B FD 46。这个结果和协议规定完全一致。C#实现用位缓冲方式最直观private static string Encode7Bit(string text) { Listbyte output new Listbyte(); int bitBuffer 0; int bitCount 0; foreach (char ch in text) { bitBuffer | (ch 0x7F) bitCount; bitCount 7; while (bitCount 8) { output.Add((byte)(bitBuffer 0xFF)); bitBuffer 8; bitCount - 8; } } if (bitCount 0) { output.Add((byte)(bitBuffer 0xFF)); } StringBuilder sb new StringBuilder(); foreach (byte b in output) { sb.Append(b.ToString(X2)); } return sb.ToString(); }注意输出是十六进制字符串后面需要拼接进PDU串。这里有个容易踩的坑GSM的7bit打包和普通二进制位补齐的方向正好相反是低位在前。如果按常规“高位先行”的思路去写编出来的字节完全不对查错会非常痛苦。2.4 中文走UCS2编码中文在GSM 7bit字母表里没有必须用UCS2即UTF-16大端编码DCS字段写08。C#里直接用Encoding.BigEndianUnicode就能拿到符合要求的字节序列。你好的UCS2十六进制是4F 60 59 7DUDL字段表示用户数据的字节数中文每个字占2字节所以UDL04。完整PDU串如下00 11 00 0D 68 31 08 10 83 00 F0 00 08 00 04 4F 60 59 7D从11到7D数一遍TPDU一共18字节所以ATCMGS18。UCS2编码下短信长度上限是70个汉字这是协议定的程序里要做判断避免超过模块处理范围。3. 实操从串口到短信发出的完整链路3.1 硬件连接与串口参数硬件连接不复杂GSM模块我用过SIM800C、移远EC20接上电源、插上SIM卡模块的串口TX/RX接USB转串口小板接到电脑。模块供电一定要用独立电源或者大电流USB口GSM发射瞬间电流能到2A供电不足会导致模块重启或者发送失败。串口参数一般是波特率9600或115200数据位8、停止位1、无校验具体看模块手册。调试前我习惯先用串口助手手动敲一遍AT指令确认模块能响应、SIM卡能注册网络。注册网络用ATCREG?查询返回CREG: 0,1说明已注册。信号质量用ATCSQ返回值越大越好如果返回99说明无信号。这一步能过滤掉大量硬件问题别上来就直接写代码。3.2 AT指令流程拆解发送一条PDU短信的完整指令序列如下步骤指令期望返回说明1ATOK模块就绪2ATCMGF0OK切换到PDU模式3ATCSCA?CSCA: 8613800xxx500查询短信中心可选4ATCMGS19“”通知模块准备接收PDU5发送PDU十六进制串等待注意不要加回车6发送0x1ACMGS: 1 回车 OKCtrlZ结束发送ATCMGS19后面的回车我用\r不用\r\n部分模块对\r\n敏感。第5步发送PDU时直接Write十六进制字符串不要调用WriteLine因为模块此时处于等待数据状态多出来的\r会被当成PDU数据的一部分。发送完PDU后紧跟0x1ACtrlZ表示结束。C#里核心发送逻辑如下public static bool SendPdu(SerialPort sp, string pduHex) { int tpdulength (pduHex.Length / 2) - 1; // 减去SCA长度字节 sp.DiscardInBuffer(); sp.Write(ATCMGF0\r); // 这里应等待模块返回OK代码从略 Thread.Sleep(200); sp.Write(ATCMGS tpdulength \r); // 等待模块返回提示符 string response ReadLine(sp, 3000); if (!response.Contains()) { return false; } sp.Write(pduHex); sp.Write(\x1A); // 等待最终结果 response ReadUntil(sp, OK, ERROR, 5000); return response.Contains(OK); }cshar ReadLine是我自己封装的逐字符读取方法重点在于超时控制和逐字节处理不能依赖SerialPort.ReadLine因为模块返回的结尾不一定是\r\n有时只有\r。3.3 几个影响稳定性的细节串口接收区要定期清理。GSM模块上电时通常会主动发一段乱码或者RDY之类的字符串如果不清空后面判断返回内容时容易误判。发送节奏要控制不要连续快速发AT指令。模块处理AT指令需要时间每条指令之间至少等200到500毫秒或者严格按返回内容驱动。用固定Sleep做原型没问题但产品化建议改成状态机根据模块返回判断下一步。程序退出或者发送失败时记得发一次ATCMEE1让模块返回具体的CMS错误码方便定位。4. 常见问题与排查技巧实录4.1 发送返回ERROR的定位顺序PDU发送返回ERROR是新手遇到最多的问题。我总结的定位顺序是检查串口参数确认模块本身能正常响应AT确认ATCMGF0设置成功Text模式下发送PDU会报错检查ATCMGS后面的TPDU长度是否算对多1少1都不行检查PDU串有没有包含SCA长度字节导致偏移检查目标号码编码是否规范号码位数奇偶和补F是否处理正确。经验是先用一个已知正确的例子比如上面hello那条跑通再改成自己的内容。自己拼的PDU一旦出错光看字符串很难肉眼发现。4.2 中文乱码的两种根源中文乱码就两个原因。第一DCS字段没写08模块仍按7bit解码自然乱码。第二pdu_class生成的UCS2字节序错了应该大端结果用成了小端。我记得有一次我用Encoding.Unicode去转直接翻车因为Windows上Unicode默认是UTF-16LE必须显式用Encoding.BigEndianUnicode。这两个原因在代码里各留一行注释就不会反复踩。4.3 短信中心号码的坑SCA字段写00是让模块用SIM卡内置短信中心这是最稳的。如果硬编码短信中心号码要注意不同运营商的号码不一样而且同一运营商在不同地区可能还有差异。万一号码写错模块能连上网络但短信就是发不出去表现为发送成功后对方收不到。排查办法是ATCSCA?查询当前SIM卡的短信中心然后和代码里的对比。4.4 160字符限制与长短信处理7bit编码上限160字符UCS2上限70个汉字这是GSM协议定的。业务上超过这个长度要么自动截断并提醒要么做长短信拼接。长短信要用TP-UDHI字段把用户数据拆成几段每段前加一个6字节的用户头包含参考号、总段数、当前段序号。这个功能不是每个模块都支持得好我自己做的时候优先保证单条短信稳定长短信只做了协议层面的封装实际交付时还是建议业务侧主动拆分内容。4.5 问题排查速查表现象可能原因处理办法AT无响应串口参数错/模块没供电检查接线、波特率、独立供电ATCSQ返回99SIM卡没插好或没信号重插SIM卡、检查天线发送返回CMS ERRORPDU长度算错/短信中心异常按正文顺序逐项检查发送成功但收不到短信中心号码硬编码错误用SCA00或ATCSCA?查询中文乱码DCS没写08/字节序错改DCS用BigEndianUnicode刚上电发送失败模块未完成注册上电后等2-3秒再操作最后说一个我自己的习惯。pdu_class这种工具类不要一次想塞太多功能进去。我重写第三版的时候才把7bit和UCS2的自动切换做干净核心逻辑控制在两百行以内。每加一个功能都用固定PDU样例做回归避免改一处带崩一片。这套东西在几个无人值守项目里跑了好几年一年到头也就SIM卡欠费这种故障。如果你也在搞C#串口通信或者设备告警系统希望这篇能帮你少走点弯路。本文还有配套的精品资源点击获取
返回列表