summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorChristian Breunig <christian@breunig.cc>2026-09-24 15:28:39 +0000
committerChristian Breunig <christian@breunig.cc>2026-09-24 15:28:39 +0000
commit9c88884a67c01b4e5d74872abee41820a8f65331 (patch)
tree56bbedac930fb0300c9f6267807f5faa5af1315c /src
parent112fb62b8b15653c0a26e0de56f326cd2d67da9b (diff)
downloadvyos-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-xsrc/op_mode/show_wwan.py148
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)