modemd: handle preferred-mode set without allowed-mode

Setting just preferred-mode in YANG (no allowed-mode leaf-list) built an
invalid mode string with an empty allowed:

  Mode 'allowed: ; preferred: 4g' is not supported

The empty allowed: can never match anything in the modem's supported-modes
list, so the user got a cryptic 'is not supported' even when their preferred
mode was, in principle, supported.

When only preferred-mode is configured, walk the modem's supported-modes list
for an entry that already has that preferred, and reuse its allowed set.  If
no supported entry has that preferred mode, fail with a message that lists
every combination the modem does support, easier to diagnose than the silent
'empty allowed' case.

Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
This commit is contained in:
Joachim Wiberg
2026-06-07 20:51:04 +02:00
parent 36ef190c3d
commit 009b31d465
+62 -16
View File
@@ -523,6 +523,12 @@ class ModemThread(threading.Thread):
# power cycle anyway — see MODEM.md.
self._reset_unsupported = False
# Signature of the last (allow, pref, supported-modes) tuple that
# prepare_modes rejected. Repeats are silent until the user changes
# the config (SIGHUP recycles the thread, clearing this) or the
# modem reports a different supported-modes set.
self._modes_failed_sig = None
self.init()
def stop(self):
@@ -1218,20 +1224,17 @@ class ModemThread(threading.Thread):
return self.prepare_profile_bearers(self.bearers["list"])
def prepare_bands(self):
self.info("Preparing bands")
bands = self.cfg.get("band", [])
if len(bands) == 0 or "any" in bands:
return True
self.info("Preparing bands")
if not self._mmcli("--set-current-bands=%s" % "|".join(bands)):
self.err("Unable to set band")
return False
return True
def prepare_modes(self):
self.info("Preparing modes")
pref = self.cfg.get("preferred-mode")
if not pref or pref == "any":
pref = "none"
@@ -1240,33 +1243,75 @@ class ModemThread(threading.Thread):
if "any" in allow:
allow = []
supported_modes = self.status.get("supported-modes", [])
# Same (config, modem-supports) inputs we already rejected — stay
# silent so the 3 s prepare-retry loop doesn't spam syslog with
# the same error. Cleared on success below, on SIGHUP (thread
# rebuild), or whenever the modem reports a different supported
# set. frozenset so leaf-list reorderings don't defeat the cache.
sig = (frozenset(allow), pref, frozenset(supported_modes))
if self._modes_failed_sig == sig:
return False
self.info("Preparing modes")
if pref == "none" and len(allow) == 0:
# User didn't pin anything; let the modem choose.
self._mmcli("--set-allowed-modes=any", check=False)
self._modes_failed_sig = None
return True
# If only preferred-mode is set (no allowed-mode), derive allowed
# from supported combinations that have this preferred. Skipping
# this would build an invalid 'allowed: ; preferred: X' string
# that never matches. Pick the widest supported allow-set so the
# user gets maximum fallback (e.g. 2g/3g/4g over 4g-only when both
# have 'preferred: 4g').
if len(allow) == 0:
best = None
for m in supported_modes:
if ("preferred: %s" % pref) not in m:
continue
head = m.split("allowed: ", 1)
if len(head) != 2:
continue
allowed_part = head[1].split(";", 1)[0].strip()
candidate = [t.strip() for t in allowed_part.split(",")
if t.strip()]
if best is None or len(candidate) > len(best):
best = candidate
if not best:
supported = "; ".join(supported_modes) or "(none reported)"
self.err("Modem does not support preferred mode '%s'. "
"Supported: %s" % (pref, supported))
self._modes_failed_sig = sig
return False
allow = best
mode = "allowed: %s; preferred: %s" % (", ".join(allow), pref)
supported = False
for m in self.status.get("supported-modes", []):
if m == mode:
supported = True
break
if not supported:
self.err("Mode '%s' is not supported" % mode)
if mode not in supported_modes:
supported = "; ".join(supported_modes) or "(none reported)"
self.err("Mode '%s' is not supported. Supported: %s" %
(mode, supported))
self._modes_failed_sig = sig
return False
current = self.status.get("current-modes", "")
if current == mode:
self.info("Mode '%s' already set" % mode)
self._modes_failed_sig = None
return True
self.info("Setting mode '%s'" % mode)
args = []
if len(allow) > 0:
args.append("--set-allowed-modes=%s" % "|".join(allow))
args = ["--set-allowed-modes=%s" % "|".join(allow)]
if pref != "none":
args.append("--set-preferred-mode=%s" % pref)
return self._mmcli(*args)
ok = self._mmcli(*args)
if ok:
self._modes_failed_sig = None
return ok
def _gps_at_command(self, enable):
"""Vendor-specific AT command to toggle GPS NMEA, or None.
@@ -1368,8 +1413,9 @@ class ModemThread(threading.Thread):
if not self.prepare_bands():
self.err("Unable to prepare bands")
return False
# prepare_modes logs a specific error on first failure and goes
# quiet on cached repeats; don't add a generic line on top.
if not self.prepare_modes():
self.err("Unable to prepare mode")
return False
if not self.prepare_bearers():
self.err("Unable to prepare bearers")