diff options
| author | Christian Breunig <christian@breunig.cc> | 2026-09-24 15:28:39 +0000 |
|---|---|---|
| committer | Christian Breunig <christian@breunig.cc> | 2026-09-24 15:28:39 +0000 |
| commit | 9c88884a67c01b4e5d74872abee41820a8f65331 (patch) | |
| tree | 56bbedac930fb0300c9f6267807f5faa5af1315c /src | |
| parent | 112fb62b8b15653c0a26e0de56f326cd2d67da9b (diff) | |
| download | vyos-1x-9c88884a67c01b4e5d74872abee41820a8f65331.tar.gz vyos-1x-9c88884a67c01b4e5d74872abee41820a8f65331.zip | |
wwan: T9350: query ModemManager instead of the QMI control port
show_wwan.py drove qmicli against /dev/cdc-wdmN, which assumes the modem
exposes a QMI control port. An MBIM modem does not, so every subcommand of
"show interfaces wwan wwanN" failed on one - after waiting out a five second
transaction timeout, twice over for "signal":
$ show interfaces wwan wwan0 signal
Command not supported by Modem (10.6s)
$ qmicli --device=/dev/cdc-wdm0 --device-open-proxy --nas-get-signal-info
error: couldn't create client for the 'nas' service: CID allocation failed
in the CTL client: Transaction timed out
ports: cdc-wdm0 (mbim), ttyUSB0 (at), wwan0 (net)
ModemManager has already probed the modem and speaks whichever protocol it
exposes - QMI, MBIM or plain AT. It is a hard dependency of "interfaces wwan"
anyway, as conf_mode cannot dial without it, and it answers from its own cache
over D-Bus without touching the device:
$ show interfaces wwan wwan0 signal
Access technology: lte (0.2s)
Signal quality: 58%
Operator: Telekom.de
Registration: home
Packet service: attached
State: connected
Diffstat (limited to 'src')
| -rwxr-xr-x | src/op_mode/show_wwan.py | 148 |
1 files changed, 113 insertions, 35 deletions
diff --git a/src/op_mode/show_wwan.py b/src/op_mode/show_wwan.py index 53b94d411..c9371f410 100755 --- a/src/op_mode/show_wwan.py +++ b/src/op_mode/show_wwan.py @@ -16,9 +16,17 @@ import argparse +from json import JSONDecodeError +from json import loads from sys import exit + from vyos.configquery import ConfigTreeQuery -from vyos.utils.process import cmdl +from vyos.utils.dict import dict_search +from vyos.utils.process import is_systemd_service_active +from vyos.utils.process import popen +from vyos.utils.process import STDOUT +from vyos.utils.wwan import modem_index +from vyos.utils.wwan import service_name parser = argparse.ArgumentParser() parser.add_argument("--model", help="Get module model", action="store_true") @@ -29,60 +37,130 @@ parser.add_argument("--imsi", help="Get module IMSI", action="store_true") parser.add_argument("--msisdn", help="Get module MSISDN", action="store_true") parser.add_argument("--sim", help="Get SIM card status", action="store_true") parser.add_argument("--signal", help="Get current RF signal info", action="store_true") -parser.add_argument("--firmware", help="Get current RF signal info", action="store_true") +parser.add_argument("--firmware", help="Get module firmware information", action="store_true") required = parser.add_argument_group('Required arguments') required.add_argument("--interface", help="WWAN interface name, e.g. wwan0", required=True) -def qmi_cmd(device, command, silent=False): +def mmcli(*args: str) -> dict: + """ Query ModemManager and return its answer as a dictionary. + + ModemManager has already probed the modem and speaks whatever control + protocol it exposes - QMI, MBIM or plain AT - so this works on every modem + we support. Talking to /dev/cdc-wdmN directly does not: it assumes the + modem offers a QMI control port, and an MBIM modem only answers with a + CID allocation timeout after several seconds. + """ + if not is_systemd_service_active(service_name): + raise ValueError('ModemManager is not running, no modem can be queried') + + # mmcli reports why a modem did not answer on stderr - fold it into the + # output so it can be handed to the operator verbatim + tmp, code = popen(['mmcli', '--output-json', *args], shell=False, stderr=STDOUT) + if code != 0: + raise ValueError(tmp.strip() or + f'ModemManager query failed with exit code {code}') + try: - tmp = cmdl(['qmicli', f'--device={device}', '--device-open-proxy'] + command.split()) - tmp = tmp.replace(f'[{cdc}] ', '') - if not silent: - # skip first line as this only holds the info headline - for line in tmp.splitlines()[1:]: - print(line.lstrip()) - return tmp - except: - print('Command not supported by Modem') - exit(1) + return loads(tmp) + except JSONDecodeError as e: + raise ValueError(f'Malformed answer from ModemManager: {e}') from e -if __name__ == '__main__': +def modem_info(ifname: str) -> dict: + """ Return everything ModemManager knows about the modem behind ifname """ + return mmcli('--modem', modem_index(ifname))['modem'] + +def sim_info(modem: dict) -> dict: + """ Return the properties of the SIM card inserted into a modem """ + sim = dict_search('generic.sim', modem) + if not sim or sim == '--': + raise ValueError('No SIM card inserted') + return mmcli('--sim', sim.split('/')[-1])['sim']['properties'] + +def format_value(value) -> str: + # ModemManager renders an unset scalar as "--" and an unset list as an + # empty one - neither is useful to read + if isinstance(value, list): + return ', '.join(str(v) for v in value) if value else 'n/a' + if value in (None, '', '--'): + return 'n/a' + return str(value) + +def show(data: dict, fields: dict) -> None: + """ Print a "Description: value" block, one field per line. Every field maps + to the dotted path of its value, optionally followed by "|" and the unit to + append to it. """ + width = max(len(field) for field in fields) + 2 + for field, path in fields.items(): + path, _, unit = path.partition('|') + value = format_value(dict_search(path, data)) + if unit and value != 'n/a': + value += unit + print(f'{field + ":":<{width}}{value}') + +def main(): args = parser.parse_args() - tmp = ConfigTreeQuery() - if not tmp.exists(['interfaces', 'wwan', args.interface]): + conf = ConfigTreeQuery() + if not conf.exists(['interfaces', 'wwan', args.interface]): print(f'Interface "{args.interface}" unconfigured!') exit(1) - # remove the WWAN prefix from the interface, required for the CDC interface - if_num = args.interface.replace('wwan','') - cdc = f'/dev/cdc-wdm{if_num}' + modem = modem_info(args.interface) if args.model: - qmi_cmd(cdc, '--dms-get-model') - elif args.capabilities: - qmi_cmd(cdc, '--dms-get-capabilities') - qmi_cmd(cdc, '--dms-get-band-capabilities') + show(modem, {'Manufacturer': 'generic.manufacturer', + 'Model': 'generic.model', + 'Hardware revision': 'generic.hardware-revision'}) elif args.revision: - qmi_cmd(cdc, '--dms-get-revision') + show(modem, {'Firmware revision': 'generic.revision', + 'Hardware revision': 'generic.hardware-revision', + 'Carrier configuration': 'generic.carrier-configuration'}) + elif args.capabilities: + show(modem, {'Current capabilities': 'generic.current-capabilities', + 'Supported capabilities': 'generic.supported-capabilities', + 'Current bands': 'generic.current-bands', + 'Supported bands': 'generic.supported-bands', + 'Current modes': 'generic.current-modes', + 'Supported IP families': 'generic.supported-ip-families'}) elif args.imei: - qmi_cmd(cdc, '--dms-get-ids') + show(modem, {'IMEI': '3gpp.imei', + 'Equipment identifier': 'generic.equipment-identifier'}) elif args.imsi: - qmi_cmd(cdc, '--dms-uim-get-imsi') + show(sim_info(modem), {'IMSI': 'imsi'}) elif args.msisdn: - qmi_cmd(cdc, '--dms-get-msisdn') + show(modem, {'MSISDN': 'generic.own-numbers'}) elif args.sim: - qmi_cmd(cdc, '--uim-get-card-status') + show(sim_info(modem), {'Operator': 'operator-name', + 'Operator code': 'operator-code', + 'ICCID': 'iccid', + 'IMSI': 'imsi', + 'Active': 'active'}) elif args.signal: - qmi_cmd(cdc, '--nas-get-signal-info') - qmi_cmd(cdc, '--nas-get-rf-band-info') + # ModemManager only exposes RSRP/RSRQ/SNR through its extended signal + # API, which has to be armed with "mmcli --signal-setup" first - a + # persistent change to the modem we have no business making from a + # "show" command, and one plenty of modems do not support at all. + show(modem, {'Access technology': 'generic.access-technologies', + 'Signal quality': 'generic.signal-quality.value|%', + 'Operator': '3gpp.operator-name', + 'Registration': '3gpp.registration-state', + 'Packet service': '3gpp.packet-service-state', + 'State': 'generic.state'}) elif args.firmware: - tmp = qmi_cmd(cdc, '--dms-get-manufacturer', silent=True) - if 'Sierra Wireless' in tmp: - qmi_cmd(cdc, '--dms-swi-get-current-firmware') - else: - qmi_cmd(cdc, '--dms-get-software-version') + show(modem, {'Firmware revision': 'generic.revision', + 'Carrier configuration': 'generic.carrier-configuration', + 'Carrier configuration revision': 'generic.carrier-configuration-revision'}) else: parser.print_help() exit(1) + +if __name__ == '__main__': + try: + main() + except (OSError, ValueError) as e: + # Do not swallow the reason - a modem that is not answering, a missing + # SIM and a ModemManager that is not running all used to surface as the + # very same "Command not supported by Modem" + print(e) + exit(1) |
