ARTICLE DETAIL

资讯详情

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

LuatOS短信库实战:物联网设备短信兜底告警与远程命令全攻略

LuatOS短信库实战:物联网设备短信兜底告警与远程命令全攻略 前阵子调试一台装在配电箱里的采集终端趁周末没人我往SIM卡里塞了张测试卡然后花了一下午把LuatOS的sms库整个调通了。后来正式上线的30多台设备全部配了“短信告警远程命令”这条兜底通道。不少人觉得物联网项目里短信是上个世纪的产物但真到了现场才发现设备断网、后台域名解析失败、TCP连接死活建不起来的时候短信反而是唯一能“隔着墙敲一下”的通道。LuatOS的sms库就是干这件事的。它把蜂窝模组Cat.1/2G的短信收发能力封装成了Lua API不需要你去折腾PDU编码、AT指令直接调用函数就能发短信、收短信、管理SIM卡里的短信存储。这篇文章我把从发送、回调、存储到排查的完整经验写出来适合正在做LuatOS二次开发、或者准备给设备加短信兜底功能的同学。代码可以直接抄坑也尽量帮你们提前踩掉。1. 一条短信的“算法”为什么物联网项目仍需要短信兜底先别急着看API先想清楚短信在设备方案里的定位。很多开发者习惯把所有通信都交给TCP长连接或HTTP轮询但如果你的设备可能在弱网环境工作比如山区、地下室、配电房或者部署在运营商覆盖不稳定的区域那TCP断线重连的体验可能非常痛苦。短信的底层走的是蜂窝网络的信令通道它和有没有建立数据连接、域名能不能解析、服务器通不通完全无关。只要模组能注网短信基本就能发出去。适用场景设备异常告警温度越限、电压异常、设备离线发一条短信给运维人员。远程指令控制白名单号码发送特定指令设备执行重启、恢复出厂、上报定位等操作。注册/绑定流程某些行业设备没有屏幕没有按键用短信验证码完成信息绑定。平台侧兜底设备周期上报的同时只在关键事件时用短信作为第二通道。不适合的场景也要说清楚高频数据上报短信有资费、有频率限制不适合代替数据通道。强实时性控制短信的端到端延迟可以从几秒到几分钟不能用于毫秒级控制。大流量内容单条短信最多几百字节传不了文件。我一般把短信定位成“最后一道保险”。实际项目中设备优先走MQTT/NTRIP等数据通道只有在通道异常或者发生关键事件时才触发短信。这样做的好处是数据通道故障时运维人员还能收到一条“设备离线”的短信及时介入而不是等用户投诉。另外要提醒一点sms库只适用于带蜂窝通信能力的LuatOS模组比如Air724UG、Air780E这类Cat.1/2G方案。ESP32模组没有SIM卡槽根本没有短信能力别浪费时间折腾。选型前务必确认模组是否支持蜂窝通信。2. sms库API全景文档容易忽略的几个关键行为LuatOS的sms库函数不多但每个函数的行为都有好几个坑。下面这些函数以我目前用的固件为蓝本不完全一定和你手上的版本逐一对应。一切以LuatOS仓库里的官方文档为最终依据。我在代码注释里尽量标清楚“哪个版本差异比较大”避免你踩到版本坑。2.1 发送函数send与sendNew-- 发送短信最基础的用法 local ok sms.send(13800000000, hello luatos) if ok then log.info(sms, send submit ok) else log.error(sms, send failed) endsms.send(phone, content)是发起发送动作返回值“成功”只代表短信已经提交给模组的协议栈不代表对方已经收到。这个认知非常重要否则你会在调试时被假象迷惑接口返回成功但手机就是收不到最后查出来是短信中心号码配置有问题或者SIM卡欠费。旧版本LuatOS还有一个sms.sendNew功能和send类似但有细微差别主要是入队方式和长短信处理逻辑上的差异。新固件里send基本已经覆盖了sendNew的能力。我的建议是只依赖sms.send并且把返回值当作“提交是否被接受”来看待。2.2 接收回调setNewSmsCallbacksms.setNewSmsCallback(function(phone, content, smsId) log.info(sms, new sms from:, phone, content:, content, id:, smsId) end)新短信到达时底层会触发这个回调。重点在于phonecontentsmsId这三个参数在不同固件版本里的顺序和字段名基本是统一的但我见过有modem平台在部分固件版本里多出一个参数比如短信时间戳。保险做法是回调第一行先log.info把arg全部打出来确认参数结构再写业务逻辑。回调是异步事件驱动的不是Lua协程逐行执行那么简单。不要在回调里做耗时操作比如发TCP请求、读写文件、执行复杂的加密逻辑。回调触发时LuatOS底层通常处于事件派发上下文中过长的处理会影响其他Lua任务的调度。正确做法是回调里只做数据搬运把接收到的内容通过sys.publish(SMS_NEW, phone, content)这样的方式发给专门的任务去处理。2.3 读取与删除getAllSms、getSmsNumber、deleteSmsSIM卡内部其实是一个小型的短信存储区LuatOS把这部分能力也开放了出来。-- 获取所有短信返回一个数组 local all sms.getAllSms() if all then for _,item in ipairs(all) do log.info(sms, phone:, item.phone, content:, item.content, index:, item.id) -- 处理完就删除避免SIM卡存储满 sms.deleteSms(item.id) end end -- 获取当前SIM卡短信条数部分版本有 local used, total sms.getSmsNumber() log.info(sms, used:, used, total:, total)getAllSms如果不传参数通常返回SIM卡里所有短信记录部分版本支持传过滤条件比如只看某个号码。字段名id在有的版本里叫index或者idx这种情况下打印item字段即可。删除短信是必须养成习惯的操作。SIM卡的短信存储空间非常小通常只有几十条满了之后新短信可能直接被网络侧拒收或者底层收下但无法保存回调也不会触发。我看到很多项目只收短信不删短信跑几个月后设备“突然收不到短信了”一查存储满就是这个原因。2.4 一个容易忽略的点短信中心号码短信中心号码SMSC一般由SIM卡配置文件自动下发不需要开发者设置。但物联网卡经常出现“能上网不能发短信”的情况很大概率就是卡商没有在卡里写短信中心号码。排查方法很简单用串口工具连上模组的AT口执行ATCSCA?查询当前短信中心。如果返回空或报错手动执行ATCSCA8613800755000之类设置后再试。LuatOS底层没有把sms.setSmsCenterNumber暴露成常规API所以这个操作更多是AT阶段的调试手段调试完也不建议在业务代码里频繁调用交给卡商解决更省事。3. 可直接“抄作业”的短信收发脚手架光罗列函数不够我给一个实际项目里验证过的脚手架。这个结构解决的是三个问题发送任务不能阻塞主流程、接收回调不能做耗时操作、短信存储需要定期清理。下面是我在Air780E模组上跑通的代码。3.1 为什么我用这种结构组织短信任务我见过很多开发者直接在一个定时器里循环sms.send短时间发几十条测试短信结果后续发送失败率飙升。原因是模组自带短信发送队列深度很浅满的时候再提交会被协议栈拒绝。所以我用任务队列的方式把发送请求串行化处理每条短信之间留出间隔确保发送稳定。回调部分也一样收到短信之后先发一个Lua消息唤醒一个专门的任务去执行真正的工作始终保持回调函数“秒级返回”。最后每次开机后清理一次SIM卡里的历史短信避免存储耗尽。3.2 完整示例代码与运行效果local sms require(sms) -- 有些固件sms是隐式加载的无需require local whitelist { [13800000000] true, [13900000001] true, } local function handle_new_sms(phone, content) -- 在这里执行真正的业务逻辑比如解析指令 if not whitelist[phone] then log.warn(sms, unknown phone:, phone) return end if content reboot then log.info(sms, do reboot now) rtos.reboot() elseif string.match(content, ^report$) then -- 上报当前设备信息 sms.send(phone, device online, version: 1.0.0) end end sys.taskInit(function() -- 开机注册回调 sms.setNewSmsCallback(function(phone, content, smsId) log.info(sms, recv:, phone, content) sys.publish(SMS_PACK, phone, content) end) -- 开机清理历史短信 sys.wait(1000) local all sms.getAllSms() if all then for _,item in ipairs(all) do log.info(sms, clean old sms:, item.id) sms.deleteSms(item.id) end end end) -- 独立任务处理短信业务 sys.taskInit(function() while true do local result sys.waitUntil(SMS_PACK, 5000) if result then local phone, content table.unpack(result, 1, 2) handle_new_sms(phone, content) end end end) -- 示例外部任务需要发短信时统一走这个接口 local function send_sms_queue(phone, content) sys.taskInit(function() sys.wait(100) local ok sms.send(phone, content) log.info(sms, send:, phone, ok) end) end -- 业务侧调用 send_sms_queue(13800000000, device offline, code0x01)这个脚手架里有个细节sys.waitUntil(SMS_PACK, 5000)带超时因为消息在事件循环里以广播形式派发用waitUntil拿到的是匹配消息的第一个返回值组table.unpack时注意别越界。还有一个经验是回调里先做号码白名单过滤再发消息如果直接合法性判断放在业务任务里会导致非白名单号码的短信也被存入SIM卡占用存储。3.3 首次调通必做的三件事我每次在新硬件上跑这个代码都会按下面三步检查能省下大量排查时间确认SIM卡能被识别用mobile库的mobile.getImei()和sim.getIccid()检查至少确保卡已经注册。没有ICCID直接排查卡槽接触问题。用真机回环测试两个LuatOS设备互发短信或手机发短信给设备、设备发短信给手机。两台设备互发的好处是能同时验证发送和回调两条链路。回调参数打印新固件上先不打任何过滤把手机上发来的短信所触发的回调参数完整打出来确认字段顺序和类型。我第一次在一个量产板上跑就是漏了第三步想当然认为回调第二参数是短信内容结果收到的是短信时间戳排错排了半天。所以每到一个新固件务必“先打印后写逻辑”。4. 从“发不出去”到“收不到回调”的排查链路这个章节写给那些短信功能做完但实际“时灵时不灵”的朋友。我整理了一套完整的排查链路每一步都对应着一个真实踩过的坑。4.1 发送成功但对方收不到如果sms.send返回成功但手机始终收不到按以下顺序排查SIM卡是否欠费或停网物联网卡尤其常见后台显示正常但你拨打短信中心总是失败。短信中心号码是否配置执行ATCSCA?返回CSCA: 8613800755000,145之类才正常。空值就补。设备是否真正注网看mobile库的注网状态和信号强度弱信号环境下短信极不稳定。接收端是否屏蔽部分手机/平台会自动拦截陌生号码短信测试时先用白名单号码。这里有一个调AT指令的小技巧LuatOS调试口通常是串口AT口或虚拟AT口我用串口助手直接执行ATCSQ看信号强度ATCIMI看卡识别ATCSCA?看短信中心。这几条AT指令基本能覆盖80%的发送问题。4.2 手机发来短信但回调不触发这个更隐蔽因为设备侧看起来“一切正常”但就是没有日志输出。可能原因和排查方法没有注册回调参考上一章节setNewSmsCallback必须在模组开机注网前就调用否则新短信事件会被丢掉。SIM卡短信存储已满顶栏getSmsNumber确认使用条数赶紧清理历史短信。对新短信类型不支持部分平台下发短信是Flash短信Class 0或特殊编码模组底层不一定触发setNewSmsCallback需要AT调试确认ATCNMI的新消息指示配置。回调代码抛异常被吞Lua脚本里回调函数如果中途报错且没有做pcall保护事件会中断。回调第一行打日志最可靠。我还遇到过一次特别内伤的案例某设备在回调里写了string.find(phone, 10086)结果回调正常触发了但phone参数是带国家码的8610086匹配不上后续代码直接走else分支啥也没干。所以回调参数打印这个习惯真的能救命。4.3 排查工具箱能用到的AT指令与log结合LuatOS的log.info以及AT调试口我总结了下面这张表遇到问题先对照一遍现象检查命令/操作说明发送返回失败ATCSQ信号强度至少10以上弱信号时发不出去发送返回成功但收不到ATCSCA?短信中心号码是否存在发送返回成功但收不到ATCNMI?新消息指示是否开启影响发送状态报告收不到回调ATCPMS?检查SIM卡存储状态used条数是否接近上限收发频率不对串口log观察确认LuatOS固件版本和基带固件版本匹配这些AT指令本身不是LuatOS API但在调试阶段很有用。生产代码里不需要集成AT命令底层已经帮你处理了我只是建议你在“功能异常”时把它们当作探针来用。5. 真机上线的注意点存储、编码、资费与安全调试通过只是第一步我见过太多项目在实验室跑得好好的一到现场就出问题。这一章全部是上线前必须考虑的细节。5.1 中文/长短信的编码边界LuatOS的sms.send会自动处理PDU编码开发者不用关心底层但需要了解短信长度计算逻辑纯英文数字单条约160字符超过部分会被拆成多条。包含中文或非GSM字符单条约70汉字超过部分会被拆成多条。某些特殊符号比如“囧”“”可能导致整条短信改用UCS2编码容量直接减半。我的建议是所有业务短信都主动做内容截断local MAX_GSM_LEN 160 local MAX_UCS2_LEN 70 local function truncate_content(content) if #content MAX_GSM_LEN then return string.sub(content, 1, MAX_GSM_LEN - 3) .. ... end return content end注意#content按字节数计算中文和UTF-8编码下长度计算要额外小心。更稳妥的方式是按字符数截断可以引入utf8库处理。我在实际项目中直接把异常信息压缩成编码字符串再发送比如“OVR_TMP 82.5C”能显著降低长短信拆条率和资费开销。5.2 SIM卡存储的“过期清理”策略前面提到过SIM卡存储空间极小这里给出一个可落地的清理策略开机时清理一次之后每次收到新短信后只保留白名单号码的已读内容其他一律删除。如果短信里需要保存关键数据比如告警时间点把数据提取到文件系统或数据库存储而不是留在SIM卡里。有一款物联网卡厂商提供的SIM卡短信存储上限是50条我在压测时连续发了40条告警第41条开始短信发送成功率直线下降。从那以后我在代码里做了一个防护每次发送前先检查短信条数超过阈值先删除已读短信再发。5.3 安全策略白名单、防重放与二次确认短信通道天然不具备加密性任何知道设备号码的人都能往里发短信。如果设备支持短信远程指令务必要做安全设计号码白名单只响应预设的手机号其他号码一律忽略。指令鉴权在短信内容中带上动态口令比如设备当前时间/序列号生成的哈希值。防重放执行完指令后立刻回复一条确认短信并把指令时间戳记录下来同一指令短时间内重复执行只生效一次。不执行危险操作远程格式化、擦除配置这类高危命令至少要双重确认比如连续两条短信或者短信内容包含两层校验字段。我在某次演示项目中做了这样一个流程白名单号码发送“reboot abc123”设备校验abc123是预生成的当天动态口令后重启重启完成后再向该号码发送“reboot ok”。整个过程既演示了API发送能力也示范了安全交互设计。5.4 资费别忽略这个隐形成本短信资费虽然看起来便宜但设备量上来之后也不容小觑。我算过一笔账如果300台设备每天各发两条告警短信按单条0.05元算一个月就是900元。这个成本得在产品设计阶段就纳入考虑。省钱策略只有两条减少发送频率和聚合内容。比如一台采集终端可以先在本地重试两次数据上报确实失败后再触发告警短信同时把最近一个小时的错误码打包成一条短信。这样既保证兜底能力又不会每天轰炸运维人员。6. 我的经验清单最后再交代几句做了这么多设备短信方案我最大的体会是短信不是替代品而是救命通道。它频率低、容量小、有延迟但它在关键时候能打通一条主流数据通道打不通的路。LuatOS把短信封装的足够简单真正考验人的是对异步回调、SIM卡存储、编码和资费的理解。最后分享一个小技巧如果你手头没有手机号专门做测试可以准备一张不用的卡把卡插在一个支持短信功能的普通手机上和LuatOS设备互发短信。用普通手机做接收端再配合一台运行着类似脚本的开发板做发送端你就能在没有运营商短信平台的情况下完整验证收发链路。这对调试“为什么收不到回调”特别有效。另外一个细节短信内容里的空格和换行在部分运营商会做特殊处理所以业务指令解析时记得先string.gsub(content, %s, )把空白字符全部去掉再和预设指令做匹配。我因为这个原因丢过的指令至少三次。LuatOS的sms库文档其实很短但它背后的蜂窝短信通信体系很庞大。希望这篇经验整理能让你少走我走过的弯路。如果你用的固件函数签名和本文有出入优先以官方仓库的最新文档为准毕竟不同模组和基带版本之间的差异确实没法在一篇文章里完全覆盖。
返回列表