EC20 模块+Issabel 实现网络电话
使用移远 EC20 模块与 Issabel 搭建集中管理的网络电话和短信转发服务。
作为一个卡粉+赛博世界游民(不是),拥有一堆各国手机号码是基本的素养,这就导致我手里的各种卡变得非常的多,同时管理 114514 个号码就成了难题。
我刚开始用的是 pppscn/SmsForwarder ,通过 Telegram 转发短信到我手里。但是这套方案用下来有几个缺点:
- 只能接收短信,不能接打电话;
- 50 元以下的能接收短信的手机就算是双卡也只能是 4G+2G 双卡双待,然而联通 2G 基本退网,相当于单卡手机;
- 不利于远程控制、集中管理。
也想过猫池,但鱼上看了一圈,这价格还是算了。
之前做过需要 4G 联网的物联网项目,买过移远的模块,就上天猫的移远旗舰店走了一圈,可惜客服说的支持语音的最便宜的模块还挺贵的,只好作罢。
后来找到这两篇文章:
看起来还不错,而且鱼上找到了一家卖 48 块钱一片的 EC20CEFAG-512-SGNS 的店,相当于单卡成本 48(模块)+12(MiniPCIE转接板)+6(天线)=66 元 ,比较划算,直接安排。
他们使用的 PBX 是 FreePBX,于是我在跟着他们成功地领略了 FreePBX Distro 遥遥领先的 CentOS 7 + Kernel 3.10 + 超多商业广告糊脸之后格盘了。上述第一篇文章中提到的 FreePBX Distro 不支持 UAC(USB Audio Class)就是因为 Kernel 3.10 不支持 UAC。
了解到 Issabel,它是开源 PBX Elastix 的分支,由用户/开发人员社区在 3CX 收购 Elastix ,关闭社区,停止分发和开发开源版本并发布专有版本后开发。IssabelPBX 2 使用经现代化调教的 FreePBX 面板,风格比 FreePBX 更简洁、美观,而且没有商业广告和多余的插件,官方提供了安装脚本;Issabel 4 综合了服务器管理、邮件服务器、传真服务器等,官方也提供了脚本,但必须安装在 CentOS 7 上。这两个版本都在更新。
真不懂 CentOS 7 有什么好的。反正 IssabelPBX 2 够用。
配置模块
如果是在 Windows 下配置,需要先安装移远的驱动 。
驱动安装成功后,模块释放出 4 个 ttyUSB 端口:
| 接口 | 描述 |
|---|---|
| /dev/ttyUSB0 | |
| /dev/ttyUSB1 | PCM 语音,GPS 信号 |
| /dev/ttyUSB2 | AT 控制命令 |
| /dev/ttyUSB3 |
这里 提供了移远 EC20 系列的 AT 命令文档。
重置模块
重置命令为 at+qprtpara=3,以防前主人错误调教;重置后需要AT+CFUN=1,1重启。
启用 VoLTE
按理来说自带的 VoLTE 是可以用,但是移远开灵车有什么办法,不知道是不是联通特色。
据上面的文章说可能是针对物联网卡的配置,可是能打电话的物联网卡我真没听说。
- 打开 IMS:
AT+QCFG="ims",1 - 关闭自动选择mbn文件:
AT+QMBNCFG="AutoSel",0 - 反激活当前的mbn:
at+qmbncfg="deactivate" - 强制选择3gpp:
AT+QMBNCFG="select","ROW_Generic_3GPP" - 重启:
AT+CFUN=1,1
重启完了以后,可以使用 AT+QCFG="ims" 查询 VoLTE 激活状态。 如果是 +QCFG: "ims",1,1,就代表 VoLTE 已经启用并激活, 如果是 +QCFG: "ims",1,0 就代表 VoLTE 启用了但没有激活, 如果是 +QCFG: "ims",0,0 则代表 VoLTE 没有启用。
启用 UAC
不知道什么情况,我的模块在不开启 UAC 的情况下,对方会听到吱吱的噪音,非常影响通话。
AT+QCFG="usbcfg",0x2C7C,0x0125,1,1,1,1,1,0,1
倒数第二个参数是启用 adb,倒数第一个参数是启用 UAC。
激活之后可以通过 adb devices 或 aplay -L 查看有没有一个android设备。
root@ayaka:~# adb devices
List of devices attached
(no serial number) device
root@ayaka:~# aplay -L
......
hw:CARD=Android,DEV=0
Android, USB Audio
Direct hardware device without any conversions
plughw:CARD=Android,DEV=0
Android, USB Audio
Hardware device with all software conversions
default:CARD=Android
Android, USB Audio
Default Audio Device
sysdefault:CARD=Android
Android, USB Audio
Default Audio Device
front:CARD=Android,DEV=0
Android, USB Audio
Front output / input
surround21:CARD=Android,DEV=0
Android, USB Audio
2.1 Surround output to Front and Subwoofer speakers
surround40:CARD=Android,DEV=0
Android, USB Audio
4.0 Surround output to Front and Rear speakers
surround41:CARD=Android,DEV=0
Android, USB Audio
4.1 Surround output to Front, Rear and Subwoofer speakers
surround50:CARD=Android,DEV=0
Android, USB Audio
5.0 Surround output to Front, Center and Rear speakers
surround51:CARD=Android,DEV=0
Android, USB Audio
5.1 Surround output to Front, Center, Rear and Subwoofer speakers
surround71:CARD=Android,DEV=0
Android, USB Audio
7.1 Surround output to Front, Center, Side, Rear and Woofer speakers
iec958:CARD=Android,DEV=0
Android, USB Audio
IEC958 (S/PDIF) Digital Audio Output
dmix:CARD=Android,DEV=0
Android, USB Audio
Direct sample mixing device
usbstream:CARD=Android
Android
USB Stream Output
安装 Issabel
官方网站的 ISO 是 Issabel 4,系统是 CentOS 7,我选择在纯净的 Debian 11 上安装 IssabelPBX 2。
安装本体
官方提供了安装脚本,一行命令解决:
curl http://repo.issabel.org/install-debian-install_amp | bash
该脚本会安装 Issabel 和 Asterisk 16 及其相关依赖,Issabel 默认占用 80 端口和 443 端口,请保持畅通,安装后可修改。
安装移远驱动
git clone https://github.com/IchthysMaranatha/asterisk-chan-quectel
cd asterisk-chan-quectel
./bootstrap
./configure --with-astversion=16
make
make install
cp uac/quectel.conf /etc/asterisk
编辑 /etc/asterisk/quectel.conf,将最后四行的注释去掉:
[quectel0]
audio=/dev/ttyUSB1 ; tty port for Audio, set as ttyUSB4 for Simcom if no other dev present
data=/dev/ttyUSB2 ; tty port for AT commands;no default value
quec_uac=1 ; Uncomment line if using UAC mode
alsadev=hw:CARD=Android,DEV=0 ; Uncomment if using UAC, set device name or index as reqd
编辑 /etc/asterisk/extensions_custom.conf ,添加以下内容:
[incoming-mobile]
exten => sms,1,Verbose(Incoming SMS from ${CALLERID(num)} ${BASE64_DECODE(${SMS_BASE64})})
;store
exten => sms,n,System(echo '${STRFTIME(${EPOCH},,%Y-%m-%d %H:%M:%S)} - ${QUECTELNAME} - ${CALLERID(num)}: ${BASE64_DECODE(${SMS_BASE64})}' >> /var/log/asterisk/sms.txt)
;for tg bot use
;exten => sms,n,System(echo '${STRFTIME(${EPOCH},,%Y-%m-%d %H:%M:%S)} - ${QUECTELNAME} - ${CALLERID(num)}\n${BASE64_DECODE(${SMS_BASE64})}' >> /var/log/asterisk/unread_sms/${STRFTIME(${EPOCH},,%Y%m%d%H%M%S)}-${CALLERID(num)}.txt)
exten => sms,n,Hangup()
exten => ussd,1,Verbose(Incoming USSD: ${BASE64_DECODE(${USSD_BASE64})})
exten => ussd,n,System(echo '${STRFTIME(${EPOCH},,%Y-%m-%d %H:%M:%S)} - ${QUECTELNAME}: ${BASE64_DECODE(${USSD_BASE64})}' >> /var/log/asterisk/ussd.txt)
exten => ussd,n,Hangup()
exten => _.,1,Set(CALLERID(name)=${CALLERID(num)})
exten => _.,n,Goto(from-trunk,${EXTEN},1)
短信默认保存到 /var/log/asterisk/sms.txt;如果需要转发到 Telegram,就把 ;for tg bot use 下方的一行的注释去掉,并创建 /var/log/asterisk/unread_sms/ 文件夹,用户组和所有者都设为 asterisk。
设备权限配置
asterisk 用户默认没有访问 /dev/ttyUSB* 的权限。通过 stat /dev/ttyUSB2 命令可以看到这些设备属于 dialout 组:
root@ayaka:~# stat /dev/ttyUSB2
文件:/dev/ttyUSB2
大小:0 块:0 IO 块:4096 字符特殊文件
设备:5h/5d Inode:479 硬链接:1 设备类型:bc,2
权限:(0777/crwxrwxrwx) Uid:( 0/ root) Gid:( 20/ dialout)
需要将 asterisk 用户加入 dialout 组,否则启动 Asterisk 时会不断报告 Permission Denied。
如果频繁报告 Device or resource busy ,用 lsof 查看占用:

发现 ModemManager 占用了设备, systemctl disable --now ModemManager 禁用即可。
最后重启 asterisk:
systemctl restart asterisk
配置 Issabel
浏览器输入 Issabel 所在的 IP 地址打开 Issabel 控制台,点击中央的 IssabelPBX Administration 输入用户名 admin 和安装过程中设置的密码登录。

添加分机
Applications-Extensions 添加分机号。
这里选择 IAX2 Device,因为 NAT 配置方便,只需要转发 4569 端口即可;而 SIP 需要转发 5060 和 10000-20000 所有的端口。

右下角点击 Submit 继续。

User Extension 为所指定的用户分机号,Display Name 为显示名,可以在客户端中看到。
Secret 为登录密码,可以自己指定。
其他部分不需要修改,Submit 提交即可。
Issabel 的列表比较隐蔽,在右上角的小按钮;默认为添加页面,我也差点找不到以为出 bug 了。

如果有其它设备,需要逐个添加;如果是 SIP 话机或者内网设备,可以考虑添加 SIP 设备;如果是手机,建议使用 IAX2。
我曾将 SIP 端口 5060 穿透到公网,第三天 VPS 流量没了,一查日志发现 SIP 被人爆破了;IAX2 暂时没出事。
添加中继
Connectivity-Trunks,选择 Add Custom Trunk 。
Trunk Name 随便填,Custom Dial String 填 Quectel/quectel0/$OUTNUM$,直接 Submit。
记得删掉除了刚刚添加的 Trunk 外其它自动生成的 Trunk。
添加响铃组
如果有多台设备,在呼入时要求所有设备同时响铃,其中一台设备接听后其它设备自动停止响铃,需要将这些设备加入 Ring Group。
Applications-Ring Groups 添加响铃组,Ring-Group Number 要独立于其它已添加的分机号,Group Description 随便填,Extension List 用换行符分隔,可以用 Extension Quick Pick 直接选择, Destination if no answer 可以选 Terminate Call 即挂断。
添加入站路由
Connectivity-Inbound Routes 添加入站路由。
Description 随便填,Set Destination 选择 Extension 或者 Ring Group,然后在下方的出口里选择刚添加的。
添加出站路由
Connectivity-Outbound Routes 添加出站路由。
Route Name 随便填,Trunk Sequence for Matched Routes 选刚刚添加的 Trunk,设置一个 X. 作为 Pattern 以匹配所有号码。

测试呼叫
到这里,点击左上角的 Apply Config,然后就可以测试电话了。我使用的是 Zoiper,填写后会自动识别使用的协议。给自己打个电话就可以测试了。
短信转发
拿上面博主的脚本稍微改了一下,添加了自动重试。
import os
import requests
import json
import time
import schedule
from retrying import retry
tg_bot_token = "TOKEN"
sms_log_dir = "/var/log/asterisk/unread_sms/"
tg_send_msg_url = "https://api.telegram.org/bot"+tg_bot_token+"/sendMessage"
tg_receive_msg_url = "https://api.telegram.org/bot"+tg_bot_token+"/getUpdates"
tg_receive_msg_init_count = 0
#iterate all files in sms_log_dir
@retry(stop_max_attempt_number=3)
def receive_sms():
print("recv poll start")
for file in os.listdir(sms_log_dir):
f = open(sms_log_dir+file, "r")
sms_text = f.read()
send_body = {}
send_body['chat_id'] = "CHAT ID"
send_body['text'] = sms_text
requests.get(tg_send_msg_url, json=send_body)
os.remove(sms_log_dir+file)
@retry(stop_max_attempt_number=3)
def update_tg_receive_msg_init_count():
new_msg = requests.get(tg_receive_msg_url).json()
if new_msg['result']:
with open('processed_update_id.txt', 'w') as f:
f.write(str(new_msg['result'][-1]['update_id']))
@retry(stop_max_attempt_number=3)
def send_sms():
print("send poll start")
f = open('processed_update_id.txt', "r")
processed_update_id = f.read()
new_msg = requests.get(tg_receive_msg_url).json()
if new_msg['result']:
for i in new_msg['result']:
if i['update_id'] > int(processed_update_id):
with open('processed_update_id.txt', 'w') as g:
g.write(str(i['update_id']))
if ('text' in i['message']):
command = str(i['message']['text'])
#/sendsms quectel0 10010 cxll
if command.startswith('/sendsms'):
command_list = command.split(' ')
if len(command_list) == 4:
dongle = command_list[1]
dest = command_list[2]
context = command_list[3]
#asterisk -rx 'quectel sms quectel0 10010 "cxll"'
asterisk_command = "asterisk -rx " + "'quectel sms " + dongle + " " + dest + " " + context + "'"
print(asterisk_command)
os.system(asterisk_command)
schedule.every(5).seconds.do(send_sms)
schedule.every(5).seconds.do(receive_sms)
update_tg_receive_msg_init_count()
while True:
schedule.run_pending()
time.sleep(1)
写在最后
这台机器可能会顺便做主要服务器之一使用吧。
之前装 FreePBX 的废物 CentOS 的时候可没这个想法。