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