GSM 话机、Linux 网关、自动化语音系统之间一直有一个常见需求:把一台 Nexus 5 放到 Ubuntu 服务器旁边,让 Ubuntu 接管这台手机的来去电能力,从而实现自动拨号、自动接听,甚至在通话中处理第二个来电。标题里的“同时接听来电+去电”不是双 SIM 并发,而是指系统在无人操作的情况下,既能发起去电,也能响应来电,并且在呼叫等待时保持当前通话、接通新呼叫。
这类场景常见于呼叫中心、测试机群、应急通知网关、物联网语音报警等。开发者在网上搜“Nexus 5 Ubuntu 同时接听来电+去电”,往往是因为已经确认了设备,却卡在串口不识别、AT 命令返回 ERROR、电话通了没有声音这几个环节。这篇内容会按实际落地的顺序展开:先把 Nexus 5 在 Ubuntu 中的角色讲清楚,再准备环境,然后打通 AT 命令、音频路由和并发呼叫,最后给出一个最小 Python 脚本和排查清单。
1. 先理清 Nexus 5 在 Ubuntu 系统中的角色
1.1 Nexus 5 本质是一个 GSM Modem,而不是普通手机
Nexus 5 使用高通基带处理器,负责 GSM/WCDMA/LTE 网络注册、语音编解码、射频收发。Android 系统跑在应用处理器上,通过底层的 modem 接口与基带通信。常规使用中,用户通过电话 App 拨号,App 再调用系统电话服务,系统最终把 AT/QMI 指令下发给基带。
当我们要在 Ubuntu 侧接管电话能力时,目标不是去模拟点击 Android 屏幕,而是要跨过 Android 上层,让 Ubuntu 直接访问基带提供的调制解调器接口。Linux 内核中常见的 modem 设备节点是/dev/ttyUSB0、/dev/ttyACM0,它们会以串口或者 ACM 设备形式出现。Nexus 5 在部分 ROM 和内核配置下,通过 USB 连接电脑后会枚举为串口设备;如果只看到 MTP 或 RNDIS 网络设备,说明当前没有暴露 AT 命令通道,需要先调整 USB 配置或换用支持该模式的系统镜像。
1.2 “同时接听来电和去电”到底是什么状态
一块 GSM 调制解调器通常只有一路射频资源。一边正在通话,一边又来新来电时,网络侧通过呼叫等待(Call Waiting)把第二个呼叫送到 modem。modem 上报+CCWA通知事件,主控端可以选择保持当前通话、接听新呼叫,也可以拒绝新呼叫。
所以从系统状态来看,“同时”并不代表射频并行,而是 modem 在同一时间维持多个呼叫状态:一路处于通话中(Active),另一路处于保持中(Held)或等待接听状态。实现的关键在于 AT 指令要支持AT+CHLD,运营商要开通呼叫等待业务,SIM 卡所在网络也要支持这项功能。
1.3 为什么用 AT 命令而不是 Android 上层
Android 上层提供的电话 API 需要在系统内运行 App,而且 Nexus 5 如果刷入原生 Android,远程控制需要 adb、root 权限或者编写 Android App。这些方式都不适合 Ubuntu 侧做独立的自动语音网关。
AT 命令是传统 modem 的标准控制方式。只要串口通路打通,Ubuntu 就可以发送ATD去电、ATA接听、ATH挂断,并解析RING、+CLIP、+CCWA等主动上报。它不依赖手机屏幕是否亮着、不依赖某个 App 是否运行,更适合无人值守环境。
注意:AT 命令控制的是信令层。电话里的语音需要通过音频通路进入 Ubuntu,因此后面必须单独处理音频路由。
2. Ubuntu 端环境准备:驱动、依赖和串口权限
2.1 安装串口和 modem 调试工具
本文以 Ubuntu 20.04/22.04/24.04 为例。先用包管理器安装基础工具:
sudo apt update sudo apt install minicom picocom python3 python3-pip python3-serial modemmanager usb-modeswitch这里有几个工具的角色不同:
minicom和picocom:终端串口调试工具,用来先确认 AT 通道是否正常。python3-serial:Python 脚本读写串口时要用的库。modemmanager:Ubuntu 自带的 modem 管理服务。实际开发时它可能自动占用串口,所以调试阶段建议先停掉。usb-modeswitch:部分 USB 设备需要切换到 modem 模式时会用到。
如果后面还要处理音频,建议安装:
sudo apt install pulseaudio pulseaudio-utils alsa-utils2.2 停掉 ModemManager,避免串口被占用
Ubuntu 上的 ModemManager 会自动探测新接入的串口 modem。它存在的好处是方便图形化网络连接管理,但对 AT 命令调试会造成干扰:你打开串口发AT,ModemManager 可能已经抢占了设备,导致没有任何响应。
临时停止服务:
sudo systemctl stop ModemManager sudo systemctl disable ModemManagerdisable 会让服务开机不启动。如果只是临时调试,用 stop 就够了;如果要长期作为语音网关运行,建议保留 stop 状态,用自己写的守护进程管理 modem。
2.3 确认串口设备号和权限
接入 Nexus 5 后,用dmesg查看内核日志:
dmesg | tail -n 30常见输出是:
usb 1-1.2: new high-speed USB device number 8 using xhci_hcd usb 1-1.2: New USB device found: idVendor=18d1, idProduct=4ee1 usb 1-1.2: New USB device strings: Mfr=1, Product=2, SerialNumber=3 cdc_acm 1-1.2:1.0: ttyACM0: USB ACM device此时设备节点是/dev/ttyACM0。不同的内核和 ROM 也可能是/dev/ttyUSB0。查看设备:
ls -l /dev/ttyACM0一般输出crw-rw---- 1 root dialout ...。当前用户要加入dialout组才能读写串口:
sudo usermod -aG dialout $USER重新登录后使用picocom测试:
picocom -b 115200 /dev/ttyACM0如果串口通,输入AT回车,会回显OK。如果不通,先检查设备号、权限、ModemManager 是否已停止。
如果是在虚拟机里跑 Ubuntu,还要把 Nexus 5 的 USB 设备从宿主机透传到虚拟机。VMware 的“虚拟机 -> 可移动设备”和 VirtualBox 的“设备 -> USB”菜单里都能找到设备名称。
3. 用 AT 命令打通来电和去电
3.1 必须掌握的 AT 命令组
AT 命令是文本指令,以\r结尾。手动调试时最容易忘的就是换行符和结果码。下面这组命令是语音网关最常用的。
| 命令 | 作用 | 典型返回 |
|---|---|---|
AT | 检测 modem 是否在线 | OK |
ATI | 查询设备信息 | Nexus 5...或厂商信息 |
AT+CPIN? | 查询 SIM 卡 PIN 状态 | +CPIN: READY |
AT+CSQ | 查询信号强度 | +CSQ: 18,99 |
AT+CREG? | 查询网络注册状态 | +CREG: 0,1 |
AT+CLIP=1 | 开启来电号码显示 | OK |
AT+CCWA=1 | 开启呼叫等待提示 | OK |
ATD13800138000; | 语音拨号,注意分号 | OK或CONNECT |
ATA | 接听来电 | OK或CONNECT |
ATH0 | 挂断电话 | OK |
AT+CHUP | 强制挂断当前呼叫 | OK |
AT+CLCC | 列出当前呼叫状态 | +CLCC: 1,... |
AT+CHLD=2 | 保持当前通话,接听新来电 | OK |
AT+CHLD=3 | 发起多方通话 | OK |
3.2 去电的完整流程
用串口工具连接后,拨一个号码:
ATD13800138000;注意末尾分号不能省。AT 命令里,ATD后面直接跟号码再加分号,表示语音呼叫;不加分号会被当成数据呼叫,在很多 modem 上会返回错误或进入数据连接流程。
拨号后的状态结果有两种常见情况:
- 返回
OK:呼叫已建立成功,或者系统还在等待对方接听。 - 返回
CONNECT:某些 modem 在通话建立时会上报这个结果码。
要确认当前通话状态,可以主动查询:
AT+CLCC可能的返回:
+CLCC: 1,0,0,0,0,"13800138000",129字段含义分别是:呼叫序号、呼叫方向(0 表示去电)、状态(0 表示正在呼叫)、模式(0 表示语音)、多媒体属性、号码、号码类型。挂断使用:
ATH0也可以使用AT+CHUP强制挂断,后者在异常状态下的容忍度更高。
3.3 来电接听的完整流程
来电时 modem 会主动上报:
RING +CLIP: "13800138000",129,"",0,"",0RING是响铃提示,+CLIP是来电号码。如果看不到+CLIP,可以先执行AT+CLIP=1开启来电显示。
接听命令:
ATA此时电话接通,双方可以开始通话。挂断同样使用:
ATH0如果来电时不想接听,直接发送ATH0可以拒接。注意发送ATA前要保证串口没有其他未完成指令,否则 modem 可能处于忙状态,接听命令会被忽略。
3.4 并发呼叫:通话中来电怎么处理
通话状态下,如果第二个电话呼入,modem 会上报+CCWA:
+CCWA: "13800138000",1,1此时需要根据业务需求选择处理方式。
| 命令 | 效果 | 使用场景 |
|---|---|---|
AT+CHLD=1 | 释放当前通话,接听新来电 | 只保留最新呼叫 |
AT+CHLD=2 | 保持当前通话,接听新来电 | 需要处理多个呼叫 |
AT+CHLD=3 | 将两路通话并成会议 | 需要撮合多方通话 |
在“同时接听来电+去电”的网关场景中,最常见的策略是AT+CHLD=2:保持第一通电话,接起第二通电话,然后再根据业务录音或转接。可以在收到+CCWA时自动执行AT+CHLD=2。
4. 语音通话的音频通路:没有声音的电话没有意义
4.1 AT 命令只控制信令,声音需要单独路由
AT 命令完成的是“建立呼叫”“接听呼叫”这类控制动作。电话接通后,语音会以 PCM 编码形式在基带芯片和音频设备之间流动。Nexus 5 的音频输出路径可以是耳机孔、蓝牙 SCO 或 USB 音频设备。
在 Ubuntu 侧,音频路由决定了两件事:
- Ubuntu 能不能听到对方说话。
- 对方能不能听到 Ubuntu 上的麦克风输入。
如果不配置音频通路,就会出现“电话拨通了、对方听不到声音、自己也听不到声音”的现象,这在调试中非常容易混淆。
4.2 用 ALSA/PulseAudio 选择音频设备
先用arecord -l查看系统中可用的录音设备:
arecord -l如果 Nexus 5 通过 USB 暴露为声卡,会多出一个 USB Audio 设备。查看当前 PulseAudio 源:
pactl list short sources pactl list short sinks将默认录音源和播放输出切到 nexus:
pactl set-default-source alsa_input.usb-LGE_Nexus_5_...analog-stereo pactl set-default-sink alsa_output.usb-LGE_Nexus_5_...analog-stereo如果是蓝牙通路,要先配对:
bluetoothctl pair 11:22:33:44:55:66 connect 11:22:33:44:55:66 trust 11:22:33:44:55:66蓝牙耳机配好后,PulseAudio 默认会使用 A2DP,但通话语音需要的是 HFP/HSP 的 SCO 音频,所以还要手动切换 profile:
pactl set-card-profile bluez_card.11_22_33_44_55_66 headset_head_unit不同 Ubuntu 版本的名字和命令略有差别,实际项目要以pactl list cards返回的卡名为准。
4.3 音频切换必须和呼叫状态机配合
音频路由不能随便切。比如去电时,应该在发送ATD之后等对方接听,再切音频设备和麦克风;如果提前切到还没接通的设备,会出现一串错误提示或静音。
4.43 一个简单的语音路由示例
#!/bin/bash # 去电并接通后调用 pactl set-default-source "your_nexus_source_name" pactl set-default-sink "your_nexus_sink_name"实际项目中,可以在 Python 脚本的dial()之后检测OK,再调用subprocess.run()切换 PulseAudio 设备。
5. 一个最小 Python 脚本:自动拨号、自动接听、处理呼叫等待
5.1 脚本设计
Python 脚本需要同时处理两件事:用户主动发起的去电,以及 modem 主动上报的来电。串口是单通道,多个线程同时写会导致命令交叉,所以读写必须加锁。这里采用一个简单方案:一个监听线程循环读串口,一个主线程发起去电或执行其他业务。
核心点:
- 用
threading.Lock保护串口写操作。 - 监听线程解析
RING、+CLIP、+CCWA。 - 当前没有通话时,
RING触发ATA。 - 当前已有通话时,
+CCWA触发AT+CHLD=2。
5.2 核心代码
#!/usr/bin/env python3 import serial import threading import time class NexusModem: def __init__(self, port, baud=115200): self.ser = serial.Serial(port, baud, timeout=1) self.lock = threading.Lock() self.active_calls = 0 def cmd(self, command, wait=2): response = b"" with self.lock: self.ser.reset_input_buffer() self.ser.write((command + "\r").encode()) end = time.time() + wait while time.time() < end: chunk = self.ser.read(1) if chunk: response += chunk if b"OK" in response or b"ERROR" in response or b"CONNECT" in response: break return response.decode(errors="ignore") def dial(self, number): print(f"去电: {number}") return self.cmd(f"ATD{number};", 5) def answer(self): print("接听来电") return self.cmd("ATA", 3) def hangup(self): print("挂断") return self.cmd("ATH0", 2) def hold_and_answer(self): print("保持当前通话,接听新来电") return self.cmd("AT+CHLD=2", 3) def monitor(self): while True: try: line = self.ser.readline().decode(errors="ignore").strip() except serial.SerialException: print("串口异常,等待重连") break if not line: continue print(f"< {line}") if line.startswith("RING"): if self.active_calls > 0: self.hold_and_answer() else: self.answer() self.active_calls += 1 elif line.startswith("+CCWA"): self.hold_and_answer() elif line.startswith("NO CARRIER"): self.active_calls = max(0, self.active_calls - 1) if __name__ == "__main__": modem = NexusModem("/dev/ttyACM0", 115200) print(modem.cmd("AT")) print(modem.cmd("AT+CLIP=1")) print(modem.cmd("AT+CCWA=1")) listener = threading.Thread(target=modem.monitor, daemon=True) listener.start() # 示例:启动后拨出一个号码 # modem.dial("13800138000") while True: time.sleep(1)这段脚本只做演示。它把active_calls简单增删,实际项目中还要根据+CLCC返回的每一路呼叫状态维护精确状态机。
5.3 运行和验证
先给脚本执行权限并运行:
chmod +x gateway.py python3 gateway.py /dev/ttyACM0验证方式:
- 用另一台手机拨打 Nexus 5 的号码。
- 观察终端出现
RING和+CLIP。 - 脚本自动发送
ATA。 - 如果音频路由已经配置,双方应能正常通话。
- 在通话中让第三方拨打该号码,观察
+CCWA和AT+CHLD=2。
如果只是测试去电,取消脚本中modem.dial("13800138000")的注释,或者单独写一个测试脚本调用dial()。
6. 常见问题排查:从串口不通到电话无声音
6.1 常见问题速查表
| 问题现象 | 常见原因 | 检查方式 | 处理建议 |
|---|---|---|---|
没有/dev/ttyACM0节点 | USB 枚举为 MTP/RNDIS,或者驱动未加载 | lsusb、dmesg | 安装 usb-modeswitch;确认 ROM 支持 modem 模式 |
| AT 命令没响应 | ModemManager 占用了串口 | sudo systemctl status ModemManager | 停止 ModemManager,断开重连 |
| AT 命令返回 ERROR | 波特率不对,或者命令格式错误 | 尝试 9600/115200/460800 | 使用AT测试,确认返回 OK 再执行业务指令 |
| 拨号返回 ERROR | SIM 卡 PIN 锁定 | AT+CPIN? | 先解锁 SIM 或关闭 PIN |
| 去电后对方不响 | 号码末尾没有分号 | 发送ATD13800138000; | 确认命令末尾有英文分号 |
| 来电没有 RING | 未开启 CLIP/CCWA | AT+CLIP=1、AT+CCWA=1 | 开启来电显示和呼叫等待 |
| 电话通了没声音 | 音频路由未切到 USB/蓝牙 | pactl list short sources | 切换默认音频源和输出 |
| 同时接听失败 | 运营商未开通呼叫等待 | AT+CCWA? | 联系运营商开通呼叫等待业务 |
| 串口输入卡在半个命令 | 多线程同时写 | 检查threading.Lock | 所有写串口操作都要加锁 |
6.2 串口设备不出现的排查链路
先确认物理连接和枚举:
lsusb如果设备列表里看不到 Nexus 5,先检查 USB 线和接口。如果看到的是18d1:4ee1这类 Android 设备,需要继续看dmesg判断内核将设备识别成什么。
dmesg | grep -E "usb|tty|ACM"如果内核已经创建了ttyACM0,说明设备枚举成功,下一步就是权限问题。如果只创建了rndis_host网络接口,说明当前模式不是 modem 模式,需要切换或刷入支持 modem 模式的系统镜像。
6.3 通话建立但没有声音的处理顺序
通话没有声音时,先不要把问题归到 AT 命令。按以下顺序检查:
- 确认通话是否真的建立:使用
AT+CLCC看呼叫状态是否为0(Active)。 - 确认音频设备列表里有没有 USB/蓝牙设备:
arecord -l、pactl list short sources。 - 确认 Ubuntu 默认录音源和输出是否正确。
- 确认音量没有静音:
alsamixer里检查 PCM、Microphone 静音状态。 - 确认 USB 音频或蓝牙 SCO 的 profile 是通话模式,而不是 A2DP 播放模式。
7. 用于生产环境还需要补齐的工程能力
7.1 从调试脚本到自启动服务
上面这个 Python 脚本只能算最小演示。生产环境会面对串口断开、SIM 卡松动、modem 死锁、电源断电等问题。建议增加以下模块:
- 串口断线重连:监听
SerialException,延迟后重新打开设备。 - 看门狗:周期性发送
AT,连续多次无响应则重启脚本或调用usb_modeswitch重置设备。 - 呼叫状态机:不要用简单的
active_calls计数,要基于AT+CLCC结果维护真实状态。 - 日志:所有 AT 收发、音频切换、异常恢复都要记录到文件,方便事后排查。
- 告警:语音网关无人值守,重要呼叫失败要能推送钉钉、企业微信或邮件。
7.2 发布前检查清单
下面是一份可以直接使用的上线前检查清单。
| 检查项 | 是否通过 |
|---|---|
| SIM 卡已安装,PIN 已关闭 | |
| 串口设备节点固定,不使用动态变化的 tty 名 | |
| ModemManager 已禁用,不抢占串口 | |
AT、AT+CPIN?、AT+CSQ、AT+CREG?均正常返回 | |
来电显示AT+CLIP=1已开启 | |
呼叫等待AT+CCWA=1已开启 | |
| 音频设备名称已确认,PulseAudio 路由脚本可执行 | |
| 去电和来电两种场景均已人工验证 | |
通话中第二个来电已验证AT+CHLD=2 | |
| 脚本异常退出后有重启机制 | |
| 关键日志已落盘,且不无限增长 |
7.3 进一步扩展方向
Nexus 5 在 Ubuntu 侧一旦稳定工作,就可以继续向两个方向扩展。
一个是上层业务集成。把 AT 指令封装成 HTTP 或 MQTT 接口,让其他系统调用“拨打”“接听”“挂断”“发起会议”能力。例如一个网页后台点击按钮,服务器通过 Python 脚本控制 Nexus 5 拨打电话,并把通话状态实时回传。
另一个是通信平台对接。Asterisk、FreeSWITCH 这类软交换系统通过 chan_dongle、SIP 网关或 DAHDI 接入 GSM modem,可以把 Nexus 5 当作一个外线通道,实现 GSM 和 SIP 的互通。此时并发呼叫处理不再是简单的一路保持,而是进入完整的多路呼叫路由逻辑。
回到最初的问题:“Nexus 5+Nexus 5 Ubuntu 同时接听来电+去电”。实际上要完成的是设备枚举、AT 信令、音频路由、并发呼叫状态机四层工作。只要这四层打通,Nexus 5 就不再是一部手机,而是一个可以被 Ubuntu 系统任意控制的标准 GSM 语音网关。建议初学者先不要急着接并发呼叫,而是用ATD和ATA把单路去话、单路来话跑通,再从+CCWA事件开始处理呼叫等待,这样排查问题时不会把信令问题和音频问题混在一起。