发布于  更新于 

Grandstream HT813 双模语音网关 FreePBX 配置指北

概述

Grandstream HT813 是一款双模语音网关,支持 FXS 和 FXO 两种接口,可以连接传统的电话机和 PSTN 线路,也可以连接 VoIP 服务器。HT813 最重要的特点是便宜,咸鱼二手价格在 130 元左右,同价位其他语音网关往往只有 FXS 或者 FXO 一种接口。FreePBX 是一款开源的 IP 电话系统,支持 SIP 协议,可以用来搭建企业电话系统。尽管 FreePBX 并不好用(前端卡的离谱),但是免费,而且有大量的社区支持。本文介绍一种使用 HT813 和 FreePBX 搭建简易电话系统的方法,主要列出配置步骤,不涉及具体的技术原理。

我们准备搭建的系统包含以下元素:

  • 有一条中国电信的 PSTN 线路,用于接收来电和拨打外线
  • 有一部传统的电话机,用于拨打和接听电话
  • 有一台运行 FreePBX 的服务器,用于管理电话系统和接收来电
  • 局域网内有若干手机和电脑上的 SIP 软电话,用于拨打和接听电话

希望实现以下功能:

  • 电话机和软电话有独立的分机号,可以互相拨打
  • 电话机和软电话可以通过 PSTN 线路拨打外线
  • PSTN 线路接收到来电时,所有电话机和软电话同时响铃,任意一台设备可以接听
  • PSTN 线路支持来电显示和通话录音

硬件准备

HT813 上共有 5 个接口,分别为:

  • 12V 电源接口
  • WAN 网络接口
  • LAN 网络接口
  • FXS 接口:连接传统电话机
  • FXO 接口:连接 PSTN 线路

HT813 具有 WAN 和 LAN 两个 RJ45 网络接口,自身可作为一台带有 NAT 功能的路由器。在本文中,我们不使用 HT813 的路由功能,将 HT813 的 WAN 接口和 LAN 接口配置成交换机模式,即 HT813 仅作为语音网关使用。HT813、FreePBX 服务器和所有 SIP 设备都连接到同一个局域网中。此模式下,HT813 的任何一个网络接口都可以用来连接局域网,另一个网口可再连接一个局域网内交换机或者其他设备。

HT813 配置

HT813 的 WAN 接口默认通过 DHCP 获取 IP 地址,LAN 接口作为一个 DHCP 服务器,分配 IP 地址给连接到 LAN 接口的设备。WAN 侧可通过路由器管理面板查看 HT813 的 IP 地址,如果二手 HT813 找不到 IP 地址,可以长按 HT813 的 RESET 按钮恢复出厂设置。局域网内浏览器访问 HT813 的 IP 地址,输入默认用户名和密码 adminadmin 登录。

HT813 登陆界面
HT813 登陆界面

信息页

下图是 HT813 的信息页。

HT813 信息页
HT813 信息页

红框内显示 HT813 的接口状态,图中是所有配置完成正常工作的状态。只有 HT813 的 FXS FXO 物理接口已连接,且 VOIP 服务器已注册,前面板的灯才会亮起。FXS 和 FXO 接口的状态可以通过前面板的灯和网页上的状态显示来判断。

基本设置页

主要是网络配置和无条件呼叫转移设置。

上网设置
上网设置

按照需要配置成 DHCP 客户端或静态 IP 地址。由于后续需要在 FreePBX 中配置 HT813 的 IP 地址,建议设置成静态 IP 地址,或在路由器中配置 DHCP 绑定。

语言和时区
语言和时区

建议设成英文,网上的教程大多是英文的。

NAT 模式
NAT 模式

不需要 NAT 功能,设成桥接模式。

无条件呼叫转移
无条件呼叫转移

为了把外线呼入转移到 FreePBX,需要设置无条件呼叫转移。User ID 编一个特殊的号码,如 0123456789,SIP Server 设置成 FreePBX 的 IP 地址。

改完配置后,点 Update 保存,点 Apply 生效, 部分配置按照提示需要重启 HT813 才能生效。

高级设置页

没有需要特别设置的选项。

备份和恢复
备份和恢复

最下面的备份和恢复功能可以备份 HT813 的配置,以防止配置丢失。注意只能导入 XML 格式的配置文件,如果要导入来自其他 HT813 的配置,需要把 XML 文件中的 MAC 地址改成当前 HT813 的 MAC 地址或者删掉 MAC 地址那一行。

Extension 和 FXS 配置

本例中 HT813 的 FXS 接口连接到传统电话机,在 FreePBX 侧看起来和一个 SIP 软电话一样是一个 Extension。

FreePBX 安装后似乎会创建一个默认的 Extension,没有截图。总之最后的结果是,我们需要在 FreePBX 中创建一个分机号为 1000, 用户名是 landline,类型为 PJSIP 的 Extension。

Extension 管理面板
Extension 管理面板

注意要创建一个 Extension,不是 User Manager

不管 User,没用
不管 User,没用
1000 分机详情页面
1000 分机详情页面

分机设置都是默认的,Secret 是 FreePBX 自动生成的密码,后续需要在 HT813 中配置这个密码。创建完分机后,左下角 Submit 保存,再点右上角红色 Apply Config 生效。

HT813 的 FXS 接口配置页面:

重试超时可以改小一点,方便调试。

端口号要改一下,防止和 FXO 的端口号冲突。

只留 PCMU 和 PCMA,其他的编码格式不需要,多了似乎会导致不出声音。

这两个地区选项可以改一下,这一页改完后点 Update 保存,再点 Apply 生效。

以上配置完成后,HT813 的 FXS 指示灯应该亮起,状态页面上显示 FXS 接口已注册。

添加 SIP 用户

再添加其他 SIP 用户,如手机上的软电话,步骤和添加 FXS Extension 类似,类型,默认 PJSIP,分机号和用户名可以随便编(最好有规律,比如 10 开头,后面会用到)。

SIP 客户端以 Linphone 为例,配置如下:

这样 SIP 客户端之间,以及 SIP 客户端和 HT813 的 FXS 接口上的固定电话之间就可以互相拨打电话了。

Trunk 和 FXO 配置

FXO 接口在 FreePBX 中作为一个 Trunk。

编个密码,后面 FreePBX 配置时会用到。

重试超时改小一点。

注意 FXO 默认端口号是 5062.

不知道为啥要改这个,反正改了。

只留 PCMU 和 PCMA。

不知道为啥要改增益,反正改了。

PSTN 挂断检测
PSTN 挂断检测

注意: 中国大陆的用户需要修改 FXO 挂断检测选项,否则不能检测到外线挂断,导致外线挂断后 FreePBX 持续响铃。此处不使用基于电流的挂断检测,而是使用基于忙音的挂断检测。按照上图的配置,中国大陆标准的忙音频率是 450Hz 单音,半周期为 350ms.

在 FreePBX 中添加一个 Trunk:

Trunk 配置页面
Trunk 配置页面

创建一个 pjsip 类型的 Trunk。

这里呼出号码前加 0,这是 HT813 的配置要求,拨打外线时要加 0。

这里填 HT813 的 IP 地址,端口号是 HT813 的 FXO 端口号,用户名和密码是 HT813 的 FXO 配置页面里设置的用户名和密码。

Codec 只留下 ulaw 和 alaw 就够了。

为了消除 Console 里的一个警告,需要在 Admin -> Config Edit 里面给 pjsip.aor_custom_post.conf 文件添加一行:

1
max_contacts=1

配到这里两边都应用设置之后,HT813 的 FXO 灯应该亮起,状态页面上显示 FXO 接口已注册。

外线呼入

记得在 HT813 的 Basic Settings 里设置了无条件呼叫转移:

无条件呼叫转移
无条件呼叫转移

为了能让所有电话都响铃,需要在 FreePBX 的 Ring Groups 里创建一个 Ring Group:

把所有电话都加进去,这样外线呼入时所有电话都会响铃。

然后转到 Inbound Routes,添加一个 Inbound Route:

在 Settings -> On Hold Music 里可以上传等待音乐,这样外线呼入时等待接听时会播放音乐。

外线呼出

在 FreePBX 的 Outbound Routes 里添加一个 Outbound Route:

这里把 10XX 的内线号码排除掉,这样 SIP 客户端可以直接拨打外线号码,不用加 0。

通话记录和录音

点击 Reports -> CDR Reports,可以看到通话记录和录音。

自动化处理通话记录

可以配置自定义的 Dial Plan,在外线呼入、通话结束等事件发生时,调用任意的程序处理通话记录,比如将通话记录推送到外部的数据库或者 API。下面是一个简单的例子。

在 Admin -> Config Edit 里面找到 extensions_custom.conf 文件,添加以下内容:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
[macro-dial-ringall-predial-hook]
exten => s,1,Noop(Entering user defined context macro-dialout-one-predial-hook in extensions_custom.conf)
exten => s,n,TrySystem(echo "Dialing from ${CALLERID(num)}" >> /tmp/dialout.log)
exten => s,n,Set(CHANNEL(hangup_handler_push)=do-this-on-hangup,s,1)
exten => s,n,MacroExit

[do-this-on-hangup]
exten => s,1,Noop(Entering user defined context do-this-on-hangup in extensions_custom.conf)
exten => s,n,TrySystem(echo "Call from ${CALLERID(num)} ended" >> /tmp/dialout.log)
exten => s,n,ExecIf($["${DIALSTATUS}" = "CANCEL"]?TrySystem(echo "Call from ${CALLERID(num)} cancelled" >> /tmp/dialout.log))
exten => s,n,ExecIf($["${DIALSTATUS}" = "NOANSWER"]?TrySystem(echo "Call from ${CALLERID(num)} missed" >> /tmp/dialout.log))
exten => s,n,ExecIf($["${DIALSTATUS}" = "BUSY"]?TrySystem(echo "Call from ${CALLERID(num)} busy" >> /tmp/dialout.log))
exten => s,n,ExecIf($["${DIALSTATUS}" = "ANSWER"]?TrySystem(echo "Call from ${CALLERID(num)} answered" >> /tmp/dialout.log))
exten => s,n,Return

这个 Dial Plan 会在外线呼入、通话结束等事件发生时,将通话记录写入 /tmp/dialout.log 文件。按照需要编写脚本并在 TrySystem 里调用,就能实现自定义的通话记录处理。

1
2
3
4
5
6
/var/spool/asterisk/monitor/
├── 2023
│ ├── 03
│ │ ├── 27
│ │ │ ├── in-0123456789-180xxxxxxxx-20230327-160626-1679904386.9.wav
│ │ │ └── out-180xxxxxxxx-1000-20230327-160920-1679904560.17.wav

录音文件存放在 /var/spool/asterisk/monitor/ 目录下,按照通话日期整理目录,文件名包含通话的方向、被叫号码、主叫号码、通话日期(年月日)、通话时间(时分秒)、通话起始的 UNIX 时间戳。

下面的例子进一步演示了如何在通话结束时自动处理录音文件。首先是 extensions_custom.conf 文件的配置:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
[macro-dial-ringall-predial-hook]
exten => s,1,Noop(Entering user defined context macro-dialout-one-predial-hook in extensions_custom.conf)
exten => s,n,TrySystem(echo "Called from ${CALLERID(num)}" >> /tmp/dialout.log)
exten => s,n,Set(CHANNEL(hangup_handler_push)=macro-hangup-dial-in,s,1)
exten => s,n,Return

[macro-hangup-dial-in]
exten => s,1,Noop(Entering user defined context macro-hangup-dial-in in extensions_custom.conf)
exten => s,n,TrySystem(echo "Call from ${CALLERID(num)} ended, status ${DIALSTATUS}" >> /tmp/dialout.log)
exten => s,n,TrySystem(process-call-log.sh ${DIALSTATUS} in ${CALLERID(num)})
exten => s,n,Return

[macro-dialout-trunk-predial-hook]
exten => s,1,Noop(Entering user defined context macro-dialout-one-predial-hook in extensions_custom.conf)
exten => s,n,Set(CHANNEL(hangup_handler_push)=macro-hangup-dial-out,s,1)
exten => s,n,Return

[macro-hangup-dial-out]
exten => s,1,Noop(Entering user defined context macro-hangup-dial-out in extensions_custom.conf)
exten => s,n,TrySystem(echo "Call to ${DIALEDPEERNUMBER} ended, status ${DIALSTATUS}" >> /tmp/dialout.log)
exten => s,n,TrySystem(process-call-log.sh ${DIALSTATUS} out ${DIALEDPEERNUMBER})
exten => s,n,Return

/usr/local/bin/ 目录下创建一个名为 process-call-log.sh 的脚本文件并添加执行权限,脚本内容如下:

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
66
67
68
69
#!/bin/bash

# Usage: process-call-log <dialstatus> <direction> <callerid>
# Example: process-call-log ANSWER in 12345678
# phonenumber: CID of the caller
# direction: in or out
# status: asterisk dialstatus (ANSWER, NOANSWER, CANCEL, ...)
# This script is intended to be called by Asterisk dialplan to process call logs
# It is designed to be non-blocking, so that the call can be processed without waiting for the response

function push_call_log {
DIALSTATUS=$1
DIRECTION=$2
CALLERID=$3

# If $CALLERID has '@', for example "135792468@trunk", remove the suffix
CALLERID=$(echo $CALLERID | cut -d'@' -f1)

# Delay 3 seconds to ensure the recording file is ready
sleep 3

TODAY=$(date +%Y/%m/%d)
RECORDING_FOLDER="/var/spool/asterisk/monitor/$TODAY"
if [ -d $RECORDING_FOLDER ]; then
RECORDING_FILE=$(ls -t $RECORDING_FOLDER | grep $CALLERID | head -n 1)
if [ -n "$RECORDING_FILE" ]; then
IFS='-' read -r -a RECORDING_INFO <<< "$RECORDING_FILE"
DIRECTION=${RECORDING_INFO[0]}
CALLEE=${RECORDING_INFO[1]}
CALLER=${RECORDING_INFO[2]}
DATESTR=${RECORDING_INFO[3]}
TIMESTR=${RECORDING_INFO[4]}
UNIXTIME=$(echo ${RECORDING_INFO[5]} | sed 's/\.[^.]*$//')
DURATION=$(ffprobe -i "$RECORDING_FOLDER/$RECORDING_FILE" -show_entries format=duration -v quiet -of csv="p=0")
if ! [[ $DURATION =~ ^[0-9]+([.][0-9]+)?$ ]]; then
DURATION=0
fi
# if $DIRECTION is "in", then $PHONE is $CALLEE, otherwise $PHONE is $CALLER
if [ "$DIRECTION" == "in" ]; then
PHONE=$CALLER
else
PHONE=$CALLEE
fi
fi
fi

if [ -z "$RECORDING_FILE" ]; then
PHONE=$CALLERID
DATESTR=$(date +%Y%m%d)
TIMESTR=$(date +%H%M%S)
UNIXTIME=$(date +%s)
DURATION=0
fi

# If $PHONE has 12 digits, and starts with "0", remove the prefix
if [[ $PHONE =~ ^0[0-9]{11}$ ]]; then
PHONE=${PHONE:1}
fi

# >>> Add your code to push the call log to your server or database here <<<
# You can use curl or any other method to send the data

# Write log to /tmp/push-call-log.log
echo "[$(date)] $CALLERID $DIALSTATUS $RECORDING_FILE $DURATION $ATTACHMENT_ID" >> /tmp/push-call-log.log
}

push_call_log "$1" "$2" "$3" &
disown
exit 0

这个脚本会在通话结束时被调用,处理通话记录和录音文件。它会将通话状态、方向、主叫号码等信息写入日志文件,并可以进一步处理录音文件,比如上传到服务器或数据库。这样就可以避免使用 FreePBX 卡的要死的前端界面来查看通话记录了。