mirror of
https://github.com/kernelkit/infix.git
synced 2026-07-30 12:33:02 +02:00
Cellular modem location data (GPS coordinates, 3GPP cell info) was
previously CLI-only — 'show modem modem0' shelled out to modem-info
which queried mmcli on demand and rendered straight to terminal.
NETCONF/RESTCONF clients had no view into modem location, and there
was no way to tell how stale any modem-state field was.
This commit moves both into the YANG operational tree so they are
accessible through every northbound interface, not just the CLI. It
is Phase 1 of the modem GPS / location integration plan; Phase 2 will
route the modem's NMEA TTY into the existing gpsd → chronyd pipeline
so cellular modems can also serve as an NTP fallback time source.
YANG (infix-hardware.yang, revision 2026-05-10):
- modem-state/last-change (yang:date-and-time): freshness indicator
for the modem-state subtree, bumped on each modemd poll cycle
- modem-state/location-state container: source (gps|3gpp|...),
latitude/longitude/altitude (decimal64 6/6/1 fraction-digits),
cell-id/lac/tac/mcc/mnc, and its own last-change
Runtime path:
- modemd's check_location() now writes
/run/modemd/modemN/location/data.json with the full location data
plus an RFC3339 last-change. Atomic via tmp+rename. Walks the
mmcli output once instead of twice.
- check() touches /run/modemd/modemN/state.json with a fresh
last-change after each successful state poll.
- yanger/infix_modem.py reads both files via HOST.read_json and
folds them into the modem-state operational subtree.
- cli_pretty show_modem_detail rewritten to consume the operational
tree (same path as 'show modem' overview), with new 'Last Update'
lines for both top-level state and location.
- bin/show/modem() consolidated: both 'show modem' and
'show modem REF' use get_json('/ietf-hardware:hardware') — no
more direct modem-info subprocess call.
Cleanup along the way:
- Removed the legacy /run/modemd/modemN/location/{up,down,disabled,
failed} marker file writes — they had no readers anywhere in the
tree. In-memory self.location['state'] is kept purely as the
edge-trigger for the 'Retrieved GPS location' log line.
- Aligned now_rfc3339() output with yanger.common.YangDate (+00:00
form, not Z) so consumers see one consistent yang:date-and-time
format regardless of producer.
- Fixed multi-modem SIM scoping in show_modem_detail: now derives
sim<N> from modem<N> instead of picking the first SIM globally.
The bearer / hardware-revision / phone-number / supported-carrier
sections of 'show modem REF' are deliberately omitted for now —
those fields are not yet in the YANG tree. Bearer details in
particular likely belong on the wwan interface
(ietf-interfaces:interfaces-state) rather than on the modem hardware
component, and can be addressed in a follow-up.
Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
104 lines
3.4 KiB
Python
104 lines
3.4 KiB
Python
"""
|
|
Modem (wwan) interface operational state.
|
|
|
|
Populates `infix-interfaces:wwan/bearer-state` from modemd's modem-info
|
|
JSON output. Yanger calls wwan(ifname) from link.interface() when an
|
|
interface is classified as 'infix-if-type:modem'.
|
|
|
|
The mapping from wwan interface to bearer:
|
|
|
|
- modem-info reports a list of bearers per modem. Each bearer dict
|
|
has an 'interface' field set to the kernel device name (e.g.
|
|
'wwan0') when the bearer is connected; absent when disconnected.
|
|
- For a connected bearer we match by 'interface' directly.
|
|
- For disconnected bearers we fall back to modem index: bearers on
|
|
'modem{N}' default to 'wwan{N}' when no better hint is available.
|
|
|
|
That matches modemd's own naming convention for the single-bearer
|
|
case, which is the only case the codebase exercises today. Multi-
|
|
bearer setups will need a richer cross-reference (e.g. APN matching
|
|
against sysrepo config) — a follow-on once multi-bearer is in
|
|
serious use.
|
|
"""
|
|
|
|
from ..host import HOST
|
|
|
|
|
|
# modem-info bearer key -> bearer-state YANG leaf. Keys absent from
|
|
# the source dict are simply not emitted.
|
|
_BEARER_KEY_MAP = {
|
|
"path": "path",
|
|
"connected": "connected",
|
|
"connection-failed-reason": "connection-failed-reason",
|
|
"interface": "interface",
|
|
"ipv4-address": "ipv4-address",
|
|
"ipv4-prefix": "ipv4-prefix-length",
|
|
"ipv6-address": "ipv6-address",
|
|
"ipv6-prefix": "ipv6-prefix-length",
|
|
"in-bytes": "in-bytes",
|
|
"out-bytes": "out-bytes",
|
|
"total-in-bytes": "total-in-bytes",
|
|
"total-out-bytes": "total-out-bytes",
|
|
"total-duration": "total-duration",
|
|
}
|
|
|
|
|
|
def _bearer_state(bearer):
|
|
state = {}
|
|
for src, dst in _BEARER_KEY_MAP.items():
|
|
if src not in bearer:
|
|
continue
|
|
val = bearer[src]
|
|
if val in (None, "", "--"):
|
|
continue
|
|
if dst in ("ipv4-prefix-length", "ipv6-prefix-length"):
|
|
try:
|
|
val = int(val)
|
|
except (TypeError, ValueError):
|
|
continue
|
|
if val == 0:
|
|
continue
|
|
state[dst] = val
|
|
return state or None
|
|
|
|
|
|
def _find_bearer(modems, ifname):
|
|
"""Return the bearer dict matching ifname, or None.
|
|
|
|
Prefers an exact 'interface' match (connected case), falls back to
|
|
the bearer list of modem{N} when ifname is wwan{N}.
|
|
"""
|
|
for modem in modems:
|
|
bearers = (modem.get("status") or {}).get("bearer") or []
|
|
for b in bearers:
|
|
if b.get("interface") == ifname:
|
|
return b
|
|
|
|
# No connected match — fall back to wwan{N} → modem{N}.
|
|
if ifname.startswith("wwan"):
|
|
try:
|
|
idx = int(ifname[4:])
|
|
except ValueError:
|
|
return None
|
|
for modem in modems:
|
|
if modem.get("index") == idx:
|
|
bearers = (modem.get("status") or {}).get("bearer") or []
|
|
if bearers:
|
|
return bearers[0]
|
|
return None
|
|
|
|
|
|
def wwan(ifname):
|
|
modems = HOST.run_json(['/usr/libexec/modemd/modem-info'], [])
|
|
if not modems:
|
|
return None
|
|
|
|
bearer = _find_bearer(modems, ifname)
|
|
if not bearer:
|
|
return None
|
|
|
|
state = _bearer_state(bearer)
|
|
if not state:
|
|
return None
|
|
return {"bearer-state": state}
|