Site Overlay

利用树莓派和GSM模块搭建一个短信平台

前言

也许是我比较古板,但是在目前的所有“即时”通信手段中,我一直觉得短信是实时性最强、传输最可靠、最能确保送达的手段(如果不算电话的话),其他的工具,要么像邮件一样依赖推送,如果定时轮询会造成续航难题,要么像微信推送动辄爆炸,而且接口非常不开放,少数比较良心不太反人类的软件比如Telegram,在开放和推送的及时性上倒是足够,但是合法性又有一点问题,所以都不如短信来的直截了当。虽然短信往往不是免费的,但是当你出门在外接到UPS报警的时候,你就知道短信的实时性是多么可贵了(而某些小而美的即时通信软件天天吃通知,果然是食言而肥)。最后,短信还可以根据联系人进行专注模式的分别设置,确保在专注模式下也能及时收到通知。

之前,我一直使用的是Clickatell提供的短信平台服务,主要是因为他是群晖DSM内置的短信API,尽管在极少的情况下有过延迟送达的情况,不过直到去年之前都没有出过什么大岔子,但是去年开始大规模反诈后,由于Clickatell是外国公司,发送短信的手机也都是外国的,难免被误伤,导致有几个月的时间完全接收不到任何短信通知(但钱照扣),尽管最近现象有好转,但是丢包依然严重,已经接近于张小龙吃通知的频率,所以更换方案已经是当务之急。

其他可能的方案

换个接收号码?

由于我有一张KnowRoaming的SIM卡,所以考虑过使用这张卡来收短信,但是一方面似乎是反诈,这张卡的电话和短信功能也在无限接近微信,另一方面,因为是美国号码,美国要求短信网关必须支持双向通信,也就是说我要给美国号码发短信,我自己必须有一个固定的号码来接受他发回的短信,而Clickatell的固定号码月租10美元起步,够我发半年的通知还有余,所以至少从经济上看,这个方案是不划算的。

换个短信平台?

既然国外短信平台不行,那可以换一个国内的平台吗?可惜也不行。事实上,我在Clickatell之前就尝试了阿里云,作为国内平台确实稳定了很多,但是阿里云要求必须预先定义短信模板并审核通过,才能发送短信,而且短信模板中自定义空间极小,根本不可能用来发送通知短信。

所以在对比之下,看起来只有自建一个方式了,正好手头有一张空闲的联通卡,一个空闲的树莓派,只需要再选一个GSM模块就好。

硬件选择

我手头有一个HUAWEI E8372h-155的4G卡托,在之前Zynq + More一文中,已经提及了这款产品只要切换到--huawei-alt-mode模式,就可以直接访问串口,从而通过AT命令控制,但是实际使用中我发现,这款网卡在连接后就会被udev自动切换到以太网卡模式,而一旦切换完成就没法更改模式了,在多次修改udev规则后,仍然没有找到这个网卡对应的规则,只好暂时作罢。

另一个选择则是买一个专门的模块,正好我也觉得树莓派上插个USB设备巨丑无比,网上有很多给这些开发板准备的模块,有不少专门针对树莓派的尺寸和排针进行的设计,在外观和接口方面比USB设备要好不少,在比较后,我选择了微雪提供的基于SIMCom A7670C芯片的LTE模块。

选用微雪的A7670C模块主要有以下几个原因:

  1. 模块直接使用了树莓派的40针GPIO接口,傻子都不会装错(但某个傻子就是装错了烧掉了自己的Pi Zero),而且模块大小和Pi Zero一样,甚至提供了安装孔位可以和Zero叠一起。不过我最后考虑到有线网口,还是用了老树莓派3B。
  2. 这款模块相比其他的GSM模块比如SIM868而言便宜30块,只要133(虽然我记得在芯片大涨价之前好像这类模块只要几十块)而且SIM868里面多出来的GPS功能我也用不到,加钱没有意义。
  3. 这款模块支持LTE网络,虽然仅仅是CAT-1,但是考虑到联通电信可能会在未来逐渐放弃GSM网络(虽然我觉得不太可能),买一个支持LTE的模块在长远上看还是更合适的。

因此,最终的硬件组合为树莓派3B + A7670C LTE模块。

软件方案

Gammu是一个著名的开源手机控制程序,支持控制、访问手机的电话、短信、通讯录等功能,Gammu提供gammu-smsd作为短信收发的守护程序和python-gammu作为Python接口。

在Gammu提供的Python接口的基础上,我选择了Python的Web服务器网关接口(WSGI)模块对其进行包装,以提供一个可供群晖DSM和其他程序调用的API接口。

正式开始

树莓派配置

使用树莓派官方提供的RPi imager烧入镜像,在烧写镜像的时候可以选择提前配置好WiFi、用户密码和SSH,省去修改文件的功夫。

镜像烧完后插上GSM模块和网线,上电开机,根据路由器分配的IP地址SSH到板上,通过sudo raspi-config命令,关闭串口登录,开启串口,这样就可以通过/dev/ttyS0设备访问串口。

也可以通过在SD卡根目录的config.txt中加入enable_uart=1来打开串口。

Gammu连接测试

在开始前,先通过minicom工具直接连接串口测试,A7670C模块默认使用115200N1的参数进行串口通信,在minicom配置好参数后,打开串口,发送AT,如果配置正常,则可以收到OK的回复,证明模块连接没有问题。

AT指令参考:A7600 Series AT Command Manual

然后安装gammu:

sudo apt install gammu gammu-smsd python3-gammu

gammu提供了一个小工具,用于配置连接GSM模块的方式,而配置内容则是保存在用户目录下的.gammurc文件中。普通用户并没有访问串口硬件的权限,因此这里通常有两种做法:一种是将需要访问的用户添加到dialout用户组,授予权限,另一种就是直接用root用户操作,显然,这两者用到的.gammurc文件将是不同的。

需要注意的是,gammu-smsd并不需要此配置文件,因此本节后续步骤仅处于验证目的,可以不做。

我采取了比较简单粗暴的方式:直接使用root用户操作。

首先运行配置工具:

sudo gammu-config

Port中填入/dev/ttyS0Connection选择at115200Model选择at,其余保持原样即可。

配置完成后,输入sudo gammu identify命令,可以看到正确识别到了GSM模块。

gammu-smsd配置

gammu相似,gammu-smsd也是通过一个配置文件/etc/gammu-smsdrc保存配置的,只需要修改文件中[gammu]部分的portconnection就能完成连接。

# Gammu library configuration, see gammurc(5)
[gammu]
# Please configure this!
port = /dev/ttyS0
connection = at
# Debugging
#logformat = textall

现在就可以通过gammu-smsd发送短信了:

sudo gammu-smsd-inject TEXT 123456 -text "Across the Great Wall we can reach every corner of the world."

收信转发到Telegram

虽然我放在模块里面的这张卡理论来说除了高中的时候马超同学用来注册过崩3,就没有其他的用途了,但是有的时候会有一些运营商的消息提醒,比如欠费提示,如果全都放着不管,必然会耽误不少事情,所以有必要将到卡上的通信转发出去。另外,将运营商的定时通知转发出去,也有利于确认整个短信平台正常工作。

电话的呼叫转移理论来说可以通过AT命令办理,但是我在之前就已经在手机上做过了,所以这里只研究如何把短信转发出去。

gammu-smsd支持将收到的短信保存并执行特定的脚本,修改/etc/gammu-smsdrc[smsd]部分,加入一条自定义的RunOnReceive脚本配置:

# SMSD configuration, see gammu-smsdrc(5)
[smsd]
service = files
logfile = syslog
# Increase for debugging information
debuglevel = 0
RunOnReceive=/home/pi/gammu/receive-sms.sh

# Paths where messages are stored
inboxpath = /var/spool/gammu/inbox/
outboxpath = /var/spool/gammu/outbox/
sentsmspath = /var/spool/gammu/sent/
errorsmspath = /var/spool/gammu/error/

这样,当收到新的短信时,gammu-smsd就会自动执行/home/pi/gammu/receive-sms.sh这个脚本内容,按照脚本中的设定处理短信。

我考虑过应当把收到的短信向哪里转发,直接转发短信到主力号码当然是最直观的,但是收到的短信并不算是重要信息,而且短信毕竟是收费的,每天联通发上一次流量提醒,由于短信长度较长,转发一下就是3毛,所以不如发到不算特别实时的Telegram平台,也避免了被早八短信吵醒的问题。

编辑/home/pi/gammu/receive-sms.sh

#!/bin/sh

for i in `seq $SMS_MESSAGES` ; do
          eval "/usr/bin/python3 /home/pi/gammu/tg_send.py Incoming\ SMS\ from\ \"\${SMS_${i}_NUMBER}\"\ to\ YOUR_NUMBER:\  \"\${SMS_${i}_TEXT}\""
done

其中,tg_send.py是我的Telegram机器人发送消息的脚本,用法是python3 tg_send.py <消息前缀> <消息内容>${SMS_${i}_NUMBER}等则是gammu-smsd传给脚本的环境变量,环境变量的信息可以在官方文档的RunOnReceive Directive页面查到。

WSGI接口包装

完整的代码可以在这里查看:https://git.nju.edu.cn/Minaduki/gammu-sms-server/-/blob/master/sms_server.py

gammu-smsd提供的Python接口非常的简单,使用Python发送短信,概括来说只需要三步:

  1. /etc/gammu-smsdrc配置文件构造一个gammu.smsd.SMSD类的对象。
  2. 按照规定的格式,创建字典,作为一条信息。
  3. 调用对象的InjectSMS方法,将一条或多条信息作为数组传入。

因此,构建两个函数,用来完成对象的初始化和短信的包装:

def smsd_init(config):
    return gammu.smsd.SMSD(config)

这个函数输入字符串作为配置文件的位置,返回一个gammu.smsd.SMSD类的对象。

def send_sms(smsd, recipient, content):
    message = {
        'Text': content,
        'SMSC': {'Location': 1},
        'Number': recipient,
        'Coding': 'Unicode_No_Compression',
    }
    smsd.InjectSMS([message])

这个函数输入SMSD对象、收件人、短信内容,会调用对象函数将短信发送出去。

接下来,就需要用WSGI接口来包装这两个函数,WSGI通过make_server命令启动:

port = 5088
httpd = make_server('0.0.0.0', port, application)
httpd.serve_forever()

其中,application对应的就是在收到request之后如何处理的函数,其内容如下:

def application(environ, start_response):
    if (environ['PATH_INFO'] == '/favicon.ico'):
        start_response('404 Not Found', [('Content-Type', 'text/html')])
        return ['File not found.'.encode()]

    # environ是当前请求的所有数据,包括Header和URL,body,这里只涉及到get
    # 获取当前get请求的所有数据,返回是string类型
    params = parse_qs(environ['QUERY_STRING'])

    # 获取get中收件人和短信内容的字段
    recipient = params.get('recipient', [''])[0]
    content = params.get('content', [''])[0]

    if (recipient == ''):
        print('Error: recipient is empty!')
        start_response('400 Bad Request', [('Content-Type', 'text/html'), ('Access-Control-Allow-Origin', '*')])
        return ['Error: recipient is empty!'.encode()]

    if (content == ''):
        print('Error: content is empty!')
        start_response('400 Bad Request', [('Content-Type', 'text/html'), ('Access-Control-Allow-Origin', '*')])
        return ['Error: content is empty!'.encode()]

    start_response('200 OK', [('Content-Type', 'text/html'), ('Access-Control-Allow-Origin', '*')])

    print('To: ' + recipient)
    print('Content: ' + content)

    send_sms(smsd, recipient, content)
    return ['Message appended.'.encode()]

函数会提取请求的字段,并判断收件人和短信内容是否为空,如果不为空,则调用发送函数,否则返回错误。

由于之前测试时,后端接口没有对空的收件人和内容进行检查,导致GSM模块存储了一条待发送的全空的短信,然后gammu就无法获取信息了,不停报错Error getting SMS: Unknown error. (UNKNOWN[27]),反复重置重启也没有用。我最后的方法是使用AT命令AT+CPMS="SM","SM","SM",指定模块使用SIM卡的存储空间存储短信,暂时规避了这个问题。

最后,将Python脚本包装成一个系统服务,以便于开机自启动。编辑/lib/systemd/system/minaduki-sms-server.service

[Unit]
Description=Minaduki's SMS server
After=network.target

[Service]
Type=simple
User=root
Restart=always
RestartSec=5s
ExecStart=/usr/bin/python3 /home/pi/gammu/sms_server.py

[Install]
WantedBy=multi-user.target

然后调用systemd启动服务:

sudo systemctl start minaduki-sms-server.service
sudo systemctl enable minaduki-sms-server.service

这样,整个平台就搭建完成了,只要访问“http://ip:5088/?recipient=收件人&content=短信内容”就可以发送短信。由于只用于内网使用,所以暂时没有添加认证手段,如果日后需要上公网,可以考虑在Header部分使用Token进行认证。

尾声

群晖支持使用自定义的API发送短信,在添加短信服务提供商中,测试网址填入“http://ip:5088/?recipient=你的手机&content=hello world”,HTTP方法选择GET,然后在“编辑HTTP请求标题”步骤留空,在“选择以下网站参数所映射的类型”步骤中,将recipient设置为“电话号码”,content设置为短信内容即可。

要注意的是,测试网址中的content似乎必须填hello world,否则群晖会无法识别。

经过一番折腾,吃灰的树莓派用起来了,好久没有收到的短信通知又有了,虽然现在不住宿舍,已经不太可能有UPS警报,但是总体来说折腾的很开心,还是挺值得的,尤其是考虑到现在树莓派价格离谱,仿佛自己赚到了什么一样。

也许下一步可以把退休的NUC整起来当个HTPC?(不过Xbox不是已经当机顶盒用了,电信还有个IPTV,再加个机顶盒岂不是得哪吒才看的过来)

参考:A7600 Series AT Command Manual
The Gammu Manual — Gammu 1.42.0 documentation
用树莓派做一个短信收发平台 | yGin.pro

发表回复

您的电子邮箱地址不会被公开。 必填项已用*标注

此站点使用Akismet来减少垃圾评论。了解我们如何处理您的评论数据