mirror of
https://github.com/kernelkit/infix.git
synced 2026-07-22 01:13:00 +02:00
modem: migrate from infix-modem to ietf-hardware/ietf-interfaces
The initial modem import used a standalone infix-modem YANG module with
an integer-indexed list under /infix-modem:modems carrying a bearer
config mixed in with hardware config. Infix has adopted the ietf-hardware
model to allow for a clearer separation of concerns, this commit aims to
improve on that situation by refactoring the modem model to use the same
architectural pattern already used for WiFi radios, GPS receivers, and
USB ports:
Hardware:
/ietf-hardware:hardware/component[class=infix-hardware:modem]
/ietf-hardware:hardware/component[class=infix-hardware:sim]
Interface:
/ietf-interfaces:interfaces/interface[type=infix-if-type:modem]
Configuration split:
modem component — admin-state (enable/disable), radio access mode,
frequency bands, location services, probe-timeout
sim component — PIN, PUK, carrier profile
wwan interface — APN, IP type, roaming, route-preference, auth
This also enables the modem to appear in 'show hardware' alongside
other hardware components, and to be managed over NETCONF/RESTCONF
using the same ietf-hardware RPCs used for USB and WiFi.
YANG changes:
- infix-modem.yang removed (1089 lines, standalone module)
- infix-hardware.yang: new modem and sim containers, modem-state and
sim-state operational state, typedefs for bands/modes/location,
notification status-update; actions restart/reset/send-sms are
augmented directly on the component list entry (not inside container
modem) to work around sysrepo NP-container parent validation — when
checking a nested action's parent, sysrepo restricts its operational
data fetch to the container path, excluding the sibling 'class' leaf
needed to evaluate the container's 'when' condition, so the container
is never instantiated and the parent check fails
- infix-if-modem.yang: new submodule (same pattern as wifi/wireguard)
adds the wwan bearer container to ietf-interfaces
- infix-if-type.yang: new modem identity for wwan interface type
- confd version bumped to 1.9
confd:
- if-modem.c: new file replaces the stub modem_gen() in modem.c;
generates dagger init scripts with probe-timeout wait loop and
unconditional dummy wwan creation so downstream IP config succeeds
when a USB modem is slow to enumerate at boot
- hardware.c: start/stop/enable/disable modemd (and modem-manager)
in response to admin-state changes on the modem hardware component;
use finit_enable/disable consistently
- hardware_cand_infer_class(): auto-set class for modemN/simN names
- modem.c: stripped to RPC/notification forwarding only; the old
genconf() / enable() / disable() logic is gone — hardware.c owns
the lifecycle now
- interfaces.c: remove wwan from wait-interface timeout (the modem
interface appearance is managed by modemd, not confd's init scripts)
- migrate/1.9: script fully migrates /infix-modem:modems to the new
split model; each modem[N] becomes a modemN hardware component and
simN sim component; each bearer becomes a wwanN interface (bearers
numbered globally across modems); APN, IP type, roaming, preferred-
mode, bands, location, PIN/PUK/carrier are all carried over; plain-
text bearer credentials are moved to a named keystore symmetric-key
(base64-encoded) referenced from bearer/authentication/password;
apn-type, firewall-enabled, dns-enabled and default-route are
dropped (no equivalent in the new model)
modemd:
- load_config() replaces the infix-modem sysrepocfg call; reads from
ietf-hardware (modem/sim config), ietf-interfaces (bearer config),
and ietf-keystore (credentials) — three standard modules instead of
one proprietary one; modems absent from sysrepo config but present
in system.json are still started (physical detection wins)
- Default route written to /etc/net.d/<iface>.conf (picked up by
netd/staticd/zebra) instead of a direct 'ip route add'; routes are
now visible to FRR for redistribution and metric-based failover
- Addresses tagged with proto wwan (rt_addrprotos entry 7) for
precise flush on disconnect without touching other address sources
- _derive_gateway() handles point-to-point bearers where modem
reports gateway as "--"; derives peer from link-scope subnet route
- SimThread exits cleanly on platforms without /dev/simctrl
- sim-setup, modem-info, modem-udev, modem-rpc, modem-sms all ported
to the ietf-hardware/ietf-interfaces namespace
statd:
- infix_modem.py now emits modem-state and sim-state into ietf-hardware
component entries (modem0, sim0, ...) instead of a separate tree;
sim-state reports physical slot, lock state, and SIM operator name
- ietf_hardware.py: hardware component names deduplicated by rename
(cpu → cpu, cpu1, ...) to prevent LY_EVALID when hwmon and thermal
sysfs both normalise to the same sensor name
- show hardware: Cellular Modems and SIM Cards tables added; sensor
table width now adapts to the widest section in the output
- 00-probe: USB modem detection writes to system.json["modem"], same
source as wifi-radios, feeding gen-hardware which emits the
ietf-hardware component entries at factory config generation time
CLI:
- 'show modem [ref]': without ref shows Cellular Modems and SIM Cards
tables (same data subset as 'show hardware'); with ref shows a full
per-modem dump (hardware info, status, cellular, SIM, bearers, GPS)
- 'modem restart <ref>': disconnect and reconnect all bearers without
a full hardware reset
- 'modem reset <ref>': factory-reset the modem firmware
- 'modem sms <ref> <number> <message>': send an SMS via the modem
(uses the signalling plane; no active data bearer required)
- MODEMS PTYPE provides tab-completion reading modem names from
/run/system.json
doc:
- Add initial User Guide for modems
- Update ChangeLog with modem news
- Use absolute URLs in ChangeLog. Relative links only work for in-tree
references, not in release notes
Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
This commit is contained in:
@@ -5,3 +5,4 @@
|
||||
4 static
|
||||
5 dhcp
|
||||
6 random
|
||||
7 wwan
|
||||
|
||||
@@ -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" +
|
||||
|
||||
+9
-4
@@ -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
|
||||
-------------------------
|
||||
|
||||
@@ -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)
|
||||
|
||||
+438
@@ -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:
|
||||
|
||||
<pre class="cli"><code>admin@example:/> <b>show modem</b>
|
||||
──────────────────────────────────────────────────────────────
|
||||
<span class="title">Cellular Modems</span>
|
||||
NAME MANUFACTURER MODEL STATE NETWORK SIGNAL
|
||||
modem0 Quectel EM06-E registered lte good
|
||||
──────────────────────────────────────────────────────────────
|
||||
<span class="title">SIM Cards</span>
|
||||
NAME SLOT STATE OPERATOR
|
||||
sim0 1 unlocked Tele2
|
||||
</code></pre>
|
||||
|
||||
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:
|
||||
|
||||
<pre class="cli"><code>admin@example:/> <b>configure</b>
|
||||
admin@example:/config/> <b>edit hardware component modem0</b>
|
||||
admin@example:/config/hardware/component/modem0/> <b>set state admin-state unlocked</b>
|
||||
admin@example:/config/hardware/component/modem0/> <b>end</b>
|
||||
admin@example:/config/hardware/> <b>edit component sim0</b>
|
||||
admin@example:/config/hardware/component/sim0/> <b>set state admin-state unlocked</b>
|
||||
admin@example:/config/hardware/component/sim0/> <b>leave</b>
|
||||
admin@example:/>
|
||||
</code></pre>
|
||||
|
||||
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:
|
||||
|
||||
<pre class="cli"><code>admin@example:/config/> <b>edit hardware component modem0</b>
|
||||
admin@example:/config/hardware/component/modem0/> <b>set state admin-state locked</b>
|
||||
admin@example:/config/hardware/component/modem0/> <b>leave</b>
|
||||
</code></pre>
|
||||
|
||||
#### 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.):
|
||||
|
||||
<pre class="cli"><code>admin@example:/config/> <b>edit hardware component modem0 modem</b>
|
||||
admin@example:/config/hardware/component/modem0/modem/> <b>leave</b>
|
||||
</code></pre>
|
||||
|
||||
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:
|
||||
|
||||
<pre class="cli"><code>admin@example:/> <b>configure</b>
|
||||
admin@example:/config/> <b>edit interface wwan0</b>
|
||||
admin@example:/config/interface/wwan0/> <b>set wwan modem modem0</b>
|
||||
admin@example:/config/interface/wwan0/> <b>set wwan sim sim0</b>
|
||||
admin@example:/config/interface/wwan0/> <b>set wwan bearer apn internet</b>
|
||||
admin@example:/config/interface/wwan0/> <b>leave</b>
|
||||
</code></pre>
|
||||
|
||||
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:
|
||||
|
||||
<pre class="cli"><code>admin@example:/> <b>configure</b>
|
||||
admin@example:/config/> <b>edit keystore symmetric-key apn-pass</b>
|
||||
admin@example:/config/keystore/…/apn-pass/> <b>set key-format passphrase-key-format</b>
|
||||
admin@example:/config/keystore/…/apn-pass/> <b>change cleartext-symmetric-key</b>
|
||||
Passphrase: ************
|
||||
Retype passphrase: ************
|
||||
admin@example:/config/keystore/…/apn-pass/> <b>leave</b>
|
||||
</code></pre>
|
||||
|
||||
Then point the bearer's authentication at it:
|
||||
|
||||
<pre class="cli"><code>admin@example:/> <b>configure</b>
|
||||
admin@example:/config/> <b>edit interface wwan0 wwan bearer</b>
|
||||
admin@example:/config/interface/wwan0/wwan/bearer/> <b>set authentication username myuser</b>
|
||||
admin@example:/config/interface/wwan0/wwan/bearer/> <b>set authentication password apn-pass</b>
|
||||
admin@example:/config/interface/wwan0/wwan/bearer/> <b>leave</b>
|
||||
</code></pre>
|
||||
|
||||
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:
|
||||
|
||||
<pre class="cli"><code>admin@example:/config/interface/wwan0/wwan/bearer/> <b>set authentication type pap</b>
|
||||
</code></pre>
|
||||
|
||||
### 5. Configure SIM PIN
|
||||
|
||||
If the SIM requires a PIN to unlock, configure it on the SIM hardware
|
||||
component:
|
||||
|
||||
<pre class="cli"><code>admin@example:/config/> <b>edit hardware component sim0</b>
|
||||
admin@example:/config/hardware/component/sim0/> <b>set sim pin 1234</b>
|
||||
admin@example:/config/hardware/component/sim0/> <b>leave</b>
|
||||
</code></pre>
|
||||
|
||||
### 6. Verify Connectivity
|
||||
|
||||
Once connected, the `wwan0` interface receives an IP address from the
|
||||
carrier and modemd installs the default route:
|
||||
|
||||
<pre class="cli"><code>admin@example:/> <b>show interface wwan0</b>
|
||||
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
|
||||
</code></pre>
|
||||
|
||||
Check the full modem state including signal quality and registration:
|
||||
|
||||
<pre class="cli"><code>admin@example:/> <b>show modem modem0</b>
|
||||
<span class="header">MODEM: modem0 </span>
|
||||
──────────────────────────────────────────────────────────────
|
||||
<span class="title">Hardware Information</span>
|
||||
Manufacturer : Quectel
|
||||
Model : EM06-E
|
||||
Firmware Version : EM06ELAR04A07M4G
|
||||
Serial Number : 352753090141905
|
||||
IMSI : 240021234567890
|
||||
ICCID : 8946020000001234567
|
||||
──────────────────────────────────────────────────────────────
|
||||
<span class="title">Status</span>
|
||||
State : connected
|
||||
Power State : on
|
||||
Signal : Quality: 72% good RSSI: -59 dBm RSRP: -95 dBm RSRQ: -11.0 dB
|
||||
──────────────────────────────────────────────────────────────
|
||||
<span class="title">Cellular</span>
|
||||
Registration : home
|
||||
Service State : attached
|
||||
Operator : Tele2
|
||||
Operator ID : 23002
|
||||
Network Type : lte
|
||||
──────────────────────────────────────────────────────────────
|
||||
<span class="title">SIM Card</span>
|
||||
Name : sim0
|
||||
Slot : 1
|
||||
Lock State : unlocked
|
||||
Operator : Tele2
|
||||
</code></pre>
|
||||
|
||||
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:
|
||||
|
||||
<pre class="cli"><code>admin@example:/config/> <b>edit interface wwan0 wwan bearer</b>
|
||||
admin@example:/config/interface/wwan0/wwan/bearer/> <b>set route-preference 100</b>
|
||||
admin@example:/config/interface/wwan0/wwan/bearer/> <b>leave</b>
|
||||
</code></pre>
|
||||
|
||||
Lower `route-preference` = higher priority.
|
||||
|
||||
## Roaming
|
||||
|
||||
Data roaming is disabled by default. To allow the modem to connect
|
||||
when on a foreign (roaming) network:
|
||||
|
||||
<pre class="cli"><code>admin@example:/config/> <b>edit interface wwan0 wwan bearer</b>
|
||||
admin@example:/config/interface/wwan0/wwan/bearer/> <b>set roaming true</b>
|
||||
admin@example:/config/interface/wwan0/wwan/bearer/> <b>leave</b>
|
||||
</code></pre>
|
||||
|
||||
> [!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:
|
||||
|
||||
<pre class="cli"><code>admin@example:/> <b>modem restart modem0</b>
|
||||
</code></pre>
|
||||
|
||||
### 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:
|
||||
|
||||
<pre class="cli"><code>admin@example:/> <b>modem reset modem0</b>
|
||||
</code></pre>
|
||||
|
||||
> [!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:
|
||||
|
||||
<pre class="cli"><code>admin@example:/> <b>modem sms modem0 +46701234567 "Hello from Infix"</b>
|
||||
</code></pre>
|
||||
|
||||
> [!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 <code>`
|
||||
- `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
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 <<EOF
|
||||
{
|
||||
"name": "$modem",
|
||||
"class": "infix-hardware:modem",
|
||||
"state": {
|
||||
"admin-state": "unlocked"
|
||||
}
|
||||
}
|
||||
EOF
|
||||
}
|
||||
|
||||
first=1
|
||||
cat <<EOF
|
||||
{
|
||||
@@ -81,6 +100,13 @@ for radio in $wifi_radios; do
|
||||
first=0;
|
||||
gen_radio $radio
|
||||
done
|
||||
for modem in $modem_names; do
|
||||
if [ $first -eq 0 ]; then
|
||||
echo -n ','
|
||||
fi
|
||||
first=0;
|
||||
gen_modem "$modem"
|
||||
done
|
||||
cat <<EOF
|
||||
]}
|
||||
}
|
||||
|
||||
+123
@@ -0,0 +1,123 @@
|
||||
#!/bin/sh
|
||||
# Migrate infix-modem:modems config to ietf-hardware/ietf-interfaces.
|
||||
#
|
||||
# Old model (infix-modem:modems/modem[index]):
|
||||
# enabled, sim, pin, puk, carrier, preferred-mode, allowed-mode[],
|
||||
# band[], location{}, bearer[]{apn, ip-type, allow-roaming, username, password}
|
||||
#
|
||||
# New model:
|
||||
# ietf-hardware:hardware/component[modemN] -- class infix-hardware:modem
|
||||
# ietf-hardware:hardware/component[simN] -- class infix-hardware:sim
|
||||
# ietf-interfaces:interfaces/interface[wwanN] -- type infix-if-type:modem
|
||||
#
|
||||
# Dropped (no equivalent in new model):
|
||||
# bearer.apn-type, bearer.firewall-enabled, bearer.dns-enabled, bearer.default-route
|
||||
#
|
||||
# Credentials: plaintext password is moved to the keystore (base64-encoded)
|
||||
# and referenced by name from bearer.authentication.password.
|
||||
|
||||
file=$1
|
||||
temp=${file}.tmp
|
||||
|
||||
jq '
|
||||
if ."infix-modem:modems" then
|
||||
."infix-modem:modems".modem as $modems |
|
||||
|
||||
# Flat list of all bearers across all modems, in order, with a global wwan index.
|
||||
([ $modems[] | . as $m | (.bearer // [])[] | {modem: $m, bearer: .} ]
|
||||
| to_entries
|
||||
| map(.value + {wwan_index: .key})
|
||||
) as $bearer_list |
|
||||
|
||||
# Hardware components for each modem (modemN).
|
||||
($modems | map(. as $m |
|
||||
{
|
||||
"name": ("modem" + ($m.index | tostring)),
|
||||
"class": "infix-hardware:modem",
|
||||
"state": {
|
||||
"admin-state": (if ($m.enabled // false) then "unlocked" else "locked" end)
|
||||
}
|
||||
} + (
|
||||
(if $m["preferred-mode"] then {"preferred-mode": $m["preferred-mode"]} else {} end) +
|
||||
(if (($m["allowed-mode"] // []) | length) > 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"
|
||||
@@ -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
|
||||
|
||||
@@ -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 \
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -0,0 +1,112 @@
|
||||
/* SPDX-License-Identifier: BSD-3-Clause */
|
||||
|
||||
#include <srx/lyx.h>
|
||||
#include <srx/srx_val.h>
|
||||
|
||||
#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/<ifname>.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/<ifname>.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;
|
||||
}
|
||||
@@ -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");
|
||||
|
||||
@@ -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);
|
||||
|
||||
+76
-200
@@ -1,11 +1,5 @@
|
||||
/* SPDX-License-Identifier: BSD-3-Clause */
|
||||
|
||||
#include <assert.h>
|
||||
#include <ctype.h>
|
||||
#include <dirent.h>
|
||||
#include <pwd.h>
|
||||
#include <sys/utsname.h>
|
||||
#include <sys/sysinfo.h>
|
||||
#include <sys/types.h>
|
||||
#include <sys/socket.h>
|
||||
#include <sys/un.h>
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
)
|
||||
|
||||
@@ -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).";
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
infix-hardware.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/<ifname>.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/<ifname>.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/<ifname>.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.";
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
infix-if-modem.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;
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
infix-if-type.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";
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
infix-interfaces.yang
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1 +0,0 @@
|
||||
infix-modem.yang
|
||||
@@ -0,0 +1,5 @@
|
||||
MODULES=(
|
||||
"infix-hardware -e modem"
|
||||
"infix-if-type -e modem"
|
||||
"infix-interfaces -e modem"
|
||||
)
|
||||
@@ -113,6 +113,15 @@
|
||||
<ACTION sym="STRING"/>
|
||||
</PTYPE>
|
||||
|
||||
<PTYPE name="MODEMS">
|
||||
<COMPL>
|
||||
<ACTION sym="script">
|
||||
jq -r '.modem[]? | "modem" + (.index|tostring)' /run/system.json 2>/dev/null
|
||||
</ACTION>
|
||||
</COMPL>
|
||||
<ACTION sym="STRING"/>
|
||||
</PTYPE>
|
||||
|
||||
<PTYPE name="FIREWALL_ZONES">
|
||||
<COMPL>
|
||||
<ACTION sym="firewall_zones@infix"/>
|
||||
@@ -335,6 +344,30 @@ echo "Public: $pub"
|
||||
</COMMAND>
|
||||
</COMMAND>
|
||||
|
||||
<COMMAND name="modem" help="Cellular modem management" mode="switch">
|
||||
<COMMAND name="restart" help="Restart modem connection (disconnect and reconnect bearers)">
|
||||
<PARAM name="ref" ptype="/MODEMS" help="Modem name (e.g. modem0)"/>
|
||||
<ACTION sym="script" in="tty" out="tty" interrupt="true">
|
||||
rpc "/ietf-hardware:hardware/component[name='$KLISH_PARAM_ref']/infix-hardware:restart"
|
||||
</ACTION>
|
||||
</COMMAND>
|
||||
<COMMAND name="reset" help="Factory-reset modem firmware to default state">
|
||||
<PARAM name="ref" ptype="/MODEMS" help="Modem name (e.g. modem0)"/>
|
||||
<ACTION sym="script" in="tty" out="tty" interrupt="true">
|
||||
rpc "/ietf-hardware:hardware/component[name='$KLISH_PARAM_ref']/infix-hardware:reset"
|
||||
</ACTION>
|
||||
</COMMAND>
|
||||
<COMMAND name="sms" help="Send an SMS message via the modem">
|
||||
<PARAM name="ref" ptype="/MODEMS" help="Modem name (e.g. modem0)"/>
|
||||
<PARAM name="number" ptype="/STRING" help="Recipient phone number (e.g. +46701234567)"/>
|
||||
<PARAM name="message" ptype="/STRING" help="SMS message text (max 160 characters)"/>
|
||||
<ACTION sym="script" in="tty" out="tty" interrupt="true">
|
||||
rpc "/ietf-hardware:hardware/component[name='$KLISH_PARAM_ref']/infix-hardware:send-sms" \
|
||||
phone-number "$KLISH_PARAM_number" message-text "$KLISH_PARAM_message"
|
||||
</ACTION>
|
||||
</COMMAND>
|
||||
</COMMAND>
|
||||
|
||||
<COMMAND name="show" help="Show system status and configuration files" mode="switch">
|
||||
|
||||
<COMMAND name="bridge" help="Show bridge (ports/fdb/mdb/vlans)">
|
||||
@@ -573,6 +606,15 @@ echo "Public: $pub"
|
||||
</ACTION>
|
||||
</COMMAND>
|
||||
|
||||
<COMMAND name="modem" help="Show cellular modem status">
|
||||
<SWITCH name="optional" min="0" max="1">
|
||||
<PARAM name="ref" ptype="/MODEMS" help="Modem name (e.g. modem0)" />
|
||||
</SWITCH>
|
||||
<ACTION sym="script" in="tty" out="tty" interrupt="true">
|
||||
show modem "$KLISH_PARAM_ref" |pager
|
||||
</ACTION>
|
||||
</COMMAND>
|
||||
|
||||
<!-- https://www.cisco.com/c/en/us/td/docs/wireless/access_point/mob_exp/83/cmd-ref/me_cr_book/me_ports_and_interfaces_cli.html -->
|
||||
<COMMAND name="interface" help="Show interface info, default an overview of all">
|
||||
<SWITCH name="optional" min="0" max="1">
|
||||
|
||||
+150
-35
@@ -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:
|
||||
|
||||
@@ -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)
|
||||
|
||||
+11
-5
@@ -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)
|
||||
|
||||
|
||||
+35
-13
@@ -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)
|
||||
|
||||
|
||||
+189
-113
@@ -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)
|
||||
|
||||
+32
-7
@@ -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"]:
|
||||
|
||||
@@ -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":
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user