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
}
}