EC20 模块+Issabel 实现网络电话

EC20 模块+Issabel 实现网络电话

Hanako

作为一个卡粉+赛博世界游民(不是),拥有一堆各国手机号码是基本的素养,这就导致我手里的各种卡变得非常的多,同时管理 114514 个号码就成了难题。

我刚开始用的是 pppscn/SmsForwarder ,通过 Telegram 转发短信到我手里。但是这套方案用下来有几个缺点:

  1. 只能接收短信,不能接打电话;
  2. 50 元以下的能接收短信的手机就算是双卡也只能是 4G+2G 双卡双待,然而联通 2G 基本退网,相当于单卡手机;
  3. 不利于远程控制、集中管理。

也想过猫池,但鱼上看了一圈,这价格还是算了。

之前做过需要 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 是可以用,但是移远开灵车有什么办法,不知道是不是联通特色。

据上面的文章说可能是针对物联网卡的配置,可是能打电话的物联网卡我真没听说。

  1. 打开 IMS:AT+QCFG="ims",1
  2. 关闭自动选择mbn文件:AT+QMBNCFG="AutoSel",0
  3. 反激活当前的mbn:at+qmbncfg="deactivate"
  4. 强制选择3gpp:AT+QMBNCFG="select","ROW_Generic_3GPP"
  5. 重启: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 的情况下,对方会听到吱吱的噪音,非常影响通话。

1
AT+QCFG="usbcfg",0x2C7C,0x0125,1,1,1,1,1,0,1

倒数第二个参数是启用 adb,倒数第一个参数是启用 UAC。

激活之后可以通过 adb devicesaplay -L 查看有没有一个android设备。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
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。

安装本体

官方提供了安装脚本,一行命令解决:

1
curl http://repo.issabel.org/install-debian-install_amp | bash

该脚本会安装 Issabel 和 Asterisk 16 及其相关依赖,Issabel 默认占用 80 端口和 443 端口,请保持畅通,安装后可修改。

安装移远驱动

1
2
3
4
5
6
7
8
9
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,将最后四行的注释去掉:

1
2
3
4
5
[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 ,添加以下内容:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
[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 组:

1
2
3
4
5
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,填写后会自动识别使用的协议。给自己打个电话就可以测试了。

短信转发

拿上面博主的脚本稍微改了一下,添加了自动重试。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
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 的时候可没这个想法。

  • 标题: EC20 模块+Issabel 实现网络电话
  • 作者: Hanako
  • 创建于 : 2023-10-22 01:49:33
  • 更新于 : 2023-10-22 22:49:18
  • 链接: https://hanako.me/ec20_issabel.html
  • 版权声明: 本文章采用 CC BY-NC-SA 4.0 进行许可。