From f78f161dbe80a0c4398fd5f72b1212b52fa029c1 Mon Sep 17 00:00:00 2001 From: Joachim Wiberg Date: Sun, 3 May 2026 22:46:41 +0200 Subject: [PATCH] modem: migrate from infix-modem to ietf-hardware/ietf-interfaces MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The initial modem import used a standalone infix-modem YANG module with an integer-indexed list under /infix-modem:modems carrying a bearer config mixed in with hardware config. Infix has adopted the ietf-hardware model to allow for a clearer separation of concerns, this commit aims to improve on that situation by refactoring the modem model to use the same architectural pattern already used for WiFi radios, GPS receivers, and USB ports: Hardware: /ietf-hardware:hardware/component[class=infix-hardware:modem] /ietf-hardware:hardware/component[class=infix-hardware:sim] Interface: /ietf-interfaces:interfaces/interface[type=infix-if-type:modem] Configuration split: modem component — admin-state (enable/disable), radio access mode, frequency bands, location services, probe-timeout sim component — PIN, PUK, carrier profile wwan interface — APN, IP type, roaming, route-preference, auth This also enables the modem to appear in 'show hardware' alongside other hardware components, and to be managed over NETCONF/RESTCONF using the same ietf-hardware RPCs used for USB and WiFi. YANG changes: - infix-modem.yang removed (1089 lines, standalone module) - infix-hardware.yang: new modem and sim containers, modem-state and sim-state operational state, typedefs for bands/modes/location, notification status-update; actions restart/reset/send-sms are augmented directly on the component list entry (not inside container modem) to work around sysrepo NP-container parent validation — when checking a nested action's parent, sysrepo restricts its operational data fetch to the container path, excluding the sibling 'class' leaf needed to evaluate the container's 'when' condition, so the container is never instantiated and the parent check fails - infix-if-modem.yang: new submodule (same pattern as wifi/wireguard) adds the wwan bearer container to ietf-interfaces - infix-if-type.yang: new modem identity for wwan interface type - confd version bumped to 1.9 confd: - if-modem.c: new file replaces the stub modem_gen() in modem.c; generates dagger init scripts with probe-timeout wait loop and unconditional dummy wwan creation so downstream IP config succeeds when a USB modem is slow to enumerate at boot - hardware.c: start/stop/enable/disable modemd (and modem-manager) in response to admin-state changes on the modem hardware component; use finit_enable/disable consistently - hardware_cand_infer_class(): auto-set class for modemN/simN names - modem.c: stripped to RPC/notification forwarding only; the old genconf() / enable() / disable() logic is gone — hardware.c owns the lifecycle now - interfaces.c: remove wwan from wait-interface timeout (the modem interface appearance is managed by modemd, not confd's init scripts) - migrate/1.9: script fully migrates /infix-modem:modems to the new split model; each modem[N] becomes a modemN hardware component and simN sim component; each bearer becomes a wwanN interface (bearers numbered globally across modems); APN, IP type, roaming, preferred- mode, bands, location, PIN/PUK/carrier are all carried over; plain- text bearer credentials are moved to a named keystore symmetric-key (base64-encoded) referenced from bearer/authentication/password; apn-type, firewall-enabled, dns-enabled and default-route are dropped (no equivalent in the new model) modemd: - load_config() replaces the infix-modem sysrepocfg call; reads from ietf-hardware (modem/sim config), ietf-interfaces (bearer config), and ietf-keystore (credentials) — three standard modules instead of one proprietary one; modems absent from sysrepo config but present in system.json are still started (physical detection wins) - Default route written to /etc/net.d/.conf (picked up by netd/staticd/zebra) instead of a direct 'ip route add'; routes are now visible to FRR for redistribution and metric-based failover - Addresses tagged with proto wwan (rt_addrprotos entry 7) for precise flush on disconnect without touching other address sources - _derive_gateway() handles point-to-point bearers where modem reports gateway as "--"; derives peer from link-scope subnet route - SimThread exits cleanly on platforms without /dev/simctrl - sim-setup, modem-info, modem-udev, modem-rpc, modem-sms all ported to the ietf-hardware/ietf-interfaces namespace statd: - infix_modem.py now emits modem-state and sim-state into ietf-hardware component entries (modem0, sim0, ...) instead of a separate tree; sim-state reports physical slot, lock state, and SIM operator name - ietf_hardware.py: hardware component names deduplicated by rename (cpu → cpu, cpu1, ...) to prevent LY_EVALID when hwmon and thermal sysfs both normalise to the same sensor name - show hardware: Cellular Modems and SIM Cards tables added; sensor table width now adapts to the widest section in the output - 00-probe: USB modem detection writes to system.json["modem"], same source as wifi-radios, feeding gen-hardware which emits the ietf-hardware component entries at factory config generation time CLI: - 'show modem [ref]': without ref shows Cellular Modems and SIM Cards tables (same data subset as 'show hardware'); with ref shows a full per-modem dump (hardware info, status, cellular, SIM, bearers, GPS) - 'modem restart ': disconnect and reconnect all bearers without a full hardware reset - 'modem reset ': factory-reset the modem firmware - 'modem sms ': send an SMS via the modem (uses the signalling plane; no active data bearer required) - MODEMS PTYPE provides tab-completion reading modem names from /run/system.json doc: - Add initial User Guide for modems - Update ChangeLog with modem news - Use absolute URLs in ChangeLog. Relative links only work for in-tree references, not in release notes Signed-off-by: Joachim Wiberg --- .../common/rootfs/etc/iproute2/rt_addrprotos | 1 + .../rootfs/usr/libexec/infix/init.d/00-probe | 68 + doc/ChangeLog.md | 13 +- doc/README.md | 1 + doc/modem.md | 438 +++++++ mkdocs.yml | 1 + package/confd/confd.mk | 7 + package/feature-modem/feature-modem.mk | 1 + src/bin/show/__init__.py | 28 + src/confd/bin/gen-hardware | 26 + .../migrate/1.9/10-remove-infix-modem.sh | 123 ++ src/confd/share/migrate/1.9/Makefile.am | 4 +- src/confd/src/Makefile.am | 1 + src/confd/src/hardware.c | 53 +- src/confd/src/if-modem.c | 112 ++ src/confd/src/interfaces.c | 7 +- src/confd/src/interfaces.h | 6 +- src/confd/src/modem.c | 276 ++--- src/confd/yang/confd.inc | 5 +- src/confd/yang/confd/infix-hardware.yang | 737 ++++++++++- .../yang/confd/infix-hardware@2026-04-27.yang | 1 + src/confd/yang/confd/infix-if-modem.yang | 315 +++++ .../yang/confd/infix-if-modem@2026-04-27.yang | 1 + src/confd/yang/confd/infix-if-type.yang | 17 + .../yang/confd/infix-if-type@2026-04-27.yang | 1 + src/confd/yang/confd/infix-interfaces.yang | 6 + .../confd/infix-interfaces@2026-04-27.yang | 1 + src/confd/yang/confd/infix-modem.yang | 1089 ----------------- .../yang/confd/infix-modem@2024-03-15.yang | 1 - src/confd/yang/modem.inc | 5 + src/klish-plugin-infix/xml/infix.xml | 42 + src/modemd/modem-info | 185 ++- src/modemd/modem-rpc | 9 +- src/modemd/modem-sms | 16 +- src/modemd/modem-udev | 48 +- src/modemd/modemd | 302 +++-- src/modemd/sim-setup | 39 +- src/statd/python/cli_pretty/cli_pretty.py | 311 ++++- src/statd/python/yanger/__main__.py | 3 - src/statd/python/yanger/ietf_hardware.py | 38 +- src/statd/python/yanger/infix_modem.py | 84 +- 41 files changed, 2920 insertions(+), 1502 deletions(-) create mode 100644 doc/modem.md create mode 100755 src/confd/share/migrate/1.9/10-remove-infix-modem.sh create mode 100644 src/confd/src/if-modem.c create mode 120000 src/confd/yang/confd/infix-hardware@2026-04-27.yang create mode 100644 src/confd/yang/confd/infix-if-modem.yang create mode 120000 src/confd/yang/confd/infix-if-modem@2026-04-27.yang create mode 120000 src/confd/yang/confd/infix-if-type@2026-04-27.yang create mode 120000 src/confd/yang/confd/infix-interfaces@2026-04-27.yang delete mode 100644 src/confd/yang/confd/infix-modem.yang delete mode 120000 src/confd/yang/confd/infix-modem@2024-03-15.yang create mode 100644 src/confd/yang/modem.inc diff --git a/board/common/rootfs/etc/iproute2/rt_addrprotos b/board/common/rootfs/etc/iproute2/rt_addrprotos index f31bdca2..3de5ebd2 100644 --- a/board/common/rootfs/etc/iproute2/rt_addrprotos +++ b/board/common/rootfs/etc/iproute2/rt_addrprotos @@ -5,3 +5,4 @@ 4 static 5 dhcp 6 random +7 wwan diff --git a/board/common/rootfs/usr/libexec/infix/init.d/00-probe b/board/common/rootfs/usr/libexec/infix/init.d/00-probe index e2054cef..45691039 100755 --- a/board/common/rootfs/usr/libexec/infix/init.d/00-probe +++ b/board/common/rootfs/usr/libexec/infix/init.d/00-probe @@ -620,6 +620,73 @@ def probe_ptp_capabilities(out): out.setdefault("interfaces", {}).update(ifaces) +def _usb_root_from_sysfs(dev_path): + p = dev_path + while p not in ("/", "/sys"): + parent = os.path.realpath(os.path.join(p, "..")) + basename = os.path.basename(parent) + if basename.startswith("usb") and basename[3:].isdigit(): + return parent + p = parent + return None + + +def _read_usb_vid_pid(dev_path): + p = dev_path + while p not in ("/", "/sys"): + try: + vid = open(os.path.join(p, "idVendor")).read().strip() + pid = open(os.path.join(p, "idProduct")).read().strip() + return vid, pid + except OSError: + pass + p = os.path.dirname(p) + return "", "" + + +def probe_modems(out): + """devpath is the USB bus root; ModemManager matches by path prefix. + seen_roots deduplicates cdc-wdm and ttyUSB ports of the same physical modem.""" + seen_roots = set() + modems = [] + idx = 0 + + def add_modem(dev_path): + nonlocal idx + usb_root = _usb_root_from_sysfs(dev_path) + if usb_root is None or usb_root in seen_roots: + return + seen_roots.add(usb_root) + vid, pid = _read_usb_vid_pid(dev_path) + modems.append({ + "index": idx, + "name": "modem%d" % idx, + "devpath": usb_root, + "vid": vid, + "pid": pid, + }) + idx += 1 + + for cls_dir, prefix in (("/sys/class/usbmisc", "cdc-wdm"), + ("/sys/class/tty", "ttyUSB")): + try: + entries = os.listdir(cls_dir) + except OSError: + continue + for entry in entries: + if not entry.startswith(prefix): + continue + try: + dev_path = os.path.realpath( + os.path.join(cls_dir, entry, "device")) + add_modem(dev_path) + except OSError: + pass + + if modems: + out["modem"] = modems + + def main(): out = { "vendor": None, @@ -647,6 +714,7 @@ def main(): probe_wifi_radios(out) probe_ptp_capabilities(out) + probe_modems(out) if not out["factory-password-hash"]: sys.stdout.write("\n\n\033[31mCRITICAL BOOTSTRAP ERROR\n" + diff --git a/doc/ChangeLog.md b/doc/ChangeLog.md index 5e9855ce..52a1b440 100644 --- a/doc/ChangeLog.md +++ b/doc/ChangeLog.md @@ -10,13 +10,17 @@ All notable changes to the project are documented in this file. - Add Wi-Fi roaming for fast, seamless handoff between access points that share an SSID: 802.11k, 802.11v and 802.11r (over-the-air FT). See the - [Wi-Fi][wifi] guide for details + [Wi-Fi User's Guide][wifi] for details - Add Wi-Fi 802.11s mesh support, letting access points form a wireless backhaul between each other without cabling - Add band steering for dual-band access points, nudging dual-band clients onto the faster 5/6 GHz band - Add `legacy-rates` option to re-enable 802.11b rates on 2.4 GHz for old IoT devices (disabled by default) +- Add cellular modem (WWAN) support for USB-attached MBIM/QMI modems, + including USB dongles, mPCIe cards, and M.2 Key-B modules. Multiple + APNs per modem, SIM PIN configuration, and NMEA routing from modem + GPS to gpsd. See the [Modem User's Guide][modem] for details ### Fixes @@ -24,7 +28,8 @@ All notable changes to the project are documented in this file. sysctls. You must now enable IP forwarding explicitly on the interfaces that should route traffic; enabling NAT alone is no longer enough -[wifi]: wifi.md +[wifi]: https://www.kernelkit.org/infix/latest/wifi/ +[modem]: https://www.kernelkit.org/infix/latest/modem/ [v26.05.0][] - 2026-05-29 ------------------------- @@ -83,9 +88,9 @@ All notable changes to the project are documented in this file. show`, which were not expanded, so `show interface` and other operational reads failed. Ranges are now expanded and listed correctly -[ethernet]: ethernet.md#restricting-advertised-link-modes +[ethernet]: https://www.kernelkit.org/infix/latest/ethernet/#restricting-advertised-link-modes [BPI-R3]: https://docs.banana-pi.org/en/BPI-R3/BananaPi_BPI-R3 -[AcerConnectVero]: ../board/aarch64/acer-connect-vero-w6m/ +[AcerConnectVero]: https://github.com/kernelkit/infix/tree/main/board/aarch64/acer-connect-vero-w6m/ [v26.04.0][] - 2026-04-30 ------------------------- diff --git a/doc/README.md b/doc/README.md index 8b7b74d8..ea24daf9 100644 --- a/doc/README.md +++ b/doc/README.md @@ -23,6 +23,7 @@ regression test system solely relies on NETCONF and RESTCONF. - [Network Configuration](networking.md) - [Wi-Fi](wifi.md) - [DHCP Server](dhcp.md) + - [Cellular Modem (WWAN)](modem.md) - [Syslog Support](syslog.md) - **Infix In-Depth** - [Boot Procedure](boot.md) diff --git a/doc/modem.md b/doc/modem.md new file mode 100644 index 00000000..25d4443d --- /dev/null +++ b/doc/modem.md @@ -0,0 +1,438 @@ +# Cellular Modem (WWAN) + +Infix supports cellular modem connectivity via modems that expose a +QMI or MBIM control interface over USB. Form factor does not matter: +USB dongles, mPCIe cards, and M.2 Key-B modules all work as long as +the modem chipset uses USB on the connector (the typical case for +4G/LTE modems). See *Supported Modems* below for the exceptions. + +Setup involves three configuration items: + +- A `modem0` hardware component for the physical modem +- A `sim0` hardware component for the SIM card slot +- A `wwan0` network interface that references both and carries the + bearer (APN) configuration + +## Architecture + +Infix uses a three-layer architecture for cellular modem support: + +1. **Modem hardware component (modem0)**: the physical modem + - Configured via `ietf-hardware` with class `infix-hardware:modem` + - `admin-state` controls whether ModemManager and modemd are active + - Holds physical-layer config: allowed bands, preferred mode, location + - Auto-discovered into factory-default config when the modem is + present at first boot + +2. **SIM hardware component (sim0)**: the SIM card slot + - Configured via `ietf-hardware` with class `infix-hardware:sim` + - Holds PIN/PUK credentials and carrier profile + - Auto-discovered into factory-default config alongside the modem + +3. **Network interface (wwan0)**: data bearer to the cellular network + - Configured via `ietf-interfaces` with type `infix-if-type:modem` + - References a modem component and a SIM component + - Holds bearer config: APN, IP type, roaming, route preference, + authentication + - Always added by the user, never auto-created + +## Naming Conventions + +| **Name Pattern** | **Type** | **Description** | +|------------------|----------------------|-------------------------------------------------| +| `modemN` | Modem hardware | Hardware component for the physical modem | +| `simN` | SIM hardware | Hardware component for the SIM card slot | +| `wwanN` | Modem interface | Network interface for cellular data | + +Where `N` is a number (0, 1, 2, ...). + +> [!TIP] +> Using these naming conventions simplifies configuration since type and +> class are automatically inferred. Creating a hardware component named +> `modem0` automatically sets its class to `infix-hardware:modem`, and +> creating an interface named `wwan0` automatically sets its type to +> `infix-if-type:modem`. +> +> **Note:** This inference only works via the CLI. When configuring +> over NETCONF or RESTCONF the class and type must be set explicitly. + +## Multi-Bearer (Multiple APNs) + +Multiple wwan interfaces can reference the same modem component, each +with a different APN. Same idea as multi-SSID on a WiFi radio: one +hardware modem, several independent data connections. + +Configure `wwan0` and `wwan1` both pointing to `modem0`: + +``` +edit interface wwan0 wwan +set modem modem0 +set bearer apn internet + +edit interface wwan1 wwan +set modem modem0 +set bearer apn corporate.vpn.apn +``` + +## Current Limitations + +- The modem must be present at boot. Hot-plug is not supported. +- If the modem is absent at boot, and no `probe-timeout` is set, a + dummy `wwan0` placeholder is created immediately so IP configuration + can proceed. A reboot is required once the hardware is inserted. + +## Supported Modems + +Modems exposing a CDC-WDM control interface over USB are supported, +regardless of physical form factor. Two protocols are handled by +ModemManager: + +- **MBIM**: Mobile Broadband Interface Model (e.g. Sierra Wireless, + Quectel EM06/EM12) +- **QMI**: Qualcomm MSM Interface (e.g. Sierra Wireless EM7xxx, + Quectel EM/RMxxx) + +Most 4G/LTE modules (USB dongles, mPCIe cards, M.2 Key-B) use USB on +the connector even when the slot also carries PCIe lanes. From Infix's +view they are all USB modems. PCIe-only modems (some 5G NR modules) +are not supported, since the modemd / ModemManager pipeline assumes a +USB-attached control interface. + +## Step-by-step Setup + +### 1. Hardware Detection + +At first boot, USB modems detected by the kernel are written to +`/run/system.json` and added as hardware components in the +factory-default configuration. Verify the modem appears: + +
admin@example:/> show modem
+──────────────────────────────────────────────────────────────
+Cellular Modems
+NAME    MANUFACTURER  MODEL   STATE       NETWORK  SIGNAL
+modem0  Quectel       EM06-E  registered  lte      good
+──────────────────────────────────────────────────────────────
+SIM Cards
+NAME  SLOT  STATE     OPERATOR
+sim0  1     unlocked  Tele2
+
+ +The `STATE`, `SIM STATE`, and `SIGNAL` columns are color-coded so the +healthy / attention / problem cases are visible at a glance: green for +`registered`, `connected`, `unlocked`, `excellent`, `good`; yellow for +transient or attention states like `enabling`, `pin-required`, `poor`; +red for `failed`, `not-inserted`, `bad`. + +If `modem0` does not appear: + +- Verify the kernel sees the modem (`dmesg | grep -i mbim` or + `dmesg | grep -i qmi`). Without a kernel driver no further setup + is possible. +- On boards with a custom factory configuration, or after replacing + hardware on a running system, the components may need to be added + manually. See Step 2. + +### 2. Hardware Components + +If `show modem` already lists `modem0` and `sim0`, the components were +auto-discovered into the factory-default configuration and you can skip +ahead to Step 3. + +Otherwise, add them manually. The class is inferred from the component +name (`modemN` → `infix-hardware:modem`, `simN` → `infix-hardware:sim`). +The `admin-state` must be set explicitly. There is no implicit default, +and without `unlocked` confd will not start ModemManager or modemd: + +
admin@example:/> configure
+admin@example:/config/> edit hardware component modem0
+admin@example:/config/hardware/component/modem0/> set state admin-state unlocked
+admin@example:/config/hardware/component/modem0/> end
+admin@example:/config/hardware/> edit component sim0
+admin@example:/config/hardware/component/sim0/> set state admin-state unlocked
+admin@example:/config/hardware/component/sim0/> leave
+admin@example:/>
+
+ +To take the modem offline cleanly without removing the configuration, +set `admin-state locked`. All related services are stopped and the +bearer is torn down before the modem goes offline: + +
admin@example:/config/> edit hardware component modem0
+admin@example:/config/hardware/component/modem0/> set state admin-state locked
+admin@example:/config/hardware/component/modem0/> leave
+
+ +#### Slow USB Modems + +USB modems can be slow to enumerate at boot. The kernel wwan interface +may not appear until several seconds after confd starts applying +configuration. The `probe-timeout` leaf inside the `modem` hardware +configuration container controls how long confd waits. It defaults to +30 seconds when the container is present. + +To enable the timeout, create the modem configuration container (this +also lets you configure bands, preferred mode, etc.): + +
admin@example:/config/> edit hardware component modem0 modem
+admin@example:/config/hardware/component/modem0/modem/> leave
+
+ +With `probe-timeout` at its default of 30, confd waits up to 30 seconds +for the wwan interface to appear before proceeding. For most modems it +is ready in 2-5 seconds. If the modem has not appeared within the +timeout, a dummy placeholder interface is created and a reboot is +required for the real interface to take over. Set `probe-timeout 0` +to disable waiting entirely. + +> [!NOTE] +> Some Quectel modules (e.g. EM05) re-enumerate the USB device several +> times during a cold-boot firmware-init sequence that takes around 50 +> to 60 seconds. Bump `probe-timeout` to 90 for those, otherwise the +> dummy placeholder fires before the modem settles and the real +> interface ends up with a different name (`wwan1` instead of `wwan0`). + +### 3. Configure the Bearer (APN) + +Bearer configuration lives on the `wwan0` interface. Reference the +modem hardware component and set the APN: + +
admin@example:/> configure
+admin@example:/config/> edit interface wwan0
+admin@example:/config/interface/wwan0/> set wwan modem modem0
+admin@example:/config/interface/wwan0/> set wwan sim sim0
+admin@example:/config/interface/wwan0/> set wwan bearer apn internet
+admin@example:/config/interface/wwan0/> leave
+
+ +The modem will connect automatically once the bearer is configured and +the hardware is unlocked. + +**Key bearer parameters:** + +- `apn`: Access Point Name, required, provided by your operator + (e.g. `internet`, `data.vodafone.com`, `web.tele2.se`) +- `route-preference`: Administrative distance for the default route + (default: `200`). Higher value = lower priority. The default of 200 + places cellular behind wired Ethernet (distance 5) and WiFi, so + cellular acts as a failover when the wired link is up +- `roaming`: Allow data when roaming on a foreign network + (default: `false`) +- `ip-type`: `ipv4`, `ipv6`, or `ipv4v6` dual-stack + (default: `ipv4v6`) + +### 4. Configure Authentication + +Most consumer APNs connect without credentials. If your operator +requires authentication, first store the password in the keystore, then +reference it from the bearer. + +Create the keystore entry for the APN password: + +
admin@example:/> configure
+admin@example:/config/> edit keystore symmetric-key apn-pass
+admin@example:/config/keystore/…/apn-pass/> set key-format passphrase-key-format
+admin@example:/config/keystore/…/apn-pass/> change cleartext-symmetric-key
+Passphrase: ************
+Retype passphrase: ************
+admin@example:/config/keystore/…/apn-pass/> leave
+
+ +Then point the bearer's authentication at it: + +
admin@example:/> configure
+admin@example:/config/> edit interface wwan0 wwan bearer
+admin@example:/config/interface/wwan0/wwan/bearer/> set authentication username myuser
+admin@example:/config/interface/wwan0/wwan/bearer/> set authentication password apn-pass
+admin@example:/config/interface/wwan0/wwan/bearer/> leave
+
+ +Setting any leaf inside `authentication` creates the container, which +enables authentication. The `password` leaf is a reference to a +symmetric key in the keystore, not the plaintext password itself. + +The authentication protocol defaults to `chap`. To use PAP instead: + +
admin@example:/config/interface/wwan0/wwan/bearer/> set authentication type pap
+
+ +### 5. Configure SIM PIN + +If the SIM requires a PIN to unlock, configure it on the SIM hardware +component: + +
admin@example:/config/> edit hardware component sim0
+admin@example:/config/hardware/component/sim0/> set sim pin 1234
+admin@example:/config/hardware/component/sim0/> leave
+
+ +### 6. Verify Connectivity + +Once connected, the `wwan0` interface receives an IP address from the +carrier and modemd installs the default route: + +
admin@example:/> show interface wwan0
+name                : wwan0
+type                : modem
+index               : 5
+mtu                 : 1500
+operational status  : up
+ip forwarding       : enabled
+physical address    : 12:34:56:78:9a:bc
+ipv4 addresses      : 10.142.87.33/30 (wwan)
+ipv6 addresses      : 2001:db8:1:2::1/64 (wwan)
+in-octets           : 84213
+out-octets          : 31456
+
+ +Check the full modem state including signal quality and registration: + +
admin@example:/> show modem modem0
+MODEM: modem0                                                 
+──────────────────────────────────────────────────────────────
+Hardware Information
+  Manufacturer        : Quectel
+  Model               : EM06-E
+  Firmware Version    : EM06ELAR04A07M4G
+  Serial Number       : 352753090141905
+  IMSI                : 240021234567890
+  ICCID               : 8946020000001234567
+──────────────────────────────────────────────────────────────
+Status
+  State               : connected
+  Power State         : on
+  Signal              : Quality: 72% good  RSSI: -59 dBm  RSRP: -95 dBm  RSRQ: -11.0 dB
+──────────────────────────────────────────────────────────────
+Cellular
+  Registration        : home
+  Service State       : attached
+  Operator            : Tele2
+  Operator ID         : 23002
+  Network Type        : lte
+──────────────────────────────────────────────────────────────
+SIM Card
+  Name                : sim0
+  Slot                : 1
+  Lock State          : unlocked
+  Operator            : Tele2
+
+ +When the modem is in trouble, the same view shows why. A +`State: failed` line is followed by `Failed Reason` (e.g. +`sim-missing`), and the `SIM Card` section's `Lock State` reads +`not-inserted` or `pin-required` to match. + +## Cellular Failover + +The default `route-preference` (200) is deliberately higher than the +distances used by wired Ethernet (udhcpc default: 5) and WiFi. When a +wired link is up it takes precedence automatically; cellular becomes +the active path only if higher-priority routes are withdrawn. + +A default route via the bearer is always installed when the bearer +connects. To adjust the failover priority between two cellular modems, +or to prefer cellular over WiFi, set `route-preference` explicitly: + +
admin@example:/config/> edit interface wwan0 wwan bearer
+admin@example:/config/interface/wwan0/wwan/bearer/> set route-preference 100
+admin@example:/config/interface/wwan0/wwan/bearer/> leave
+
+ +Lower `route-preference` = higher priority. + +## Roaming + +Data roaming is disabled by default. To allow the modem to connect +when on a foreign (roaming) network: + +
admin@example:/config/> edit interface wwan0 wwan bearer
+admin@example:/config/interface/wwan0/wwan/bearer/> set roaming true
+admin@example:/config/interface/wwan0/wwan/bearer/> leave
+
+ +> [!IMPORTANT] +> Enabling roaming may incur significant charges depending on your +> mobile subscription. Check with your operator before enabling. + +## Management Commands + +### Restart Bearer + +Disconnect and reconnect all bearers without resetting the modem +hardware. Use this after changing APN or authentication settings: + +
admin@example:/> modem restart modem0
+
+ +### Reset Modem + +Factory-reset the modem firmware. This clears all modem-internal +settings and takes longer than a restart. Only use it if the modem is +in a bad state that a bearer restart cannot fix: + +
admin@example:/> modem reset modem0
+
+ +> [!NOTE] +> Not all modules accept `--reset` from ModemManager. Quectel EM05, +> for example, rejects both `--reset` and `--factory-reset`. The only +> way to recover from a hung firmware on these is a physical power +> cycle. When the modem reports the rejection, modemd logs it once and +> stops retrying. + +### Send SMS + +Send an SMS message via the signalling plane. No active data bearer +is required; the modem only needs to be registered on the network: + +
admin@example:/> modem sms modem0 +46701234567 "Hello from Infix"
+
+ +> [!NOTE] +> Some SIM cards have Fixed Dialing Number (FDN) enabled, which +> restricts outgoing SMS and calls to a pre-configured whitelist. If +> `modem sms` fails, check whether FDN is active with `mmcli -m 0` and +> look for `enabled locks: fixed-dialing` in the output. + +## Troubleshooting + +**Modem not detected (`show modem` shows no modem entry)** + +- Verify the modem is connected and recognized by the kernel: check + `dmesg` for `cdc_mbim` or `qmi_wwan` driver messages +- Confirm `/sys/class/usbmisc/` contains a `cdc-wdm*` entry +- The modem must be present at boot; hotplug after boot is not + supported + +**`wwan0` shows as `down` or has no IP address** + +- Check `show modem modem0`. The State should show `registered` or + `connected`, not `failed` +- If the State is `failed`, look at the `Failed Reason` line that + immediately follows: + - `sim-missing`: the SIM Card section will also show + `Lock State: not-inserted`. Power off the device, insert the + SIM, power back on. A warm reboot is not enough; on most M.2 + slots the SIM tray stays powered through reboot, so the modem + keeps the original not-inserted reading + - `unlock-required`: the SIM Card section will show + `Lock State: pin-required`. Configure the PIN with + `set hardware component sim0 sim pin ` + - `sim-wrong` / `sim-error`: the SIM is not compatible or is + damaged. Try a different SIM +- Verify the APN is correct for your operator +- Check system logs with `show log` for modemd or ModemManager + messages + +**`wwan0` interface is a dummy (no data flows, no carrier address)** + +- The modem was not enumerated by the kernel before confd applied + config +- Create the modem hardware configuration container (see Step 2), + which enables the default 30-second probe-timeout so confd waits for + the wwan interface before falling back to a dummy placeholder + +**High latency or poor signal** + +- Use `show modem modem0` to check signal quality and RSRP +- Signal below -110 dBm RSRP typically indicates poor coverage +- Consider repositioning the antenna or the device diff --git a/mkdocs.yml b/mkdocs.yml index b372c903..f4f5f53f 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -44,6 +44,7 @@ nav: - Overview: vpn.md - WireGuard: vpn-wireguard.md - Wireless LAN (WiFi): wifi.md + - Cellular Modem (wwan): modem.md - Services: - Device Discovery: discovery.md - DHCP Server: dhcp.md diff --git a/package/confd/confd.mk b/package/confd/confd.mk index ccb50d2b..b4408c95 100644 --- a/package/confd/confd.mk +++ b/package/confd/confd.mk @@ -91,6 +91,12 @@ define CONFD_INSTALL_YANG_MODULES_GPS $(BR2_EXTERNAL_INFIX_PATH)/utils/srload $(@D)/yang/gps.inc endef endif +ifeq ($(BR2_PACKAGE_FEATURE_MODEM),y) +define CONFD_INSTALL_YANG_MODULES_MODEM + $(COMMON_SYSREPO_ENV) \ + $(BR2_EXTERNAL_INFIX_PATH)/utils/srload $(@D)/yang/modem.inc +endef +endif # PER_PACKAGE_DIR # Since the last package in the dependency chain that runs sysrepoctl is confd, we need to @@ -121,6 +127,7 @@ CONFD_POST_INSTALL_TARGET_HOOKS += CONFD_INSTALL_YANG_MODULES CONFD_POST_INSTALL_TARGET_HOOKS += CONFD_INSTALL_YANG_MODULES_CONTAINERS CONFD_POST_INSTALL_TARGET_HOOKS += CONFD_INSTALL_YANG_MODULES_WIFI CONFD_POST_INSTALL_TARGET_HOOKS += CONFD_INSTALL_YANG_MODULES_GPS +CONFD_POST_INSTALL_TARGET_HOOKS += CONFD_INSTALL_YANG_MODULES_MODEM CONFD_POST_INSTALL_TARGET_HOOKS += CONFD_INSTALL_IN_ROMFS CONFD_TARGET_FINALIZE_HOOKS += CONFD_CLEANUP diff --git a/package/feature-modem/feature-modem.mk b/package/feature-modem/feature-modem.mk index d5a7827b..52c9a66e 100644 --- a/package/feature-modem/feature-modem.mk +++ b/package/feature-modem/feature-modem.mk @@ -14,6 +14,7 @@ define FEATURE_MODEM_LINUX_CONFIG_FIXUPS $(call KCONFIG_ENABLE_OPT,CONFIG_USB_WDM) $(call KCONFIG_ENABLE_OPT,CONFIG_USB_NET_QMI_WWAN) $(call KCONFIG_ENABLE_OPT,CONFIG_USB_CDC_MBIM) + $(call KCONFIG_ENABLE_OPT,CONFIG_USB_NET_CDC_MBIM) $(if $(filter y,$(BR2_PACKAGE_FEATURE_MODEM_QUALCOMM)), $(call KCONFIG_SET_OPT,CONFIG_QRTR,m) diff --git a/src/bin/show/__init__.py b/src/bin/show/__init__.py index e4b050b3..8352b455 100755 --- a/src/bin/show/__init__.py +++ b/src/bin/show/__init__.py @@ -81,6 +81,33 @@ def hardware(args: List[str]) -> None: cli_pretty(data, "show-hardware") +def modem(args: List[str]) -> None: + ref = args[0] if args else None + + if ref: + try: + result = subprocess.run(["/usr/libexec/modemd/modem-info"], + capture_output=True, text=True, check=True) + data = json.loads(result.stdout) if result.stdout.strip() else [] + except (subprocess.CalledProcessError, json.JSONDecodeError): + data = [] + + if RAW_OUTPUT: + print(json.dumps(data, indent=2)) + return + cli_pretty({"modem-list": data}, "show-modem-detail", ref) + else: + data = get_json("/ietf-hardware:hardware") + if not data: + print("No modem data available.") + return + + if RAW_OUTPUT: + print(json.dumps(data, indent=2)) + return + cli_pretty(data, "show-modem") + + def ntp(args: List[str]) -> None: # Create argument parser for ntp subcommands parser = argparse.ArgumentParser(prog='show ntp', add_help=False) @@ -744,6 +771,7 @@ def execute_command(command: str, args: List[str]): 'keystore': keystore, 'lldp': lldp, 'mdns': mdns, + 'modem': modem, 'nacm': nacm, 'ntp': ntp, 'ospf': ospf, diff --git a/src/confd/bin/gen-hardware b/src/confd/bin/gen-hardware index 8679bc73..c419500b 100755 --- a/src/confd/bin/gen-hardware +++ b/src/confd/bin/gen-hardware @@ -11,6 +11,11 @@ if jq -e '.["wifi-radios"]' /run/system.json > /dev/null 2>&1; then else wifi_radios="" fi +if jq -e '.["modem"]' /run/system.json > /dev/null 2>&1; then + modem_names=$(jq -r '.["modem"][].name' /run/system.json) +else + modem_names="" +fi gen_port() @@ -61,6 +66,20 @@ gen_radio() EOF } +gen_modem() +{ + modem="$1" + cat < 0 + then {"allowed-mode": [$m["allowed-mode"][].mode]} + else {} end) + + (if (($m.band // []) | length) > 0 + then {"band": [$m.band[].band]} + else {} end) + + (if $m.location then + {"location": ( + {"enabled": ($m.location.enabled // false)} + + (if (($m.location.source // []) | length) > 0 + then {"source": [$m.location.source[].source]} + else {} end) + )} + else {} end) + | if . == {} then {} else {"infix-hardware:modem": .} end) + )) as $modem_comps | + + # Hardware components for each SIM (simN, keyed by modem.sim index). + ($modems | map(. as $m | + select($m.sim != null) | + { + "name": ("sim" + ($m.sim | tostring)), + "class": "infix-hardware:sim" + } + ( + (if $m.pin then {"pin": $m.pin} else {} end) + + (if $m.puk then {"puk": $m.puk} else {} end) + + (if $m.carrier then {"carrier": $m.carrier} else {} end) + | if . == {} then {} else {"infix-hardware:sim": .} end) + )) as $sim_comps | + + # Keystore entries for bearers that have a non-empty password. + ($bearer_list | map( + select((.bearer.password // "") != "") | + { + "name": ("apn-wwan" + (.wwan_index | tostring) + "-pass"), + "key-format": "infix-crypto-types:passphrase-key-format", + "cleartext-symmetric-key": ((.bearer.password // "") | @base64) + } + )) as $ks_entries | + + # wwan interfaces, one per bearer. + ($bearer_list | map(. as $entry | + ($entry.modem) as $m | + ($entry.bearer) as $b | + ("wwan" + ($entry.wwan_index | tostring)) as $wname | + ("apn-wwan" + ($entry.wwan_index | tostring) + "-pass") as $kname | + { + "name": $wname, + "type": "infix-if-type:modem", + "infix-interfaces:wwan": { + "modem": ("modem" + ($m.index | tostring)), + "sim": ("sim" + ($m.sim | tostring)), + "bearer": ( + (if $b.apn then {"apn": $b.apn} else {} end) + + (if $b["ip-type"] then {"ip-type": $b["ip-type"]} else {} end) + + (if ($b["allow-roaming"] // false) then {"roaming": true} else {} end) + + (if (($b.username // "") != "") then + {"authentication": {"username": $b.username, "password": $kname}} + else {} end) + ) + } + } + )) as $wwan_ifaces | + + # Apply: delete old tree, inject new hardware components, keystore entries, interfaces. + del(."infix-modem:modems") + + | .["ietf-hardware:hardware"]["component"] = ( + (.["ietf-hardware:hardware"]?.component // []) + $modem_comps + $sim_comps) + + | if ($ks_entries | length) > 0 then + .["ietf-keystore:keystore"]["symmetric-keys"]["symmetric-key"] = ( + (.["ietf-keystore:keystore"]?."symmetric-keys"?."symmetric-key" // []) + $ks_entries) + else . end + + | .["ietf-interfaces:interfaces"]["interface"] = ( + (.["ietf-interfaces:interfaces"]?.interface // []) + $wwan_ifaces) + +else + . +end +' "$file" > "$temp" && mv "$temp" "$file" diff --git a/src/confd/share/migrate/1.9/Makefile.am b/src/confd/share/migrate/1.9/Makefile.am index 4331804f..9a084795 100644 --- a/src/confd/share/migrate/1.9/Makefile.am +++ b/src/confd/share/migrate/1.9/Makefile.am @@ -1,2 +1,4 @@ migratedir = $(pkgdatadir)/migrate/1.9 -dist_migrate_DATA = 10-ethernet-advertise.sh +dist_migrate_DATA = \ + 10-ethernet-advertise.sh \ + 10-remove-infix-modem.sh diff --git a/src/confd/src/Makefile.am b/src/confd/src/Makefile.am index 979e56e7..70ab7d3f 100644 --- a/src/confd/src/Makefile.am +++ b/src/confd/src/Makefile.am @@ -48,6 +48,7 @@ confd_plugin_la_SOURCES = \ if-vxlan.c \ if-wifi.c \ if-wireguard.c \ + if-modem.c \ modem.c \ keystore.c \ system.c \ diff --git a/src/confd/src/hardware.c b/src/confd/src/hardware.c index 06629d36..692831a6 100644 --- a/src/confd/src/hardware.c +++ b/src/confd/src/hardware.c @@ -174,6 +174,16 @@ static int hardware_cand_infer_class(json_t *root, sr_session_ctx_t *session, co err = srx_set_item(session, &inferred, 0, "%s/class", xpath); } + if (!fnmatch("modem+([0-9])", name, FNM_EXTMATCH)) { + inferred.data.string_val = "infix-hardware:modem"; + err = srx_set_item(session, &inferred, 0, "%s/class", xpath); + } + + if (!fnmatch("sim+([0-9])", name, FNM_EXTMATCH)) { + inferred.data.string_val = "infix-hardware:sim"; + err = srx_set_item(session, &inferred, 0, "%s/class", xpath); + } + out_free_name: free(name); out_free_xpath: @@ -1231,10 +1241,51 @@ int hardware_change(sr_session_ctx_t *session, struct lyd_node *config, struct l } state = lydx_get_child(cif, "state"); admin_state = lydx_get_cattr(state, "admin-state"); - if (usb_authorize(confd->root, name, !strcmp(admin_state, "unlocked"))) { + if (usb_authorize(confd->root, name, + admin_state && !strcmp(admin_state, "unlocked"))) { rc = SR_ERR_INTERNAL; goto err; } + } else if (!strcmp(class, "infix-hardware:modem")) { + /* + * Modem hardware component: drive modemd enable/disable based on + * admin-state. modemd in turn drives ModemManager. + * + * On DELETE or admin-state=locked: stop and disable both modemd and + * modem-manager. On admin-state=unlocked: enable and (re)start both. + * + * Why modemd controls ModemManager (not hardware.c directly): modemd + * handles the full bearer lifecycle (connect, disconnect, retry) and + * polls operational state for statd. Stopping it cleanly disconnects + * the bearer before taking ModemManager down. + */ + if (event != SR_EV_DONE) + continue; + + if (op == LYDX_OP_DELETE) { + NOTE("Modem %s removed, disabling modemd", name); + systemf("initctl -bfqn stop modemd"); + finit_disable("modemd"); + systemf("initctl -bfqn stop modem-manager"); + finit_disable("modem-manager"); + continue; + } + + state = lydx_get_child(cif, "state"); + admin_state = lydx_get_cattr(state, "admin-state"); + if (admin_state && !strcmp(admin_state, "unlocked")) { + NOTE("Modem %s enabled, starting modemd", name); + finit_enable("modem-manager"); + finit_enable("modemd"); + systemf("initctl -bfqn restart modem-manager"); + systemf("initctl -bfqn restart modemd"); + } else { + NOTE("Modem %s locked, disabling modemd", name); + systemf("initctl -bfqn stop modemd"); + finit_disable("modemd"); + systemf("initctl -bfqn stop modem-manager"); + finit_disable("modem-manager"); + } } else if (!strcmp(class, "infix-hardware:wifi")) { struct lyd_node *interfaces_config, *interfaces_diff; struct lyd_node **wifi_iface_list = NULL; diff --git a/src/confd/src/if-modem.c b/src/confd/src/if-modem.c new file mode 100644 index 00000000..2c5a419f --- /dev/null +++ b/src/confd/src/if-modem.c @@ -0,0 +1,112 @@ +/* SPDX-License-Identifier: BSD-3-Clause */ + +#include +#include + +#include "core.h" +#include "dagger.h" +#include "interfaces.h" + +/* + * Modem interface lifecycle management + * + * The kernel (cdc_mbim, qmi_wwan driver) creates the wwan interface automatically + * when a USB modem attaches. confd does NOT need to run 'ip link add'. Instead: + * + * - On CREATE: generate a dagger init script that waits for the wwan interface to + * appear in sysfs. If hardware is absent (no modem detected), create a dummy + * placeholder so that downstream IP/address config works. Signal modemd to + * (re)connect with the latest bearer config. + * + * - On DELETE: remove the /etc/net.d/.conf route file so that netd/FRR + * withdraws the default route. Then delete the interface — either the real + * wwan one or the dummy placeholder. + * + * Address management is handled by modemd (analogous to the udhcpc script for + * DHCP). modemd uses 'ip addr add ... proto wwan' so that protocol-tagged flush + * on disconnect is precise and does not touch addresses added by other means. + * + * Routes are written by modemd to /etc/net.d/.conf, picked up by netd + * via inotify, and installed by staticd/zebra. This keeps all routes visible to + * FRR and enables proper metric-based failover behind wired/WiFi interfaces. + */ + +static int modem_get_probe_timeout(sr_session_ctx_t *session, const char *modem) +{ + char *str; + int timeout = 0; + + str = srx_get_str(session, + "/ietf-hardware:hardware/component[name='%s']/infix-hardware:modem/probe-timeout", + modem); + if (str) { + timeout = atoi(str); + free(str); + } + return timeout; +} + +int modem_add_iface(struct lyd_node *cif, struct dagger *net) +{ + const char *ifname = lydx_get_cattr(cif, "name"); + struct lyd_node *wwan = lydx_get_child(cif, "wwan"); + const char *modem_hw = wwan ? lydx_get_cattr(wwan, "modem") : NULL; + int probe_timeout = 0; + FILE *sh; + + if (modem_hw) + probe_timeout = modem_get_probe_timeout(net->session, modem_hw); + + sh = dagger_fopen_net_init(net, ifname, NETDAG_INIT_PRE, "modem-iface.sh"); + if (!sh) { + ERRNO("Failed to open dagger file for modem interface creation"); + return SR_ERR_INTERNAL; + } + + /* Wait for wwan interface if probe-timeout is set (slow USB modems) */ + if (probe_timeout > 0) { + fprintf(sh, "timeout=%d\n", probe_timeout); + fprintf(sh, "while [ $timeout -gt 0 ]; do\n"); + fprintf(sh, " ip link show %s >/dev/null 2>&1 && break\n", ifname); + fprintf(sh, " sleep 1\n"); + fprintf(sh, " timeout=$((timeout - 1))\n"); + fprintf(sh, "done\n"); + } + + /* If interface doesn't exist, create a dummy placeholder so downstream IP config works. + * modemd will reconfigure the real interface once the modem attaches. */ + fprintf(sh, "if ! ip link show %s >/dev/null 2>&1; then\n", ifname); + fprintf(sh, " logger -t modem \"%s: interface not yet available, creating dummy placeholder\"\n", ifname); + fprintf(sh, " ip link add %s type dummy\n", ifname); + fprintf(sh, "fi\n\n"); + + /* Signal modemd to pick up any updated bearer config. */ + fprintf(sh, "initctl -bfq touch modemd 2>/dev/null || true\n"); + + fclose(sh); + return SR_ERR_OK; +} + +int modem_del_iface(struct lyd_node *dif, struct dagger *net) +{ + const char *ifname = lydx_get_cattr(dif, "name"); + FILE *sh; + + sh = dagger_fopen_net_exit(net, ifname, NETDAG_EXIT_POST, "modem-iface.sh"); + if (!sh) { + ERRNO("Failed to open dagger file for modem interface deletion"); + return SR_ERR_INTERNAL; + } + + fprintf(sh, "# Generated by Infix confd - Modem Interface Cleanup\n\n"); + + /* Remove route config so netd/FRR withdraws the default route */ + fprintf(sh, "rm -f /etc/net.d/%s.conf\n", ifname); + + /* Bring down and remove — works for both real wwan and dummy placeholder */ + fprintf(sh, "ip link set %s down 2>/dev/null || true\n", ifname); + fprintf(sh, "ip link del %s 2>/dev/null || true\n", ifname); + + fclose(sh); + return SR_ERR_OK; +} diff --git a/src/confd/src/interfaces.c b/src/confd/src/interfaces.c index 7585aa29..bafa2a93 100644 --- a/src/confd/src/interfaces.c +++ b/src/confd/src/interfaces.c @@ -393,7 +393,7 @@ static int netdag_gen_afspec_add(sr_session_ctx_t *session, struct dagger *net, case IFT_VXLAN: return vxlan_gen(NULL, cif, ip); case IFT_MODEM: - return modem_gen(NULL, cif, net); + return modem_add_iface(cif, net); case IFT_WIFI: return wifi_validate_secret(session, cif) ? : wifi_add_iface(cif, net); @@ -559,7 +559,7 @@ static int netdag_gen_iface_del(struct dagger *net, struct lyd_node *dif, veth_gen_del(dif, ip); break; case IFT_MODEM: - modem_gen_del(dif, net); + modem_del_iface(dif, net); break; case IFT_WIFI: wifi_del_iface(dif, net); @@ -583,8 +583,7 @@ static int netdag_gen_iface_del(struct dagger *net, struct lyd_node *dif, static sr_error_t netdag_gen_iface_timeout(struct dagger *net, const char *ifname, const char *iftype) { - if (!strcmp(iftype, "infix-if-type:ethernet") || - !strcmp(iftype, "infix-if-type:modem")) { + if (!strcmp(iftype, "infix-if-type:ethernet")) { FILE *wait; wait = dagger_fopen_net_init(net, ifname, NETDAG_INIT_TIMEOUT, "wait-interface.sh"); diff --git a/src/confd/src/interfaces.h b/src/confd/src/interfaces.h index aece9e46..b3c2b474 100644 --- a/src/confd/src/interfaces.h +++ b/src/confd/src/interfaces.h @@ -168,9 +168,9 @@ int ifchange_cand_infer_dhcp(sr_session_ctx_t *session, const char *path); /* if-vxlan.c */ int vxlan_gen(struct lyd_node *dif, struct lyd_node *cif, FILE *ip); -/* modem.c */ -int modem_gen(struct lyd_node *dif, struct lyd_node *cif, struct dagger *net); -int modem_gen_del(struct lyd_node *dif, struct dagger *net); +/* if-modem.c */ +int modem_add_iface(struct lyd_node *cif, struct dagger *net); +int modem_del_iface(struct lyd_node *dif, struct dagger *net); /* infix-if-wireguard */ int wireguard_validate_peers(sr_session_ctx_t *session, struct lyd_node *cif); diff --git a/src/confd/src/modem.c b/src/confd/src/modem.c index a7d83ddd..e86357fb 100644 --- a/src/confd/src/modem.c +++ b/src/confd/src/modem.c @@ -1,11 +1,5 @@ /* SPDX-License-Identifier: BSD-3-Clause */ -#include -#include -#include -#include -#include -#include #include #include #include @@ -17,35 +11,33 @@ #include "core.h" -#define MAX_MODEMS 8 -#define RUN_DIR "/run/modemd" -#define SOCK RUN_DIR "/modemd.sock" +#define RUN_DIR "/run/modemd" +#define SOCK RUN_DIR "/modemd.sock" -#define MODULE "infix-modem" -#define ROOT_XPATH "/infix-modem:" -#define CFG_XPATH ROOT_XPATH "modems" +#define HW_BASE "ietf-hardware" +#define HW_MODULE "infix-hardware" +#define HW_COMP "/ietf-hardware:hardware/component" +#define IH_MODEM HW_COMP "/infix-hardware:modem" +#define IH_COMP_ACTION HW_COMP "/infix-hardware:" -static int xpath_get_index(const char *xpath) +static int component_index(const char *xpath) { regmatch_t pmatch[2]; regex_t regex; - char buf[32]; - int ret, len, i; + char name[32]; int index = -1; + int len; - if (regcomp(®ex, "index='([0-9]+)'", REG_EXTENDED)) + if (regcomp(®ex, "name='([^']*)'", REG_EXTENDED)) return -1; - ret = regexec(®ex, xpath, 2, pmatch, 0); - if (!ret) { - len = (pmatch[1].rm_eo - pmatch[1].rm_so); - if (len < (int)(sizeof(buf)-1)) { - for (i = 0; i < len; i++) - buf[i] = xpath[pmatch[1].rm_so + i]; - - buf[i] = '\0'; - index = (int) strtoul(buf, NULL, 10); + if (regexec(®ex, xpath, 2, pmatch, 0) == 0) { + len = pmatch[1].rm_eo - pmatch[1].rm_so; + if (len < (int)(sizeof(name) - 1)) { + memcpy(name, xpath + pmatch[1].rm_so, len); + name[len] = '\0'; + sscanf(name, "modem%d", &index); } } regfree(®ex); @@ -53,131 +45,6 @@ static int xpath_get_index(const char *xpath) return index; } -static int node_index(struct lyd_node *node) -{ - const char *s; - - s = lydx_get_cattr(node, "index"); - if (!s || !s[0]) - return -1; - - return (int) strtoul(s, NULL, 10); -} - -static int disable(void) -{ - NOTE("Disabling modemd"); - - /* disable modem-manager */ - systemf("initctl -bfqn stop modem-manager"); - systemf("initctl -bfqn disable modem-manager"); - - /* disable modemd */ - systemf("initctl -bnq stop modemd"); - systemf("initctl -bnq disable modemd"); - - return SR_ERR_OK; -} - -static int enable(void) -{ - int enabled, reload = 0; - - NOTE("Enabling modemd"); - - /* enable modem-manager */ - enabled = !systemf("initctl -bfq status modem-manager"); - if (!enabled) { - systemf("initctl -bfqn enable modem-manager"); - reload = 1; - } - /* enable modemd */ - enabled = !systemf("initctl -bfq status modemd"); - if (!enabled) { - systemf("initctl -bfqn enable modemd"); - reload = 1; - } - /* reload if required */ - if (reload) - systemf("initctl -b reload"); - - /* restart modem-manager */ - systemf("initctl -bfqn restart modem-manager"); - - /* restart modemd */ - systemf("initctl -bfqn restart modemd"); - - return SR_ERR_OK; -} - -static int genconf(sr_data_t *cfg, struct lyd_node *diff) -{ - struct lyd_node *node, *tree; - uint8_t enabled[MAX_MODEMS]; - int index; - - memset(enabled, 0, sizeof(enabled)); - - tree = lydx_get_descendant(cfg->tree, "modems", "modem", NULL); - LYX_LIST_FOR_EACH(tree, node, "modem") { - index = node_index(node); - if (index < MAX_MODEMS) - enabled[index] = lydx_get_bool(node, "enabled") ? 1 : 0; - } - - tree = lydx_get_descendant(diff, "modems", "modem", NULL); - LYX_LIST_FOR_EACH(tree, node, "modem") { - index = node_index(node); - if (index < MAX_MODEMS && lydx_get_op(node) == LYDX_OP_DELETE) - enabled[index] = 0; - } - - for (index = 0; index < MAX_MODEMS; index++) { - if (enabled[index]) { - if (enable() == SR_ERR_OK) { - return SR_ERR_OK; - } else { - ERROR("Cannot enable modem%d", index); - break; - } - } - } - - return disable(); -} - -static int infix_modem_change(sr_session_ctx_t *session, uint32_t sub_id, const char *module, - const char *xpath, sr_event_t event, unsigned request_id, void *confd) -{ - sr_data_t *cfg = NULL; - struct lyd_node *diff = NULL; - sr_error_t err; - - if (event != SR_EV_DONE) { - return SR_ERR_OK; - } - err = sr_get_data(session, CFG_XPATH "//.", 0, 0, 0, &cfg); - if (err) { - ERROR("Can't get data"); - goto out; - } - err = srx_get_diff(session, &diff); - if (err) { - ERROR("Can't get diff"); - goto out; - } - err = genconf(cfg, diff); - if (err) { - ERROR("Can't gen conf"); - goto out; - } -out: - if (diff) lyd_free_tree(diff); - if (cfg) sr_release_data(cfg); - - return err; -} - static int infix_modem_rpcsend(char *msg, int len) { struct sockaddr_un addr; @@ -199,9 +66,8 @@ static int infix_modem_rpcsend(char *msg, int len) FD_SET(sock, &wfds); if (select(sock + 1, NULL, &wfds, NULL, &tv) > 0) { - if (write(sock, msg, len) == len) { + if (write(sock, msg, len) == len) ret = 0; - } } } close(sock); @@ -209,7 +75,7 @@ static int infix_modem_rpcsend(char *msg, int len) return ret; } -static int infix_modem_rpc(const char *xpath, const char *rpc, const char *data) +static int infix_modem_rpc(const char *rpc, const char *data) { char msg[1024]; int len; @@ -233,98 +99,108 @@ static int infix_modem_sendsms(sr_session_ctx_t *session, uint32_t sub_id, const unsigned request_id, sr_val_t **output, size_t *output_cnt, void *priv) { char data[1024]; + int index; - if (input_cnt < 3) { - ERROR("Not enough input parameters"); + index = component_index(xpath); + if (index < 0) { + ERROR("Cannot parse modem index from xpath: %s", xpath); + return SR_ERR_INVAL_ARG; + } + if (input_cnt < 2) { + ERROR("send-sms: not enough input parameters"); return SR_ERR_SYS; } - snprintf(data, sizeof(data)-1, - "{ \"index\" : %s, \"number\" : \"%s\", \"text\" : \"%s\" }", + snprintf(data, sizeof(data) - 1, + "{ \"index\" : %d, \"number\" : \"%s\", \"text\" : \"%s\" }", + index, input[0].data.string_val, - input[1].data.string_val, - input[2].data.string_val); + input[1].data.string_val); - return infix_modem_rpc(xpath, "send-sms", data); + return infix_modem_rpc("send-sms", data); } static int infix_modem_restart(sr_session_ctx_t *session, uint32_t sub_id, const char *xpath, const sr_val_t *input, const size_t input_cnt, sr_event_t event, unsigned request_id, sr_val_t **output, size_t *output_cnt, void *priv) { - char data[1024]; + char data[64]; + int index; - if (input_cnt < 1) { - ERROR("Not enough input parameters"); - return SR_ERR_SYS; + index = component_index(xpath); + if (index < 0) { + ERROR("Cannot parse modem index from xpath: %s", xpath); + return SR_ERR_INVAL_ARG; } - snprintf(data, sizeof(data)-1, - "{ \"index\" : %s }", input[0].data.string_val); - - return infix_modem_rpc(xpath, "restart", data); + snprintf(data, sizeof(data), "{ \"index\" : %d }", index); + return infix_modem_rpc("restart", data); } static int infix_modem_reset(sr_session_ctx_t *session, uint32_t sub_id, const char *xpath, const sr_val_t *input, const size_t input_cnt, sr_event_t event, unsigned request_id, sr_val_t **output, size_t *output_cnt, void *priv) { - char data[1024]; + char data[64]; + int index; - if (input_cnt < 1) { - ERROR("Not enough input parameters"); - return SR_ERR_SYS; + index = component_index(xpath); + if (index < 0) { + ERROR("Cannot parse modem index from xpath: %s", xpath); + return SR_ERR_INVAL_ARG; } - snprintf(data, sizeof(data)-1, - "{ \"index\" : %s }", input[0].data.string_val); - - return infix_modem_rpc(xpath, "reset", data); + snprintf(data, sizeof(data), "{ \"index\" : %d }", index); + return infix_modem_rpc("reset", data); } -static void infix_modem_notif (sr_session_ctx_t *session, uint32_t sub_id, - const sr_ev_notif_type_t notif_type, const char *xpath, - const sr_val_t *values, const size_t values_cnt, - struct timespec *timestamp, void *confd) +static void infix_modem_notif(sr_session_ctx_t *session, uint32_t sub_id, + const sr_ev_notif_type_t notif_type, const char *xpath, + const sr_val_t *values, const size_t values_cnt, + struct timespec *timestamp, void *confd) { int index; - index = xpath_get_index(xpath); + index = component_index(xpath); if (index < 0) { - ERROR("No index"); + ERROR("Cannot parse modem index from xpath: %s", xpath); return; } if (values_cnt < 1) { - ERROR("No values"); + ERROR("No values in status-update notification"); return; } - NOTE("Notification from modem%d: %s", - index, values[0].data.string_val); + NOTE("Notification from modem%d: %s", index, values[0].data.string_val); } int modem_init(struct confd *confd) { + const struct lys_module *mod; + sr_conn_ctx_t *conn; + const struct ly_ctx *ly_ctx; int rc; - REGISTER_CHANGE(confd->session, MODULE, CFG_XPATH, 0, infix_modem_change, confd, &confd->sub); - REGISTER_NOTIF(confd->session, MODULE, CFG_XPATH "/modem/status-update", infix_modem_notif, confd, &confd->sub); - REGISTER_RPC(confd->session, ROOT_XPATH "restart", infix_modem_restart, NULL, &confd->sub); - REGISTER_RPC(confd->session, ROOT_XPATH "reset", infix_modem_reset, NULL, &confd->sub); - REGISTER_RPC(confd->session, ROOT_XPATH "send-sms", infix_modem_sendsms, NULL, &confd->sub); + conn = sr_session_get_connection(confd->session); + ly_ctx = sr_acquire_context(conn); + mod = ly_ctx_get_module_implemented(ly_ctx, HW_MODULE); + sr_release_context(conn); + + if (!mod || lys_feature_value(mod, "modem") != LY_SUCCESS) + return SR_ERR_OK; + + REGISTER_NOTIF(confd->session, HW_BASE, + IH_MODEM "/status-update", + infix_modem_notif, confd, &confd->sub); + REGISTER_RPC(confd->session, IH_COMP_ACTION "restart", + infix_modem_restart, NULL, &confd->sub); + REGISTER_RPC(confd->session, IH_COMP_ACTION "reset", + infix_modem_reset, NULL, &confd->sub); + REGISTER_RPC(confd->session, IH_COMP_ACTION "send-sms", + infix_modem_sendsms, NULL, &confd->sub); return SR_ERR_OK; fail: ERROR("init failed: %s", sr_strerror(rc)); return rc; } - -int modem_gen(struct lyd_node *dif, struct lyd_node *cif, struct dagger *net) -{ - return 0; -} - -int modem_gen_del(struct lyd_node *dif, struct dagger *net) -{ - return 0; -} diff --git a/src/confd/yang/confd.inc b/src/confd/yang/confd.inc index bf088c06..e569886a 100644 --- a/src/confd/yang/confd.inc +++ b/src/confd/yang/confd.inc @@ -27,10 +27,10 @@ MODULES=( "infix-syslog@2025-11-17.yang" "iana-hardware@2018-03-13.yang" "ietf-hardware@2018-03-13.yang -e hardware-state -e hardware-sensor" - "infix-hardware@2026-03-01.yang" + "infix-hardware@2026-04-27.yang" "ieee802-dot1q-types@2022-10-29.yang" "infix-ip@2026-04-28.yang" - "infix-if-type@2026-01-07.yang" + "infix-if-type@2026-04-27.yang" "infix-routing@2026-03-11.yang" "ieee802-dot1ab-lldp@2022-03-15.yang" "infix-lldp@2025-05-05.yang" @@ -57,5 +57,4 @@ MODULES=( "ieee1588-ptp-tt@2023-08-14.yang -e timestamp-correction" "ieee802-dot1as-gptp@2025-12-10.yang" "infix-ptp@2026-04-07.yang" - "infix-modem@2024-03-15.yang" ) diff --git a/src/confd/yang/confd/infix-hardware.yang b/src/confd/yang/confd/infix-hardware.yang index b73f9a68..7735d180 100644 --- a/src/confd/yang/confd/infix-hardware.yang +++ b/src/confd/yang/confd/infix-hardware.yang @@ -12,7 +12,9 @@ module infix-hardware { import ietf-yang-types { prefix yang; } - + import ietf-netconf-acm { + prefix nacm; + } import infix-wifi-country-codes { prefix iwcc; } @@ -21,6 +23,25 @@ module infix-hardware { contact "kernelkit@googlegroups.com"; description "Vital Product Data augmentation of ieee-hardware and deviations."; + revision 2026-05-08 { + description "Move modem actions (restart/reset/send-sms) from container modem to a + direct augment on component with when/if-feature guard, fixing sysrepo + NP-container parent validation for nested YANG actions."; + reference "internal"; + } + revision 2026-05-03 { + description "Add cellular modem and SIM hardware config containers. + Add modem typedefs (modem-access-mode, modem-band, modem-location-source). + Add actions restart/reset/send-sms and status-update notification on modem. + Add sim container with pin, puk, carrier. + Add sim-state operational container with slot, lock state, operator-name."; + reference "internal"; + } + revision 2026-04-27 { + description "Add cellular modem and SIM hardware class identities. + Add modem-state operational augment for statd."; + reference "internal"; + } revision 2026-03-01 { description "Add channel-width leaf for WiFi radio container."; reference "internal"; @@ -58,6 +79,10 @@ module infix-hardware { description "GPS support is an optional build-time feature in Infix."; } + feature modem { + description "Cellular modem support is an optional build-time feature in Infix."; + } + typedef country-code { type string { length 2; @@ -133,6 +158,303 @@ module infix-hardware { - 160 MHz: Requires VHT capable hardware (5/6 GHz only)"; } + /* + * Modem-specific typedefs + */ + + typedef modem-access-mode { + type enumeration { + enum cs { description "Circuit-switched (CSD, GSM)."; } + enum 2g { description "2G (GPRS, EDGE)."; } + enum 3g { description "3G (UMTS, HSxPA)."; } + enum 4g { description "4G (LTE)."; } + enum 5g { description "5G (NR)."; } + enum any { description "Any technology."; } + } + description "Cellular radio access technology."; + } + + typedef modem-band { + type enumeration { + // 2G GSM + enum egsm { description "GSM 900 MHz, Extended (880-960 MHz)."; } + enum dcs { description "GSM 1800 MHz, DCS (Digital Cellular System)."; } + enum pcs { description "GSM 1900 MHz, PCS (Personal Communications Service)."; } + enum g850 { description "GSM 850 MHz (Americas, Pacific)."; } + // 3G UMTS/WCDMA + enum utran-1 { description "UMTS Band I, 2100 MHz FDD (Europe, Asia, global)."; } + enum utran-2 { description "UMTS Band II, 1900 MHz FDD (Americas, PCS band)."; } + enum utran-3 { description "UMTS Band III, 1800 MHz FDD."; } + enum utran-4 { description "UMTS Band IV, 1700/2100 MHz FDD, AWS (Americas)."; } + enum utran-5 { description "UMTS Band V, 850 MHz FDD (Americas, Pacific)."; } + enum utran-6 { description "UMTS Band VI, 800 MHz FDD (Japan)."; } + enum utran-7 { description "UMTS Band VII, 2600 MHz FDD (Europe)."; } + enum utran-8 { description "UMTS Band VIII, 900 MHz FDD (Europe, Asia)."; } + enum utran-9 { description "UMTS Band IX, 1700 MHz FDD (Japan)."; } + enum utran-10 { description "UMTS Band X, 1700/2100 MHz FDD, AWS-II (Americas)."; } + enum utran-11 { description "UMTS Band XI, 1500 MHz FDD (Japan)."; } + enum utran-12 { description "UMTS Band XII, 700 MHz FDD (Americas, lower block)."; } + enum utran-13 { description "UMTS Band XIII, 700 MHz FDD (Americas, upper C block)."; } + enum utran-14 { description "UMTS Band XIV, 700 MHz FDD (Americas, upper D block)."; } + enum utran-19 { description "UMTS Band XIX, 800 MHz FDD (Japan)."; } + enum utran-20 { description "UMTS Band XX, 800 MHz FDD (Europe, digital dividend)."; } + enum utran-21 { description "UMTS Band XXI, 1500 MHz FDD (Japan)."; } + enum utran-22 { description "UMTS Band XXII, 3500 MHz FDD."; } + enum utran-25 { description "UMTS Band XXV, 1900 MHz FDD (Americas, PCS extended)."; } + enum utran-26 { description "UMTS Band XXVI, 850 MHz FDD (Americas, extended)."; } + enum utran-32 { description "UMTS Band XXXII, 1500 MHz SDL (supplemental downlink)."; } + // 4G LTE + enum eutran-1 { description "LTE Band 1, 2100 MHz FDD (Europe, Asia, global)."; } + enum eutran-2 { description "LTE Band 2, 1900 MHz FDD, PCS (Americas)."; } + enum eutran-3 { description "LTE Band 3, 1800 MHz FDD (global)."; } + enum eutran-4 { description "LTE Band 4, 1700/2100 MHz FDD, AWS (Americas)."; } + enum eutran-5 { description "LTE Band 5, 850 MHz FDD, CLR (Americas, Pacific)."; } + enum eutran-6 { description "LTE Band 6, 800 MHz FDD (Japan)."; } + enum eutran-7 { description "LTE Band 7, 2600 MHz FDD (Europe, global)."; } + enum eutran-8 { description "LTE Band 8, 900 MHz FDD (Europe, Asia)."; } + enum eutran-9 { description "LTE Band 9, 1800 MHz FDD (Japan)."; } + enum eutran-10 { description "LTE Band 10, 1700/2100 MHz FDD, AWS (Americas)."; } + enum eutran-11 { description "LTE Band 11, 1500 MHz FDD (Japan)."; } + enum eutran-12 { description "LTE Band 12, 700 MHz FDD (Americas, lower A/B/C)."; } + enum eutran-13 { description "LTE Band 13, 700 MHz FDD (Americas, upper C)."; } + enum eutran-14 { description "LTE Band 14, 700 MHz FDD (Americas, upper D, public safety)."; } + enum eutran-17 { description "LTE Band 17, 700 MHz FDD (Americas, lower B/C)."; } + enum eutran-18 { description "LTE Band 18, 800 MHz FDD (Japan)."; } + enum eutran-19 { description "LTE Band 19, 800 MHz FDD (Japan)."; } + enum eutran-20 { description "LTE Band 20, 800 MHz FDD (Europe, digital dividend)."; } + enum eutran-21 { description "LTE Band 21, 1500 MHz FDD (Japan)."; } + enum eutran-22 { description "LTE Band 22, 3500 MHz FDD."; } + enum eutran-23 { description "LTE Band 23, 2000 MHz FDD."; } + enum eutran-24 { description "LTE Band 24, 1600 MHz FDD (Americas, L-band)."; } + enum eutran-25 { description "LTE Band 25, 1900 MHz FDD, PCS extended (Americas)."; } + enum eutran-26 { description "LTE Band 26, 850 MHz FDD, CLR extended (Americas)."; } + enum eutran-27 { description "LTE Band 27, 800 MHz FDD, SMR (Americas)."; } + enum eutran-28 { description "LTE Band 28, 700 MHz FDD, APT (Asia-Pacific)."; } + enum eutran-29 { description "LTE Band 29, 700 MHz FDD, SDL (Americas, downlink only)."; } + enum eutran-30 { description "LTE Band 30, 2300 MHz FDD, WCS (Americas)."; } + enum eutran-31 { description "LTE Band 31, 450 MHz FDD."; } + enum eutran-32 { description "LTE Band 32, 1500 MHz FDD, SDL (Europe, downlink only)."; } + enum eutran-33 { description "LTE Band 33, 1900 MHz TDD."; } + enum eutran-34 { description "LTE Band 34, 2000 MHz TDD."; } + enum eutran-35 { description "LTE Band 35, 1900 MHz TDD."; } + enum eutran-36 { description "LTE Band 36, 1900 MHz TDD."; } + enum eutran-37 { description "LTE Band 37, 1910 MHz TDD."; } + enum eutran-38 { description "LTE Band 38, 2600 MHz TDD (China, Europe)."; } + enum eutran-39 { description "LTE Band 39, 1900 MHz TDD (China)."; } + enum eutran-40 { description "LTE Band 40, 2300 MHz TDD (India, Asia)."; } + enum eutran-41 { description "LTE Band 41, 2500 MHz TDD (China, Americas)."; } + enum eutran-42 { description "LTE Band 42, 3500 MHz TDD."; } + enum eutran-43 { description "LTE Band 43, 3700 MHz TDD."; } + enum eutran-44 { description "LTE Band 44, 700 MHz TDD, APT."; } + enum eutran-45 { description "LTE Band 45, 1500 MHz TDD (China)."; } + enum eutran-46 { description "LTE Band 46, 5200 MHz TDD, ULAA (unlicensed)."; } + enum eutran-47 { description "LTE Band 47, 5900 MHz TDD, V2X (vehicle-to-everything)."; } + enum eutran-48 { description "LTE Band 48, 3500 MHz TDD, CBRS (Americas, shared)."; } + enum eutran-49 { description "LTE Band 49, 3500 MHz TDD."; } + enum eutran-50 { description "LTE Band 50, 1500 MHz TDD."; } + enum eutran-51 { description "LTE Band 51, 1500 MHz TDD."; } + enum eutran-52 { description "LTE Band 52, 3300 MHz TDD."; } + enum eutran-53 { description "LTE Band 53, 2500 MHz TDD."; } + enum eutran-54 { description "LTE Band 54, 1600 MHz TDD."; } + enum eutran-55 { description "LTE Band 55, 1800 MHz TDD."; } + enum eutran-56 { description "LTE Band 56, 700 MHz TDD."; } + enum eutran-57 { description "LTE Band 57, 700 MHz TDD."; } + enum eutran-58 { description "LTE Band 58, 700 MHz TDD."; } + enum eutran-59 { description "LTE Band 59, 700 MHz TDD."; } + enum eutran-60 { description "LTE Band 60, 1500 MHz FDD."; } + enum eutran-61 { description "LTE Band 61, 1500 MHz FDD."; } + enum eutran-62 { description "LTE Band 62, 1500 MHz FDD."; } + enum eutran-63 { description "LTE Band 63, 1900 MHz FDD."; } + enum eutran-64 { description "LTE Band 64, 1900 MHz FDD."; } + enum eutran-65 { description "LTE Band 65, 2100 MHz FDD, extended (Europe)."; } + enum eutran-66 { description "LTE Band 66, 1700/2100 MHz FDD, AWS-3 (Americas)."; } + enum eutran-67 { description "LTE Band 67, 700 MHz FDD, SDL (Europe)."; } + enum eutran-68 { description "LTE Band 68, 700 MHz FDD (Middle East, Africa)."; } + enum eutran-69 { description "LTE Band 69, 2500 MHz FDD, SDL."; } + enum eutran-70 { description "LTE Band 70, 1700/2100 MHz FDD, AWS-4 (Americas)."; } + enum eutran-71 { description "LTE Band 71, 600 MHz FDD (Americas)."; } + enum eutran-85 { description "LTE Band 85, 700 MHz FDD, APT extended."; } + // 5G NR + enum ngran-1 { description "NR Band n1, 2100 MHz FDD (Europe, Asia)."; } + enum ngran-2 { description "NR Band n2, 1900 MHz FDD, PCS (Americas)."; } + enum ngran-3 { description "NR Band n3, 1800 MHz FDD (global)."; } + enum ngran-5 { description "NR Band n5, 850 MHz FDD (Americas, Pacific)."; } + enum ngran-7 { description "NR Band n7, 2600 MHz FDD (Europe)."; } + enum ngran-8 { description "NR Band n8, 900 MHz FDD (Europe, Asia)."; } + enum ngran-12 { description "NR Band n12, 700 MHz FDD (Americas)."; } + enum ngran-13 { description "NR Band n13, 700 MHz FDD (Americas)."; } + enum ngran-14 { description "NR Band n14, 700 MHz FDD (Americas, public safety)."; } + enum ngran-18 { description "NR Band n18, 850 MHz FDD (Japan)."; } + enum ngran-20 { description "NR Band n20, 800 MHz FDD (Europe, digital dividend)."; } + enum ngran-25 { description "NR Band n25, 1900 MHz FDD, PCS extended (Americas)."; } + enum ngran-26 { description "NR Band n26, 850 MHz FDD, CLR extended."; } + enum ngran-28 { description "NR Band n28, 700 MHz FDD, APT (Asia-Pacific)."; } + enum ngran-29 { description "NR Band n29, 700 MHz FDD, SDL (Americas)."; } + enum ngran-30 { description "NR Band n30, 2300 MHz FDD, WCS (Americas)."; } + enum ngran-34 { description "NR Band n34, 2000 MHz TDD."; } + enum ngran-38 { description "NR Band n38, 2600 MHz TDD."; } + enum ngran-39 { description "NR Band n39, 1900 MHz TDD (China)."; } + enum ngran-40 { description "NR Band n40, 2300 MHz TDD (India, Asia)."; } + enum ngran-41 { description "NR Band n41, 2500 MHz TDD (China, Americas)."; } + enum ngran-48 { description "NR Band n48, 3500 MHz TDD, CBRS (Americas, shared)."; } + enum ngran-50 { description "NR Band n50, 1500 MHz TDD."; } + enum ngran-51 { description "NR Band n51, 1500 MHz TDD."; } + enum ngran-53 { description "NR Band n53, 2500 MHz TDD."; } + enum ngran-65 { description "NR Band n65, 2100 MHz FDD, extended."; } + enum ngran-66 { description "NR Band n66, 1700/2100 MHz FDD, AWS-3."; } + enum ngran-70 { description "NR Band n70, 1700/2100 MHz FDD, AWS-4."; } + enum ngran-71 { description "NR Band n71, 600 MHz FDD (Americas)."; } + enum ngran-74 { description "NR Band n74, 1500 MHz FDD."; } + enum ngran-75 { description "NR Band n75, 1500 MHz SDL."; } + enum ngran-76 { description "NR Band n76, 1500 MHz SDL."; } + enum ngran-77 { description "NR Band n77, 3700 MHz TDD (Europe, Americas)."; } + enum ngran-78 { description "NR Band n78, 3500 MHz TDD (Europe, Asia, global)."; } + enum ngran-79 { description "NR Band n79, 4700 MHz TDD (Japan, China)."; } + enum ngran-80 { description "NR Band n80, 1800 MHz SUL (supplemental uplink)."; } + enum ngran-81 { description "NR Band n81, 900 MHz SUL (supplemental uplink)."; } + enum ngran-82 { description "NR Band n82, 800 MHz SUL (supplemental uplink)."; } + enum ngran-83 { description "NR Band n83, 700 MHz SUL, APT (supplemental uplink)."; } + enum ngran-84 { description "NR Band n84, 2100 MHz SUL (supplemental uplink)."; } + enum ngran-86 { description "NR Band n86, 1700 MHz SUL (supplemental uplink)."; } + enum ngran-89 { description "NR Band n89, 850 MHz SUL (supplemental uplink)."; } + enum ngran-90 { description "NR Band n90, 2500 MHz TDD."; } + enum ngran-91 { description "NR Band n91, 800+1800 MHz SUL (supplemental uplink)."; } + enum ngran-92 { description "NR Band n92, 800+2300 MHz SUL (supplemental uplink)."; } + enum ngran-93 { description "NR Band n93, 900+1800 MHz SUL (supplemental uplink)."; } + enum ngran-94 { description "NR Band n94, 900+2300 MHz SUL (supplemental uplink)."; } + enum ngran-95 { description "NR Band n95, 2100 MHz SUL (supplemental uplink)."; } + enum ngran-257 { description "NR Band n257, 28 GHz mmWave (Americas)."; } + enum ngran-258 { description "NR Band n258, 26 GHz mmWave (Europe, Asia)."; } + enum ngran-260 { description "NR Band n260, 39 GHz mmWave (Americas)."; } + enum ngran-261 { description "NR Band n261, 28 GHz mmWave (Americas)."; } + // Wildcard + enum any { description "All bands supported by the hardware."; } + } + description + "Cellular frequency band identifier using ModemManager's MMModemBand + enum value names (as used with 'mmcli --set-current-bands'). + An empty band list in the parent container is equivalent to 'any'."; + } + + typedef modem-location-source { + type enumeration { + enum gps { description "GPS location source."; } + enum agps-msa { description "MSA A-GPS location source."; } + enum agps-msb { description "MSB A-GPS location source."; } + enum 3gpp { description "3GPP location source."; } + enum cdma { description "CDMA location source."; } + } + description "Location source for the modem's location subsystem."; + } + + typedef modem-state { + type enumeration { + enum failed { description "Unrecoverable error; see state-failed-reason for the cause."; } + enum unknown { description "State unknown; modem has not yet reported in."; } + enum initializing { description "Modem is initialising hardware."; } + enum locked { description "Modem is locked (SIM lock or otherwise) and cannot be used."; } + enum disabled { description "Modem is administratively disabled (low power)."; } + enum disabling { description "Modem is transitioning to the disabled state."; } + enum enabling { description "Modem is transitioning to the enabled state."; } + enum enabled { description "Modem is enabled but not yet registered."; } + enum searching { description "Searching for a cellular network."; } + enum registered { description "Registered on a cellular network."; } + enum disconnecting { description "Tearing down an active data connection."; } + enum connecting { description "Bringing up a data connection."; } + enum connected { description "Data connection established."; } + } + description + "Cellular modem state, mirroring ModemManager's MMModemState + enumeration. Read-only operational data."; + } + + typedef modem-state-failed-reason { + type enumeration { + enum none { description "No failure recorded."; } + enum unknown { description "Failure reason is unknown."; } + enum sim-missing { description "No SIM card is inserted."; } + enum sim-error { description "The SIM card reports an error."; } + enum sim-wrong { description "The SIM card is not accepted by the modem (PLMN mismatch, etc.)."; } + enum sim-busy { description "The SIM card is busy."; } + enum unlock-required { description "PIN or PUK unlock is required."; } + enum modem-busy { description "The modem is busy and could not complete the operation."; } + enum modem-disabled { description "The modem is administratively disabled."; } + enum mode-not-supported { description "The configured radio access mode is not supported."; } + enum esim-without-profiles { description "An eSIM is present but has no profiles."; } + } + description + "Reason for a modem state of 'failed', mirroring + ModemManager's MMModemStateFailedReason enumeration."; + } + + typedef modem-power-state { + type enumeration { + enum unknown { description "Power state cannot be reported."; } + enum off { description "Modem is fully powered down."; } + enum low { description "Modem is in a low-power state; radio idle."; } + enum on { description "Modem is fully powered."; } + } + description + "Modem power state, mirroring ModemManager's MMModemPowerState + enumeration."; + } + + typedef modem-access-tech { + type enumeration { + enum unknown { description "Access technology unknown or unreported."; } + enum pots { description "Plain old telephone service."; } + enum gsm { description "2G GSM."; } + enum gsm-compact { description "2G GSM compact."; } + enum gprs { description "2.5G GPRS."; } + enum edge { description "2.75G EDGE."; } + enum umts { description "3G UMTS."; } + enum hsdpa { description "3.5G HSDPA."; } + enum hsupa { description "3.5G HSUPA."; } + enum hspa { description "3.5G HSPA."; } + enum hspa-plus { description "3.75G HSPA+."; } + enum 1xrtt { description "CDMA2000 1xRTT."; } + enum evdo0 { description "CDMA2000 EV-DO rev. 0."; } + enum evdoa { description "CDMA2000 EV-DO rev. A."; } + enum evdob { description "CDMA2000 EV-DO rev. B."; } + enum lte { description "4G LTE."; } + enum 5gnr { description "5G New Radio."; } + } + description + "Cellular radio access technology currently in use, mirroring + ModemManager's MMModemAccessTechnology bitmask (Infix exposes + it as a single best-match enum, not a leaf-list)."; + } + + typedef modem-3gpp-facility { + type enumeration { + enum sim { description "SIM PIN."; } + enum fixed-dialing { description "Fixed dialling numbers (FDN)."; } + enum phone-sim { description "Phone SIM lock."; } + enum phone-first-sim { description "First-inserted-SIM lock."; } + enum phone-network { description "Network personalisation."; } + enum phone-network-subset { description "Network-subset personalisation."; } + enum phone-service-provider { description "Service-provider personalisation."; } + enum phone-corporate { description "Corporate personalisation."; } + enum phone-first-sim-network { description "First-SIM network personalisation."; } + } + description + "3GPP facility that may be locked or active on the modem, + mirroring ModemManager's MMModem3gppFacility enumeration."; + } + + typedef modem-packet-service-state { + type enumeration { + enum unknown { description "Packet service state cannot be determined."; } + enum attached { description "Packet service is attached; data sessions can be brought up."; } + enum detached { description "Packet service is detached; data sessions are unavailable."; } + } + description + "3GPP packet-service attach state. Independent of the + circuit-switched registration-state — a modem may be + registered but packet-detached, meaning voice/SMS work but + data does not."; + } + /* * Hardware class identities */ @@ -157,6 +479,26 @@ module infix-hardware { description "GPS/GNSS receiver for time synchronization"; } + identity modem { + if-feature modem; + base iahw:hardware-class; + description "Cellular modem hardware component. + One modem0 component per physical modem module. + Auto-discovered by 00-probe from /sys/class/usbmisc/cdc-wdm* + and written to /run/system.json['modem']; gen-hardware emits + the corresponding ietf-hardware component entries."; + } + + identity sim { + if-feature modem; + base iahw:hardware-class; + description "SIM/UICC card hardware component. + One sim0 component per SIM slot on hardware that exposes + SIM multiplexing via /sys/class/sim/ (e.g. Minex modules). + On hardware without a SIM multiplexer, a synthetic sim0 is + inferred from the modem's active SIM."; + } + deviation "/iehw:hardware/iehw:component/iehw:state/iehw:admin-state" { deviate add { must ". = 'locked' or . = 'unlocked'" { @@ -604,6 +946,354 @@ module infix-hardware { } } + /* + * Modem hardware configuration (when class = 'ih:modem') + */ + + container modem { + if-feature modem; + when "derived-from-or-self(../iehw:class, 'ih:modem')"; + presence "Modem hardware configuration"; + description + "Cellular modem hardware configuration. + + Controls radio access technology, frequency bands, and location + services. The modem is enabled or disabled via the hardware + component admin-state (unlocked = enabled, locked = disabled). + + Actions restart and reset operate on the modem hardware directly. + send-sms uses the modem's SMS capability independent of any data + bearer or APN — SMS travels over the signalling plane, not the + data bearer."; + + leaf preferred-mode { + type modem-access-mode; + description + "Preferred radio access technology. The modem will attempt to + use this technology first; falls back to allowed modes if not + available. Omit (or set to 'any') for automatic selection."; + } + + leaf-list allowed-mode { + type modem-access-mode; + description "Restrict the modem to these radio access technologies. + Empty list means all technologies are allowed."; + } + + leaf-list band { + type modem-band; + description + "Restrict the modem to these frequency bands. An empty list + (or a list containing only 'any') means all bands supported + by the hardware are allowed."; + } + + container location { + description "Modem location/GPS subsystem configuration."; + leaf enabled { + type boolean; + default false; + description "Enable location gathering via the modem."; + } + leaf-list source { + type modem-location-source; + description "Location sources to enable."; + } + } + + leaf probe-timeout { + type uint8; + default 30; + description + "Seconds to wait for the wwan interface to appear at boot. + + USB modems may be slow to initialize due to firmware loading + and USB enumeration. The default of 30 seconds covers most + USB modems; the interface typically appears within 2-5 seconds. + + If the interface is not detected within the timeout, a dummy + placeholder is created so downstream configuration succeeds. + modemd will reconfigure the real interface when it attaches. + + Set to 0 to disable waiting and create the dummy immediately."; + } + + notification status-update { + description "Emitted by modemd when the modem state changes."; + leaf desc { + type string; + mandatory true; + description "Human-readable description of the state change."; + } + } + } + + /* + * Modem operational state (when class = 'ih:modem') + * Populated by statd/infix_modem.py from /run/modemd/modemN/ runtime files. + */ + + container modem-state { + if-feature modem; + when "derived-from-or-self(../iehw:class, 'ih:modem')"; + config false; + description + "Cellular modem operational state. + + Written by statd/infix_modem.py which reads modemd runtime files + from /run/modemd/modem0/. The split is intentional: modemd drives + ModemManager and writes runtime files; statd reads them on demand + and populates sysrepo operational nodes. modemd never touches + sysrepo operational nodes directly."; + + leaf manufacturer { + type string; + description "Modem manufacturer (e.g. 'Quectel', 'Sierra Wireless')."; + } + + leaf model { + type string; + description "Modem model (e.g. 'EM05', 'EM7455')."; + } + + leaf firmware-version { + type string; + description "Firmware revision reported by the modem."; + } + + leaf serial-number { + type string; + description "IMEI serial number."; + } + + leaf imsi { + type string; + description "IMSI from the active SIM card."; + } + + leaf iccid { + type string; + description "ICCID from the active SIM card."; + } + + leaf state { + type modem-state; + description "Cellular modem state (see modem-state typedef)."; + } + + leaf state-failed-reason { + type modem-state-failed-reason; + description + "Detailed reason for the failed state. Only meaningful when + 'state' is 'failed'; absent or 'none' otherwise."; + } + + leaf power-state { + type modem-power-state; + description "Modem RF power state."; + } + + leaf-list enabled-locks { + type modem-3gpp-facility; + description + "3GPP facility locks currently enabled on the modem (e.g. + 'fixed-dialing' for FDN-restricted SIMs that whitelist + outbound calls / SMS)."; + } + + leaf hardware-revision { + type string; + description "Hardware revision string reported by the modem."; + } + + leaf-list phone-number { + type string; + description + "MSISDN(s) associated with the active SIM, as reported by + the modem. Typically empty unless provisioned in the SIM."; + } + + leaf selected-carrier { + type string; + description + "Operator carrier profile currently selected by the modem + firmware (e.g. 'default', 'att', 'verizon')."; + } + + leaf-list supported-carrier { + type string; + description + "Carrier profiles supported by this modem firmware. Used + to validate the SIM-component 'carrier' configuration."; + } + + leaf signal-quality { + type uint8; + units "percent"; + description "Signal quality as a percentage (0-100)."; + } + + leaf signal-quality-recent { + type boolean; + description + "TRUE if the most recent signal-quality measurement is fresh. + FALSE indicates a cached value that may no longer reflect + current conditions."; + } + + leaf signal-rssi { + type int16; + units "dBm"; + description "Received Signal Strength Indicator."; + } + + leaf signal-rsrp { + type int16; + units "dBm"; + description "LTE Reference Signal Received Power."; + } + + leaf signal-rsrq { + type decimal64 { fraction-digits 1; } + units "dB"; + description "LTE Reference Signal Received Quality."; + } + + leaf signal-sinr { + type decimal64 { fraction-digits 1; } + units "dB"; + description "Signal-to-Interference-plus-Noise Ratio."; + } + + container cellular { + description "Cellular network information."; + + leaf registration-state { + type enumeration { + enum idle { description "Not registered, not searching."; } + enum home { description "Registered on home network."; } + enum searching { description "Searching for a network."; } + enum denied { description "Registration denied."; } + enum roaming { description "Registered on a roaming network."; } + enum unknown { description "Registration state unknown."; } + } + description "3GPP registration state."; + } + + leaf service-state { + type modem-packet-service-state; + description + "3GPP packet-service attach state. A modem can be + registered but not packet-attached, in which case + data connections cannot be established."; + } + + leaf operator-name { + type string; + description "Name of the registered cellular operator."; + } + + leaf operator-id { + type string { + pattern '[0-9]{5,6}'; + } + description + "Operator PLMN code: MCC (3 digits) concatenated with + MNC (2 or 3 digits)."; + } + + leaf network-type { + type modem-access-tech; + description "Active cellular radio access technology."; + } + } + } + + /* + * SIM card configuration (when class = 'ih:sim') + */ + + container sim { + if-feature modem; + when "derived-from-or-self(../iehw:class, 'ih:sim')"; + presence "SIM card configuration"; + description + "SIM card / subscription configuration. + + PIN and PUK are per-SIM credentials used to unlock the card. + carrier selects the operator profile that modemd applies to the + modem firmware when this SIM is active (band preferences, firmware + quirks). On hardware with a SIM multiplexer (/dev/simctrl) each + SIM slot gets its own component (sim0, sim1, …); on hardware without + a multiplexer a synthetic sim0 represents the single SIM present."; + + leaf pin { + type string; + nacm:default-deny-all; + description + "PIN code to unlock this SIM card. Leave unset if the SIM has + PIN protection disabled."; + } + + leaf puk { + type string; + nacm:default-deny-all; + description + "PUK (PIN Unblocking Key) to unblock a PIN-locked SIM. Only + needed after too many incorrect PIN attempts."; + } + + leaf carrier { + type string; + description + "Operator profile name applied to the modem firmware when this + SIM is active (e.g. 'default', 'att', 'verizon'). Profiles + are provided by the modem-carrier helper in modemd. Omit to + use the modem's built-in defaults."; + } + } + + /* + * SIM card operational state (when class = 'ih:sim') + * Populated by statd/infix_modem.py from modemd runtime data. + */ + + container sim-state { + if-feature modem; + when "derived-from-or-self(../iehw:class, 'ih:sim')"; + config false; + description + "SIM card operational state. + + Shows which physical slot the SIM occupies, its lock state, and + the home operator name stored on the card. Useful for remote + diagnostics (e.g. confirming which SIM slot is active)."; + + leaf slot { + type uint8; + description + "Physical SIM slot number (1-based). + For single-SIM modems this is always 1."; + } + + leaf state { + type enumeration { + enum unknown { description "Modem reports a SIM but it cannot be read (busy, transient error)."; } + enum not-inserted { description "No SIM card in this slot."; } + enum unlocked { description "SIM is present and accessible."; } + enum pin-required { description "SIM requires PIN to unlock."; } + enum puk-required { description "SIM PIN blocked; PUK needed to unblock."; } + enum permanently-blocked { description "SIM permanently blocked; cannot be used."; } + } + description "SIM card lock and presence state."; + } + + leaf operator-name { + type string; + description "Home operator name stored on the SIM card."; + } + } + /* * GPS/GNSS Receiver configuration (when class = 'ih:gps') */ @@ -726,4 +1416,49 @@ module infix-hardware { } } } + + /* + * Modem actions — augmented directly on component so sysrepo can + * resolve the parent node (list entry) without needing to instantiate + * the NP container modem, which requires the sibling 'class' leaf. + */ + augment "/iehw:hardware/iehw:component" { + if-feature modem; + when "derived-from-or-self(iehw:class, 'ih:modem')" { + description "Applies only to modem hardware components."; + } + + action restart { + description + "Restart the modem: disconnect all bearers and reconnect. + Use this to recover from a stuck connection without a full + hardware reset."; + } + + action reset { + description + "Factory-reset the modem firmware to its default state. + This clears all modem-internal profiles and settings. + Modem will re-initialise and reconnect automatically."; + } + + action send-sms { + description + "Send an SMS message via the modem. + SMS uses the signalling plane and does not require an active + data bearer; the modem only needs to be registered on a network."; + input { + leaf phone-number { + type string; + mandatory true; + description "Recipient phone number in international format (e.g. +46701234567)."; + } + leaf message-text { + type string; + mandatory true; + description "SMS message body (max 160 characters for single-part SMS)."; + } + } + } + } } diff --git a/src/confd/yang/confd/infix-hardware@2026-04-27.yang b/src/confd/yang/confd/infix-hardware@2026-04-27.yang new file mode 120000 index 00000000..154924a0 --- /dev/null +++ b/src/confd/yang/confd/infix-hardware@2026-04-27.yang @@ -0,0 +1 @@ +infix-hardware.yang \ No newline at end of file diff --git a/src/confd/yang/confd/infix-if-modem.yang b/src/confd/yang/confd/infix-if-modem.yang new file mode 100644 index 00000000..6b8a7ebe --- /dev/null +++ b/src/confd/yang/confd/infix-if-modem.yang @@ -0,0 +1,315 @@ +submodule infix-if-modem { + yang-version 1.1; + belongs-to infix-interfaces { + prefix infix-if; + } + + import ietf-interfaces { + prefix if; + } + import ietf-inet-types { + prefix inet; + } + import ietf-yang-types { + prefix yang; + } + import ietf-keystore { + prefix ks; + } + import ietf-hardware { + prefix iehw; + } + import infix-hardware { + prefix ih; + } + import infix-if-type { + prefix infixift; + } + + organization "KernelKit"; + contact "kernelkit@googlegroups.com"; + description + "Cellular modem (wwan) interface configuration for ietf-interfaces. + + A modem interface (type 'infix-if-type:modem') is a network interface + created by the kernel when a cellular modem attaches. The kernel names + these wwan[0-9]+ via the wwan subsystem (cdc_mbim, qmi_wwan). + + Each wwan interface references: + - A hardware component of class 'ih:modem' (e.g. modem0) via 'modem'. + - A hardware component of class 'ih:sim' (e.g. sim0) via 'sim'. + + The modem component holds hardware-level config (bands, modes, location). + The SIM component holds credentials (PIN/PUK) and carrier profile. + This interface holds the bearer (connection) config: APN, credentials, + IP addressing type, and routing preference. + + Multi-bearer: configure multiple wwan interfaces referencing the same + modem component, each with a different APN. modemd manages them all in + one thread per hardware modem, analogous to multi-SSID on a WiFi radio. + + Routes are written by modemd to /etc/net.d/.conf and installed + by staticd/zebra through FRR. The default route is always installed when + the bearer is connected; route-preference controls administrative distance + so that cellular acts as a natural failover path behind wired and WiFi."; + + revision 2026-05-03 { + description + "Redesign: rename container modem->wwan, device->modem, active-sim->sim. + Rebuild bearer: replace metric+default-route with route-preference (always + on), replace auth-type/username/password with authentication presence + container referencing keystore, drop dns-enabled/firewall-enabled/apn-type."; + reference "internal"; + } + revision 2026-04-27 { + description "Initial revision."; + reference "internal"; + } + + feature modem { + description "Cellular modem support is an optional build-time feature in Infix."; + } + + augment "/if:interfaces/if:interface" { + when "derived-from-or-self(if:type, 'infixift:modem')" { + description "Applies only to interfaces of type 'modem'."; + } + container wwan { + if-feature modem; + + description + "Cellular modem (wwan) interface configuration. + + References a modem hardware component for physical layer config + and a SIM hardware component for subscription credentials. + The bearer sub-container configures the data connection (APN, auth, + IP type, routing preference)."; + + leaf modem { + type leafref { + path "/iehw:hardware/iehw:component/iehw:name"; + } + must "derived-from-or-self(/iehw:hardware/iehw:component[iehw:name=current()]/iehw:class, 'ih:modem')" { + error-message "Referenced hardware component must be a cellular modem (class 'ih:modem')."; + } + description + "Reference to the modem hardware component (e.g. modem0). + + The hardware component carries physical-layer configuration + (bands, modes, location) and is auto-discovered at boot. + Its admin-state controls whether the modem is powered + (unlocked = enabled, locked = disabled)."; + } + + leaf sim { + type leafref { + path "/iehw:hardware/iehw:component/iehw:name"; + } + must "derived-from-or-self(/iehw:hardware/iehw:component[iehw:name=current()]/iehw:class, 'ih:sim')" { + error-message "Referenced hardware component must be a SIM card (class 'ih:sim')."; + } + description + "Reference to the SIM hardware component (e.g. sim0). + + The SIM component carries PIN/PUK credentials and the carrier + profile. On hardware with a SIM multiplexer, changing this + reference switches the active SIM slot."; + } + + container bearer { + description + "Data bearer (connection) configuration. + + These settings are passed to ModemManager by modemd to establish + the cellular data connection. The APN is mandatory; authentication + is only needed when the operator requires it. + + When the bearer connects, modemd writes a default route to + /etc/net.d/.conf so that staticd/zebra installs it via FRR. + The route-preference leaf controls the administrative distance, + making cellular a natural failover behind lower-preference routes + such as wired Ethernet (udhcpc default 5) and WiFi."; + + leaf apn { + type string { + length "1..64"; + } + description "Access Point Name for the cellular data connection."; + } + + leaf ip-type { + type enumeration { + enum ipv4 { + description "Request IPv4 addressing only."; + } + enum ipv6 { + description "Request IPv6 addressing only."; + } + enum ipv4v6 { + description "Request dual-stack IPv4 and IPv6 addressing."; + } + } + default ipv4v6; + description + "IP addressing type to negotiate with the network during + bearer activation (3GPP PDN type). Use ipv4v6 unless the + operator or hardware requires a specific family."; + } + + leaf roaming { + type boolean; + default false; + description "Allow data connection when roaming on a foreign network."; + } + + leaf route-preference { + type uint32; + default 200; + description + "Administrative distance for the bearer default route installed + via /etc/net.d/.conf into staticd/zebra. + + Higher values mean lower priority. Default 200 places cellular + behind wired Ethernet (udhcpc typically uses 5) and WiFi, making + it a natural failover path. Adjust when multiple cellular modems + need relative priority ordering."; + } + + container authentication { + presence "APN authentication credentials"; + description + "Authentication credentials for the APN. + + If this container is absent, the bearer connects without + authentication (suitable for most consumer APNs). When present, + all three leaves — type, username, and password — are required."; + + leaf type { + type enumeration { + enum pap { + description "Password Authentication Protocol."; + } + enum chap { + description "Challenge Handshake Authentication Protocol (recommended)."; + } + } + default chap; + description "Authentication protocol required by the APN."; + } + + leaf username { + type string; + mandatory true; + description "Username for APN authentication."; + } + + leaf password { + type ks:central-symmetric-key-ref; + mandatory true; + description + "APN password, referenced from the system keystore. + + Store the password as a symmetric key in the keystore and + reference it here by key name. This avoids storing credentials + in plaintext in the running or startup configuration."; + } + } + } + + container bearer-state { + config false; + description + "Operational state of the modem's data bearer, mirroring the + layout of modem-state / sim-state under ietf-hardware. + + Populated by statd/yanger from modemd's modem-info JSON. + When the bearer is disconnected, addresses and counters may + be absent; 'connected' and 'connection-failed-reason' tell + the rest of the story."; + + leaf path { + type string; + description + "ModemManager D-Bus object path of the underlying bearer + (e.g. '/org/freedesktop/ModemManager1/Bearer/0')."; + } + + leaf connected { + type boolean; + description "TRUE when the bearer has an active data session."; + } + + leaf connection-failed-reason { + type string; + description + "Free-form failure reason reported by ModemManager when + a recent connect attempt failed. Absent on success."; + } + + leaf interface { + type string; + description + "Kernel network interface carrying the bearer's data + (e.g. 'wwan0'). Same as the parent interface name in + single-bearer configurations."; + } + + leaf ipv4-address { + type inet:ipv4-address; + description "IPv4 address assigned by the cellular network."; + } + + leaf ipv4-prefix-length { + type uint8 { range "0..32"; } + description "Prefix length for the assigned IPv4 address."; + } + + leaf ipv6-address { + type inet:ipv6-address; + description "IPv6 address assigned by the cellular network."; + } + + leaf ipv6-prefix-length { + type uint8 { range "0..128"; } + description "Prefix length for the assigned IPv6 address."; + } + + leaf in-bytes { + type yang:counter64; + units "bytes"; + description "Bytes received over the bearer in the current session."; + } + + leaf out-bytes { + type yang:counter64; + units "bytes"; + description "Bytes transmitted over the bearer in the current session."; + } + + leaf total-in-bytes { + type yang:counter64; + units "bytes"; + description + "Total bytes received over the bearer across all sessions + since the modem was attached."; + } + + leaf total-out-bytes { + type yang:counter64; + units "bytes"; + description + "Total bytes transmitted over the bearer across all sessions + since the modem was attached."; + } + + leaf total-duration { + type uint64; + units "seconds"; + description + "Cumulative connected time across all sessions since the + modem was attached."; + } + } + } + } +} diff --git a/src/confd/yang/confd/infix-if-modem@2026-04-27.yang b/src/confd/yang/confd/infix-if-modem@2026-04-27.yang new file mode 120000 index 00000000..3d99e0bd --- /dev/null +++ b/src/confd/yang/confd/infix-if-modem@2026-04-27.yang @@ -0,0 +1 @@ +infix-if-modem.yang \ No newline at end of file diff --git a/src/confd/yang/confd/infix-if-type.yang b/src/confd/yang/confd/infix-if-type.yang index 3674376a..f57d90cd 100644 --- a/src/confd/yang/confd/infix-if-type.yang +++ b/src/confd/yang/confd/infix-if-type.yang @@ -11,6 +11,10 @@ module infix-if-type { contact "kernelkit@googlegroups.com"; description "Infix extensions to IANA interfaces types"; + revision 2026-04-27 { + description "Add interface type modem for cellular wwan interfaces."; + reference "internal"; + } revision 2026-01-07 { description "Add interface type wifi and wireguard"; reference "internal"; @@ -47,6 +51,10 @@ module infix-if-type { * Features */ + feature modem { + description "Cellular modem support is an optional build-time feature in Infix."; + } + feature wifi { description "WiFi support is an optional build-time feature in Infix."; } @@ -121,6 +129,15 @@ module infix-if-type { base ianaift:l2vlan; description "Layer 2 Virtual LAN using 802.1Q."; } + identity modem { + if-feature modem; + base infix-interface-type; + description "Cellular modem (wwan) network interface. + Created by the kernel (cdc_mbim, qmi_wwan) when a USB modem + attaches. Interface names follow the kernel wwan subsystem + naming convention: wwan[0-9]+."; + } + identity wifi { if-feature wifi; base infix-interface-type; diff --git a/src/confd/yang/confd/infix-if-type@2026-04-27.yang b/src/confd/yang/confd/infix-if-type@2026-04-27.yang new file mode 120000 index 00000000..b0bd9018 --- /dev/null +++ b/src/confd/yang/confd/infix-if-type@2026-04-27.yang @@ -0,0 +1 @@ +infix-if-type.yang \ No newline at end of file diff --git a/src/confd/yang/confd/infix-interfaces.yang b/src/confd/yang/confd/infix-interfaces.yang index 6cd517d8..4d29f4c9 100644 --- a/src/confd/yang/confd/infix-interfaces.yang +++ b/src/confd/yang/confd/infix-interfaces.yang @@ -36,6 +36,7 @@ module infix-interfaces { include infix-if-wifi; include infix-if-wireguard; include infix-if-ptp; + include infix-if-modem; organization "KernelKit"; contact "kernelkit@googlegroups.com"; @@ -60,6 +61,11 @@ module infix-interfaces { reference "internal"; } + revision 2026-04-27 { + description "Include infix-if-modem submodule for cellular wwan interfaces."; + reference "internal"; + } + revision 2026-04-09 { description "Add ptp-capabilities submodule for per-interface PTP timestamping info."; reference "internal"; diff --git a/src/confd/yang/confd/infix-interfaces@2026-04-27.yang b/src/confd/yang/confd/infix-interfaces@2026-04-27.yang new file mode 120000 index 00000000..9f14c9e1 --- /dev/null +++ b/src/confd/yang/confd/infix-interfaces@2026-04-27.yang @@ -0,0 +1 @@ +infix-interfaces.yang \ No newline at end of file diff --git a/src/confd/yang/confd/infix-modem.yang b/src/confd/yang/confd/infix-modem.yang deleted file mode 100644 index f0a52fe6..00000000 --- a/src/confd/yang/confd/infix-modem.yang +++ /dev/null @@ -1,1089 +0,0 @@ -/* - * Infix Modem YANG module - */ - -module infix-modem { - yang-version 1.1; - namespace "urn:infix:params:xml:ns:yang:infix-modem"; - prefix "infix-modem"; - - import ietf-inet-types { - prefix "inet"; - } - import ietf-interfaces { - prefix "if"; - } - import ietf-yang-types { - prefix yang; - } - import ietf-netconf-acm { - prefix nacm; - } - - organization "KernelKit"; - contact "kernelkit@googlegroups.com"; - description "YANG data model for modems."; - - revision 2024-03-15 { - description "Initial revision"; - reference "internal"; - } - - container modems { - description "Configuration of modems."; - - list modem { - key "index"; - unique sim; - description "List of modems"; - - leaf index { - type uint8; - description "Index of the modem."; - } - leaf enabled { - type boolean; - description "Enable or disable modem."; - default false; - } - leaf path { - type string; - description "Path to modem."; - config false; - } - leaf sim { - type int8; - description "Index of SIM card."; - } - leaf pin { - type string; - description "PIN code to unlock SIM card."; - nacm:default-deny-all; - } - leaf puk { - type string; - description "PUK code to unlock SIM card."; - nacm:default-deny-all; - } - - leaf preferred-mode { - type access-mode; - description "Preferred mode."; - } - list allowed-mode { - key "mode"; - description "List of allowed modes."; - - leaf mode { - type access-mode; - description "Allowed mode."; - } - } - leaf carrier { - type string; - description "Network carrier profile."; - } - list band { - key "band"; - description "List of enabled bands."; - - leaf band { - type band; - description "Enabled band."; - } - } - list bearer { - key "index"; - description "List of bearers."; - - leaf index { - type uint8; - description "Index of the bearer."; - } - leaf apn-type { - type bearer-apn-type; - description "APN type."; - } - leaf apn { - type string; - description "Access Point Name (APN)."; - } - leaf username { - type string; - description "User name (if any) required by the network."; - default ""; - } - leaf password { - type string; - description "Password (if any) required by the network."; - default ""; - nacm:default-deny-all; - } - leaf allow-roaming { - type boolean; - description "Flag to tell whether connection is allowed during roaming."; - default false; - } - leaf ip-type { - type ip-family; - description "IP addressing type."; - default ipv4v6; - } - leaf default-route { - type boolean; - description "Set default route for connection"; - default false; - } - leaf firewall-enabled { - type boolean; - description "Enable firewall for connection."; - default false; - } - leaf dns-enabled { - type boolean; - description "Enable DNS resolver for connection."; - default true; - } - } - container location { - leaf enabled { - type boolean; - description "Enable location gathering."; - default false; - } - - list source { - key "source"; - description "List of location sources."; - - leaf source { - type location-source; - description "Source used for location gathering."; - } - } - } - - container info { - description "Modem information."; - config false; - - leaf manufacturer { - type string; - description "Manufacturer of the modem."; - } - leaf model { - type string; - description "Model name of the modem."; - } - leaf hardware-revision { - type string; - description "Hardware revision of the modem."; - } - leaf firmware-version { - type string; - description "Firmware version of the modem."; - } - leaf-list supported-carrier { - type string; - description "List of supported carriers."; - } - leaf serial-number { - type string; - description "Serial number of the modem (IMEI)."; - } - leaf-list phone-number { - type string; - description "List of own phone numbers."; - } - leaf imsi { - type string; - description "International Mobile Subscriber Identity"; - } - leaf iccid { - type string; - description "ICCID of the SIM card."; - } - } - - container status { - description "Modem Status Information."; - config false; - - leaf sim-active { - type int8; - description "Index of active SIM card."; - } - leaf sim-present { - type boolean; - description "True if SIM card is present"; - } - leaf state { - type string; - description "Modem state."; - } - leaf state-failed-reason { - type string; - description "Reason of failed modem state."; - } - leaf selected-carrier { - type string; - description "Selected carrier."; - } - leaf signal-quality { - type uint8; - description "Signal strength in percent (0-100)."; - } - leaf signal-rssi { - type string; - description "RSSI (Received Signal Strength Indication) in dBm."; - } - leaf signal-rsrp { - type string; - description "RSRP (Reference Signal Received Power) in dBm."; - } - leaf signal-rsrq { - type string; - description "RSRQ (Reference Signal Received Quality) in dB."; - } - leaf signal-rscp { - type string; - description "RSCP (Received Signal Code Power) in dBm."; - } - leaf signal-snr { - type string; - description "SNR (Signal Noise Ratio) in dB."; - } - leaf signal-sinr { - type string; - description "SINR (Signal Interference Noise Ratio) ) in dB."; - } - - container cellular { - description "Cellular network information."; - - leaf registration-state { - description "Modem registration state."; - type registration-state; - } - leaf operator-name { - type string; - description "Name of the cellular network operator."; - } - leaf operator-id { - type string; - description "Identifier of the cellular network operator (MCC/MNC)."; - } - leaf network-type { - type string; - description "Type of cellular network (e.g. LTE)"; - } - leaf packet-service-state { - description "Modem packet service state."; - type string; - } - } - - list bearer { - description "Bearer information."; - key "path"; - - leaf path { - type string; - description "Path to bearer."; - } - leaf interface { - type if:interface-ref; - description "Network interface name for the bearer."; - } - leaf connected { - type boolean; - description "Indicates whether bearer is connected."; - } - leaf connection-failed-reason { - type string; - description "Reason of failed connection of bearer."; - } - leaf ipv4-address { - type inet:ipv4-address; - description "The IPv4 address of the bearer connection."; - } - leaf ipv4-prefix { - type uint16; - description "The IPv4 prefix of the bearer connection."; - } - leaf ipv6-address { - type inet:ipv6-address; - description "The IPv6 address of the bearer connection."; - } - leaf ipv6-prefix { - type uint16; - description "The IPv6 prefix of the bearer connection."; - } - leaf in-bytes { - type yang:counter64; - description "The number of received bytes of the bearer connection."; - } - leaf out-bytes { - type yang:counter64; - description "The number of sent bytes of the bearer connection."; - } - leaf total-in-bytes { - type yang:counter64; - description "Total number of received bytes of all connections."; - } - leaf total-out-bytes { - type yang:counter64; - description "Total number of sent bytes of all connections."; - } - leaf total-duration { - type string; - description "Total duration of all connections in seconds."; - } - } - - container location { - description "Location information."; - - leaf latitude { - type string; - description "Latitude in decimal degrees."; - } - leaf longitude { - type string; - description "Longitude in decimal degrees."; - } - leaf altitude { - type string; - description "Altitude above sea level in meters."; - } - leaf mcc { - type string; - description "Mobile Country Code of the operator."; - } - leaf mnc { - type string; - description "Mobile Network Code of the operator."; - } - leaf lac { - type string; - description "Location Area Code of the operator."; - } - leaf cid { - type string; - description "Cell Identifier of the operator."; - } - leaf tac { - type string; - description "Tracking Area Code of the operator."; - } - } - } - - notification status-update { - description "Notification for modem status updates."; - leaf desc { - type string; - description "Description of the status update."; - mandatory true; - } - } - } - } - - rpc restart { - description "Restart modem."; - input { - leaf index { - type string; - description "Modem index."; - } - } - } - rpc reset { - description "Reset the modem to factory defaults."; - input { - leaf index { - type string; - description "Modem index."; - } - } - } - rpc send-sms { - description "Send an SMS message."; - input { - leaf index { - type string; - description "Modem index."; - } - leaf phone-number { - type string; - description "Recipient's phone number."; - } - leaf message-text { - type string; - description "Text of the SMS message."; - } - } - } - - typedef bearer-apn-type { - description "APN type for bearer."; - - type enumeration { - enum initial { - description "APN used for the initial attach procedure."; - } - enum default { - description "Default connection APN providing access to the Internet."; - } - enum ims { - description "APN providing access to IMS services."; - } - enum mms { - description "APN providing access to MMS services."; - } - enum management { - description "APN providing access to over-the-air device management procedures."; - } - enum voice { - description "APN providing access to voice-over-IP services."; - } - enum emergency { - description "APN providing access to emergency services."; - } - enum private { - description "APN providing access to private networks."; - } - enum purchase { - description "APN providing access to over-the-air activation sites."; - } - enum video_share { - description "APN providing access to video sharing service."; - } - enum local { - description "APN providing access to a local connection with the device."; - } - enum app { - description "APN providing access to certain applications allowed by mobile operators."; - } - enum xcap { - description "APN providing access to XCAP provisioning on IMS services."; - } - enum tethering { - description "APN providing access to mobile hotspot tethering."; - } - } - } - - typedef ip-family { - description "IP address family."; - - type enumeration { - enum ipv4 { - description "IPv4 address family."; - } - enum ipv6 { - description "IPv6 address family."; - } - enum ipv4v6 { - description "IPv4 or IPv6 address family."; - } - } - } - - typedef access-mode { - type enumeration { - enum cs { - description "Circuit-switched technologies (e.g. CSD, GSM)."; - } - enum 2g { - description "2G technologies (e.g. GPRS, EDGE)."; - } - enum 3g { - description "3G technologies (e.g. UMTS, HSxPA)."; - } - enum 4g { - description "4G technologies (e.g. LTE)."; - } - enum 5g { - description "5G technologies (e.g. 5GNR)."; - } - enum any { - description "Any technologies."; - } - } - } - - typedef registration-state { - type enumeration { - enum idle { - description "Not registered, not searching for new operator to register."; - } - enum home { - description "Registered on home network."; - } - enum searching { - description "Not registered, searching for new operator to register with."; - } - enum denied { - description "Registration denied."; - } - enum roaming { - description "Registered on a roaming network."; - } - enum unknown { - description "Unknown registration state."; - } - } - } - - typedef band { - type enumeration { - enum any { - description "Any band."; - } - enum egsm { - description "GSM/GPRS/EDGE 900 MHz."; - } - enum dcs { - description "GSM/GPRS/EDGE 1800 MHz."; - } - enum pcs { - description "GSM/GPRS/EDGE 1900 MHz."; - } - enum g850 { - description "GSM/GPRS/EDGE 850 MHz."; - } - enum g450 { - description "GSM/GPRS/EDGE 450 MHz."; - } - enum g480 { - description "GSM/GPRS/EDGE 480 MHz."; - } - enum g750 { - description "GSM/GPRS/EDGE 750 MHz."; - } - enum g380 { - description "GSM/GPRS/EDGE 380 MHz."; - } - enum g410 { - description "GSM/GPRS/EDGE 410 MHz."; - } - enum g710 { - description "GSM/GPRS/EDGE 710 MHz."; - } - enum g810 { - description "GSM/GPRS/EDGE 810 MHz."; - } - enum utran-1 { - description "UMTS 2100 MHz (IMT, UTRAN band 1)."; - } - enum utran-2 { - description "UMTS 1900 MHz (PCS A-F, UTRAN band 2)."; - } - enum utran-3 { - description "UMTS 1800 MHz (DCS, UTRAN band 3)."; - } - enum utran-4 { - description "UMTS 1700 MHz (AWS A-F, UTRAN band 4)."; - } - enum utran-5 { - description "UMTS 850 MHz (CLR, UTRAN band 5)."; - } - enum utran-6 { - description "UMTS 800 MHz (UTRAN band 6)."; - } - enum utran-7 { - description "UMTS 2600 MHz (IMT-E, UTRAN band 7)."; - } - enum utran-8 { - description "UMTS 900 MHz (E-GSM, UTRAN band 8)."; - } - enum utran-9 { - description "UMTS 1700 MHz (UTRAN band 9)."; - } - enum utran-10 { - description "UMTS 1700 MHz (EAWS A-G, UTRAN band 10)."; - } - enum utran-11 { - description "UMTS 1500 MHz (LPDC, UTRAN band 11)."; - } - enum utran-12 { - description "UMTS 700 MHz (LSMH A/B/C, UTRAN band 12)."; - } - enum utran-13 { - description "UMTS 700 MHz (USMH C, UTRAN band 13)."; - } - enum utran-14 { - description "UMTS 700 MHz (USMH D, UTRAN band 14)."; - } - enum utran-19 { - description "UMTS 800 MHz (UTRAN band 19)."; - } - enum utran-20 { - description "UMTS 800 MHz (EUDD, UTRAN band 20)."; - } - enum utran-21 { - description "UMTS 1500 MHz (UPDC, UTRAN band 21)."; - } - enum utran-22 { - description "UMTS 3500 MHz (UTRAN band 22)."; - } - enum utran-25 { - description "UMTS 1900 MHz (EPCS A-G, UTRAN band 25)."; - } - enum utran-26 { - description "UMTS 850 MHz (ECLR, UTRAN band 26)."; - } - enum utran-32 { - description "UMTS 1500 MHz (L-band, UTRAN band 32)."; - } - enum eutran-1 { - description "E-UTRAN band 1."; - } - enum eutran-2 { - description "E-UTRAN band 2."; - } - enum eutran-3 { - description "E-UTRAN band 3."; - } - enum eutran-4 { - description "E-UTRAN band 4."; - } - enum eutran-5 { - description "E-UTRAN band 5."; - } - enum eutran-6 { - description "E-UTRAN band 6."; - } - enum eutran-7 { - description "E-UTRAN band 7."; - } - enum eutran-8 { - description "E-UTRAN band 8."; - } - enum eutran-9 { - description "E-UTRAN band 9."; - } - enum eutran-10 { - description "E-UTRAN band 10."; - } - enum eutran-11 { - description "E-UTRAN band 11."; - } - enum eutran-12 { - description "E-UTRAN band 12."; - } - enum eutran-13 { - description "E-UTRAN band 13."; - } - enum eutran-14 { - description "E-UTRAN band 14."; - } - enum eutran-17 { - description "E-UTRAN band 17."; - } - enum eutran-18 { - description "E-UTRAN band 18."; - } - enum eutran-19 { - description "E-UTRAN band 19."; - } - enum eutran-20 { - description "E-UTRAN band 20."; - } - enum eutran-21 { - description "E-UTRAN band 21."; - } - enum eutran-22 { - description "E-UTRAN band 22."; - } - enum eutran-23 { - description "E-UTRAN band 23."; - } - enum eutran-24 { - description "E-UTRAN band 24."; - } - enum eutran-25 { - description "E-UTRAN band 25."; - } - enum eutran-26 { - description "E-UTRAN band 26."; - } - enum eutran-27 { - description "E-UTRAN band 27."; - } - enum eutran-28 { - description "E-UTRAN band 28."; - } - enum eutran-29 { - description "E-UTRAN band 29."; - } - enum eutran-30 { - description "E-UTRAN band 30."; - } - enum eutran-31 { - description "E-UTRAN band 31."; - } - enum eutran-32 { - description "E-UTRAN band 32."; - } - enum eutran-33 { - description "E-UTRAN band 33."; - } - enum eutran-34 { - description "E-UTRAN band 34."; - } - enum eutran-35 { - description "E-UTRAN band 35."; - } - enum eutran-36 { - description "E-UTRAN band 36."; - } - enum eutran-37 { - description "E-UTRAN band 37."; - } - enum eutran-38 { - description "E-UTRAN band 38."; - } - enum eutran-39 { - description "E-UTRAN band 39."; - } - enum eutran-40 { - description "E-UTRAN band 40."; - } - enum eutran-41 { - description "E-UTRAN band 41."; - } - enum eutran-42 { - description "E-UTRAN band 42."; - } - enum eutran-43 { - description "E-UTRAN band 43."; - } - enum eutran-44 { - description "E-UTRAN band 44."; - } - enum eutran-45 { - description "E-UTRAN band 45."; - } - enum eutran-46 { - description "E-UTRAN band 46."; - } - enum eutran-47 { - description "E-UTRAN band 47."; - } - enum eutran-48 { - description "E-UTRAN band 48."; - } - enum eutran-49 { - description "E-UTRAN band 49."; - } - enum eutran-50 { - description "E-UTRAN band 50."; - } - enum eutran-51 { - description "E-UTRAN band 51."; - } - enum eutran-52 { - description "E-UTRAN band 52."; - } - enum eutran-53 { - description "E-UTRAN band 53."; - } - enum eutran-54 { - description "E-UTRAN band 54."; - } - enum eutran-55 { - description "E-UTRAN band 55."; - } - enum eutran-56 { - description "E-UTRAN band 56."; - } - enum eutran-57 { - description "E-UTRAN band 57."; - } - enum eutran-58 { - description "E-UTRAN band 58."; - } - enum eutran-59 { - description "E-UTRAN band 59."; - } - enum eutran-60 { - description "E-UTRAN band 60."; - } - enum eutran-61 { - description "E-UTRAN band 61."; - } - enum eutran-62 { - description "E-UTRAN band 62."; - } - enum eutran-63 { - description "E-UTRAN band 63."; - } - enum eutran-64 { - description "E-UTRAN band 64."; - } - enum eutran-65 { - description "E-UTRAN band 65."; - } - enum eutran-66 { - description "E-UTRAN band 66."; - } - enum eutran-67 { - description "E-UTRAN band 67."; - } - enum eutran-68 { - description "E-UTRAN band 68."; - } - enum eutran-69 { - description "E-UTRAN band 69."; - } - enum eutran-70 { - description "E-UTRAN band 70."; - } - enum eutran-71 { - description "E-UTRAN band 71."; - } - enum eutran-85 { - description "E-UTRAN band 85."; - } - enum cdma-bc0 { - description "CDMA Band Class 0 (US Cellular 850MHz)."; - } - enum cdma-bc1 { - description "CDMA Band Class 1 (US PCS 1900MHz)."; - } - enum cdma-bc2 { - description "CDMA Band Class 2 (UK TACS 900MHz)."; - } - enum cdma-bc3 { - description "CDMA Band Class 3 (Japanese TACS)."; - } - enum cdma-bc4 { - description "CDMA Band Class 4 (Korean PCS)."; - } - enum cdma-bc5 { - description "CDMA Band Class 5 (NMT 450MHz)."; - } - enum cdma-bc6 { - description "CDMA Band Class 6 (IMT2000 2100MHz)."; - } - enum cdma-bc7 { - description "CDMA Band Class 7 (Cellular 700MHz)."; - } - enum cdma-bc8 { - description "CDMA Band Class 8 (1800MHz)."; - } - enum cdma-bc9 { - description "CDMA Band Class 9 (900MHz)."; - } - enum cdma-bc10 { - description "CDMA Band Class 10 (US Secondary 800)."; - } - enum cdma-bc11 { - description "CDMA Band Class 11 (European PAMR 400MHz)."; - } - enum cdma-bc12 { - description "CDMA Band Class 12 (PAMR 800MHz)."; - } - enum cdma-bc13 { - description "CDMA Band Class 13 (IMT2000 2500MHz Expansion)."; - } - enum cdma-bc14 { - description "CDMA Band Class 14 (More US PCS 1900MHz)."; - } - enum cdma-bc15 { - description "CDMA Band Class 15 (AWS 1700MHz)."; - } - enum cdma-bc16 { - description "CDMA Band Class 16 (US 2500MHz)."; - } - enum cdma-bc17 { - description "CDMA Band Class 17 (US 2500MHz Forward Link Only)."; - } - enum cdma-bc18 { - description "CDMA Band Class 18 (US 700MHz Public Safety)."; - } - enum cdma-bc19 { - description "CDMA Band Class 19 (US Lower 700MHz)."; - } - enum ngran-1 { - description "NGRAN band 1."; - } - enum ngran-2 { - description "NGRAN band 2."; - } - enum ngran-3 { - description "NGRAN band 3."; - } - enum ngran-5 { - description "NGRAN band 5."; - } - enum ngran-7 { - description "NGRAN band 7."; - } - enum ngran-8 { - description "NGRAN band 8."; - } - enum ngran-12 { - description "NGRAN band 12."; - } - enum ngran-13 { - description "NGRAN band 13."; - } - enum ngran-14 { - description "NGRAN band 14."; - } - enum ngran-18 { - description "NGRAN band 18."; - } - enum ngran-20 { - description "NGRAN band 20."; - } - enum ngran-25 { - description "NGRAN band 25."; - } - enum ngran-26 { - description "NGRAN band 26."; - } - enum ngran-28 { - description "NGRAN band 28."; - } - enum ngran-29 { - description "NGRAN band 29."; - } - enum ngran-30 { - description "NGRAN band 30."; - } - enum ngran-34 { - description "NGRAN band 34."; - } - enum ngran-38 { - description "NGRAN band 38."; - } - enum ngran-39 { - description "NGRAN band 39."; - } - enum ngran-40 { - description "NGRAN band 40."; - } - enum ngran-41 { - description "NGRAN band 41."; - } - enum ngran-48 { - description "NGRAN band 48."; - } - enum ngran-50 { - description "NGRAN band 50."; - } - enum ngran-51 { - description "NGRAN band 51."; - } - enum ngran-53 { - description "NGRAN band 53."; - } - enum ngran-65 { - description "NGRAN band 65."; - } - enum ngran-66 { - description "NGRAN band 66."; - } - enum ngran-70 { - description "NGRAN band 70."; - } - enum ngran-71 { - description "NGRAN band 71."; - } - enum ngran-74 { - description "NGRAN band 74."; - } - enum ngran-75 { - description "NGRAN band 75."; - } - enum ngran-76 { - description "NGRAN band 76."; - } - enum ngran-77 { - description "NGRAN band 77."; - } - enum ngran-78 { - description "NGRAN band 78."; - } - enum ngran-79 { - description "NGRAN band 79."; - } - enum ngran-80 { - description "NGRAN band 80."; - } - enum ngran-81 { - description "NGRAN band 81."; - } - enum ngran-82 { - description "NGRAN band 82."; - } - enum ngran-83 { - description "NGRAN band 83."; - } - enum ngran-84 { - description "NGRAN band 84."; - } - enum ngran-86 { - description "NGRAN band 86."; - } - enum ngran-89 { - description "NGRAN band 89."; - } - enum ngran-90 { - description "NGRAN band 90."; - } - enum ngran-91 { - description "NGRAN band 91."; - } - enum ngran-92 { - description "NGRAN band 92."; - } - enum ngran-93 { - description "NGRAN band 93."; - } - enum ngran-94 { - description "NGRAN band 94."; - } - enum ngran-95 { - description "NGRAN band 95."; - } - enum ngran-257 { - description "NGRAN band 257."; - } - enum ngran-258 { - description "NGRAN band 258."; - } - enum ngran-260 { - description "NGRAN band 260."; - } - enum ngran-261 { - description "NGRAN band 261."; - } - } - } - - typedef location-source { - type enumeration { - enum gps { - description "GPS location source."; - } - enum agps-msa { - description "MSA A-GPS location source."; - } - enum agps-msb { - description "MSB A-GPS location source."; - } - enum 3gpp { - description "3GPP location source."; - } - enum cdma { - description "CDMA location source."; - } - } - } -} diff --git a/src/confd/yang/confd/infix-modem@2024-03-15.yang b/src/confd/yang/confd/infix-modem@2024-03-15.yang deleted file mode 120000 index 467b07a4..00000000 --- a/src/confd/yang/confd/infix-modem@2024-03-15.yang +++ /dev/null @@ -1 +0,0 @@ -infix-modem.yang \ No newline at end of file diff --git a/src/confd/yang/modem.inc b/src/confd/yang/modem.inc new file mode 100644 index 00000000..6a9a8738 --- /dev/null +++ b/src/confd/yang/modem.inc @@ -0,0 +1,5 @@ +MODULES=( + "infix-hardware -e modem" + "infix-if-type -e modem" + "infix-interfaces -e modem" +) diff --git a/src/klish-plugin-infix/xml/infix.xml b/src/klish-plugin-infix/xml/infix.xml index bfb3b9b3..642bc246 100644 --- a/src/klish-plugin-infix/xml/infix.xml +++ b/src/klish-plugin-infix/xml/infix.xml @@ -113,6 +113,15 @@ + + + + jq -r '.modem[]? | "modem" + (.index|tostring)' /run/system.json 2>/dev/null + + + + + @@ -335,6 +344,30 @@ echo "Public: $pub" + + + + + rpc "/ietf-hardware:hardware/component[name='$KLISH_PARAM_ref']/infix-hardware:restart" + + + + + + rpc "/ietf-hardware:hardware/component[name='$KLISH_PARAM_ref']/infix-hardware:reset" + + + + + + + + rpc "/ietf-hardware:hardware/component[name='$KLISH_PARAM_ref']/infix-hardware:send-sms" \ + phone-number "$KLISH_PARAM_number" message-text "$KLISH_PARAM_message" + + + + @@ -573,6 +606,15 @@ echo "Public: $pub" + + + + + + show modem "$KLISH_PARAM_ref" |pager + + + diff --git a/src/modemd/modem-info b/src/modemd/modem-info index 2f1684b2..65b71394 100755 --- a/src/modemd/modem-info +++ b/src/modemd/modem-info @@ -159,6 +159,20 @@ def get_current_carrier(index, manf, model): return "default" +def _sim_lock_state(unlock_required): + mapping = { + "none": "unlocked", + "sim-pin": "pin-required", + "sim-pin2": "pin-required", + "sim-puk": "puk-required", + "sim-puk2": "puk-required", + } + # Default to "unlocked": this function is only called when the SIM query + # succeeded, so the card is present. "--" means the modem doesn't report + # lock state but the SIM is accessible. + return mapping.get(unlock_required, "unlocked") + + def device_devpath(devpath): path = devpath while path != "/sys": @@ -171,24 +185,37 @@ def device_devpath(devpath): def modem_get_module(devpath): - if not os.path.exists("/run/modules.json"): - if os.path.exists("/run/modems.json"): - with open("/run/modems.json", "r") as fd: - data = json.load(fd) - for index, modem in enumerate(data.get("modems", [])): - if devpath in modem["devpath"]: - return {"index": index, "slot": index, "paths": [modem["devpath"]], "type": "modem"} + # Prefer system.json (new canonical source written by 00-probe) + if os.path.exists("/run/system.json"): + with open("/run/system.json", "r") as fd: + data = json.load(fd) + for modem in data.get("modem", []): + if devpath in modem.get("devpath", ""): + idx = modem.get("index", 0) + return {"index": idx, "slot": idx, + "paths": [modem["devpath"]], "type": "modem"} + + # Fallback: Minex modules.json (hardware with slot/SIM multiplexer) + if os.path.exists("/run/modules.json"): + with open("/run/modules.json", "r") as fd: + data = json.load(fd) + for module in data.get("modules", []): + if module["type"] != "modem": + continue + for path in module.get("paths", []): + if path in devpath: + return module return None - with open("/run/modules.json", "r") as fd: - data = json.load(fd) + # Fallback: legacy modems.json written by modem-udev + if os.path.exists("/run/modems.json"): + with open("/run/modems.json", "r") as fd: + data = json.load(fd) + for index, modem in enumerate(data.get("modems", [])): + if devpath in modem.get("devpath", ""): + return {"index": index, "slot": index, + "paths": [modem["devpath"]], "type": "modem"} - for module in data["modules"]: - if module["type"] != "modem": - continue - for path in module["paths"]: - if path in devpath: - return module return None @@ -228,18 +255,35 @@ def sysfs_interfaces(devpath): def print_modem(index): - with open("/run/modems.json", "r") as fd: - data = json.load(fd) + # Try system.json first (canonical source written by 00-probe) + if os.path.exists("/run/system.json"): + with open("/run/system.json", "r") as fd: + data = json.load(fd) + for modem in data.get("modem", []): + if modem.get("index") == index: + module = {"index": index, "slot": index, + "paths": [modem["devpath"]], "type": "modem"} + modem["sim"] = modem_get_sim(module) or \ + {"index": 0, "name": "sim0", "slot": 0, "present": True} + if not modem.get("interfaces"): + modem["interfaces"] = sysfs_interfaces(modem["devpath"]) + print(json.dumps(modem)) + sys.exit(0) - for modem in data["modems"]: - module = modem_get_module(modem["devpath"]) - if module["index"] == index: - modem["slot"] = module["slot"] - modem["sim"] = modem_get_sim(module) or {"index": 0, "name": "sim0", "slot": 0, "present": True} - if not modem.get("interfaces"): - modem["interfaces"] = sysfs_interfaces(modem["devpath"]) - print(json.dumps(modem)) - sys.exit(0) + # Fallback: legacy modems.json written by modem-udev + if os.path.exists("/run/modems.json"): + with open("/run/modems.json", "r") as fd: + data = json.load(fd) + for modem in data.get("modems", []): + module = modem_get_module(modem["devpath"]) + if module and module["index"] == index: + modem["slot"] = module["slot"] + modem["sim"] = modem_get_sim(module) or \ + {"index": 0, "name": "sim0", "slot": 0, "present": True} + if not modem.get("interfaces"): + modem["interfaces"] = sysfs_interfaces(modem["devpath"]) + print(json.dumps(modem)) + sys.exit(0) print(json.dumps({})) sys.exit(0) @@ -309,14 +353,23 @@ def print_all(): "phone-number": gen.get("own-numbers", []), } + sig_q = gen.get("signal-quality") or {} modem["status"] = { "state": vstr(gen["state"]), "selected-carrier": get_current_carrier(index, manf, model), - "signal-quality": vnum(gen["signal-quality"]["value"]) + "signal-quality": vnum(sig_q.get("value", "0")), + "signal-quality-recent": vbool(sig_q.get("recent", "no")), + "power-state": vstr(gen.get("power-state", "--")) or "unknown", } + locks = gpp.get("enabled-locks") or [] + if isinstance(locks, list): + modem["status"]["enabled-locks"] = [ + lk for lk in locks if lk and lk != "--" and lk != "none" + ] + if gen["state"] == "failed": - reason = vstr(gen["state-failed-reason"]) + reason = vstr(gen.get("state-failed-reason", "--")) if len(reason) > 0: modem["status"]["state-failed-reason"] = reason @@ -324,6 +377,17 @@ def print_all(): if sim is not None: modem["status"]["sim-active"] = sim["index"] modem["status"]["sim-present"] = sim["present"] + sim_name = sim["name"] + sim_slot = sim["slot"] + else: + sim_name = "sim0" + slot_raw = gen.get("primary-sim-slot", 1) + sim_slot = int(slot_raw) if str(slot_raw).isdigit() else 1 + + # rssi / rsrp are int16 dBm in YANG; rsrq / sinr are decimal64 dB. + # mmcli reports them as floating-point strings; normalise here. + int_signals = {"rssi", "rsrp"} + dec_signals = {"rsrq", "sinr"} refresh = 0 output = runcmdj(['mmcli', '-J', '-m', mpath, '--signal-get']) @@ -336,14 +400,33 @@ def print_all(): signals = ["rssi", "rsrp", "rsrq", "rscp", "snr", "sinr"] found = False for tech in output["modem"]["signal"].keys(): + if tech == "refresh": + continue for sig in signals: v = output["modem"]["signal"][tech].get(sig, "--") - if v != "--": - modem["status"]["signal-%s" % sig] = v + if v == "--": + continue + try: + if sig in int_signals: + modem["status"]["signal-%s" % sig] = int(round(float(v))) + elif sig in dec_signals: + modem["status"]["signal-%s" % sig] = "%.1f" % float(v) + else: + modem["status"]["signal-%s" % sig] = v found = True + except (TypeError, ValueError): + continue if found: break + access_techs = gen.get("access-technologies") or [] + if isinstance(access_techs, str): + access_techs = [access_techs] + access_techs = [t for t in access_techs if t and t != "--"] + # mmcli reports the active access-technology set lowest-to-highest; + # pick the most advanced for the YANG single-valued enum. + network_type = access_techs[-1].lower() if access_techs else "unknown" + modem["status"]["cellular"] = { "registration-state": venum(gpp["registration-state"], @@ -351,8 +434,10 @@ def print_all(): "unknown"), "operator-name": vstr(gpp["operator-name"]), "operator-id": vstr(gpp["operator-code"]), - "network-type": ",".join(vstr(gen["access-technologies"])).upper(), - "service-state": vstr(gpp["packet-service-state"]) + "network-type": network_type, + "service-state": + venum(gpp.get("packet-service-state", "--"), + ["attached", "detached"], "unknown"), } bearers = [] @@ -389,10 +474,40 @@ def print_all(): modem["status"]["bearer"] = bearers - output = runcmdj(['mmcli', '-J', '-m', mpath, '-i', gen["sim"]]) + sim_path = vstr(gen.get("sim", "--")) + output = None + if sim_path: + output = runcmdj(['mmcli', '-J', '-m', mpath, '-i', sim_path]) + if output: - modem["info"]["imsi"] = output["sim"]["properties"]["imsi"] - modem["info"]["iccid"] = output["sim"]["properties"]["iccid"] + sim_props = output["sim"]["properties"] + modem["info"]["imsi"] = vstr(sim_props.get("imsi", "--")) + modem["info"]["iccid"] = vstr(sim_props.get("iccid", "--")) + modem["sim-state"] = { + "name": sim_name, + "slot": sim_slot, + "state": _sim_lock_state(vstr(gen.get("unlock-required", "--"))), + "operator-name": vstr(sim_props.get("operator-name", "--")), + } + else: + # mmcli SIM query failed (or there's no SIM path at all). Emit + # sim-state anyway so a 'simN' component appears in the YANG + # tree — Andrew's report: a missing SIM should be visible from + # 'show modem', not silent. Derive the state from modem-level + # signals: an empty 'sim' path or 'sim-missing' failure reason + # both mean the slot is empty. Otherwise we genuinely don't + # know (modem busy / SIM read error / etc.) — report 'unknown'. + if (not sim_path or + vstr(gen.get("state-failed-reason", "--")) == "sim-missing"): + sim_state_value = "not-inserted" + else: + sim_state_value = "unknown" + modem["sim-state"] = { + "name": sim_name, + "slot": sim_slot, + "state": sim_state_value, + "operator-name": "", + } output = runcmdj(['mmcli', '-J', '-m', mpath, '--location-get']) if output: diff --git a/src/modemd/modem-rpc b/src/modemd/modem-rpc index 5bc7ecd7..ff9f86eb 100755 --- a/src/modemd/modem-rpc +++ b/src/modemd/modem-rpc @@ -38,6 +38,13 @@ if __name__ == "__main__": print("Sending '%s' rpc to modem%d" % (args.rpc, index)) - rpc = {"infix-modem:%s" % args.rpc: {"index": str(index)}} + rpc = { + "ietf-hardware:hardware": { + "component": [{ + "name": "modem%d" % index, + "infix-hardware:modem": {args.rpc: {}} + }] + } + } sendrpc(index, rpc) diff --git a/src/modemd/modem-sms b/src/modemd/modem-sms index 7bcb797c..cf6c8b42 100755 --- a/src/modemd/modem-sms +++ b/src/modemd/modem-sms @@ -27,11 +27,17 @@ def send(index, number, text): index = 0 rpc = { - "infix-modem:send-sms": { - "index": str(index), - "phone-number": number, - "message-text": text - } + "ietf-hardware:hardware": { + "component": [{ + "name": "modem%d" % index, + "infix-hardware:modem": { + "send-sms": { + "phone-number": number, + "message-text": text + } + } + }] + } } print("Sending SMS to modem%d" % index) diff --git a/src/modemd/modem-udev b/src/modemd/modem-udev index ba4ebfba..64d6b1e2 100755 --- a/src/modemd/modem-udev +++ b/src/modemd/modem-udev @@ -10,6 +10,7 @@ import re import os MODEMS = "/run/modems.json" +SYSTEM = "/run/system.json" LOCK = "/var/lock/modems.lock" LOGFILE = "/run/modemd/modem-udev.log" debug = False @@ -98,22 +99,42 @@ def write_modem_data(data): return True -def get_vendor(devpath): - path = "%s/idVendor" % devpath - if os.path.exists(path): - with open(path, "r") as fd: +def _sysfs_attr(devpath, name): + try: + with open("%s/%s" % (devpath, name), "r") as fd: return fd.readline().strip() - else: + except OSError: return None -def get_product(devpath): - path = "%s/idProduct" % devpath - if os.path.exists(path): - with open(path, "r") as fd: - return fd.readline().strip() +def update_system_json(modems_list): + data = {} + try: + with open(SYSTEM, "r") as fd: + data = json.load(fd) + except (IOError, json.JSONDecodeError): + pass + + entries = [] + for i, m in enumerate(modems_list): + entries.append({ + "index": i, + "name": "modem%d" % i, + "devpath": m.get("devpath", ""), + "vid": m.get("vendor", ""), + "pid": m.get("product", ""), + }) + + if not entries: + data.pop("modem", None) else: - return None + data["modem"] = entries + + tmp = SYSTEM + ".tmp" + with open(tmp, "w") as fd: + json.dump(data, fd, indent=2) + os.rename(tmp, SYSTEM) + info("Updated system.json with %d modem(s)" % len(entries)) def update(d): @@ -133,11 +154,11 @@ def update(d): if not modem: modem = {"devpath": devpath} - vendor = get_vendor(devpath) + vendor = _sysfs_attr(devpath, "idVendor") if vendor: modem["vendor"] = vendor - product = get_product(devpath) + product = _sysfs_attr(devpath, "idProduct") if product: modem["product"] = product @@ -165,6 +186,7 @@ def update(d): data["modems"].append(modem) write_modem_data(data) + update_system_json(data["modems"]) info("Updated modem %s" % devpath) diff --git a/src/modemd/modemd b/src/modemd/modemd index 695a41a0..fee12bec 100755 --- a/src/modemd/modemd +++ b/src/modemd/modemd @@ -14,6 +14,7 @@ import enum import time import os import sys +import ipaddress rundir = "/run/modemd" smsdir = "/var/sms" @@ -63,8 +64,10 @@ def opener(path, flags): def rmf(path): - if os.path.exists(path): + try: os.unlink(path) + except FileNotFoundError: + pass def rmrf(path): @@ -87,12 +90,12 @@ def mkdir(path): def fread(path): - if os.path.exists(path): + try: with open(path, "r") as fd: - output = str(fd.read()) - if output: - return output.strip() - return None + output = fd.read() + return output.strip() if output else None + except OSError: + return None def freadj(path): @@ -207,6 +210,92 @@ def list_sms(modem): return smslist +def load_config(): + hw = runcmdj(["sysrepocfg", "-f", "json", "-X", "-m", "ietf-hardware"]) + ifaces = runcmdj(["sysrepocfg", "-f", "json", "-X", "-m", "ietf-interfaces"]) + + ks = runcmdj(["sysrepocfg", "-f", "json", "-X", "-m", "ietf-keystore"]) + keys = {} + if ks: + for key in ks.get("ietf-keystore:keystore", {}) \ + .get("symmetric-keys", {}) \ + .get("symmetric-key", []): + keys[key["name"]] = key.get("cleartext-symmetric-key", "") + + # Cellular modems appear seconds after boot; use physical detection as the + # authoritative list rather than sysrepo which may not have them yet. + sysj = freadj("/run/system.json") or {} + physical = sysj.get("modem", []) + if not physical: + mj = freadj("/run/modems.json") or {} + for i, m in enumerate(mj.get("modems", [])): + physical.append({"index": i, "name": "modem%d" % i, + "devpath": m.get("devpath", "")}) + + hw_comps, sims = {}, {} + for comp in (hw or {}).get("ietf-hardware:hardware", {}).get("component", []): + cls = comp.get("class", "") + if cls == "infix-hardware:modem": + hw_comps[comp["name"]] = comp + elif cls == "infix-hardware:sim": + sims[comp["name"]] = comp.get("infix-hardware:sim", {}) + + modems = {} + for phys in physical: + name = phys.get("name", "modem%d" % phys.get("index", 0)) + index = phys.get("index", 0) + + comp = hw_comps.get(name, {}) + admin = comp.get("state", {}).get("admin-state", "unlocked") + if admin == "locked": + continue + + ih = comp.get("infix-hardware:modem", {}) + modems[name] = { + "index": index, + "preferred-mode": ih.get("preferred-mode"), + "allowed-mode": ih.get("allowed-mode", []), + "band": ih.get("band", []), + "location": ih.get("location"), + "bearer": [], + } + + for iface in (ifaces or {}).get("ietf-interfaces:interfaces", {}).get("interface", []): + if iface.get("type", "") != "infix-if-type:modem": + continue + wwan = iface.get("infix-interfaces:wwan", {}) + modem_ref = wwan.get("modem") + if not modem_ref or modem_ref not in modems: + continue + + sim_ref = wwan.get("sim") + if sim_ref and sim_ref in sims and "pin" not in modems[modem_ref]: + sim = sims[sim_ref] + modems[modem_ref].update({ + "pin": sim.get("pin"), + "puk": sim.get("puk"), + "carrier": sim.get("carrier", "default"), + }) + + bearer_cfg = wwan.get("bearer", {}) + auth = bearer_cfg.get("authentication") + bearer = { + "index": len(modems[modem_ref]["bearer"]), + "name": iface["name"], + "apn": bearer_cfg.get("apn", ""), + "ip-type": bearer_cfg.get("ip-type", "ipv4v6"), + "roaming": bearer_cfg.get("roaming", False), + "route-preference": bearer_cfg.get("route-preference", 200), + } + if auth: + bearer["username"] = auth.get("username", "") + bearer["password"] = keys.get(auth.get("password", ""), "") + + modems[modem_ref]["bearer"].append(bearer) + + return list(modems.values()) + + class RpcThread(threading.Thread): path = None server = None @@ -340,6 +429,10 @@ class SimThread(threading.Thread): th.interrupt = True def run(self): + if not os.path.exists("/dev/simctrl"): + dbg("No /dev/simctrl, SIM slot monitoring not available on this platform") + self.exited = True + return fd = os.open("/dev/simctrl", os.O_RDONLY) while not self.stopped(): res, *_ = select.select([fd], [], [], 10.0) @@ -428,8 +521,6 @@ class ModemThread(threading.Thread): self.err("No bearers configured") return False - default = None - count = 0 for bearer in sorted(bearers, key=lambda b: b["index"]): bearer["pid"] = "--" @@ -438,19 +529,7 @@ class ModemThread(threading.Thread): self.err("No APN configured") return False - apntype = bearer.get("apn-type") - if apntype == "initial": - if not self.bearers["initial"]: - self.bearers["initial"] = bearer - elif apntype == "default" and not default: - default = bearer - count += 1 - else: - self.bearers["list"].append(bearer) - count += 1 - - if default: - self.bearers["list"].insert(0, default) + self.bearers["list"].append(bearer) # at least one bearer is mandatory if len(self.bearers["list"]) == 0: @@ -475,9 +554,7 @@ class ModemThread(threading.Thread): loc = self.cfg.get("location") if loc and loc.get("enabled"): enabled = True - v = loc.get("source") - if v: - sources = [s.get("source") for s in v] + sources = loc.get("source", []) self.location = { "enabled": enabled, @@ -538,10 +615,12 @@ class ModemThread(threading.Thread): def notif(self, msg): data = { - "infix-modem:modems": { - "modem": [{ - "index": self.index, - "status-update": {"desc": msg} + "ietf-hardware:hardware": { + "component": [{ + "name": "modem%d" % self.index, + "infix-hardware:modem": { + "status-update": {"desc": msg} + } }] } } @@ -612,10 +691,6 @@ class ModemThread(threading.Thread): return True def resolvconf(self, iface, action): - enabled = iface["bearer"].get("dns-enabled", False) - if enabled is False: - return True - self.info("About to %s nameservers on %s" % (action, iface["name"])) output = runcmd(["resolvconf", "-i"], check=False) @@ -643,18 +718,38 @@ class ModemThread(threading.Thread): return True - def ipgwroute(self, iface, action): - output = runcmdj(["ip", "-j", "route", - "show", "dev", iface["name"]]) - for rt in output: - if "scope" in rt and rt["scope"] == "link": - if action == "add": - return True - else: - runcmd(["ip", "route", - "del", rt["dst"], "dev", iface["name"]]) + def _derive_gateway(self, ifname): + """Derive the peer gateway from the link-scope subnet route on ifname. + + Point-to-point bearers (MBIM/QMI) often report gateway as '--' even + though the kernel installs a link-scope subnet route (e.g. /30). The + peer — and therefore the nexthop for the default route — is the only + other host in that subnet. + """ + output = runcmdj(["ip", "-j", "route", "show", + "dev", ifname, "scope", "link"]) + for rt in (output or []): + dst = rt.get("dst", "") + src = rt.get("prefsrc", "") + if not dst or not src or "/" not in dst: + continue + try: + net = ipaddress.ip_network(dst, strict=False) + our = ipaddress.ip_address(src) + peers = [str(h) for h in net.hosts() if h != our] + if peers: + return peers[0] + except ValueError: + pass + return None + + def ipdefroute(self, iface, action): + self.info("About to %s default route on %s" % (action, iface["name"])) + + conf = "/etc/net.d/%s.conf" % iface["name"] if action != "add": + rmf(conf) return True if iface["ipv4"]["gateway"] != "--": @@ -662,52 +757,39 @@ class ModemThread(threading.Thread): elif iface["ipv6"]["gateway"] != "--": gw = iface["ipv6"]["gateway"] else: - self.err("No gateway address found") - return False - - cmd = ["ip", "route", "add", gw, "dev", iface["name"]] - if not runcmd(cmd): - self.err("Unable to add gateway route") - return False - - return True - - def ipdefroute(self, iface, action): - self.info("About to %s default route on %s" % (action, iface["name"])) - - output = runcmdj(["ip", "-j", "route", "show", "default"]) - if output: - for route in output: - if not runcmd(["ip", "route", "del", "default", - "dev", route["dev"]]): - self.err("Can't delete default route on %s" % route["dev"]) - - if not self.ipgwroute(iface, action): - return False - - if action != "add": - return True - - cmd = ["ip", "route", "add", "default", "dev", iface["name"]] - if iface["ipv4"]["gateway"] != "--": - cmd += ["via", iface["ipv4"]["gateway"]] - elif iface["ipv6"]["gateway"] != "--": - cmd += ["via", iface["ipv6"]["gateway"]] - - if not runcmd(cmd): - self.err("Unable to add default route on %s" % iface["name"]) - time.sleep(30) - return False - else: - return True - - def ifupdown(self, iface, action): - enabled = iface["bearer"].get("firewall-enabled", False) - if enabled is True: - self.info("Setting %s firewall on %s" % (action, self.iface)) - if not runcmd(["/etc/firewall/default", action, self.iface]): + # Gateway not reported by modem (common for point-to-point bearers). + # Derive it from the link-scope subnet route on the interface: the + # peer is the only other host in the subnet. + gw = self._derive_gateway(iface["name"]) + if not gw: + self.err("No gateway address found") return False + metric = iface["bearer"].get("route-preference", 200) + + # Write route to /etc/net.d/ for netd/staticd/zebra to install. + # This keeps the route visible to FRR for redistribution and allows + # metric-based failover behind wired/WiFi routes (which use lower + # metric values from udhcpc, typically 5). + content = "# Generated by modemd -- do not edit\n\n" + content += "route {\n" + if ":" in gw: + content += ' prefix = "::/0"\n' + else: + content += ' prefix = "0.0.0.0/0"\n' + content += ' nexthop = "%s"\n' % gw + content += ' distance = %d\n' % metric + content += ' tag = 100\n' + content += '}\n' + + if fread(conf) == content.strip(): + return True + next_conf = conf + "+" + fwrite(next_conf, content) + os.rename(next_conf, conf) + return True + + def ifupdown(self, iface, action): path = "/var/ifupdown/%s" % self.iface if os.path.isfile(path) or os.access(path, os.X_OK): self.info("Running %s %s" % (path, action)) @@ -721,12 +803,10 @@ class ModemThread(threading.Thread): self.ipdefroute(iface, "delete") self.ifupdown(iface, "down") - output = runcmdj(["ip", "-j", "addr", "show", iface["name"]]) - for entry in output: - for addr in entry["addr_info"]: - runcmd(["ip", "addr", "del", - addr["local"] + "/" + str(addr["prefixlen"]), - "dev", iface["name"]]) + # Flush only wwan-protocol-tagged addresses; leaves any static or + # dhcp-protocol addresses intact in case the interface is shared. + runcmd(["ip", "addr", "flush", "dev", iface["name"], + "proto", "wwan"], check=False) self.iplink(iface["name"], "down") @@ -748,16 +828,21 @@ class ModemThread(threading.Thread): self.err("No addresses yet") return False + # Flush any stale wwan-protocol addresses before adding new ones. + # The 'proto wwan' tag (rt_addrprotos entry 7) allows precise cleanup + # on disconnect without touching addresses added by other means. + runcmd(["ip", "addr", "flush", "dev", iface["name"], + "proto", "wwan"], check=False) + for addr in addrs: self.info("Setting address %s on %s" % (addr, iface["name"])) - if not runcmd(["ip", "addr", "change", - addr, "dev", iface["name"]]): + if not runcmd(["ip", "addr", "replace", addr, + "dev", iface["name"], "proto", "wwan"]): self.err("Unable to set address for %s" % iface["name"]) return False - if iface["bearer"].get("default-route"): - if not self.ipdefroute(iface, "add"): - return False + if not self.ipdefroute(iface, "add"): + return False if not self.ifupdown(iface, "up"): return False @@ -926,9 +1011,6 @@ class ModemThread(threading.Thread): "profile-id=%s" % bearer["pid"], "profile-name=profile%s" % bearer["pid"] ] - v = bearer.get("apn-type") - if v: - args.append("apn-type=%s" % v) v = bearer.get("apn") if v: args.append("apn=%s" % v) @@ -963,7 +1045,7 @@ class ModemThread(threading.Thread): if v: args.append("password=%s" % v) - v = bearer.get("allow-roaming") + v = bearer.get("roaming") if v: args.append("allow-roaming=true") else: @@ -1076,8 +1158,7 @@ class ModemThread(threading.Thread): def prepare_bands(self): self.info("Preparing bands") - v = self.cfg.get("band", []) - bands = [k.get("band", "any") for k in v] + bands = self.cfg.get("band", []) if len(bands) == 0 or "any" in bands: return True @@ -1094,9 +1175,7 @@ class ModemThread(threading.Thread): if not pref or pref == "any": pref = "none" - allow = [] - v = self.cfg.get("allowed-mode", []) - allow = [k.get("mode", "any") for k in v] + allow = self.cfg.get("allowed-mode", []) if "any" in allow: allow = [] @@ -1588,17 +1667,14 @@ if __name__ == "__main__": print(e) fatal("SIM thread caught an exception") - config = runcmdj(["sysrepocfg", "-f", "json", "-X", "-m", "infix-modem"]) - if not config or "infix-modem:modems" not in config: - fatal("Unable to read config") + modems = load_config() + if not modems: + fatal("No modems configured or enabled") if not runcmd(['/usr/libexec/modemd/sim-setup']): fatal("Unable to setup SIMs") - for cfg in config["infix-modem:modems"]["modem"]: - if not cfg["enabled"] or cfg["index"] < 0: - continue - + for cfg in modems: th = None try: th = ModemThread(cfg) diff --git a/src/modemd/sim-setup b/src/modemd/sim-setup index be400f03..a19b4a51 100755 --- a/src/modemd/sim-setup +++ b/src/modemd/sim-setup @@ -173,17 +173,42 @@ def swapslot(slot): def sim_setup(setup): changed = False - config = runcmdj(["sysrepocfg", "-f", "json", "-X", "-m", "infix-modem"]) - if not config or "infix-modem:modems" not in config: + hw = runcmdj(["sysrepocfg", "-f", "json", "-X", "-m", "ietf-hardware"]) + ifaces = runcmdj(["sysrepocfg", "-f", "json", "-X", "-m", "ietf-interfaces"]) + if not hw or not ifaces: err("Cannot read config") return False - for cfg in config["infix-modem:modems"]["modem"]: - if not cfg.get("enabled", False) or cfg.get("index", -1) < 0: - continue + modem_set = set() + for comp in hw.get("ietf-hardware:hardware", {}).get("component", []): + if comp.get("class", "") == "infix-hardware:modem" \ + and comp.get("state", {}).get("admin-state", "unlocked") == "unlocked": + name = comp["name"] + try: + modem_set.add(int(name[5:])) + except (ValueError, IndexError): + pass - modem = find_modem(setup, cfg["index"]) - sim = find_sim(setup, cfg.get("sim", -1)) + sim_for_modem = {} + for iface in ifaces.get("ietf-interfaces:interfaces", {}).get("interface", []): + if iface.get("type", "") != "infix-if-type:modem": + continue + wwan = iface.get("infix-interfaces:wwan", {}) + modem_ref = wwan.get("modem", "") + sim_ref = wwan.get("sim", "") + if not modem_ref.startswith("modem") or not sim_ref.startswith("sim"): + continue + try: + midx = int(modem_ref[5:]) + sidx = int(sim_ref[3:]) + except (ValueError, IndexError): + continue + if midx in modem_set: + sim_for_modem[midx] = sidx + + for modem_idx, sim_idx in sim_for_modem.items(): + modem = find_modem(setup, modem_idx) + sim = find_sim(setup, sim_idx) if modem and sim: sim["newslot"] = modem["slot"] if sim["newslot"] != sim["slot"]: diff --git a/src/statd/python/cli_pretty/cli_pretty.py b/src/statd/python/cli_pretty/cli_pretty.py index a686e8db..3fa0e2ea 100755 --- a/src/statd/python/cli_pretty/cli_pretty.py +++ b/src/statd/python/cli_pretty/cli_pretty.py @@ -579,6 +579,59 @@ def signal_to_status(signal): return status +def signal_quality_to_status(percent): + """Modem signal-quality (0-100%) → colored status word. + + Thresholds chosen so the output vocabulary matches signal_to_status + above: a roughly linear -110..-50 dBm modem-quality mapping puts + 80% near -62 dBm (excellent), 60% near -74 dBm (good), 40% near -86 + dBm (poor). Below that is "bad". + """ + if percent is None: + return "" + try: + percent = float(percent) + except (TypeError, ValueError): + return "" + if percent >= 80: + return Decore.bright_green("excellent") + if percent >= 60: + return Decore.green("good") + if percent >= 40: + return Decore.yellow("poor") + return Decore.red("bad") + + +def modem_state_to_status(state): + """Color the modem state word: connected/registered green, failed + red, transient states yellow, everything else plain.""" + if not state: + return "" + if state in ("connected", "registered"): + return Decore.green(state) + if state == "failed": + return Decore.red(state) + if state in ("connecting", "enabling", "disabling", "disconnecting", + "enabled", "searching", "initializing", "locked"): + return Decore.yellow(state) + return state + + +def sim_lock_to_status(state): + """Color the SIM lock-state word.""" + if not state: + return "" + if state == "unlocked": + return Decore.green(state) + if state == "not-inserted": + return Decore.yellow(state) + if state == "permanently-blocked": + return Decore.red(state) + if state in ("pin-required", "puk-required"): + return Decore.yellow(state) + return state + + def datetime_now(): if UNIT_TEST: return datetime(2023, 1, 1, 12, 0, 0, tzinfo=timezone.utc) @@ -895,8 +948,9 @@ class Sensor: else: return f"{self.value} {self.value_type}" - def print(self, indent=0): + def print(self, indent=0, name_width=None): import re + nw = name_width if name_width is not None else PadSensor.name # Add indentation for child sensors indent_str = " " * indent @@ -916,7 +970,7 @@ class Sensor: # Standalone sensor without description: use name as-is display_name = self.name - row = f"{indent_str}{display_name:<{PadSensor.name - len(indent_str)}}" + row = f"{indent_str}{display_name:<{nw - len(indent_str)}}" # For colored value, pad manually to account for ANSI codes value_str = self.get_formatted_value() # Count visible characters (strip ANSI codes for length calculation) @@ -2245,6 +2299,222 @@ def show_services(json): service_table.print() +def _build_modem_table(modems): + """SimpleTable with one row per modem hardware component. Shared + between 'show modem' and the modem section of 'show hardware'.""" + table = SimpleTable([ + Column('NAME'), + Column('MANUFACTURER', flexible=True), + Column('MODEL', flexible=True), + Column('STATE'), + Column('NETWORK'), + Column('SIGNAL'), + ]) + for component in modems: + ms = component.get("infix-hardware:modem-state", {}) + cell = ms.get("cellular", {}) + name = component.get("name", "unknown") + mfg = ms.get("manufacturer", "") + model = ms.get("model", "") + state = modem_state_to_status(ms.get("state", "unknown")) + network = cell.get("network-type", "") + signal = signal_quality_to_status(ms.get("signal-quality")) + table.row(name, mfg, model, state, network, signal) + return table + + +def _build_sim_table(sims): + """SimpleTable with one row per SIM hardware component. Shared + between 'show modem' and the SIM section of 'show hardware'.""" + table = SimpleTable([ + Column('NAME'), + Column('SLOT'), + Column('STATE'), + Column('OPERATOR', flexible=True), + ]) + for component in sims: + ss = component.get("infix-hardware:sim-state", {}) + name = component.get("name", "unknown") + slot = str(ss["slot"]) if ss.get("slot") is not None else "" + state = sim_lock_to_status(ss.get("state", "")) + operator = ss.get("operator-name", "") + table.row(name, slot, state, operator) + return table + + +def show_modem(json): + components = get_json_data([], json, "ietf-hardware:hardware", "component") + modems = [c for c in components if c.get("class") == "infix-hardware:modem"] + sims = [c for c in components if c.get("class") == "infix-hardware:sim"] + + if not modems and not sims: + print("No cellular modems found.") + return + + modem_table = _build_modem_table(modems) if modems else None + sim_table = _build_sim_table(sims) if sims else None + + width = max(modem_table.width() if modem_table else 0, + sim_table.width() if sim_table else 0, + 62) + + if modem_table: + Decore.title("Cellular Modems", width) + modem_table.adjust_padding(width) + modem_table.print() + + if sim_table: + Decore.title("SIM Cards", width) + sim_table.adjust_padding(width) + sim_table.print() + + +def _modem_signal_str(status): + parts = [] + sq = status.get("signal-quality") + if sq is not None: + parts.append(f"Quality: {sq}%") + for key, label in (("signal-rssi", "RSSI"), ("signal-rsrp", "RSRP"), + ("signal-rsrq", "RSRQ"), ("signal-sinr", "SINR")): + if key in status: + parts.append(f"{label}: {status[key]}") + return " ".join(parts) + + +def show_modem_detail(json, ref): + modems = json.get("modem-list", []) + try: + idx = int(ref.removeprefix("modem")) + except ValueError: + idx = -1 + + modem = next((m for m in modems if m.get("index") == idx), None) + if modem is None: + print(f"Modem '{ref}' not found.") + return + + info = modem.get("info", {}) + status = modem.get("status", {}) + cell = status.get("cellular", {}) + sim_state = modem.get("sim-state", {}) + bearers = status.get("bearer", []) + location = status.get("location", {}) + + width = 62 + print(Decore.invert(f"{'MODEM: ' + ref:<{width}}")) + + Decore.title("Hardware Information", width) + for label, val in ( + ("Manufacturer", info.get("manufacturer", "")), + ("Model", info.get("model", "")), + ("Hardware Revision", info.get("hardware-revision", "")), + ("Firmware Version", info.get("firmware-version", "")), + ("Serial Number", info.get("serial-number", "")), + ("IMSI", info.get("imsi", "")), + ("ICCID", info.get("iccid", "")), + ): + if val: + print(f" {label:<20}: {val}") + phone = info.get("phone-number", []) + if phone: + print(f" {'Phone Number':<20}: {', '.join(phone)}") + carriers = info.get("supported-carrier", []) + if carriers: + print(f" {'Supported Carriers':<20}: {', '.join(carriers)}") + selected = status.get("selected-carrier", "") + if selected: + print(f" {'Selected Carrier':<20}: {selected}") + + Decore.title("Status", width) + state = status.get("state", "unknown") + print(f" {'State':<20}: {state}") + if state == "failed": + reason = status.get("state-failed-reason", "") + if reason: + print(f" {'Failed Reason':<20}: {reason}") + sig_str = _modem_signal_str(status) + if sig_str: + print(f" {'Signal':<20}: {sig_str}") + + if cell: + Decore.title("Cellular", width) + for label, key in ( + ("Registration", "registration-state"), + ("Operator", "operator-name"), + ("Operator ID", "operator-id"), + ("Network Type", "network-type"), + ("Service State", "service-state"), + ): + val = cell.get(key, "") + if val: + print(f" {label:<20}: {val}") + + if sim_state: + Decore.title("SIM Card", width) + for label, key in ( + ("Name", "name"), + ("Slot", "slot"), + ("Lock State", "state"), + ("Operator", "operator-name"), + ): + val = sim_state.get(key) + if val: + print(f" {label:<20}: {val}") + + if bearers: + Decore.title("Bearers", width) + for b in bearers: + iface = b.get("interface", "") + hdr = iface if iface else b.get("path", "") + connected = b.get("connected", False) + conn_str = "connected" if connected else "disconnected" + print(f" {hdr} ({conn_str})") + if not connected: + reason = b.get("connection-failed-reason", "") + if reason: + print(f" {'Error':<18}: {reason}") + v4 = b.get("ipv4-address") + v4pfx = b.get("ipv4-prefix", 0) + if v4 and v4pfx: + print(f" {'IPv4':<18}: {v4}/{v4pfx}") + v6 = b.get("ipv6-address") + v6pfx = b.get("ipv6-prefix", 0) + if v6 and v6pfx: + print(f" {'IPv6':<18}: {v6}/{v6pfx}") + rx = b.get("in-bytes", 0) + tx = b.get("out-bytes", 0) + if rx or tx: + print(f" {'Traffic':<18}: RX {rx} B TX {tx} B") + total_rx = b.get("total-in-bytes", 0) + total_tx = b.get("total-out-bytes", 0) + duration = b.get("total-duration", 0) + if total_rx or total_tx: + print(f" {'Total Traffic':<18}: RX {total_rx} B TX {total_tx} B ({duration} s)") + + if location: + lat = location.get("latitude", "") + lon = location.get("longitude", "") + alt = location.get("altitude", "") + cid = location.get("cid", "") + lac = location.get("lac", "") + mcc = location.get("mcc", "") + mnc = location.get("mnc", "") + if lat or cid: + Decore.title("Location", width) + if lat and lon: + pos = f"{lat}, {lon}" + if alt: + pos += f" Alt: {alt} m" + print(f" {'GPS':<20}: {pos}") + if cid: + cell_id = f"CID {cid}" + if lac: + cell_id += f" LAC {lac}" + if mcc and mnc: + cell_id += f" MCC {mcc} MNC {mnc}" + print(f" {'Cell ID':<20}: {cell_id}") + + def show_hardware(json): if not json.get("ietf-hardware:hardware"): print("Error, top level \"ietf-hardware:component\" missing") @@ -2257,8 +2527,18 @@ def show_hardware(json): sensors = [c for c in components if c.get("class") == "iana-hardware:sensor"] wifi_radios = [c for c in components if c.get("class") == "infix-hardware:wifi"] gps_receivers = [c for c in components if c.get("class") == "infix-hardware:gps"] + modems = [c for c in components if c.get("class") == "infix-hardware:modem"] + sims = [c for c in components if c.get("class") == "infix-hardware:sim"] - width = max(PadSensor.table_width(), 62) + # Pre-build modem and SIM tables to get natural widths before computing + # the global width used by all section separators. + modem_table = _build_modem_table(modems) if modems else None + sim_table = _build_sim_table(sims) if sims else None + + width = max(PadSensor.table_width(), + modem_table.width() if modem_table else 0, + sim_table.width() if sim_table else 0, + 62) # Display full-width inverted heading print(Decore.invert(f"{'HARDWARE COMPONENTS':<{width}}")) @@ -2387,11 +2667,21 @@ def show_hardware(json): usb_table.print() + if modem_table: + Decore.title("Cellular Modems", width) + modem_table.adjust_padding(width) + modem_table.print() + + if sim_table: + Decore.title("SIM Cards", width) + sim_table.adjust_padding(width) + sim_table.print() + if sensors: Decore.title("Sensors", width) - # Print header - hdr = (f"{'NAME':<{PadSensor.name}}" + name_width = PadSensor.name + max(0, width - PadSensor.table_width()) + hdr = (f"{'NAME':<{name_width}}" f"{'VALUE':<{PadSensor.value}}" f"{'STATUS':<{PadSensor.status}}") print(Decore.invert(hdr)) @@ -2420,7 +2710,7 @@ def show_hardware(json): if module_name in children: for child in sorted(children[module_name], key=lambda c: c.get("name", "")): sensor = Sensor(child) - sensor.print(indent=1) + sensor.print(indent=1, name_width=name_width) # Display standalone sensors (no parent) if standalone: @@ -2428,7 +2718,7 @@ def show_hardware(json): print() # Add blank line between modules and standalone for component in sorted(standalone, key=lambda c: c.get("name", "")): sensor = Sensor(component) - sensor.print() + sensor.print(name_width=name_width) def resolve_container_network(network, all_ifaces): @@ -5989,6 +6279,9 @@ def main(): .add_argument('name', help='Container name') subparsers.add_parser('show-hardware', help='Show USB ports') + subparsers.add_parser('show-modem', help='Show cellular modem overview') + subparsers.add_parser('show-modem-detail', help='Show detailed modem info') \ + .add_argument('name', help='Modem name (e.g. modem0)') subparsers.add_parser('show-interfaces', help='Show interfaces') \ .add_argument('-n', '--name', help='Interface name') @@ -6066,6 +6359,10 @@ def main(): show_container_detail(json_data, args.name) elif args.command == "show-hardware": show_hardware(json_data) + elif args.command == "show-modem": + show_modem(json_data) + elif args.command == "show-modem-detail": + show_modem_detail(json_data, args.name) elif args.command == "show-interfaces": show_interfaces(json_data, args.name) elif args.command == "show-lldp": diff --git a/src/statd/python/yanger/__main__.py b/src/statd/python/yanger/__main__.py index 92d8a182..c88d4648 100644 --- a/src/statd/python/yanger/__main__.py +++ b/src/statd/python/yanger/__main__.py @@ -126,9 +126,6 @@ def main(): elif model == 'ieee1588-ptp-tt': from . import ieee1588_ptp yang_data = ieee1588_ptp.operational() - elif model == 'infix-modem': - from . import infix_modem - yang_data = infix_modem.operational() else: common.LOG.warning("Unsupported model %s", model) sys.exit(1) diff --git a/src/statd/python/yanger/ietf_hardware.py b/src/statd/python/yanger/ietf_hardware.py index 62e32cd7..337763b5 100644 --- a/src/statd/python/yanger/ietf_hardware.py +++ b/src/statd/python/yanger/ietf_hardware.py @@ -829,19 +829,39 @@ def gps_receiver_components(): return components +def modem_components(): + from . import infix_modem + data = infix_modem.operational() + return data.get("ietf-hardware:hardware", {}).get("component", []) + + def operational(): systemjson = HOST.read_json("/run/system.json", {}) + all_components = ( + motherboard_component(systemjson) + + vpd_components(systemjson) + + usb_port_components(systemjson) + + hwmon_sensor_components() + + thermal_sensor_components() + + wifi_radio_components() + + gps_receiver_components() + + modem_components() + ) + + name_count = {} + components = [] + for c in all_components: + name = c.get("name") + count = name_count.get(name, 0) + name_count[name] = count + 1 + if count > 0: + c = dict(c) + c["name"] = f"{name}{count}" + components.append(c) + return { "ietf-hardware:hardware": { - "component": - motherboard_component(systemjson) + - vpd_components(systemjson) + - usb_port_components(systemjson) + - hwmon_sensor_components() + - thermal_sensor_components() + - wifi_radio_components() + - gps_receiver_components() + - [], + "component": components, }, } diff --git a/src/statd/python/yanger/infix_modem.py b/src/statd/python/yanger/infix_modem.py index a2b0f6d4..be5f5a85 100644 --- a/src/statd/python/yanger/infix_modem.py +++ b/src/statd/python/yanger/infix_modem.py @@ -2,11 +2,91 @@ from .common import LOG from .host import HOST +def _modem_to_hw_state(modem): + info = modem.get("info", {}) + status = modem.get("status", {}) + cellular = status.get("cellular", {}) + + state = {} + if info.get("manufacturer"): + state["manufacturer"] = info["manufacturer"] + if info.get("model"): + state["model"] = info["model"] + if info.get("firmware-version"): + state["firmware-version"] = info["firmware-version"] + if info.get("serial-number"): + state["serial-number"] = info["serial-number"] + if info.get("imsi"): + state["imsi"] = info["imsi"] + if info.get("iccid"): + state["iccid"] = info["iccid"] + if status.get("state"): + state["state"] = status["state"] + if "signal-quality" in status: + state["signal-quality"] = status["signal-quality"] + for sig in ("signal-rssi", "signal-rsrp", "signal-rsrq", "signal-sinr"): + if sig in status: + state[sig] = status[sig] + + if cellular: + cell = {} + if cellular.get("registration-state"): + cell["registration-state"] = cellular["registration-state"] + if cellular.get("operator-name"): + cell["operator-name"] = cellular["operator-name"] + if cellular.get("operator-id"): + cell["operator-id"] = cellular["operator-id"] + if cellular.get("network-type"): + cell["network-type"] = cellular["network-type"] + if cell: + state["cellular"] = cell + + return state + + +def _sim_to_hw_state(sim_raw): + state = {} + slot = sim_raw.get("slot") + if isinstance(slot, int): + state["slot"] = slot + if sim_raw.get("state"): + state["state"] = sim_raw["state"] + if sim_raw.get("operator-name"): + state["operator-name"] = sim_raw["operator-name"] + return state + + def operational(): modems = HOST.run_json(['/usr/libexec/modemd/modem-info'], []) + hw_components = [] + for modem in modems: + idx = modem.get("index", 0) + name = "modem%d" % idx + hw_state = _modem_to_hw_state(modem) + if hw_state: + hw_components.append({ + "name": name, + "class": "infix-hardware:modem", + "infix-hardware:modem-state": hw_state + }) + + sim_raw = modem.get("sim-state") + if sim_raw: + sim_name = sim_raw.get("name", "sim%d" % idx) + sim_hw_state = _sim_to_hw_state(sim_raw) + if sim_hw_state: + hw_components.append({ + "name": sim_name, + "class": "infix-hardware:sim", + "infix-hardware:sim-state": sim_hw_state + }) + + if not hw_components: + return {} + return { - "infix-modem:modems": { - "modem": [m for m in modems] + "ietf-hardware:hardware": { + "component": hw_components } }