Nexus 5变身GSM语音网关:Ubuntu下AT指令实现同时接听来电与去电
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 指令要支持ATCHLD运营商要开通呼叫等待业务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-serialPython 脚本读写串口时要用的库。modemmanagerUbuntu 自带的 modem 管理服务。实际开发时它可能自动占用串口所以调试阶段建议先停掉。usb-modeswitch部分 USB 设备需要切换到 modem 模式时会用到。如果后面还要处理音频建议安装sudo apt install pulseaudio pulseaudio-utils alsa-utils2.2 停掉 ModemManager避免串口被占用Ubuntu 上的 ModemManager 会自动探测新接入的串口 modem。它存在的好处是方便图形化网络连接管理但对 AT 命令调试会造成干扰你打开串口发ATModemManager 可能已经抢占了设备导致没有任何响应。临时停止服务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: idVendor18d1, idProduct4ee1 usb 1-1.2: New USB device strings: Mfr1, Product2, SerialNumber3 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 是否在线OKATI查询设备信息Nexus 5...或厂商信息ATCPIN?查询 SIM 卡 PIN 状态CPIN: READYATCSQ查询信号强度CSQ: 18,99ATCREG?查询网络注册状态CREG: 0,1ATCLIP1开启来电号码显示OKATCCWA1开启呼叫等待提示OKATD13800138000;语音拨号注意分号OK或CONNECTATA接听来电OK或CONNECTATH0挂断电话OKATCHUP强制挂断当前呼叫OKATCLCC列出当前呼叫状态CLCC: 1,...ATCHLD2保持当前通话接听新来电OKATCHLD3发起多方通话OK3.2 去电的完整流程用串口工具连接后拨一个号码ATD13800138000;注意末尾分号不能省。AT 命令里ATD后面直接跟号码再加分号表示语音呼叫不加分号会被当成数据呼叫在很多 modem 上会返回错误或进入数据连接流程。拨号后的状态结果有两种常见情况返回OK呼叫已建立成功或者系统还在等待对方接听。返回CONNECT某些 modem 在通话建立时会上报这个结果码。要确认当前通话状态可以主动查询ATCLCC可能的返回CLCC: 1,0,0,0,0,13800138000,129字段含义分别是呼叫序号、呼叫方向0 表示去电、状态0 表示正在呼叫、模式0 表示语音、多媒体属性、号码、号码类型。挂断使用ATH0也可以使用ATCHUP强制挂断后者在异常状态下的容忍度更高。3.3 来电接听的完整流程来电时 modem 会主动上报RING CLIP: 13800138000,129,,0,,0RING是响铃提示CLIP是来电号码。如果看不到CLIP可以先执行ATCLIP1开启来电显示。接听命令ATA此时电话接通双方可以开始通话。挂断同样使用ATH0如果来电时不想接听直接发送ATH0可以拒接。注意发送ATA前要保证串口没有其他未完成指令否则 modem 可能处于忙状态接听命令会被忽略。3.4 并发呼叫通话中来电怎么处理通话状态下如果第二个电话呼入modem 会上报CCWACCWA: 13800138000,1,1此时需要根据业务需求选择处理方式。命令效果使用场景ATCHLD1释放当前通话接听新来电只保留最新呼叫ATCHLD2保持当前通话接听新来电需要处理多个呼叫ATCHLD3将两路通话并成会议需要撮合多方通话在“同时接听来电去电”的网关场景中最常见的策略是ATCHLD2保持第一通电话接起第二通电话然后再根据业务录音或转接。可以在收到CCWA时自动执行ATCHLD2。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将默认录音源和播放输出切到 nexuspactl 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 音频所以还要手动切换 profilepactl 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触发ATCHLD2。5.2 核心代码#!/usr/bin/env python3 import serial import threading import time class NexusModem: def __init__(self, port, baud115200): self.ser serial.Serial(port, baud, timeout1) self.lock threading.Lock() self.active_calls 0 def cmd(self, command, wait2): 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 bOK in response or bERROR in response or bCONNECT in response: break return response.decode(errorsignore) def dial(self, number): print(f去电: {number}) return self.cmd(fATD{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(ATCHLD2, 3) def monitor(self): while True: try: line self.ser.readline().decode(errorsignore).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(ATCLIP1)) print(modem.cmd(ATCCWA1)) listener threading.Thread(targetmodem.monitor, daemonTrue) 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和ATCHLD2。如果只是测试去电取消脚本中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 再执行业务指令拨号返回 ERRORSIM 卡 PIN 锁定ATCPIN?先解锁 SIM 或关闭 PIN去电后对方不响号码末尾没有分号发送ATD13800138000;确认命令末尾有英文分号来电没有 RING未开启 CLIP/CCWAATCLIP1、ATCCWA1开启来电显示和呼叫等待电话通了没声音音频路由未切到 USB/蓝牙pactl list short sources切换默认音频源和输出同时接听失败运营商未开通呼叫等待ATCCWA?联系运营商开通呼叫等待业务串口输入卡在半个命令多线程同时写检查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 命令。按以下顺序检查确认通话是否真的建立使用ATCLCC看呼叫状态是否为0Active。确认音频设备列表里有没有 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计数要基于ATCLCC结果维护真实状态。日志所有 AT 收发、音频切换、异常恢复都要记录到文件方便事后排查。告警语音网关无人值守重要呼叫失败要能推送钉钉、企业微信或邮件。7.2 发布前检查清单下面是一份可以直接使用的上线前检查清单。检查项是否通过SIM 卡已安装PIN 已关闭串口设备节点固定不使用动态变化的 tty 名ModemManager 已禁用不抢占串口AT、ATCPIN?、ATCSQ、ATCREG?均正常返回来电显示ATCLIP1已开启呼叫等待ATCCWA1已开启音频设备名称已确认PulseAudio 路由脚本可执行去电和来电两种场景均已人工验证通话中第二个来电已验证ATCHLD2脚本异常退出后有重启机制关键日志已落盘且不无限增长7.3 进一步扩展方向Nexus 5 在 Ubuntu 侧一旦稳定工作就可以继续向两个方向扩展。一个是上层业务集成。把 AT 指令封装成 HTTP 或 MQTT 接口让其他系统调用“拨打”“接听”“挂断”“发起会议”能力。例如一个网页后台点击按钮服务器通过 Python 脚本控制 Nexus 5 拨打电话并把通话状态实时回传。另一个是通信平台对接。Asterisk、FreeSWITCH 这类软交换系统通过 chan_dongle、SIP 网关或 DAHDI 接入 GSM modem可以把 Nexus 5 当作一个外线通道实现 GSM 和 SIP 的互通。此时并发呼叫处理不再是简单的一路保持而是进入完整的多路呼叫路由逻辑。回到最初的问题“Nexus 5Nexus 5 Ubuntu 同时接听来电去电”。实际上要完成的是设备枚举、AT 信令、音频路由、并发呼叫状态机四层工作。只要这四层打通Nexus 5 就不再是一部手机而是一个可以被 Ubuntu 系统任意控制的标准 GSM 语音网关。建议初学者先不要急着接并发呼叫而是用ATD和ATA把单路去话、单路来话跑通再从CCWA事件开始处理呼叫等待这样排查问题时不会把信令问题和音频问题混在一起。