mirror of
https://github.com/kernelkit/infix.git
synced 2026-07-22 17:23:00 +02:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
009b31d465 | ||
|
|
36ef190c3d | ||
|
|
600483c35b | ||
|
|
7b7baa0730 | ||
|
|
f41898d325 | ||
|
|
1168c6c8e2 | ||
|
|
f78f161dbe | ||
|
|
f0dfc6a8de | ||
|
|
ffe5ed1e32 | ||
|
|
314b0b425f | ||
|
|
985b694e51 | ||
|
|
060e0ea284 | ||
|
|
43bf6358a0 |
@@ -87,6 +87,45 @@ Infix ships with the following factory defaults.
|
||||
> are bridged into the LAN as access points. WiFi is not included with the board
|
||||
> and is not part of the factory default configuration.
|
||||
|
||||
## SIM Card Installation
|
||||
|
||||
The BPI-R4 has three SIM holders, one per PCIe slot:
|
||||
|
||||
| Slot | PCIe socket | Typical use |
|
||||
|----------|-------------|--------------------------|
|
||||
| **SIM1** | M.2 Key-B | 4G/5G WWAN modem |
|
||||
| **SIM2** | mPCIe #1 | Cellular or other modem |
|
||||
| **SIM3** | mPCIe #2 | Cellular or other modem |
|
||||
|
||||
All three take **nano-SIM (4FF)** cards.
|
||||
|
||||
> [!NOTE]
|
||||
> The official Banana Pi pages show an older board revision with a sliding
|
||||
> "sled" holder. Newer boards ship with a fixed friction-fit holder instead:
|
||||
> there is no lid to lift and no spring eject. The [closest matching photo on
|
||||
> bananapi.org][15] shows the current mechanism.
|
||||
|
||||
### Inserting a SIM
|
||||
|
||||
1. Orient the card with the gold contacts facing the PCB (printed
|
||||
side up) and the cut corner pointing toward the M.2/mPCIe socket,
|
||||
which on this board is also toward the outer edge. The silkscreen
|
||||
next to each holder shows the same outline.
|
||||
2. Push the SIM straight in until it is fully seated. There is no
|
||||
click and the holder is not spring-loaded, so press firmly until
|
||||
the contacts engage and the rear edge of the card sits flush with
|
||||
the holder.
|
||||
3. A SIM that still protrudes is not fully inserted; push again.
|
||||
|
||||
### Removing a SIM
|
||||
|
||||
The friction grip on these holders is tight. Gently pry the card out
|
||||
with a fingernail or a thin plastic spudger. Avoid metal tweezers
|
||||
near the contacts. Removing the front panel of the metal case is
|
||||
absolutely necessary to access the SIM holder.
|
||||
|
||||
For software configuration once the modem is recognised, see the
|
||||
[Cellular Modem User's Guide][16].
|
||||
|
||||
## Getting Started
|
||||
|
||||
@@ -358,3 +397,6 @@ make aarch64
|
||||
[12]: https://github.com/frank-w/u-boot/releases
|
||||
[13]: https://github.com/frank-w/u-boot/releases/download/CI-BUILD-2026-01-bpi-2026.01-2026-01-15_2013/bpi-r4_spim-nand_bl2.img
|
||||
[14]: https://github.com/frank-w/u-boot/releases/download/CI-BUILD-2026-01-bpi-2026.01-2026-01-15_2013/bpi-r4_spim-nand_fip.bin
|
||||
[15]: https://docs.banana-pi.org/bpi-r4/bpi-r4_4g5g_2.jpg
|
||||
[16]: https://www.kernelkit.org/infix/latest/modem/
|
||||
|
||||
|
||||
+8
-1
@@ -10,6 +10,13 @@
|
||||
"state": {
|
||||
"admin-state": "unlocked"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "USB2",
|
||||
"class": "infix-hardware:usb",
|
||||
"state": {
|
||||
"admin-state": "unlocked"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -366,7 +373,7 @@
|
||||
]
|
||||
},
|
||||
"infix-meta:meta": {
|
||||
"version": "1.7"
|
||||
"version": "1.8"
|
||||
},
|
||||
"infix-services:mdns": {
|
||||
"enabled": true
|
||||
|
||||
+8
-1
@@ -10,6 +10,13 @@
|
||||
"state": {
|
||||
"admin-state": "unlocked"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "USB2",
|
||||
"class": "infix-hardware:usb",
|
||||
"state": {
|
||||
"admin-state": "unlocked"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -358,7 +365,7 @@
|
||||
]
|
||||
},
|
||||
"infix-meta:meta": {
|
||||
"version": "1.7"
|
||||
"version": "1.8"
|
||||
},
|
||||
"infix-services:mdns": {
|
||||
"enabled": true
|
||||
|
||||
@@ -1 +1,5 @@
|
||||
CONFD_TIMEOUT=60
|
||||
#DEBUG=1
|
||||
|
||||
# -v debug
|
||||
CONFD_ARGS=""
|
||||
|
||||
@@ -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" +
|
||||
|
||||
@@ -149,6 +149,7 @@ INFIX_HOME="https://github.com/kernelkit/infix/"
|
||||
INFIX_DOC="https://www.kernelkit.org/infix/"
|
||||
INFIX_SUPPORT="mailto:kernelkit@googlegroups.com"
|
||||
BR2_PACKAGE_FEATURE_GPS=y
|
||||
BR2_PACKAGE_FEATURE_MODEM=y
|
||||
BR2_PACKAGE_FEATURE_WIFI=y
|
||||
BR2_PACKAGE_FEATURE_WIFI_MEDIATEK=y
|
||||
BR2_PACKAGE_FEATURE_WIFI_QUALCOMM=y
|
||||
|
||||
+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
|
||||
|
||||
@@ -2,6 +2,7 @@ menu "Packages"
|
||||
|
||||
comment "Hardware Support"
|
||||
source "$BR2_EXTERNAL_INFIX_PATH/package/feature-gps/Config.in"
|
||||
source "$BR2_EXTERNAL_INFIX_PATH/package/feature-modem/Config.in"
|
||||
source "$BR2_EXTERNAL_INFIX_PATH/package/feature-wifi/Config.in"
|
||||
|
||||
comment "Software Packages"
|
||||
@@ -30,6 +31,7 @@ source "$BR2_EXTERNAL_INFIX_PATH/package/landing/Config.in"
|
||||
source "$BR2_EXTERNAL_INFIX_PATH/package/libsrx/Config.in"
|
||||
source "$BR2_EXTERNAL_INFIX_PATH/package/lowdown/Config.in"
|
||||
source "$BR2_EXTERNAL_INFIX_PATH/package/mcd/Config.in"
|
||||
source "$BR2_EXTERNAL_INFIX_PATH/package/modemd/Config.in"
|
||||
source "$BR2_EXTERNAL_INFIX_PATH/package/mdns-alias/Config.in"
|
||||
source "$BR2_EXTERNAL_INFIX_PATH/package/netbrowse/Config.in"
|
||||
source "$BR2_EXTERNAL_INFIX_PATH/package/onieprom/Config.in"
|
||||
|
||||
@@ -1,11 +1,9 @@
|
||||
#set DEBUG=1
|
||||
|
||||
# Single daemon handles gen-config, datastore init, config load, and plugins
|
||||
# log:prio:daemon.err
|
||||
service log:console env:/etc/default/confd \
|
||||
service log:console env:/etc/default/confd \
|
||||
[S12345] <usr/ixinit> confd -f -v warning \
|
||||
-F /etc/factory-config.cfg \
|
||||
-S /cfg/startup-config.cfg \
|
||||
-E /etc/failure-config.cfg \
|
||||
-t $CONFD_TIMEOUT \
|
||||
-F /etc/factory-config.cfg \
|
||||
-S /cfg/startup-config.cfg \
|
||||
-E /etc/failure-config.cfg \
|
||||
-t $CONFD_TIMEOUT \
|
||||
$CONFD_ARGS \
|
||||
-- Configuration daemon
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
config BR2_PACKAGE_FEATURE_MODEM
|
||||
bool "Feature Modem"
|
||||
select BR2_PACKAGE_MODEMD
|
||||
select BR2_PACKAGE_MODEM_MANAGER
|
||||
select BR2_PACKAGE_MODEM_MANAGER_ATVIADBUS
|
||||
select BR2_PACKAGE_MODEM_MANAGER_LIBMBIM
|
||||
select BR2_PACKAGE_MODEM_MANAGER_LIBQMI
|
||||
select BR2_PACKAGE_MODEM_MANAGER_LIBQRTR
|
||||
help
|
||||
Enables cellular modem support in Infix via ModemManager and
|
||||
the modemd management daemon. Includes drivers for common
|
||||
USB option modems and QMI/MBIM-based devices.
|
||||
|
||||
ATVIADBUS allows raw AT commands over D-Bus (used by modemd to
|
||||
enable GPS NMEA output via vendor-specific AT commands such as
|
||||
AT+QGPS=1) without needing to start ModemManager in --debug
|
||||
mode.
|
||||
|
||||
config BR2_PACKAGE_FEATURE_MODEM_QUALCOMM
|
||||
bool "Qualcomm-based modems (QMI/QRTR/MHI)"
|
||||
depends on BR2_PACKAGE_FEATURE_MODEM
|
||||
help
|
||||
Adds kernel support for Qualcomm-based cellular modems that use
|
||||
the MHI bus and QRTR IPC router (e.g. Sierra Wireless EM7xxx,
|
||||
Quectel EM/RMxxx, Telit LN9xx).
|
||||
@@ -0,0 +1,26 @@
|
||||
################################################################################
|
||||
#
|
||||
# Cellular modem support
|
||||
#
|
||||
################################################################################
|
||||
|
||||
FEATURE_MODEM_PACKAGE_VERSION = 1.0
|
||||
FEATURE_MODEM_PACKAGE_LICENSE = MIT
|
||||
|
||||
define FEATURE_MODEM_LINUX_CONFIG_FIXUPS
|
||||
$(call KCONFIG_ENABLE_OPT,CONFIG_USB_SERIAL)
|
||||
$(call KCONFIG_ENABLE_OPT,CONFIG_USB_SERIAL_WWAN)
|
||||
$(call KCONFIG_ENABLE_OPT,CONFIG_USB_SERIAL_OPTION)
|
||||
$(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)
|
||||
$(call KCONFIG_SET_OPT,CONFIG_MHI_BUS,m)
|
||||
$(call KCONFIG_ENABLE_OPT,CONFIG_QRTR_MHI)
|
||||
)
|
||||
endef
|
||||
|
||||
$(eval $(generic-package))
|
||||
@@ -0,0 +1,6 @@
|
||||
config BR2_PACKAGE_MODEMD
|
||||
bool "modemd"
|
||||
select BR2_PACKAGE_MODEM_MANAGER
|
||||
select BR2_PACKAGE_PYTHON3
|
||||
help
|
||||
Daemon which manages modems.
|
||||
@@ -0,0 +1,2 @@
|
||||
# Locally calculated
|
||||
sha256 25b33026a661c4c550374cfcba6890a4363bf19db0c1c31a6e65b5edd113ecf0 LICENSE
|
||||
@@ -0,0 +1,56 @@
|
||||
################################################################################
|
||||
#
|
||||
# modemd
|
||||
#
|
||||
################################################################################
|
||||
|
||||
MODEMD_VERSION = 1.0
|
||||
MODEMD_SITE_METHOD = local
|
||||
MODEMD_SITE = $(BR2_EXTERNAL_INFIX_PATH)/src/modemd
|
||||
MODEMD_LICENSE = BSD-3-Clause
|
||||
MODEMD_LICENSE_FILES = LICENSE
|
||||
MODEMD_REDISTRIBUTE = NO
|
||||
MODEMD_DEPENDENCIES = modem-manager jansson python3 \
|
||||
host-python3 host-python-pypa-build host-python-installer \
|
||||
host-python-poetry-core
|
||||
|
||||
define MODEMD_BUILD_CMDS
|
||||
$(TARGET_CC) $(TARGET_CFLAGS) $(TARGET_LDFLAGS) \
|
||||
$(MODEMD_DIR)/modem-command.c -o $(MODEMD_DIR)/modem-command -ljansson
|
||||
endef
|
||||
|
||||
define MODEMD_BUILD_PYTHON
|
||||
cd $(MODEMD_SITE) && \
|
||||
$(PKG_PYTHON_PEP517_ENV) $(HOST_DIR)/bin/python3 $(PKG_PYTHON_PEP517_BUILD_CMD) -o $(@D)/dist
|
||||
mkdir -p $(TARGET_DIR)/usr/libexec/modemd
|
||||
rm -f $(TARGET_DIR)/usr/libexec/modemd/modemd \
|
||||
$(TARGET_DIR)/usr/libexec/modemd/modem-* \
|
||||
$(TARGET_DIR)/usr/libexec/modemd/sim-setup
|
||||
cd $(@D) && \
|
||||
$(HOST_DIR)/bin/python3 $(TOPDIR)/support/scripts/pyinstaller.py \
|
||||
dist/*.whl \
|
||||
--interpreter=/usr/bin/python3 \
|
||||
--script-kind=posix \
|
||||
--purelib=$(TARGET_DIR)/usr/lib/python$(PYTHON3_VERSION_MAJOR)/site-packages \
|
||||
--headers=$(TARGET_DIR)/usr/include/python$(PYTHON3_VERSION_MAJOR) \
|
||||
--scripts=$(TARGET_DIR)/usr/libexec/modemd \
|
||||
--data=$(TARGET_DIR)
|
||||
endef
|
||||
MODEMD_POST_INSTALL_TARGET_HOOKS += MODEMD_BUILD_PYTHON
|
||||
|
||||
define MODEMD_INSTALL_TARGET_CMDS
|
||||
mkdir -p $(TARGET_DIR)/usr/libexec/modemd
|
||||
mkdir -p $(TARGET_DIR)/lib/udev/rules.d
|
||||
mkdir -p $(FINIT_D)/available/
|
||||
mkdir -p $(TARGET_DIR)/sbin
|
||||
$(INSTALL) -D -m 0644 $(MODEMD_DIR)/finit.conf $(FINIT_D)/available/modemd.conf
|
||||
install -m 644 $(MODEMD_DIR)/modemd.rules $(TARGET_DIR)/lib/udev/rules.d/90-modemd.rules
|
||||
install -m 644 $(MODEMD_DIR)/qmi-wwan-ids.rules $(TARGET_DIR)/lib/udev/rules.d/91-qmi-wwan-ids.rules
|
||||
install -m 644 $(MODEMD_DIR)/77-mm-dell-port-types.rules $(TARGET_DIR)/etc/udev/rules.d/77-mm-dell-port-types.rules
|
||||
install -m 644 $(MODEMD_DIR)/77-mm-modem-gps.rules $(TARGET_DIR)/etc/udev/rules.d/77-mm-modem-gps.rules
|
||||
install -D -m 644 $(MODEMD_DIR)/modemd.modules-load $(TARGET_DIR)/etc/modules-load.d/modemd.conf
|
||||
install -m 755 $(MODEMD_DIR)/modem-command $(TARGET_DIR)/sbin/modem-command
|
||||
ln -sf /usr/libexec/modemd/modemd $(TARGET_DIR)/sbin/modemd
|
||||
endef
|
||||
|
||||
$(eval $(generic-package))
|
||||
@@ -0,0 +1 @@
|
||||
service [2345789] ModemManager -- ModemManager daemon
|
||||
@@ -0,0 +1,21 @@
|
||||
diff --git a/src/qmi-firmware-update/qfu-helpers-udev.c b/src/qmi-firmware-update/qfu-helpers-udev.c
|
||||
index bda9106..40d648f 100644
|
||||
--- a/src/qmi-firmware-update/qfu-helpers-udev.c
|
||||
+++ b/src/qmi-firmware-update/qfu-helpers-udev.c
|
||||
@@ -364,8 +364,14 @@ device_matches (GUdevDevice *device,
|
||||
if (!device_sysfs_path)
|
||||
goto out;
|
||||
|
||||
- if (g_strcmp0 (device_sysfs_path, sysfs_path) != 0)
|
||||
- goto out;
|
||||
+ /*
|
||||
+ * don't compare sysfs path for download mode as it
|
||||
+ * changes from USB4 to USB3 and thus may use a different host controller
|
||||
+ */
|
||||
+ if (mode != QFU_HELPERS_DEVICE_MODE_DOWNLOAD) {
|
||||
+ if (g_strcmp0 (device_sysfs_path, sysfs_path) != 0)
|
||||
+ goto out;
|
||||
+ }
|
||||
|
||||
if (device_mode != mode)
|
||||
return NULL;
|
||||
@@ -0,0 +1,13 @@
|
||||
diff --git a/meson.build b/meson.build
|
||||
index 8cddf77..e97f862 100644
|
||||
--- a/meson.build
|
||||
+++ b/meson.build
|
||||
@@ -188,7 +188,7 @@ endif
|
||||
config_h.set('WITH_UDEV', enable_udev)
|
||||
|
||||
# udev base directory (required to install rules even when udev support is disabled)
|
||||
-udev_udevdir = get_option('udevdir')
|
||||
+udev_udevdir = '/lib/udev'
|
||||
if udev_udevdir == ''
|
||||
assert(enable_udev, 'udevdir must be explicitly given if udev support is disabled')
|
||||
udev_udevdir = dependency('udev').get_pkgconfig_variable('udevdir')
|
||||
@@ -0,0 +1,17 @@
|
||||
diff --git a/src/mm-bearer-qmi.c b/src/mm-bearer-qmi.c
|
||||
index 54f2e93..3c4df7b 100644
|
||||
--- a/src/mm-bearer-qmi.c
|
||||
+++ b/src/mm-bearer-qmi.c
|
||||
@@ -2494,7 +2494,11 @@ load_settings_from_bearer (MMBearerQmi *self,
|
||||
}
|
||||
|
||||
/* The link prefix hint given must be modem-specific */
|
||||
- ctx->link_prefix_hint = g_strdup_printf ("qmapmux%u.", mm_base_modem_get_dbus_id (MM_BASE_MODEM (modem)));
|
||||
+ const gchar *name = mm_port_get_device (MM_PORT (ctx->data));
|
||||
+ if (name)
|
||||
+ ctx->link_prefix_hint = g_strdup_printf ("%s.", name);
|
||||
+ else
|
||||
+ ctx->link_prefix_hint = g_strdup_printf ("qmapmux%u.", mm_base_modem_get_dbus_id (MM_BASE_MODEM (modem)));
|
||||
}
|
||||
|
||||
/* If profile id is given, we'll launch the connection specifying the profile id in use
|
||||
@@ -0,0 +1,29 @@
|
||||
When a udev rule explicitly sets ID_MM_PORT_TYPE_MBIM=1 for a port,
|
||||
treat it as definitive and skip the active MBIM open/probe. This
|
||||
mirrors the way ID_MM_PORT_TYPE_GPS is handled (set via is_gps, a
|
||||
direct result field rather than a "maybe" hint).
|
||||
|
||||
The active MBIM probe attempts to open the WDM device and send an
|
||||
MBIM OPEN message. Some modems (e.g. Dell DW5811e / Sierra Wireless
|
||||
EM7455 in USB composition 8) persistently send QMI/QMUX-framed
|
||||
notifications on the MBIM control channel before ever responding to
|
||||
MBIM OPEN, causing the probe to time out (~50 s). The probe failure
|
||||
causes mm_port_probe_list_has_mbim_port() to return FALSE, so the
|
||||
Dell plugin creates a non-MBIM modem object that cannot handle the
|
||||
usbmisc/MBIM port, resulting in "unhandled port type".
|
||||
|
||||
Since the udev rule already encodes the vendor/product/interface
|
||||
specificity required to identify the port, the active probe adds no
|
||||
information in this case.
|
||||
|
||||
diff --git a/src/mm-port-probe.c b/src/mm-port-probe.c
|
||||
--- a/src/mm-port-probe.c
|
||||
+++ b/src/mm-port-probe.c
|
||||
@@ -1529,6 +1529,7 @@ mm_port_probe_run (MMPortProbe *self,
|
||||
/* If this is a port flagged as being a MBIM port, don't do any other probing */
|
||||
if (self->priv->maybe_mbim) {
|
||||
mm_obj_dbg (self, "no AT/QCDM/QMI probing in possible MBIM port");
|
||||
+ mm_port_probe_set_result_mbim (self, TRUE);
|
||||
mm_port_probe_set_result_at (self, FALSE);
|
||||
mm_port_probe_set_result_qcdm (self, FALSE);
|
||||
mm_port_probe_set_result_qmi (self, FALSE);
|
||||
@@ -81,6 +81,30 @@ def hardware(args: List[str]) -> None:
|
||||
cli_pretty(data, "show-hardware")
|
||||
|
||||
|
||||
def modem(args: List[str]) -> None:
|
||||
ref = args[0] if args else None
|
||||
|
||||
data = get_json("/ietf-hardware:hardware")
|
||||
if not data:
|
||||
print("No modem data available.")
|
||||
return
|
||||
|
||||
# Merge bearer-state from wwan interfaces (lives under ietf-interfaces).
|
||||
# Quiet on miss — a system without wwan interfaces still shows modems.
|
||||
iface_data = get_json("/ietf-interfaces:interfaces", quiet=True)
|
||||
if iface_data:
|
||||
data.update(iface_data)
|
||||
|
||||
if RAW_OUTPUT:
|
||||
print(json.dumps(data, indent=2))
|
||||
return
|
||||
|
||||
if ref:
|
||||
cli_pretty(data, "show-modem-detail", ref)
|
||||
else:
|
||||
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)
|
||||
@@ -558,8 +582,9 @@ def system(args: List[str]) -> None:
|
||||
runtime = {}
|
||||
|
||||
# Extract CPU temperature and fan speed from hardware components
|
||||
cpu_temp = None
|
||||
cpu_temps = []
|
||||
fan_rpm = None
|
||||
_cpu_prefixes = ("cpu", "soc", "core")
|
||||
if hardware_data and "ietf-hardware:hardware" in hardware_data:
|
||||
components = hardware_data.get("ietf-hardware:hardware", {}).get("component", [])
|
||||
for component in components:
|
||||
@@ -570,18 +595,16 @@ def system(args: List[str]) -> None:
|
||||
name = component.get("name", "")
|
||||
value_type = sensor_data.get("value-type")
|
||||
|
||||
# Only capture CPU/SoC temperature (ignore phy, sfp, etc.)
|
||||
# Different platforms use different names: cpu, soc, core, etc.
|
||||
if value_type == "celsius" and name in ("cpu", "soc", "core") and cpu_temp is None:
|
||||
temp_millidegrees = sensor_data.get("value", 0)
|
||||
cpu_temp = temp_millidegrees / 1000.0
|
||||
# Capture all CPU/SoC temperatures (handles cpu, cpu1, soc, core0, etc.)
|
||||
if value_type == "celsius" and any(name == p or name.startswith(p) for p in _cpu_prefixes):
|
||||
cpu_temps.append(sensor_data.get("value", 0) / 1000.0)
|
||||
|
||||
# Capture fan speed if available
|
||||
elif value_type == "rpm" and fan_rpm is None:
|
||||
fan_rpm = sensor_data.get("value", 0)
|
||||
|
||||
if cpu_temp is not None:
|
||||
runtime["cpu_temp"] = cpu_temp
|
||||
if cpu_temps:
|
||||
runtime["cpu_temp"] = max(cpu_temps)
|
||||
if fan_rpm is not None:
|
||||
runtime["fan_rpm"] = fan_rpm
|
||||
|
||||
@@ -745,6 +768,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,8 @@ confd_plugin_la_SOURCES = \
|
||||
if-vxlan.c \
|
||||
if-wifi.c \
|
||||
if-wireguard.c \
|
||||
if-modem.c \
|
||||
modem.c \
|
||||
keystore.c \
|
||||
system.c \
|
||||
ntp.c \
|
||||
|
||||
@@ -858,7 +858,9 @@ int sr_plugin_init_cb(sr_session_ctx_t *session, void **priv)
|
||||
rc = ntp_candidate_init(&confd);
|
||||
if (rc)
|
||||
goto err;
|
||||
/* YOUR_INIT GOES HERE */
|
||||
rc = modem_init(&confd);
|
||||
if (rc)
|
||||
goto err;
|
||||
|
||||
return SR_ERR_OK;
|
||||
err:
|
||||
|
||||
@@ -133,6 +133,10 @@ typedef enum {
|
||||
if ((rc = register_rpc(s, x, c, a, u))) \
|
||||
goto fail
|
||||
|
||||
#define REGISTER_NOTIF(s,m,x,c,a,u) \
|
||||
if ((rc = register_notif(s, m, x, c, a, u))) \
|
||||
goto fail
|
||||
|
||||
struct confd {
|
||||
sr_session_ctx_t *session; /* running datastore */
|
||||
sr_session_ctx_t *startup; /* startup datastore */
|
||||
@@ -192,6 +196,17 @@ static inline int register_rpc(sr_session_ctx_t *session, const char *xpath,
|
||||
return rc;
|
||||
}
|
||||
|
||||
static inline int register_notif(sr_session_ctx_t *session, const char *module, const char *xpath,
|
||||
sr_event_notif_cb cb, void *arg, sr_subscription_ctx_t **sub)
|
||||
{
|
||||
int rc = sr_notif_subscribe(session, module, xpath,
|
||||
NULL, NULL, /* forever */
|
||||
cb, arg, SR_SUBSCR_NO_THREAD, sub);
|
||||
if (rc)
|
||||
ERROR("failed subscribing to %s notification: %s", xpath, sr_strerror(rc));
|
||||
return rc;
|
||||
}
|
||||
|
||||
|
||||
/* core.c */
|
||||
int finit_enable(const char *svc);
|
||||
@@ -274,4 +289,6 @@ int ntp_candidate_init(struct confd *confd);
|
||||
/* ptp.c */
|
||||
int ptp_change(sr_session_ctx_t *session, struct lyd_node *config, struct lyd_node *diff, sr_event_t event, struct confd *confd);
|
||||
|
||||
int modem_init(struct confd *confd);
|
||||
|
||||
#endif /* CONFD_CORE_H_ */
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -80,6 +80,8 @@ static int ifchange_cand_infer_type(sr_session_ctx_t *session, const char *path)
|
||||
|
||||
if (!fnmatch("wifi+([0-9])*", ifname, FNM_EXTMATCH))
|
||||
inferred.data.string_val = "infix-if-type:wifi";
|
||||
else if (!fnmatch("wwan+([0-9-])", ifname, FNM_EXTMATCH))
|
||||
inferred.data.string_val = "infix-if-type:modem";
|
||||
else if (iface_is_phys(ifname))
|
||||
inferred.data.string_val = "infix-if-type:ethernet";
|
||||
else if (!fnmatch("br+([0-9])", ifname, FNM_EXTMATCH))
|
||||
@@ -390,6 +392,8 @@ static int netdag_gen_afspec_add(sr_session_ctx_t *session, struct dagger *net,
|
||||
return vlan_gen(NULL, cif, ip);
|
||||
case IFT_VXLAN:
|
||||
return vxlan_gen(NULL, cif, ip);
|
||||
case IFT_MODEM:
|
||||
return modem_add_iface(cif, net);
|
||||
case IFT_WIFI:
|
||||
return wifi_validate_secret(session, cif)
|
||||
? : wifi_add_iface(cif, net);
|
||||
@@ -434,6 +438,7 @@ static int netdag_gen_afspec_set(sr_session_ctx_t *session, struct dagger *net,
|
||||
case IFT_DUMMY:
|
||||
case IFT_GRE:
|
||||
case IFT_GRETAP:
|
||||
case IFT_MODEM:
|
||||
case IFT_VETH:
|
||||
case IFT_VXLAN:
|
||||
case IFT_WIREGUARD:
|
||||
@@ -474,6 +479,8 @@ static bool netdag_must_del(struct lyd_node *dif, struct lyd_node *cif)
|
||||
return lydx_get_descendant(lyd_child(dif), "veth", NULL);
|
||||
case IFT_VXLAN:
|
||||
return lydx_get_descendant(lyd_child(dif), "vxlan", NULL);
|
||||
case IFT_MODEM:
|
||||
return false;
|
||||
case IFT_WIREGUARD:
|
||||
return lydx_get_descendant(lyd_child(dif), "wireguard", NULL);
|
||||
case IFT_UNKNOWN:
|
||||
@@ -551,6 +558,9 @@ static int netdag_gen_iface_del(struct dagger *net, struct lyd_node *dif,
|
||||
case IFT_VETH:
|
||||
veth_gen_del(dif, ip);
|
||||
break;
|
||||
case IFT_MODEM:
|
||||
modem_del_iface(dif, net);
|
||||
break;
|
||||
case IFT_WIFI:
|
||||
wifi_del_iface(dif, net);
|
||||
break;
|
||||
@@ -693,8 +703,9 @@ static sr_error_t netdag_gen_iface(sr_session_ctx_t *session, struct dagger *net
|
||||
fprintf(ip, "link set alias \"%s\" dev %s\n", attr ?: "", ifname);
|
||||
|
||||
/* Bring interface back up, if enabled */
|
||||
if (lydx_is_enabled(cif, "enabled"))
|
||||
fprintf(ip, "link set dev %s up state up\n", ifname);
|
||||
if (strcmp(iftype, "infix-if-type:modem") != 0)
|
||||
if (lydx_is_enabled(cif, "enabled"))
|
||||
fprintf(ip, "link set dev %s up state up\n", ifname);
|
||||
|
||||
err = err ? : netdag_gen_sysctl(net, cif, dif);
|
||||
|
||||
@@ -724,6 +735,7 @@ static int netdag_init_iface(struct lyd_node *cif)
|
||||
return vlan_add_deps(cif);
|
||||
case IFT_VETH:
|
||||
return veth_add_deps(cif);
|
||||
case IFT_MODEM:
|
||||
case IFT_WIFI:
|
||||
case IFT_DUMMY:
|
||||
case IFT_ETH:
|
||||
|
||||
@@ -29,6 +29,7 @@
|
||||
_map(IFT_GRETAP, "infix-if-type:gretap") \
|
||||
_map(IFT_LAG, "infix-if-type:lag") \
|
||||
_map(IFT_LO, "infix-if-type:loopback") \
|
||||
_map(IFT_MODEM, "infix-if-type:modem") \
|
||||
_map(IFT_VETH, "infix-if-type:veth") \
|
||||
_map(IFT_VLAN, "infix-if-type:vlan") \
|
||||
_map(IFT_VXLAN, "infix-if-type:vxlan") \
|
||||
@@ -167,6 +168,10 @@ 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);
|
||||
|
||||
/* 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);
|
||||
int wireguard_gen(struct lyd_node *dif, struct lyd_node *cif, FILE *ip, struct dagger *net);
|
||||
|
||||
@@ -656,9 +656,10 @@ int main(int argc, char **argv)
|
||||
log_level = LOG_WARNING;
|
||||
else if (!strcmp(optarg, "info"))
|
||||
log_level = LOG_NOTICE;
|
||||
else if (!strcmp(optarg, "debug"))
|
||||
else if (!strcmp(optarg, "debug")) {
|
||||
log_level = LOG_DEBUG;
|
||||
else {
|
||||
debug = 1;
|
||||
} else {
|
||||
fprintf(stderr, "confd error: Invalid verbosity \"%s\"\n", optarg);
|
||||
return EXIT_FAILURE;
|
||||
}
|
||||
@@ -696,6 +697,7 @@ int main(int argc, char **argv)
|
||||
|
||||
if (getenv("DEBUG")) {
|
||||
log_opts |= LOG_PERROR;
|
||||
log_level = LOG_DEBUG;
|
||||
debug = 1;
|
||||
}
|
||||
openlog("confd", log_opts, LOG_DAEMON);
|
||||
|
||||
@@ -0,0 +1,206 @@
|
||||
/* SPDX-License-Identifier: BSD-3-Clause */
|
||||
|
||||
#include <sys/types.h>
|
||||
#include <sys/socket.h>
|
||||
#include <sys/un.h>
|
||||
#include <regex.h>
|
||||
|
||||
#include <srx/common.h>
|
||||
#include <srx/lyx.h>
|
||||
#include <srx/srx_val.h>
|
||||
|
||||
#include "core.h"
|
||||
|
||||
#define RUN_DIR "/run/modemd"
|
||||
#define SOCK RUN_DIR "/modemd.sock"
|
||||
|
||||
#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 component_index(const char *xpath)
|
||||
{
|
||||
regmatch_t pmatch[2];
|
||||
regex_t regex;
|
||||
char name[32];
|
||||
int index = -1;
|
||||
int len;
|
||||
|
||||
if (regcomp(®ex, "name='([^']*)'", REG_EXTENDED))
|
||||
return -1;
|
||||
|
||||
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);
|
||||
|
||||
return index;
|
||||
}
|
||||
|
||||
static int infix_modem_rpcsend(char *msg, int len)
|
||||
{
|
||||
struct sockaddr_un addr;
|
||||
struct timeval tv;
|
||||
fd_set wfds;
|
||||
int sock, ret = -1;
|
||||
|
||||
sock = socket(AF_UNIX, SOCK_STREAM, 0);
|
||||
if (sock < 0)
|
||||
return -1;
|
||||
|
||||
addr.sun_family = AF_UNIX;
|
||||
strcpy(addr.sun_path, SOCK);
|
||||
|
||||
if (connect(sock, &addr, sizeof(addr)) == 0) {
|
||||
tv.tv_sec = 5;
|
||||
tv.tv_usec = 0;
|
||||
FD_ZERO(&wfds);
|
||||
FD_SET(sock, &wfds);
|
||||
|
||||
if (select(sock + 1, NULL, &wfds, NULL, &tv) > 0) {
|
||||
if (write(sock, msg, len) == len)
|
||||
ret = 0;
|
||||
}
|
||||
}
|
||||
close(sock);
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
static int infix_modem_rpc(const char *rpc, const char *data)
|
||||
{
|
||||
char msg[1024];
|
||||
int len;
|
||||
|
||||
NOTE("Sending rpc %s to modemd", rpc);
|
||||
|
||||
len = snprintf(msg, sizeof(msg),
|
||||
"{ \"rpc\" : \"%s\", \"data\" : %s }",
|
||||
rpc, data ? data : "null");
|
||||
|
||||
if (infix_modem_rpcsend(msg, len) < 0) {
|
||||
ERROR("Unable to send rpc");
|
||||
return SR_ERR_INTERNAL;
|
||||
}
|
||||
|
||||
return SR_ERR_OK;
|
||||
}
|
||||
|
||||
static int infix_modem_sendsms(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];
|
||||
int index;
|
||||
|
||||
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\" : %d, \"number\" : \"%s\", \"text\" : \"%s\" }",
|
||||
index,
|
||||
input[0].data.string_val,
|
||||
input[1].data.string_val);
|
||||
|
||||
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[64];
|
||||
int index;
|
||||
|
||||
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), "{ \"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[64];
|
||||
int index;
|
||||
|
||||
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), "{ \"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)
|
||||
{
|
||||
int index;
|
||||
|
||||
index = component_index(xpath);
|
||||
if (index < 0) {
|
||||
ERROR("Cannot parse modem index from xpath: %s", xpath);
|
||||
return;
|
||||
}
|
||||
if (values_cnt < 1) {
|
||||
ERROR("No values in status-update notification");
|
||||
return;
|
||||
}
|
||||
|
||||
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;
|
||||
|
||||
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;
|
||||
}
|
||||
@@ -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-05-10.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"
|
||||
|
||||
@@ -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,31 @@ module infix-hardware {
|
||||
contact "kernelkit@googlegroups.com";
|
||||
description "Vital Product Data augmentation of ieee-hardware and deviations.";
|
||||
|
||||
revision 2026-05-10 {
|
||||
description "Add modem-state/last-change and modem-state/location-state operational
|
||||
nodes so cellular modem location and state freshness are accessible over
|
||||
NETCONF/RESTCONF, not only via the CLI.";
|
||||
reference "internal";
|
||||
}
|
||||
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 +85,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 +164,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 +485,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 +952,424 @@ 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.";
|
||||
}
|
||||
}
|
||||
|
||||
container location-state {
|
||||
description
|
||||
"Location data observed by the modem. Coordinates come from the
|
||||
modem's GPS subsystem (or A-GPS); cell-id/lac/tac/mcc/mnc come
|
||||
from 3GPP signalling. Source indicates which mechanism produced
|
||||
the current fix. Empty when no source is enabled or no fix is
|
||||
available.";
|
||||
|
||||
leaf source {
|
||||
type modem-location-source;
|
||||
description "Source that produced the current fix.";
|
||||
}
|
||||
|
||||
leaf latitude {
|
||||
type decimal64 { fraction-digits 6; }
|
||||
units "degrees";
|
||||
description "Latitude in decimal degrees, negative = south.";
|
||||
}
|
||||
|
||||
leaf longitude {
|
||||
type decimal64 { fraction-digits 6; }
|
||||
units "degrees";
|
||||
description "Longitude in decimal degrees, negative = west.";
|
||||
}
|
||||
|
||||
leaf altitude {
|
||||
type decimal64 { fraction-digits 1; }
|
||||
units "meters";
|
||||
description "Altitude above mean sea level.";
|
||||
}
|
||||
|
||||
leaf cell-id {
|
||||
type uint32;
|
||||
description "Serving 3GPP cell identifier.";
|
||||
}
|
||||
|
||||
leaf lac {
|
||||
type uint16;
|
||||
description "Location Area Code (2G/3G).";
|
||||
}
|
||||
|
||||
leaf tac {
|
||||
type uint16;
|
||||
description "Tracking Area Code (LTE/5G).";
|
||||
}
|
||||
|
||||
leaf mcc {
|
||||
type string { pattern '[0-9]{3}'; }
|
||||
description "Mobile Country Code.";
|
||||
}
|
||||
|
||||
leaf mnc {
|
||||
type string { pattern '[0-9]{2,3}'; }
|
||||
description "Mobile Network Code.";
|
||||
}
|
||||
|
||||
leaf last-change {
|
||||
type yang:date-and-time;
|
||||
description "Timestamp when location-state was last refreshed.";
|
||||
}
|
||||
}
|
||||
|
||||
leaf last-change {
|
||||
type yang:date-and-time;
|
||||
description
|
||||
"Timestamp of the most recent change to any leaf in modem-state.
|
||||
Consumers can use this to gauge data freshness independent of
|
||||
individual leaf timestamps.";
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* 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 +1492,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 @@
|
||||
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
|
||||
@@ -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">
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
# do not edit this file, it will be overwritten on update
|
||||
|
||||
ACTION!="add|change|move|bind", GOTO="mm_dell_port_types_end"
|
||||
|
||||
SUBSYSTEMS=="usb", ATTRS{idVendor}=="413c", GOTO="mm_dell_vendorcheck"
|
||||
GOTO="mm_dell_port_types_end"
|
||||
|
||||
LABEL="mm_dell_vendorcheck"
|
||||
SUBSYSTEMS=="usb", ATTRS{bInterfaceNumber}=="?*", ENV{.MM_USBIFNUM}="$attr{bInterfaceNumber}"
|
||||
|
||||
# Dell DW5821e (default 0x81d7, with esim support 0x81e0)
|
||||
# if 02: primary port
|
||||
# if 03: secondary port
|
||||
# if 04: raw NMEA port
|
||||
# if 05: diag/qcdm port
|
||||
ATTRS{idVendor}=="413c", ATTRS{idProduct}=="81d7", ENV{.MM_USBIFNUM}=="02", SUBSYSTEM=="tty", ENV{ID_MM_PORT_TYPE_AT_PRIMARY}="1"
|
||||
ATTRS{idVendor}=="413c", ATTRS{idProduct}=="81d7", ENV{.MM_USBIFNUM}=="03", SUBSYSTEM=="tty", ENV{ID_MM_PORT_TYPE_AT_SECONDARY}="1"
|
||||
ATTRS{idVendor}=="413c", ATTRS{idProduct}=="81d7", ENV{.MM_USBIFNUM}=="04", SUBSYSTEM=="tty", ENV{ID_MM_PORT_TYPE_GPS}="1"
|
||||
ATTRS{idVendor}=="413c", ATTRS{idProduct}=="81d7", ENV{.MM_USBIFNUM}=="05", SUBSYSTEM=="tty", ENV{ID_MM_PORT_TYPE_QCDM}="1"
|
||||
ATTRS{idVendor}=="413c", ATTRS{idProduct}=="81e0", ENV{.MM_USBIFNUM}=="02", SUBSYSTEM=="tty", ENV{ID_MM_PORT_TYPE_AT_PRIMARY}="1"
|
||||
ATTRS{idVendor}=="413c", ATTRS{idProduct}=="81e0", ENV{.MM_USBIFNUM}=="03", SUBSYSTEM=="tty", ENV{ID_MM_PORT_TYPE_AT_SECONDARY}="1"
|
||||
ATTRS{idVendor}=="413c", ATTRS{idProduct}=="81e0", ENV{.MM_USBIFNUM}=="04", SUBSYSTEM=="tty", ENV{ID_MM_PORT_TYPE_GPS}="1"
|
||||
ATTRS{idVendor}=="413c", ATTRS{idProduct}=="81e0", ENV{.MM_USBIFNUM}=="05", SUBSYSTEM=="tty", ENV{ID_MM_PORT_TYPE_QCDM}="1"
|
||||
|
||||
# Dell DW5820e
|
||||
# if 02: AT port
|
||||
# if 04: debug port (ignore)
|
||||
# if 06: AT port
|
||||
ATTRS{idVendor}=="413c", ATTRS{idProduct}=="81d9", ENV{.MM_USBIFNUM}=="04", ENV{ID_MM_PORT_IGNORE}="1"
|
||||
|
||||
# Dell DW5811e (Sierra Wireless EM7455 rebadge)
|
||||
# USB composition 8: MBIM-only (bInterfaceClass=02 bInterfaceSubClass=0e)
|
||||
# Bound by cdc_mbim driver; no AT ports in this composition.
|
||||
# if 0c: MBIM control port
|
||||
# if 0d: data port (handled by cdc_mbim / cdc_ncm driver)
|
||||
ATTRS{idVendor}=="413c", ATTRS{idProduct}=="81b6", ENV{.MM_USBIFNUM}=="0c", SUBSYSTEM=="usbmisc", ENV{ID_MM_PORT_TYPE_MBIM}="1"
|
||||
|
||||
GOTO="mm_dell_port_types_end"
|
||||
LABEL="mm_dell_port_types_end"
|
||||
@@ -0,0 +1,41 @@
|
||||
# Cellular modem NMEA port routing
|
||||
#
|
||||
# For modems whose GPS NMEA stream is available on a dedicated USB
|
||||
# interface, expose that TTY as /dev/gpsN so the existing gpsd → chronyd
|
||||
# pipeline can consume it like any other NMEA USB GPS receiver. Setting
|
||||
# ID_MM_PORT_IGNORE=1 keeps ModemManager from claiming the NMEA port for
|
||||
# itself; modemd issues the vendor-specific AT command to enable GPS
|
||||
# output (see prepare_location() in modemd).
|
||||
#
|
||||
# When adding a new modem entry, find the NMEA interface number with
|
||||
# 'mmcli -m N' or 'lsusb -v' and add a rule below.
|
||||
|
||||
ACTION!="add|change|move|bind", GOTO="modem_gps_end"
|
||||
|
||||
# Note: bInterfaceNumber lives on the USB interface device while
|
||||
# idVendor/idProduct live on the USB device — udev's ATTRS{} requires
|
||||
# all matches in one rule to share a parent, so we match the interface
|
||||
# number via ENV{ID_USB_INTERFACE_NUM}, which udev populates from
|
||||
# bInterfaceNumber and which has no same-parent constraint.
|
||||
|
||||
# Quectel EM05 — NMEA on interface 1
|
||||
SUBSYSTEM=="tty", ATTRS{idVendor}=="2c7c", ATTRS{idProduct}=="0125", \
|
||||
ENV{ID_USB_INTERFACE_NUM}=="01", \
|
||||
ENV{ID_MM_PORT_IGNORE}="1", SYMLINK+="gps%n"
|
||||
|
||||
# Quectel EM06-E / EM06-A — NMEA on interface 1
|
||||
SUBSYSTEM=="tty", ATTRS{idVendor}=="2c7c", ATTRS{idProduct}=="0306", \
|
||||
ENV{ID_USB_INTERFACE_NUM}=="01", \
|
||||
ENV{ID_MM_PORT_IGNORE}="1", SYMLINK+="gps%n"
|
||||
|
||||
# Quectel EM12-G — NMEA on interface 1
|
||||
SUBSYSTEM=="tty", ATTRS{idVendor}=="2c7c", ATTRS{idProduct}=="0512", \
|
||||
ENV{ID_USB_INTERFACE_NUM}=="01", \
|
||||
ENV{ID_MM_PORT_IGNORE}="1", SYMLINK+="gps%n"
|
||||
|
||||
# Sierra Wireless EM7565 — NMEA on interface 2
|
||||
SUBSYSTEM=="tty", ATTRS{idVendor}=="1199", ATTRS{idProduct}=="9091", \
|
||||
ENV{ID_USB_INTERFACE_NUM}=="02", \
|
||||
ENV{ID_MM_PORT_IGNORE}="1", SYMLINK+="gps%n"
|
||||
|
||||
LABEL="modem_gps_end"
|
||||
@@ -0,0 +1,27 @@
|
||||
Copyright (c) 2024 Avvero Pty Ltd
|
||||
All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions are met:
|
||||
|
||||
* Redistributions of source code must retain the above copyright notice, this
|
||||
list of conditions and the following disclaimer.
|
||||
|
||||
* Redistributions in binary form must reproduce the above copyright notice,
|
||||
this list of conditions and the following disclaimer in the documentation
|
||||
and/or other materials provided with the distribution.
|
||||
|
||||
* Neither the name of copyright holders nor the names of
|
||||
contributors may be used to endorse or promote products derived from
|
||||
this software without specific prior written permission.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
||||
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
||||
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
||||
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
||||
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
||||
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
@@ -0,0 +1 @@
|
||||
service [2345] name:modemd /sbin/modemd -- Modem daemon
|
||||
@@ -0,0 +1,617 @@
|
||||
/* SPDX-License-Identifier: BSD-3-Clause */
|
||||
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <stdarg.h>
|
||||
#include <string.h>
|
||||
#include <unistd.h>
|
||||
#include <termios.h>
|
||||
#include <getopt.h>
|
||||
#include <fcntl.h>
|
||||
#include <syslog.h>
|
||||
#include <sys/stat.h>
|
||||
#include <sys/file.h>
|
||||
#include <time.h>
|
||||
#include <errno.h>
|
||||
#include <jansson.h>
|
||||
|
||||
static int verbose = 0;
|
||||
|
||||
#define MODEMS_LOCK "/var/lock/modems.lock"
|
||||
#define MODEMS_INFO "/run/modems.json"
|
||||
#define TIMEOUT 3
|
||||
|
||||
#define DBG(fmt, a...) print(LOG_DEBUG, fmt, ##a)
|
||||
#define SAY(fmt, a...) print(LOG_INFO, fmt, ##a)
|
||||
#define ERR(fmt, a...) print(LOG_ERR, fmt, ##a)
|
||||
#define FATAL(fmt, a...) print(LOG_ALERT, fmt, ##a)
|
||||
|
||||
static void print (int level, const char *fmt, ...)
|
||||
{
|
||||
char msg[1024];
|
||||
va_list ap;
|
||||
|
||||
if (!fmt) return;
|
||||
va_start(ap, fmt);
|
||||
vsnprintf(msg, sizeof(msg) - 1, fmt, ap);
|
||||
va_end(ap);
|
||||
|
||||
switch (level) {
|
||||
case LOG_ERR:
|
||||
fprintf(stderr, "ERROR: %s\n", msg);
|
||||
break;
|
||||
case LOG_ALERT:
|
||||
fprintf(stderr, "ERROR: %s\n", msg);
|
||||
unlink(MODEMS_LOCK);
|
||||
exit(1);
|
||||
break;
|
||||
case LOG_DEBUG:
|
||||
if (verbose) {
|
||||
printf("DEBUG: %s\n", msg);
|
||||
}
|
||||
break;
|
||||
default:
|
||||
printf("%s\n", msg);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
static void printser (int dir, char *ser)
|
||||
{
|
||||
char quoted[1024];
|
||||
char *p;
|
||||
int len = 0;
|
||||
|
||||
for (p = ser; *p; p++) {
|
||||
if (*p == '\r') {
|
||||
len += snprintf(quoted + len, sizeof(quoted) - len, "<CR>");
|
||||
} else if (*p == '\n') {
|
||||
len += snprintf(quoted + len, sizeof(quoted) - len, "<NL>");
|
||||
} else {
|
||||
len += snprintf(quoted + len, sizeof(quoted) - len, "%c", *p);
|
||||
}
|
||||
}
|
||||
if (dir == 0) {
|
||||
DBG("Received: '%s'", quoted);
|
||||
} else {
|
||||
DBG("Sending '%s'", quoted);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
struct ttymode {
|
||||
int set;
|
||||
int speed;
|
||||
int parity;
|
||||
int bits;
|
||||
int stopbits;
|
||||
};
|
||||
|
||||
static int parse_mode (const char *str, struct ttymode *mode)
|
||||
{
|
||||
char buf[256];
|
||||
char *t1, *t2;
|
||||
|
||||
/* parse string */
|
||||
if (strlen(str) >= sizeof(buf)) {
|
||||
return -1;
|
||||
}
|
||||
strncpy(buf, str, sizeof(buf));
|
||||
t1 = strtok(buf, " ");
|
||||
if (!t1) {
|
||||
ERR("Invalid format");
|
||||
return -1;
|
||||
}
|
||||
t2 = strtok(NULL, " ");
|
||||
if (!t2 || strlen(t2) != 3) {
|
||||
ERR("Invalid format");
|
||||
return -1;
|
||||
}
|
||||
|
||||
/* parse speed */
|
||||
switch (atoi(t1)) {
|
||||
case 9600:
|
||||
mode->speed = B9600;
|
||||
break;
|
||||
case 19200:
|
||||
mode->speed = B19200;
|
||||
break;
|
||||
case 38400:
|
||||
mode->speed = B38400;
|
||||
break;
|
||||
case 57600:
|
||||
mode->speed = B57600;
|
||||
break;
|
||||
case 115200:
|
||||
mode->speed = B115200;
|
||||
break;
|
||||
default:
|
||||
ERR("Invalid baudrate");
|
||||
return -1;
|
||||
}
|
||||
|
||||
/* parse bits */
|
||||
switch (t2[0]) {
|
||||
case '5':
|
||||
mode->bits = CS5;
|
||||
break;
|
||||
case '6':
|
||||
mode->bits = CS6;
|
||||
break;
|
||||
case '7':
|
||||
mode->bits = CS7;
|
||||
break;
|
||||
case '8':
|
||||
mode->bits = CS8;
|
||||
break;
|
||||
default:
|
||||
ERR("Invalid bits");
|
||||
return -1;
|
||||
}
|
||||
|
||||
/* parse parity */
|
||||
switch (t2[1]) {
|
||||
case 'N':
|
||||
mode->parity = 0;
|
||||
break;
|
||||
case 'E':
|
||||
mode->parity = PARENB;
|
||||
break;
|
||||
case 'O':
|
||||
mode->parity = PARENB | PARODD;
|
||||
break;
|
||||
default:
|
||||
ERR("Invalid parity");
|
||||
return -1;
|
||||
}
|
||||
|
||||
/* parse stopbits */
|
||||
switch (t2[2]) {
|
||||
case '1':
|
||||
mode->stopbits = 0;
|
||||
break;
|
||||
case '2':
|
||||
mode->stopbits = CSTOPB;
|
||||
break;
|
||||
default:
|
||||
ERR("Invalid stop bits %c");
|
||||
return -1;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
||||
static int setup (const char *tty, int timeout, struct ttymode *mode)
|
||||
{
|
||||
struct termios stbuf;
|
||||
struct stat st;
|
||||
char dev[32];
|
||||
int i, fd;
|
||||
|
||||
if (strncmp(tty, "/dev", 4) == 0) {
|
||||
strncpy(dev, tty, sizeof(dev));
|
||||
} else {
|
||||
snprintf(dev, sizeof(dev), "/dev/%s", tty);
|
||||
}
|
||||
|
||||
/* wait for device */
|
||||
for (i = 0; i < timeout; i++) {
|
||||
if (lstat(dev, &st) == 0) break;
|
||||
sleep(1);
|
||||
}
|
||||
if (i >= timeout) {
|
||||
ERR("Timeout waiting for %s", dev);
|
||||
return -1;
|
||||
}
|
||||
|
||||
DBG("Opening %s", tty);
|
||||
fd = open(dev, O_RDWR | O_EXCL | O_NONBLOCK | O_NOCTTY);
|
||||
if (fd < 0) {
|
||||
ERR("Unable to open '%s'", dev);
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (!mode->set) return fd;
|
||||
|
||||
memset(&stbuf, 0, sizeof (struct termios));
|
||||
if (tcgetattr(fd, &stbuf) != 0) {
|
||||
ERR("Unable to get serial attributes");
|
||||
goto error;
|
||||
}
|
||||
/* default settings */
|
||||
stbuf.c_cflag &= ~(CBAUD | CSIZE | CSTOPB | PARENB | PARODD | CRTSCTS);
|
||||
stbuf.c_iflag &= ~(IGNCR | ICRNL | IUCLC | INPCK | IXON | IXOFF | IXANY );
|
||||
stbuf.c_oflag &= ~(OPOST | OLCUC | OCRNL | ONLCR | ONLRET);
|
||||
stbuf.c_lflag &= ~(ICANON | ECHO | ECHOE | ECHONL);
|
||||
stbuf.c_cc[VMIN] = 1;
|
||||
stbuf.c_cc[VTIME] = 0;
|
||||
stbuf.c_cc[VEOF] = 1;
|
||||
|
||||
/* ignore parity */
|
||||
stbuf.c_iflag |= IGNPAR;
|
||||
|
||||
/* configure serial attributes */
|
||||
stbuf.c_cflag |= (mode->bits | mode->parity | mode->stopbits | CLOCAL | CREAD);
|
||||
|
||||
/* disable XON/XOFF flow control */
|
||||
stbuf.c_iflag &= ~(IXON | IXOFF | IXANY);
|
||||
|
||||
/* disable RTS/CTS flow control */
|
||||
stbuf.c_cflag &= ~(CRTSCTS);
|
||||
|
||||
if (cfsetispeed(&stbuf, mode->speed) != 0 ||
|
||||
cfsetospeed(&stbuf, mode->speed) != 0) {
|
||||
ERR("Unable to set port speed");
|
||||
goto error;
|
||||
}
|
||||
|
||||
if (tcsetattr(fd, TCSANOW, &stbuf) != 0) {
|
||||
ERR("Unable to set serial attributes");
|
||||
goto error;
|
||||
}
|
||||
|
||||
return fd;
|
||||
error:
|
||||
close(fd);
|
||||
return -1;
|
||||
}
|
||||
|
||||
static int serial_write (int fd, int timeout, char *buf, int len)
|
||||
{
|
||||
struct timeval tv;
|
||||
fd_set fds;
|
||||
|
||||
FD_ZERO(&fds);
|
||||
FD_SET(fd, &fds);
|
||||
tv.tv_sec = timeout;
|
||||
tv.tv_usec = 0;
|
||||
|
||||
if (select(fd + 1, NULL, &fds, NULL, &tv) <= 0) {
|
||||
DBG("Cannot write to serial device");
|
||||
return -1;
|
||||
}
|
||||
alarm(timeout);
|
||||
tcflush(fd, TCIOFLUSH);
|
||||
|
||||
printser(1, buf);
|
||||
if (write(fd, buf, len) != len) {
|
||||
return -1;
|
||||
}
|
||||
tcdrain(fd);
|
||||
alarm(0);
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int serial_read (int fd, int timeout, char *buf, int size)
|
||||
{
|
||||
struct timeval tv;
|
||||
fd_set fds;
|
||||
int rc, len = 0;
|
||||
|
||||
FD_ZERO(&fds);
|
||||
FD_SET(fd, &fds);
|
||||
tv.tv_sec = timeout;
|
||||
tv.tv_usec = 0;
|
||||
memset(buf, 0, size);
|
||||
|
||||
rc = select(fd + 1, &fds, NULL, NULL, &tv);
|
||||
if (rc <= 0) {
|
||||
DBG("Cannot read from serial device (%d)", rc);
|
||||
return -1;
|
||||
}
|
||||
do {
|
||||
errno = 0;
|
||||
alarm(timeout);
|
||||
rc = read(fd, buf+len, size-len-1);
|
||||
alarm(0);
|
||||
|
||||
if (rc > 0) {
|
||||
len += rc;
|
||||
} else {
|
||||
if (rc < 0 && errno == -EAGAIN) {
|
||||
continue;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
} while (1);
|
||||
|
||||
if (len > 0) buf[len] = '\0';
|
||||
|
||||
printser(0, buf);
|
||||
|
||||
return len;
|
||||
}
|
||||
|
||||
static int execute (int fd, int timeout, const char *cmd, const char *expect)
|
||||
{
|
||||
char buf[1024];
|
||||
time_t start;
|
||||
long elapsed;
|
||||
int len, ret = -1;
|
||||
|
||||
if (cmd) {
|
||||
start = time(NULL);
|
||||
len = snprintf(buf, sizeof(buf), "%s\r\n", cmd);
|
||||
if (serial_write(fd, timeout, buf, len) < 0) {
|
||||
DBG("Cannot write to serial device");
|
||||
return -1;
|
||||
}
|
||||
elapsed = time(NULL) - start;
|
||||
timeout -= elapsed;
|
||||
}
|
||||
|
||||
DBG("Waiting for response");
|
||||
|
||||
while (timeout > 0) {
|
||||
start = time(NULL);
|
||||
len = serial_read(fd, timeout, buf, sizeof(buf));
|
||||
if (len > 0) {
|
||||
buf[len] = '\0';
|
||||
SAY("%s", buf);
|
||||
|
||||
if (strstr(buf, expect)) {
|
||||
ret = 0;
|
||||
break;
|
||||
}
|
||||
}
|
||||
elapsed = time(NULL) - start;
|
||||
timeout -= elapsed;
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
static int probe (int fd)
|
||||
{
|
||||
char buf[256];
|
||||
int i, len;
|
||||
|
||||
DBG("Probing serial port");
|
||||
|
||||
/* try 3 times */
|
||||
for (i = 3; i > 0; i--) {
|
||||
len = snprintf(buf, sizeof(buf), "AT\r\n");
|
||||
if (serial_write(fd, 1, buf, len) < 0) {
|
||||
DBG("Cannot write to tty");
|
||||
continue;
|
||||
}
|
||||
len = serial_read(fd, 1, buf, sizeof(buf));
|
||||
if (len <= 0) {
|
||||
DBG("Cannot read from tty");
|
||||
continue;
|
||||
}
|
||||
if (strstr(buf, "OK")) {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
DBG("Probing failed");
|
||||
return -1;
|
||||
}
|
||||
|
||||
static int modemidx (const char *devpath)
|
||||
{
|
||||
json_t *json, *modules, *module, *mod, *paths, *obj;
|
||||
const char *p;
|
||||
int i, j, index = -1;
|
||||
|
||||
json = json_load_file("/run/modules.json", 0, NULL);
|
||||
if (!json) {
|
||||
return -1;
|
||||
}
|
||||
modules = json_object_get(json, "modules");
|
||||
if (!modules || !json_is_array(modules)) {
|
||||
goto out;
|
||||
}
|
||||
for (i = 0; i < json_array_size(modules); i++) {
|
||||
module = json_array_get(modules, i);
|
||||
if (!module) continue;
|
||||
|
||||
obj = json_object_get(module, "type");
|
||||
if (!obj || !json_is_string(obj)) continue;
|
||||
p = json_string_value(obj);
|
||||
if (strcmp(p, "modem") != 0) continue;
|
||||
|
||||
obj = json_object_get(module, "index");
|
||||
if (!obj || !json_is_integer(obj)) continue;
|
||||
index = json_integer_value(obj);
|
||||
|
||||
paths = json_object_get(module, "paths");
|
||||
if (!paths || !json_is_array(paths)) continue;
|
||||
|
||||
for (j = 0; j < json_array_size(paths); j++) {
|
||||
obj = json_array_get(paths, j);
|
||||
if (!obj) continue;
|
||||
p = json_string_value(obj);
|
||||
|
||||
if (strstr(devpath, p)) {
|
||||
/* found */
|
||||
goto out;
|
||||
}
|
||||
}
|
||||
}
|
||||
index = -1;
|
||||
out:
|
||||
json_decref(json);
|
||||
return index;
|
||||
}
|
||||
|
||||
static char *modemdev (int index, int port)
|
||||
{
|
||||
json_t *json, *modems, *modem, *ports, *obj;
|
||||
char *dev = NULL;
|
||||
int i, p, fd;
|
||||
|
||||
fd = open(MODEMS_LOCK,
|
||||
O_WRONLY | O_CREAT | O_EXCL,
|
||||
S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH);
|
||||
if (fd < 0) {
|
||||
ERR("Cannot open lock");
|
||||
return NULL;
|
||||
}
|
||||
if (flock(fd, LOCK_EX) < 0) {
|
||||
ERR("Cannot to lock");
|
||||
close(fd);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
json = json_load_file(MODEMS_INFO, 0, NULL);
|
||||
if (!json) {
|
||||
goto out;
|
||||
}
|
||||
modems = json_object_get(json, "modems");
|
||||
if (!modems || !json_is_array(modems)) {
|
||||
goto out;
|
||||
}
|
||||
for (i = 0; i < json_array_size(modems); i++) {
|
||||
modem = json_array_get(modems, i);
|
||||
if (!modem)
|
||||
continue;
|
||||
|
||||
obj = json_object_get(modem, "devpath");
|
||||
if (!obj || !json_is_string(obj))
|
||||
continue;
|
||||
|
||||
if (modemidx(json_string_value(obj)) != index)
|
||||
continue;
|
||||
|
||||
ports = json_object_get(modem, "atports");
|
||||
if (!ports || !json_is_array(ports) || json_array_size(ports) == 0)
|
||||
goto out;
|
||||
|
||||
if (json_array_size(ports) >= port)
|
||||
port = 0;
|
||||
|
||||
obj = json_array_get(ports, port);
|
||||
if (!obj || !json_is_string(obj))
|
||||
goto out;
|
||||
|
||||
/* found */
|
||||
dev = strdup(json_string_value(obj));
|
||||
goto out;
|
||||
}
|
||||
dev = NULL;
|
||||
|
||||
out:
|
||||
if (json) json_decref(json);
|
||||
|
||||
unlink(MODEMS_LOCK);
|
||||
flock(fd, LOCK_UN);
|
||||
close(fd);
|
||||
|
||||
return dev;
|
||||
}
|
||||
|
||||
static struct option options[] = {
|
||||
{ "index", required_argument, 0, 'i' },
|
||||
{ "device", required_argument, 0, 'd' },
|
||||
{ "mode", required_argument, 0, 'm' },
|
||||
{ "command", required_argument, 0, 'c' },
|
||||
{ "expect", required_argument, 0, 'e' },
|
||||
{ "timeout", required_argument, 0, 't' },
|
||||
{ "primary", no_argument, 0, 'P' },
|
||||
{ "secondary", no_argument, 0, 'S' },
|
||||
{ "verbose", no_argument, 0, 'v' },
|
||||
{ "help", no_argument, 0, 'h' },
|
||||
{ 0, 0, 0, 0 }
|
||||
};
|
||||
|
||||
static void usage (void)
|
||||
{
|
||||
fprintf(stderr, "Usage: modem-command [OPTIONS] [command]\n\n");
|
||||
fprintf(stderr, " --index <idx> Index of modem\n");
|
||||
fprintf(stderr, " --device <tty> TTY device of modem\n");
|
||||
fprintf(stderr, " --mode <mode> Serial mode for TTY (e.g. '115200 8N1')\n");
|
||||
fprintf(stderr, " --expect <str> String to expect\n");
|
||||
fprintf(stderr, " --timeout <to> Timeout for operation\n");
|
||||
fprintf(stderr, " --primary Use primary atport\n");
|
||||
fprintf(stderr, " --secondary Use secondary atport\n");
|
||||
fprintf(stderr, " --verbose Be verbose\n");
|
||||
fprintf(stderr, " --help print help\n\n");
|
||||
exit(1);
|
||||
}
|
||||
|
||||
int main (int argc, char *argv[])
|
||||
{
|
||||
const char *cmd = NULL, *expect = "OK";
|
||||
char *dev = NULL;
|
||||
struct ttymode mode;
|
||||
int timeout = TIMEOUT;
|
||||
int index = 0, port = 0;
|
||||
int opt, c, fd;
|
||||
int ret = 1;
|
||||
|
||||
memset(&mode, 0, sizeof(struct ttymode));
|
||||
|
||||
while (1) {
|
||||
c = getopt_long(argc, argv, "i:d:m:e:t:PSvh", options, &opt);
|
||||
if (c < 0) break;
|
||||
|
||||
switch (c) {
|
||||
case 'i':
|
||||
index = atoi(optarg);
|
||||
break;
|
||||
case 'd':
|
||||
dev = strdup(optarg);
|
||||
break;
|
||||
case 'm':
|
||||
if (parse_mode(optarg, &mode) < 0) {
|
||||
FATAL("Invalid serial mode specified");
|
||||
}
|
||||
mode.set = 1;
|
||||
break;
|
||||
case 'c':
|
||||
cmd = optarg;
|
||||
break;
|
||||
case 'e':
|
||||
expect = optarg;
|
||||
break;
|
||||
case 't':
|
||||
timeout = atoi(optarg);
|
||||
break;
|
||||
case 'P':
|
||||
port = 0;
|
||||
break;
|
||||
case 'S':
|
||||
port = 1;
|
||||
break;
|
||||
case 'v':
|
||||
verbose = 1;
|
||||
break;
|
||||
default:
|
||||
usage();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (optind < argc) {
|
||||
cmd = argv[optind];
|
||||
if (strlen(cmd) == 0)
|
||||
FATAL("Invalid command");
|
||||
}
|
||||
dev = modemdev(index, port);
|
||||
if (!dev || strlen(dev) == 0) {
|
||||
FATAL("No device found");
|
||||
}
|
||||
fd = setup(dev, timeout, &mode);
|
||||
if (fd < 0) {
|
||||
FATAL("Unable to setup device");
|
||||
}
|
||||
|
||||
if (probe(fd) < 0) {
|
||||
ERR("Unable to probe device");
|
||||
goto abort;
|
||||
}
|
||||
if (execute(fd, timeout, cmd, expect) < 0) {
|
||||
ERR("Command failed");
|
||||
goto abort;
|
||||
}
|
||||
ret = 0;
|
||||
|
||||
abort:
|
||||
close(fd);
|
||||
if (dev) free(dev);
|
||||
return ret;
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
# Placeholder — add module names here if any modem drivers need explicit
|
||||
# loading (e.g. when device IDs are not yet upstream in the driver table).
|
||||
@@ -0,0 +1,8 @@
|
||||
ACTION!="add", GOTO="modemd_end"
|
||||
|
||||
SUBSYSTEM=="net", ENV{INTERFACE}=="wwan[0-9]*", RUN+="/usr/libexec/modemd/modem-udev"
|
||||
SUBSYSTEM=="tty", ENV{ID_MM_PORT_TYPE_AT_PRIMARY}=="1", RUN+="/usr/libexec/modemd/modem-udev"
|
||||
SUBSYSTEM=="tty", ENV{ID_MM_PORT_TYPE_AT_SECONDARY}=="1", RUN+="/usr/libexec/modemd/modem-udev"
|
||||
SUBSYSTEM=="usbmisc", KERNEL=="cdc-wdm[0-9]*",, RUN+="/usr/libexec/modemd/modem-udev"
|
||||
|
||||
LABEL="modemd_end"
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,316 @@
|
||||
import subprocess
|
||||
import argparse
|
||||
import json
|
||||
import syslog
|
||||
import sys
|
||||
import os
|
||||
import re
|
||||
|
||||
syslog.openlog(logoption=syslog.LOG_PID, facility=syslog.LOG_SYSLOG)
|
||||
|
||||
|
||||
def log(msg):
|
||||
syslog.syslog(syslog.LOG_INFO, msg)
|
||||
|
||||
|
||||
def err(msg):
|
||||
syslog.syslog(syslog.LOG_ERR, msg)
|
||||
|
||||
|
||||
def fatal(msg):
|
||||
syslog.syslog(syslog.LOG_ALERT, msg)
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
def runcmd(cmd):
|
||||
ret = None
|
||||
try:
|
||||
res = subprocess.run(cmd, check=True, stdout=subprocess.PIPE,
|
||||
stderr=subprocess.PIPE, text=True)
|
||||
if res.returncode == 0:
|
||||
if res.stdout:
|
||||
ret = res.stdout.strip()
|
||||
else:
|
||||
ret = True
|
||||
except subprocess.CalledProcessError:
|
||||
return None
|
||||
finally:
|
||||
return ret
|
||||
|
||||
|
||||
def runcmdj(cmd):
|
||||
output = runcmd(cmd)
|
||||
if not output:
|
||||
return None
|
||||
try:
|
||||
data = json.loads(output)
|
||||
except json.JSONDecodeError:
|
||||
return None
|
||||
finally:
|
||||
return data
|
||||
|
||||
|
||||
def read_file(path):
|
||||
if os.path.exists(path):
|
||||
with open(path, "r") as fd:
|
||||
output = str(fd.read())
|
||||
if output:
|
||||
return output.strip()
|
||||
return None
|
||||
|
||||
|
||||
# Telit LE910
|
||||
le910_carriers = [
|
||||
{"num": 0, "name": "AT&T"},
|
||||
{"num": 1, "name": "Verizon"},
|
||||
{"num": 2, "name": "T-Mobile"},
|
||||
{"num": 3, "name": "Bell"},
|
||||
{"num": 4, "name": "Telus"},
|
||||
{"num": 10, "name": "NTT-Docomo"},
|
||||
{"num": 11, "name": "Telstra"},
|
||||
{"num": 12, "name": "KDDI"},
|
||||
{"num": 13, "name": "Softbank"},
|
||||
{"num": 14, "name": "Vodafone New Zealand"},
|
||||
{"num": 15, "name": "Spark New Zealand"},
|
||||
{"num": 20, "name": "China Mobile"},
|
||||
{"num": 21, "name": "China Unicom"},
|
||||
{"num": 22, "name": "China Telecom"},
|
||||
{"num": 30, "name": "Sprint"},
|
||||
{"num": 31, "name": "SouthernLINC"},
|
||||
{"num": 40, "name": "Global"},
|
||||
{"num": 101, "name": "T-Mobile Germany"},
|
||||
{"num": 102, "name": "AT&T Mexico"},
|
||||
{"num": 103, "name": "Orange-WW"},
|
||||
{"num": 104, "name": "Southern Linc USA"},
|
||||
{"num": 105, "name": "Vodafone DE"}
|
||||
]
|
||||
|
||||
|
||||
def le910_carrier_bynum(num):
|
||||
for carrier in le910_carriers:
|
||||
if carrier["num"] == num:
|
||||
return carrier
|
||||
return None
|
||||
|
||||
|
||||
def le910_carrier_byname(name):
|
||||
for carrier in le910_carriers:
|
||||
if carrier["name"] == name:
|
||||
return carrier
|
||||
return None
|
||||
|
||||
|
||||
def le910_is_supported(variant):
|
||||
if variant == "NS":
|
||||
return True
|
||||
elif variant == "AP":
|
||||
return True
|
||||
elif variant == "NF" or variant == "NFD":
|
||||
return True
|
||||
elif variant == "CN":
|
||||
return True
|
||||
elif variant == "APX":
|
||||
return True
|
||||
elif variant == "WWX" or variant == "WWXD":
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
|
||||
|
||||
def le910_carrier_default(variant):
|
||||
if variant == "NF" or variant == "WWX" or variant == "WWXD":
|
||||
num = 0
|
||||
elif variant == "AP" or variant == "APX":
|
||||
num = 10
|
||||
elif variant == "CN":
|
||||
num = 20
|
||||
elif variant == "NS":
|
||||
num = 30
|
||||
elif variant == "EU" or variant == "EUX":
|
||||
num = 40
|
||||
else:
|
||||
return None
|
||||
|
||||
return le910_carrier_bynum(num)
|
||||
|
||||
|
||||
def le910_command(index, command):
|
||||
cmd = ['/sbin/modem-command',
|
||||
'--index', str(index),
|
||||
'--mode', '115200 8N1',
|
||||
'--secondary',
|
||||
'--timeout', '5',
|
||||
command]
|
||||
|
||||
for attempt in range(1, 3):
|
||||
output = runcmd(cmd)
|
||||
if output:
|
||||
return output
|
||||
|
||||
err("Command '%s' failed" % " ".join(cmd))
|
||||
return None
|
||||
|
||||
|
||||
def le910_list_carriers(index):
|
||||
output = le910_command(index, 'AT#FWSWITCH=?')
|
||||
if output is None:
|
||||
return None
|
||||
|
||||
r = re.search(r"#FWSWITCH: \(([^\)]*)\)", output)
|
||||
if not r:
|
||||
err("Unable to list carriers on modem%d" % index)
|
||||
return None
|
||||
|
||||
rlist = r.group(1)
|
||||
nums = []
|
||||
for rl in rlist.split(","):
|
||||
r = re.search(r"(\d+)-(\d+)", rl)
|
||||
if r:
|
||||
start = int(r.group(1))
|
||||
end = int(r.group(2))
|
||||
for i in range(start, end + 1):
|
||||
nums.append(i)
|
||||
else:
|
||||
nums.append(int(rl))
|
||||
|
||||
carriers = []
|
||||
for num in nums:
|
||||
carrier = le910_carrier_bynum(num)
|
||||
if carrier:
|
||||
carriers.append(carrier)
|
||||
else:
|
||||
err("Unknown carrier %d" % num)
|
||||
|
||||
return carriers
|
||||
|
||||
|
||||
def le910_set_carrier(index, carrier):
|
||||
output = le910_command(index, 'AT#FWSWITCH=%d' % carrier["num"])
|
||||
if output is None:
|
||||
return False
|
||||
else:
|
||||
return True
|
||||
|
||||
|
||||
def le910_get_carrier(index):
|
||||
output = le910_command(index, 'AT#FWSWITCH?')
|
||||
if output is None:
|
||||
return -1
|
||||
|
||||
r = re.search(r"#FWSWITCH: (\d+)", output)
|
||||
if not r:
|
||||
err("Unable to query carrier on modem%d" % index)
|
||||
return -1
|
||||
num = int(r.group(1))
|
||||
|
||||
return le910_carrier_bynum(num)
|
||||
|
||||
|
||||
def runcheck(manf, model):
|
||||
if manf == "Telit" and model[:5] == "LE910":
|
||||
return le910_is_supported(model.split("-")[1])
|
||||
else:
|
||||
return False
|
||||
|
||||
|
||||
def runset(index, manf, model, name):
|
||||
if not runcheck(manf, model):
|
||||
fatal("Unsupported modem")
|
||||
|
||||
if name == "default":
|
||||
carrier = le910_carrier_default(model.split("-")[1])
|
||||
else:
|
||||
carrier = le910_carrier_byname(name)
|
||||
if not carrier:
|
||||
fatal("Invalid carrier '%s'" % name)
|
||||
|
||||
current = le910_get_carrier(index)
|
||||
if not current:
|
||||
fatal("Cannot get current carrier")
|
||||
|
||||
if current["name"] == carrier["name"]:
|
||||
log("Carrier '%s' already set on modem%d" % (carrier["name"], index))
|
||||
print(json.dumps({"action": "none"}))
|
||||
sys.exit(0)
|
||||
|
||||
log("Setting carrier '%s' on modem%d" % (carrier["name"], index))
|
||||
if not le910_set_carrier(index, carrier):
|
||||
fatal("Unable to set carrier on modem%d" % index)
|
||||
|
||||
print(json.dumps({"action": "reset"}))
|
||||
sys.exit(0)
|
||||
|
||||
|
||||
def runget(index, manf, model):
|
||||
if not runcheck(manf, model):
|
||||
fatal("Unsupported modem")
|
||||
|
||||
carrier = le910_get_carrier(index)
|
||||
if not carrier:
|
||||
fatal("Unable to get carrier")
|
||||
|
||||
print(json.dumps(carrier))
|
||||
sys.exit(0)
|
||||
|
||||
|
||||
def runlist(index, manf, model):
|
||||
if not runcheck(manf, model):
|
||||
print(json.dumps([]))
|
||||
sys.exit(0)
|
||||
|
||||
carriers = le910_list_carriers(index)
|
||||
if not carriers:
|
||||
fatal("No carriers")
|
||||
|
||||
names = []
|
||||
for carrier in sorted(carriers, key=lambda c: c["name"]):
|
||||
names.append(carrier["name"])
|
||||
|
||||
print(json.dumps(names))
|
||||
sys.exit(0)
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(prog='modem-carrier')
|
||||
parser.add_argument("-i", "--index", default=0, help="Modem index")
|
||||
parser.add_argument("-m", "--manf", default=0, help="Modem manufacturer")
|
||||
parser.add_argument("-M", "--model", default=0, help="Modem model")
|
||||
parser.add_argument("-s", "--set", help="Set carrier")
|
||||
parser.add_argument("-g", "--get", help="Get carrier",
|
||||
action="store_true")
|
||||
parser.add_argument("-l", "--list", help="List supported carriers",
|
||||
action="store_true")
|
||||
parser.add_argument("-c", "--check", help="Check support for carriers",
|
||||
action="store_true")
|
||||
args = parser.parse_args()
|
||||
|
||||
if not args.manf:
|
||||
fatal("No manufacturer given")
|
||||
else:
|
||||
manf = args.manf
|
||||
|
||||
if not args.model:
|
||||
fatal("No model given")
|
||||
else:
|
||||
model = args.model
|
||||
|
||||
if args.index:
|
||||
index = int(args.index)
|
||||
else:
|
||||
index = 0
|
||||
|
||||
if args.check:
|
||||
if not runcheck(manf, model):
|
||||
sys.exit(1)
|
||||
elif args.list:
|
||||
runlist(index, manf, model)
|
||||
elif args.set:
|
||||
runset(index, manf, model, args.set)
|
||||
elif args.get:
|
||||
runget(index, manf, model)
|
||||
|
||||
sys.exit(0)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,549 @@
|
||||
import subprocess
|
||||
import argparse
|
||||
import syslog
|
||||
import json
|
||||
import sys
|
||||
import os
|
||||
import re
|
||||
|
||||
debug = False
|
||||
|
||||
syslog.openlog(logoption=syslog.LOG_PID, facility=syslog.LOG_SYSLOG)
|
||||
|
||||
|
||||
def dbg(msg):
|
||||
global debug
|
||||
if debug:
|
||||
syslog.syslog(syslog.LOG_INFO, msg)
|
||||
|
||||
|
||||
def runcmd(cmd):
|
||||
ret = None
|
||||
try:
|
||||
res = subprocess.run(cmd, check=True, stdout=subprocess.PIPE,
|
||||
stderr=subprocess.PIPE, text=True)
|
||||
if res.returncode == 0:
|
||||
if res.stdout:
|
||||
ret = res.stdout.strip()
|
||||
else:
|
||||
ret = True
|
||||
except subprocess.CalledProcessError:
|
||||
return None
|
||||
finally:
|
||||
return ret
|
||||
|
||||
|
||||
def runcmdj(cmd):
|
||||
output = runcmd(cmd)
|
||||
if not output:
|
||||
return None
|
||||
try:
|
||||
data = json.loads(output)
|
||||
except json.JSONDecodeError:
|
||||
return None
|
||||
finally:
|
||||
return data
|
||||
|
||||
|
||||
def vbool(val):
|
||||
if val == "yes" or val == "true":
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
|
||||
|
||||
def vnum(val):
|
||||
if val != "--":
|
||||
return int(val)
|
||||
else:
|
||||
return 0
|
||||
|
||||
|
||||
def vnumstr(val):
|
||||
if val != "" and val != "--":
|
||||
return val
|
||||
else:
|
||||
return "0"
|
||||
|
||||
|
||||
def vstr(val):
|
||||
if val != "--":
|
||||
return val
|
||||
else:
|
||||
return ""
|
||||
|
||||
|
||||
def venum(val, enums, default):
|
||||
enval = vstr(val)
|
||||
for en in enums:
|
||||
if enval == en:
|
||||
return en
|
||||
return default
|
||||
|
||||
|
||||
def vip4(val):
|
||||
val = vstr(val)
|
||||
if val == "":
|
||||
return "0.0.0.0"
|
||||
else:
|
||||
return val
|
||||
|
||||
|
||||
def vip6(val):
|
||||
val = vstr(val)
|
||||
if val == "":
|
||||
return "::"
|
||||
else:
|
||||
return val
|
||||
|
||||
|
||||
def fread(path):
|
||||
if os.path.exists(path):
|
||||
with open(path, "r") as fd:
|
||||
output = str(fd.read())
|
||||
if output:
|
||||
return output.strip()
|
||||
return None
|
||||
|
||||
|
||||
def freadj(path):
|
||||
output = fread(path)
|
||||
if not output:
|
||||
return None
|
||||
try:
|
||||
data = json.loads(output)
|
||||
except json.JSONDecodeError:
|
||||
dbg("Unable to parse json output")
|
||||
return None
|
||||
finally:
|
||||
return data
|
||||
|
||||
|
||||
def get_manufacturer(index, gen):
|
||||
path = "/run/modemd/modem%d/manufacturer" % index
|
||||
manf = fread(path)
|
||||
if manf:
|
||||
return manf
|
||||
|
||||
manf = vstr(gen.get("manufacturer", ""))
|
||||
return manf
|
||||
|
||||
|
||||
def get_model(index, gen):
|
||||
path = "/run/modemd/modem%d/model" % index
|
||||
model = fread(path)
|
||||
if model:
|
||||
return model
|
||||
|
||||
model = vstr(gen.get("model", ""))
|
||||
return model
|
||||
|
||||
|
||||
def get_supported_carriers(index, manf, model):
|
||||
path = "/run/modemd/modem%d/supported-carriers" % index
|
||||
output = freadj(path)
|
||||
if output:
|
||||
return output
|
||||
else:
|
||||
return []
|
||||
|
||||
|
||||
def get_current_carrier(index, manf, model):
|
||||
path = "/run/modemd/modem%d/current-carrier" % index
|
||||
output = freadj(path)
|
||||
if output:
|
||||
return output
|
||||
else:
|
||||
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":
|
||||
path = os.path.realpath("%s/.." % path)
|
||||
subdir = os.path.basename(path)
|
||||
r = re.search(r"usb\d", subdir)
|
||||
if r:
|
||||
return path
|
||||
return None
|
||||
|
||||
|
||||
def modem_get_module(devpath):
|
||||
# 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
|
||||
|
||||
# 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"}
|
||||
|
||||
return None
|
||||
|
||||
|
||||
def modem_get_sim(module):
|
||||
path = "/sys/class/sim"
|
||||
for index in range(0, 5):
|
||||
name = "sim%d" % index
|
||||
if not os.path.exists("%s/%s" % (path, name)):
|
||||
continue
|
||||
|
||||
slot = int(fread("%s/%s/slot" % (path, name)))
|
||||
if slot != module["slot"]:
|
||||
continue
|
||||
|
||||
present = int(fread("%s/%s/present" % (path, name)))
|
||||
|
||||
return {
|
||||
"index": index,
|
||||
"name": name,
|
||||
"slot": slot,
|
||||
"present": bool(present)
|
||||
}
|
||||
|
||||
return None
|
||||
|
||||
|
||||
def sysfs_interfaces(devpath):
|
||||
ifaces = []
|
||||
try:
|
||||
for iface in os.listdir("/sys/class/net"):
|
||||
p = os.path.realpath("/sys/class/net/%s" % iface)
|
||||
if p.startswith(devpath):
|
||||
ifaces.append(iface)
|
||||
except OSError:
|
||||
pass
|
||||
return ifaces
|
||||
|
||||
|
||||
def print_modem(index):
|
||||
# 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)
|
||||
|
||||
# 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)
|
||||
|
||||
|
||||
def print_all():
|
||||
modems = []
|
||||
|
||||
output = runcmdj(['mmcli', '-J', '-L'])
|
||||
if not output:
|
||||
dbg("mmcli command failed")
|
||||
print(json.dumps(modems))
|
||||
sys.exit(0)
|
||||
|
||||
for mpath in output['modem-list']:
|
||||
output = runcmdj(['mmcli', '-J', '-m', mpath])
|
||||
if not output:
|
||||
dbg("no output for %s" % mpath)
|
||||
continue
|
||||
modem = output.get("modem")
|
||||
if modem is None:
|
||||
dbg("no modem for %s" % mpath)
|
||||
continue
|
||||
gen = modem.get("generic")
|
||||
if gen is None:
|
||||
dbg("no generic for %s" % mpath)
|
||||
continue
|
||||
gpp = modem.get("3gpp")
|
||||
if gpp is None:
|
||||
dbg("no 3gpp for %s" % mpath)
|
||||
continue
|
||||
|
||||
devpath = device_devpath(gen["device"])
|
||||
if devpath is None:
|
||||
dbg("no devpath for %s" % mpath)
|
||||
continue
|
||||
|
||||
module = modem_get_module(devpath)
|
||||
if module is None:
|
||||
dbg("no modem module for %s" % devpath)
|
||||
continue
|
||||
|
||||
index = module["index"]
|
||||
|
||||
manf = get_manufacturer(index, gen)
|
||||
if manf is None:
|
||||
dbg("no manufacturer for modem%d" % index)
|
||||
continue
|
||||
|
||||
model = get_model(index, gen)
|
||||
if model is None:
|
||||
dbg("no model for modem%d" % index)
|
||||
continue
|
||||
|
||||
modem = {
|
||||
"index": index,
|
||||
"path": mpath
|
||||
}
|
||||
|
||||
modem["info"] = {
|
||||
"manufacturer": manf,
|
||||
"model": model,
|
||||
"supported-carrier": get_supported_carriers(index, manf, model),
|
||||
"hardware-revision": vstr(gen["hardware-revision"]),
|
||||
"firmware-version": vstr(gen["revision"]),
|
||||
"serial-number": vstr(gen["equipment-identifier"]),
|
||||
"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(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.get("state-failed-reason", "--"))
|
||||
if len(reason) > 0:
|
||||
modem["status"]["state-failed-reason"] = reason
|
||||
|
||||
sim = modem_get_sim(module)
|
||||
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'])
|
||||
if output:
|
||||
refresh = int(output["modem"]["signal"]["refresh"]["rate"])
|
||||
if refresh == 0:
|
||||
# enable extended signal info
|
||||
runcmd(['mmcli', '-J', '-m', mpath, '--signal-setup=5'])
|
||||
else:
|
||||
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 == "--":
|
||||
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"],
|
||||
["idle", "home", "searching", "denied", "roaming"],
|
||||
"unknown"),
|
||||
"operator-name": vstr(gpp["operator-name"]),
|
||||
"operator-id": vstr(gpp["operator-code"]),
|
||||
"network-type": network_type,
|
||||
"service-state":
|
||||
venum(gpp.get("packet-service-state", "--"),
|
||||
["attached", "detached"], "unknown"),
|
||||
}
|
||||
|
||||
bearers = []
|
||||
for bpath in gen["bearers"]:
|
||||
output = runcmdj(['mmcli', '-J', '-m', mpath, '-b', bpath])
|
||||
if not output:
|
||||
continue
|
||||
|
||||
b = output["bearer"]
|
||||
|
||||
bearer = {
|
||||
"path": vstr(bpath),
|
||||
"connected": vbool(b["status"]["connected"]),
|
||||
"ipv4-address": vip4(b["ipv4-config"]["address"]),
|
||||
"ipv4-prefix": vnum(b["ipv4-config"]["prefix"]),
|
||||
"ipv6-address": vip6(b["ipv6-config"]["address"]),
|
||||
"ipv6-prefix": vnum(b["ipv6-config"]["prefix"]),
|
||||
"in-bytes": vnumstr(b["stats"]["bytes-rx"]),
|
||||
"out-bytes": vnumstr(b["stats"]["bytes-tx"]),
|
||||
"total-in-bytes": vnumstr(b["stats"]["total-bytes-rx"]),
|
||||
"total-out-bytes": vnumstr(b["stats"]["total-bytes-tx"]),
|
||||
"total-duration": vnumstr(b["stats"]["total-duration"])
|
||||
}
|
||||
if b["status"]["connected"] != "yes":
|
||||
reason = vstr(b["status"]["connection-error"]["message"])
|
||||
if len(reason) > 0:
|
||||
bearer["connection-failed-reason"] = reason
|
||||
|
||||
iface = vstr(b["status"]["interface"])
|
||||
if len(iface) > 0:
|
||||
bearer["interface"] = iface
|
||||
|
||||
bearers.append(bearer)
|
||||
|
||||
modem["status"]["bearer"] = bearers
|
||||
|
||||
sim_path = vstr(gen.get("sim", "--"))
|
||||
output = None
|
||||
if sim_path:
|
||||
output = runcmdj(['mmcli', '-J', '-m', mpath, '-i', sim_path])
|
||||
|
||||
if output:
|
||||
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:
|
||||
loc = output["modem"]["location"]
|
||||
modem["status"]["location"] = {
|
||||
"latitude": vstr(loc["gps"]["latitude"]),
|
||||
"longitude": vstr(loc["gps"]["longitude"]),
|
||||
"altitude": vstr(loc["gps"]["altitude"]),
|
||||
"cid": vstr(loc["3gpp"]["cid"]),
|
||||
"lac": vstr(loc["3gpp"]["lac"]),
|
||||
"mcc": vstr(loc["3gpp"]["mcc"]),
|
||||
"mnc": vstr(loc["3gpp"]["mnc"]),
|
||||
"tac": vstr(loc["3gpp"]["tac"])
|
||||
}
|
||||
|
||||
modems.append(modem)
|
||||
|
||||
modems = sorted(modems, key=lambda m: m['index'])
|
||||
data = json.dumps(modems)
|
||||
|
||||
print(data)
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(prog='modem-info')
|
||||
parser.add_argument("-i", "--index", default=0, help="Modem index")
|
||||
args = parser.parse_args()
|
||||
|
||||
with open("/proc/cmdline", 'r') as fd:
|
||||
cmdline = fd.read()
|
||||
if "debug" in cmdline:
|
||||
debug = True
|
||||
|
||||
if args.index:
|
||||
print_modem(int(args.index))
|
||||
else:
|
||||
print_all()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,88 @@
|
||||
import subprocess
|
||||
import argparse
|
||||
import json
|
||||
import time
|
||||
import syslog
|
||||
import sys
|
||||
|
||||
syslog.openlog(logoption=syslog.LOG_PID, facility=syslog.LOG_SYSLOG)
|
||||
|
||||
|
||||
def log(msg):
|
||||
syslog.syslog(syslog.LOG_INFO, msg)
|
||||
|
||||
|
||||
def err(msg):
|
||||
syslog.syslog(syslog.LOG_ERR, msg)
|
||||
|
||||
|
||||
def fatal(msg):
|
||||
syslog.syslog(syslog.LOG_ALERT, msg)
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
def runcmd(cmd):
|
||||
ret = None
|
||||
try:
|
||||
res = subprocess.run(cmd, check=True, stdout=subprocess.PIPE,
|
||||
stderr=subprocess.PIPE, text=True)
|
||||
if res.returncode == 0:
|
||||
if res.stdout:
|
||||
ret = res.stdout.strip()
|
||||
else:
|
||||
ret = True
|
||||
except subprocess.CalledProcessError:
|
||||
return None
|
||||
finally:
|
||||
return ret
|
||||
|
||||
|
||||
def runcmdj(cmd):
|
||||
output = runcmd(cmd)
|
||||
if not output:
|
||||
return None
|
||||
try:
|
||||
data = json.loads(output)
|
||||
except json.JSONDecodeError:
|
||||
return None
|
||||
finally:
|
||||
return data
|
||||
|
||||
|
||||
def reset(index, info):
|
||||
log("Power-cycling modem %d" % index)
|
||||
|
||||
with open("%s/authorized" % info["devpath"], "w") as fd:
|
||||
fd.write("0\n")
|
||||
time.sleep(0.5)
|
||||
|
||||
path = "/sys/class/pcie/slot%d/pwrctl" % info["slot"]
|
||||
with open(path, "w") as fd:
|
||||
fd.write("cycle\n")
|
||||
|
||||
return True
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(prog='modem-power')
|
||||
parser.add_argument("-i", "--index", default=0, help="Modem index")
|
||||
args = parser.parse_args()
|
||||
|
||||
if args.index:
|
||||
index = int(args.index)
|
||||
else:
|
||||
index = 0
|
||||
|
||||
info = runcmdj(['/usr/libexec/modemd/modem-info',
|
||||
'-i', str(index)])
|
||||
if info is None:
|
||||
fatal("Unable to obtain modem info")
|
||||
|
||||
if not reset(index, info):
|
||||
fatal("Unable to power-cycle modem")
|
||||
|
||||
sys.exit(0)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,52 @@
|
||||
import argparse
|
||||
import json
|
||||
import sys
|
||||
import os
|
||||
|
||||
|
||||
def sendrpc(index, rpc):
|
||||
path = "/run/modemd/modem%d/rpc" % index
|
||||
if os.path.exists(path):
|
||||
sys.stderr.write("Error: another rpc is running\n")
|
||||
return
|
||||
|
||||
with open(path, "w") as fd:
|
||||
json.dump(rpc, fd)
|
||||
|
||||
if not os.system(['sysrepocfg', '-f', 'json', '-R', path]):
|
||||
sys.stderr.write("Error: unable to restart\n")
|
||||
|
||||
os.unlink(path)
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(prog='modem-rpc')
|
||||
parser.add_argument("-i", "--index", default=0, help="Modem index")
|
||||
parser.add_argument("-r", "--rpc", default=0, help="RPC command")
|
||||
args = parser.parse_args()
|
||||
|
||||
if args.index:
|
||||
index = int(args.index)
|
||||
else:
|
||||
index = 0
|
||||
|
||||
if not args.rpc:
|
||||
sys.stderr.write("Error: no rpc command\n")
|
||||
sys.exit(1)
|
||||
|
||||
print("Sending '%s' rpc to modem%d" % (args.rpc, index))
|
||||
|
||||
rpc = {
|
||||
"ietf-hardware:hardware": {
|
||||
"component": [{
|
||||
"name": "modem%d" % index,
|
||||
"infix-hardware:modem": {args.rpc: {}}
|
||||
}]
|
||||
}
|
||||
}
|
||||
|
||||
sendrpc(index, rpc)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,82 @@
|
||||
import argparse
|
||||
import subprocess
|
||||
import json
|
||||
import sys
|
||||
|
||||
index = 0
|
||||
|
||||
|
||||
def runcmd(cmd):
|
||||
ret = None
|
||||
try:
|
||||
res = subprocess.run(cmd, check=True, stdout=subprocess.PIPE,
|
||||
stderr=subprocess.PIPE, text=True)
|
||||
if res.returncode == 0:
|
||||
if res.stdout:
|
||||
ret = res.stdout.strip()
|
||||
else:
|
||||
ret = True
|
||||
except subprocess.CalledProcessError:
|
||||
return None
|
||||
finally:
|
||||
return ret
|
||||
|
||||
|
||||
def runcmdj(cmd):
|
||||
output = runcmd(cmd)
|
||||
if not output:
|
||||
return None
|
||||
try:
|
||||
data = json.loads(output)
|
||||
except json.JSONDecodeError:
|
||||
return None
|
||||
finally:
|
||||
return data
|
||||
|
||||
|
||||
def scan(mpath):
|
||||
output = runcmdj(['mmcli', '-J', '-m', mpath,
|
||||
'--3gpp-scan', '--timeout', '60'])
|
||||
if not output:
|
||||
return False
|
||||
|
||||
networks = []
|
||||
for entry in output["modem"]["3gpp"]["scan-networks"]:
|
||||
net = {}
|
||||
for attr in entry.split(", "):
|
||||
(key, value) = attr.split(": ")
|
||||
net[key] = value
|
||||
|
||||
if "operator-name" in net:
|
||||
networks.append(net)
|
||||
|
||||
if len(networks) == 0:
|
||||
print("No networks found")
|
||||
return True
|
||||
|
||||
for net in networks:
|
||||
print("Network '%s' %s (%s, %s)" % (
|
||||
net.get("operator-name"),
|
||||
net.get("operator-code"),
|
||||
net.get("access-technologies"),
|
||||
net.get("availability")))
|
||||
return True
|
||||
|
||||
|
||||
def main():
|
||||
global index
|
||||
parser = argparse.ArgumentParser(prog='modem-scan-networks')
|
||||
parser.add_argument("-i", "--index", default=0, help="Modem index")
|
||||
args = parser.parse_args()
|
||||
if args.index:
|
||||
index = int(args.index)
|
||||
|
||||
for m in runcmdj(['/usr/libexec/modemd/modem-info']):
|
||||
if m["index"] == index:
|
||||
print("Scanning networks on modem%d, please stand by ..." % index)
|
||||
if not scan(m["path"]):
|
||||
print("Operation failed.", file=sys.stderr)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,267 @@
|
||||
import subprocess
|
||||
import json
|
||||
import fcntl
|
||||
import struct
|
||||
import time
|
||||
import syslog
|
||||
import sys
|
||||
import os
|
||||
|
||||
syslog.openlog(logoption=syslog.LOG_PID, facility=syslog.LOG_SYSLOG)
|
||||
|
||||
def log(msg):
|
||||
syslog.syslog(syslog.LOG_INFO, msg)
|
||||
|
||||
|
||||
def err(msg):
|
||||
syslog.syslog(syslog.LOG_ERR, msg)
|
||||
|
||||
|
||||
def fatal(msg):
|
||||
syslog.syslog(syslog.LOG_ALERT, msg)
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
def runcmd(cmd):
|
||||
ret = None
|
||||
try:
|
||||
res = subprocess.run(cmd, check=True, stdout=subprocess.PIPE,
|
||||
stderr=subprocess.PIPE, text=True)
|
||||
if res.returncode == 0:
|
||||
if res.stdout:
|
||||
ret = res.stdout.strip()
|
||||
else:
|
||||
ret = True
|
||||
except subprocess.CalledProcessError:
|
||||
return None
|
||||
finally:
|
||||
return ret
|
||||
|
||||
|
||||
def runcmdj(cmd):
|
||||
output = runcmd(cmd)
|
||||
if not output:
|
||||
return None
|
||||
try:
|
||||
data = json.loads(output)
|
||||
except json.JSONDecodeError:
|
||||
return None
|
||||
finally:
|
||||
return data
|
||||
|
||||
|
||||
def fread(path):
|
||||
if os.path.exists(path):
|
||||
with open(path, "r") as fd:
|
||||
output = str(fd.read())
|
||||
if output:
|
||||
return output.strip()
|
||||
return None
|
||||
|
||||
|
||||
def freadj(path):
|
||||
output = fread(path)
|
||||
if not output:
|
||||
return None
|
||||
try:
|
||||
data = json.loads(output)
|
||||
except json.JSONDecodeError:
|
||||
err("Unable to parse json output")
|
||||
return None
|
||||
finally:
|
||||
return data
|
||||
|
||||
|
||||
def find_modem(setup, index):
|
||||
for modem in setup["modems"]:
|
||||
if modem["index"] == index:
|
||||
return modem
|
||||
return None
|
||||
|
||||
|
||||
def find_sim(setup, index):
|
||||
for sim in setup["sims"]:
|
||||
if sim["index"] == index:
|
||||
return sim
|
||||
return None
|
||||
|
||||
|
||||
def pwrctl(setup, state):
|
||||
for modem in setup["modems"]:
|
||||
path = "/sys/class/pcie/slot%d/pwrctl" % modem["slot"]
|
||||
if not os.path.exists(path):
|
||||
continue
|
||||
with open(path, "w") as fd:
|
||||
fd.write("%s\n" % state)
|
||||
|
||||
|
||||
def sim_ctrl(cmd, slots):
|
||||
req = struct.pack('2i', *slots)
|
||||
|
||||
if not os.path.exists("/dev/simctrl"):
|
||||
return None
|
||||
|
||||
try:
|
||||
fd = os.open("/dev/simctrl", os.O_RDWR)
|
||||
except OSError:
|
||||
fatal("Cannot open sim ctrl")
|
||||
|
||||
try:
|
||||
resp = fcntl.ioctl(fd, cmd, req)
|
||||
except OSError:
|
||||
fatal("Cannot run sim ctrl ioctl")
|
||||
|
||||
if not resp:
|
||||
return None
|
||||
else:
|
||||
return struct.unpack('2i', resp)
|
||||
|
||||
|
||||
def sim_get_slots():
|
||||
# NM_SIMIOC_SLOT_GET is 0x80085300 (see linux/nm-sim.h)
|
||||
return sim_ctrl(0x80085300, (0, 0))
|
||||
|
||||
|
||||
def sim_set_slots(slots):
|
||||
# NM_SIMIOC_SLOT_SET is 0x40085301 (see linux/nm-sim.h)
|
||||
return sim_ctrl(0x40085301, slots)
|
||||
|
||||
|
||||
def slot_setup(setup):
|
||||
slots = []
|
||||
for sim in setup["sims"]:
|
||||
log("Connecting sim%d to slot %d" % (sim["index"], sim["newslot"]))
|
||||
slots.append(sim["newslot"])
|
||||
|
||||
if sim_set_slots(tuple(slots)) is None:
|
||||
return False
|
||||
else:
|
||||
return True
|
||||
|
||||
|
||||
def change_setup(setup):
|
||||
log("Changing SIM setup")
|
||||
|
||||
runcmd(["initctl", "stop", "modem-manager"])
|
||||
time.sleep(1)
|
||||
|
||||
pwrctl(setup, "down")
|
||||
time.sleep(1)
|
||||
|
||||
if slot_setup(setup) is False:
|
||||
err("Unable to set up sim slots")
|
||||
|
||||
pwrctl(setup, "up")
|
||||
|
||||
time.sleep(1)
|
||||
runcmd(["initctl", "restart", "modem-manager"])
|
||||
|
||||
return True
|
||||
|
||||
|
||||
def swapslot(slot):
|
||||
if slot == 0:
|
||||
return 1
|
||||
elif slot == 1:
|
||||
return 0
|
||||
else:
|
||||
return -1
|
||||
|
||||
|
||||
def sim_setup(setup):
|
||||
changed = False
|
||||
|
||||
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
|
||||
|
||||
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
|
||||
|
||||
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"]:
|
||||
changed = True
|
||||
|
||||
if not changed:
|
||||
log("SIMs already set up")
|
||||
return True
|
||||
|
||||
if len(setup["modems"]) == 2 and len(setup["sims"]) == 2:
|
||||
for i in range(0, 2):
|
||||
o = 1 if i == 0 else 0
|
||||
if setup["sims"][i]["newslot"] == -1:
|
||||
log("Swapping slot of sim%d" % setup["sims"][i]["index"])
|
||||
setup["sims"][i]["newslot"] = swapslot(setup["sims"][o]["newslot"])
|
||||
|
||||
return change_setup(setup)
|
||||
|
||||
|
||||
def read_setup():
|
||||
setup = {
|
||||
"modems": [],
|
||||
"sims": []
|
||||
}
|
||||
|
||||
modules = freadj("/run/modules.json")
|
||||
if not modules:
|
||||
log("No modules.json found, skipping SIM setup")
|
||||
return None
|
||||
|
||||
for module in modules.get("modules", []):
|
||||
if module["slot"] > -1 and module["type"] == "modem":
|
||||
setup["modems"].append({"index": module["index"],
|
||||
"slot": module["slot"]})
|
||||
|
||||
slots = sim_get_slots()
|
||||
if slots:
|
||||
for index in range(0, len(slots)):
|
||||
setup["sims"].append({"index": index,
|
||||
"slot": slots[index],
|
||||
"newslot": -1})
|
||||
|
||||
return setup
|
||||
|
||||
|
||||
def main():
|
||||
setup = read_setup()
|
||||
if setup is None:
|
||||
sys.exit(0)
|
||||
|
||||
if sim_setup(setup) is False:
|
||||
fatal("Unable set up SIMs")
|
||||
|
||||
sys.exit(0)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,144 @@
|
||||
from datetime import datetime
|
||||
import argparse
|
||||
import locale
|
||||
import subprocess
|
||||
import json
|
||||
import glob
|
||||
import re
|
||||
import sys
|
||||
import os
|
||||
|
||||
LOCALEPATH = "/usr/lib/locale"
|
||||
|
||||
|
||||
def run_cmd(cmd):
|
||||
try:
|
||||
res = subprocess.run(cmd, capture_output=True, text=True)
|
||||
return True if res.returncode == 0 else False
|
||||
except subprocess.CalledProcessError:
|
||||
return False
|
||||
|
||||
|
||||
def send(index, number, text):
|
||||
if index < 0:
|
||||
# use first modem by default
|
||||
index = 0
|
||||
|
||||
rpc = {
|
||||
"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)
|
||||
|
||||
path = "/run/modemd/modem%d/rpc" % index
|
||||
if os.path.exists(path):
|
||||
sys.stderr.write("Error: another rpc is running\n")
|
||||
return
|
||||
|
||||
with open(path, "w") as fd:
|
||||
json.dump(rpc, fd)
|
||||
|
||||
if not run_cmd(['sysrepocfg', '-f', 'json', '-R', path]):
|
||||
sys.stderr.write("Error: Unable to send SMS\n")
|
||||
|
||||
os.unlink(path)
|
||||
|
||||
|
||||
def listsms():
|
||||
smslist = []
|
||||
files = glob.glob('/var/sms/*')
|
||||
for f in files:
|
||||
with open(f, "r") as fd:
|
||||
sms = json.load(fd)
|
||||
sms["path"] = f
|
||||
|
||||
t = sms["payload"]["properties"]["timestamp"]
|
||||
t = re.sub("[+-][\\d:]+$", "", t)
|
||||
sms["time"] = datetime.strptime(t, "%Y-%m-%dT%H:%M:%S")
|
||||
|
||||
smslist.append(sms)
|
||||
|
||||
return sorted(smslist, key=lambda sms: sms["time"])
|
||||
|
||||
|
||||
def delete(index=-1):
|
||||
count = 0
|
||||
for sms in listsms():
|
||||
if index > -1 and sms["modem"] == index:
|
||||
os.remove(sms["path"])
|
||||
count += 1
|
||||
print("Deleted %d SMS files" % count)
|
||||
|
||||
|
||||
def show(index=-1):
|
||||
for sms in listsms():
|
||||
if index > -1 and sms["modem"] != index:
|
||||
continue
|
||||
|
||||
content = sms["payload"]["content"]
|
||||
prop = sms["payload"]["properties"]
|
||||
|
||||
print("--- %s ---\n" % os.path.basename(sms["path"]))
|
||||
print("From: %s" % content["number"])
|
||||
print("SMSC: %s" % prop["smsc"])
|
||||
print("Modem: %d" % sms["modem"])
|
||||
print("Date: %s" % prop["timestamp"])
|
||||
print("State: %s" % prop["state"])
|
||||
print("\n%s\n" % content["text"])
|
||||
|
||||
|
||||
def listlocale():
|
||||
locales = []
|
||||
for f in os.listdir(LOCALEPATH):
|
||||
if os.path.isdir(os.path.join(LOCALEPATH, f)):
|
||||
locales.append(f)
|
||||
return ", ".join(locales)
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(prog='modem-sms')
|
||||
parser.add_argument("-d", action="store_true", help="Delete SMS")
|
||||
parser.add_argument("-s", action="store_true", help="Send SMS")
|
||||
|
||||
parser.add_argument("-i", "--index", default=0, help="Modem index")
|
||||
parser.add_argument("-n", "--number", default=None, help="Phone number")
|
||||
parser.add_argument("-t", "--text", default=None, help="Message text")
|
||||
|
||||
parser.add_argument("-l", "--locale", default=None, help="Set locale")
|
||||
args = parser.parse_args()
|
||||
|
||||
index = -1
|
||||
if args.index:
|
||||
index = int(args.index)
|
||||
|
||||
if args.locale:
|
||||
try:
|
||||
locale.setlocale(locale.LC_ALL, args.locale)
|
||||
except locale.Error:
|
||||
print("Error: Invalid locale (available: %s)" % listlocale())
|
||||
sys.exit(1)
|
||||
|
||||
if args.s:
|
||||
if not args.number:
|
||||
sys.stderr.write("Error: need number\n")
|
||||
elif not args.text:
|
||||
sys.stderr.write("Error: need text\n")
|
||||
else:
|
||||
send(index, args.number, args.text)
|
||||
elif args.d:
|
||||
delete(index)
|
||||
else:
|
||||
show(index)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,319 @@
|
||||
import shutil
|
||||
import errno
|
||||
import time
|
||||
import json
|
||||
import fcntl
|
||||
import sys
|
||||
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
|
||||
|
||||
|
||||
def log(msg):
|
||||
ts = '{:010.3f}'.format(time.monotonic())
|
||||
with open(LOGFILE, 'a') as fd:
|
||||
fd.write("[%s] modem-udev (pid %d) %s\n" % (ts, os.getpid(), msg))
|
||||
|
||||
|
||||
def dbg(msg):
|
||||
global debug
|
||||
if debug:
|
||||
log(msg)
|
||||
|
||||
|
||||
def info(msg):
|
||||
log(msg)
|
||||
|
||||
|
||||
def err(msg):
|
||||
log("Error: %s" % msg)
|
||||
|
||||
|
||||
def lock():
|
||||
start = time.time()
|
||||
while True:
|
||||
elapsed = time.time() - start
|
||||
if elapsed > 10:
|
||||
err("Timeout waiting for lock")
|
||||
break
|
||||
try:
|
||||
fd = open(LOCK, "a")
|
||||
fcntl.flock(fd.fileno(), fcntl.LOCK_EX)
|
||||
dbg("Acquired lock")
|
||||
return fd
|
||||
except IOError as e:
|
||||
dbg("Unable to lock (%d)" % e.errno)
|
||||
if e.errno != errno.EAGAIN:
|
||||
err("Unable to lock")
|
||||
return None
|
||||
else:
|
||||
time.sleep(1)
|
||||
|
||||
return None
|
||||
|
||||
|
||||
def unlock(fd):
|
||||
try:
|
||||
fcntl.flock(fd.fileno(), fcntl.LOCK_UN)
|
||||
fd.close()
|
||||
dbg("Released lock")
|
||||
return True
|
||||
except IOError as e:
|
||||
err("Unlock failed with %d" % e.errno)
|
||||
return False
|
||||
|
||||
|
||||
def mkdir(path):
|
||||
if os.path.isdir(path):
|
||||
return True
|
||||
try:
|
||||
os.mkdir(path, mode=0o755)
|
||||
shutil.chown(path, user="root", group="wheel")
|
||||
except OSError:
|
||||
err("Unable to mkdir %s" % path)
|
||||
return False
|
||||
finally:
|
||||
return True
|
||||
|
||||
|
||||
def read_modem_data():
|
||||
data = {}
|
||||
if os.path.exists(MODEMS):
|
||||
with open(MODEMS, "r") as fd:
|
||||
data = json.load(fd)
|
||||
return data
|
||||
|
||||
|
||||
def write_modem_data(data):
|
||||
with open(MODEMS + ".bak", 'w') as fd:
|
||||
json.dump(data, fd)
|
||||
|
||||
os.rename(MODEMS + ".bak", MODEMS)
|
||||
return True
|
||||
|
||||
|
||||
def _sysfs_attr(devpath, name):
|
||||
try:
|
||||
with open("%s/%s" % (devpath, name), "r") as fd:
|
||||
return fd.readline().strip()
|
||||
except OSError:
|
||||
return None
|
||||
|
||||
|
||||
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:
|
||||
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):
|
||||
modem = None
|
||||
devpath = d.get("devpath")
|
||||
|
||||
data = read_modem_data()
|
||||
if "modems" not in data:
|
||||
data = {"modems": []}
|
||||
else:
|
||||
for m in data["modems"]:
|
||||
if devpath == m.get("devpath"):
|
||||
data["modems"].remove(m)
|
||||
modem = m
|
||||
break
|
||||
|
||||
if not modem:
|
||||
modem = {"devpath": devpath}
|
||||
|
||||
vendor = _sysfs_attr(devpath, "idVendor")
|
||||
if vendor:
|
||||
modem["vendor"] = vendor
|
||||
|
||||
product = _sysfs_attr(devpath, "idProduct")
|
||||
if product:
|
||||
modem["product"] = product
|
||||
|
||||
atport = d.get("atport")
|
||||
atports = modem.get("atports", [])
|
||||
if atport and atport not in atports:
|
||||
ptype = d.get("ptype", "default")
|
||||
if ptype == "primary":
|
||||
atports.insert(0, atport)
|
||||
else:
|
||||
atports.append(atport)
|
||||
modem["atports"] = atports
|
||||
|
||||
qmiport = d.get("qmiport")
|
||||
qmiports = modem.get("qmiports", [])
|
||||
if qmiport and qmiport not in qmiports:
|
||||
qmiports.append(qmiport)
|
||||
modem["qmiports"] = qmiports
|
||||
|
||||
iface = d.get("iface")
|
||||
ifaces = modem.get("interfaces", [])
|
||||
if iface and iface not in ifaces:
|
||||
ifaces.append(iface)
|
||||
modem["interfaces"] = ifaces
|
||||
|
||||
data["modems"].append(modem)
|
||||
write_modem_data(data)
|
||||
update_system_json(data["modems"])
|
||||
|
||||
info("Updated modem %s" % devpath)
|
||||
|
||||
|
||||
def add_tty(devpath):
|
||||
devpath = os.path.realpath("/sys/%s/../../../../" % devpath)
|
||||
|
||||
info("Adding tty device %s" % devpath)
|
||||
|
||||
atport = os.getenv("DEVNAME")
|
||||
if not atport:
|
||||
err("No DEVNAME")
|
||||
return False
|
||||
|
||||
if os.getenv("ID_MM_PORT_TYPE_AT_PRIMARY"):
|
||||
ptype = "primary"
|
||||
elif os.getenv("ID_MM_PORT_TYPE_AT_SECONDARY"):
|
||||
ptype = "secondary"
|
||||
else:
|
||||
ptype = "default"
|
||||
|
||||
info("Adding %s atport %s for %s" % (ptype, atport, devpath))
|
||||
|
||||
update({"devpath": devpath, "atport": atport, "ptype": ptype})
|
||||
|
||||
|
||||
def add_net(devpath):
|
||||
devpath = os.path.realpath("/sys/%s/../../../" % devpath)
|
||||
|
||||
info("Adding net device %s" % devpath)
|
||||
|
||||
iface = os.getenv("INTERFACE", None)
|
||||
if not iface:
|
||||
err("No INTERFACE")
|
||||
return False
|
||||
|
||||
r = re.search(r"wwan(\d+)\.(\d+)", iface)
|
||||
if r and r.group(2):
|
||||
info("Skipping interface %s" % iface)
|
||||
return False
|
||||
|
||||
info("Adding interface %s for %s" % (iface, devpath))
|
||||
update({"devpath": devpath, "iface": iface})
|
||||
|
||||
try:
|
||||
with open("/sys/class/net/%s/qmi/raw_ip" % iface, "w") as fd:
|
||||
fd.write("Y\n")
|
||||
except (IOError, OSError):
|
||||
err("Unable to set raw-ip")
|
||||
return False
|
||||
|
||||
info("Interface %s has been set to rawip" % iface)
|
||||
|
||||
|
||||
def add_usbmisc(devpath):
|
||||
devpath = os.path.realpath("/sys/%s/../../../" % devpath)
|
||||
|
||||
info("Adding usbmisc device %s" % devpath)
|
||||
|
||||
qmiport = os.getenv("DEVNAME", None)
|
||||
if not qmiport:
|
||||
err("No DEVNAME")
|
||||
return False
|
||||
|
||||
info("Adding qmiport %s for %s" % (qmiport, devpath))
|
||||
update({"devpath": devpath, "qmiport": qmiport})
|
||||
|
||||
|
||||
def apply():
|
||||
subsystem = os.getenv("SUBSYSTEM")
|
||||
if not subsystem:
|
||||
err("No SUBSYSTEM")
|
||||
return False
|
||||
|
||||
devpath = os.getenv("DEVPATH")
|
||||
if not devpath:
|
||||
err("No DEVPATH")
|
||||
return False
|
||||
|
||||
if "usb" not in devpath:
|
||||
info("Skipping %s" % devpath)
|
||||
return False
|
||||
|
||||
if subsystem == "net":
|
||||
add_net(devpath)
|
||||
elif subsystem == "tty":
|
||||
add_tty(devpath)
|
||||
elif subsystem == "usbmisc":
|
||||
add_usbmisc(devpath)
|
||||
else:
|
||||
err("Subsystem %s unhandled" % subsystem)
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
|
||||
def main():
|
||||
global debug
|
||||
with open("/proc/cmdline", 'r') as fd:
|
||||
cmdline = fd.read()
|
||||
if "debug" in cmdline:
|
||||
debug = True
|
||||
|
||||
mkdir("/run/modemd")
|
||||
dbg("started")
|
||||
|
||||
if debug:
|
||||
for i, j in os.environ.items():
|
||||
dbg("ENV: %s=%s" % (i, j))
|
||||
else:
|
||||
if os.path.exists(LOGFILE):
|
||||
os.remove(LOGFILE)
|
||||
|
||||
fd = lock()
|
||||
if fd is None:
|
||||
sys.exit(2)
|
||||
|
||||
if apply():
|
||||
rc = 0
|
||||
else:
|
||||
err("Unable to add modem device")
|
||||
rc = 1
|
||||
|
||||
unlock(fd)
|
||||
|
||||
dbg("ended")
|
||||
|
||||
sys.exit(rc)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,392 @@
|
||||
import subprocess
|
||||
import argparse
|
||||
import urllib.parse
|
||||
import hashlib
|
||||
import syslog
|
||||
import json
|
||||
import time
|
||||
import os
|
||||
import sys
|
||||
|
||||
tempdir = "/tmp/modem-fwupdate"
|
||||
debug = False
|
||||
index = 0
|
||||
|
||||
syslog.openlog(logoption=syslog.LOG_PID, facility=syslog.LOG_SYSLOG)
|
||||
|
||||
|
||||
def dbg(msg):
|
||||
global debug
|
||||
if debug:
|
||||
syslog.syslog(syslog.LOG_INFO, msg)
|
||||
|
||||
|
||||
def info(msg):
|
||||
syslog.syslog(syslog.LOG_INFO, msg)
|
||||
|
||||
|
||||
def err(msg):
|
||||
syslog.syslog(syslog.LOG_ERR, msg)
|
||||
|
||||
|
||||
def fatal(msg):
|
||||
syslog.syslog(syslog.LOG_ALERT, msg)
|
||||
rmdir(tempdir)
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
def runcmd(cmd):
|
||||
ret = None
|
||||
try:
|
||||
res = subprocess.run(cmd, check=True, stdout=subprocess.PIPE,
|
||||
stderr=subprocess.PIPE, text=True)
|
||||
if res.returncode == 0:
|
||||
if res.stdout:
|
||||
ret = res.stdout.strip()
|
||||
else:
|
||||
ret = True
|
||||
except subprocess.CalledProcessError:
|
||||
err("Command failed")
|
||||
dbg(res.stderr)
|
||||
return None
|
||||
finally:
|
||||
return ret
|
||||
|
||||
|
||||
def runcmdj(cmd):
|
||||
output = runcmd(cmd)
|
||||
if not output:
|
||||
return None
|
||||
try:
|
||||
data = json.loads(output)
|
||||
except json.JSONDecodeError:
|
||||
return None
|
||||
finally:
|
||||
return data
|
||||
|
||||
|
||||
def exists(path):
|
||||
try:
|
||||
os.stat(path)
|
||||
except OSError:
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
def rm(path):
|
||||
if os.path.exists(path):
|
||||
os.remove(path)
|
||||
|
||||
|
||||
def rmdir(path):
|
||||
if os.path.isdir(path):
|
||||
for f in os.listdir(path):
|
||||
p = os.path.join(path, f)
|
||||
if os.path.isdir(p):
|
||||
rmdir(p)
|
||||
else:
|
||||
os.remove(p)
|
||||
|
||||
|
||||
def mkdir(path):
|
||||
if os.path.isdir(path):
|
||||
return True
|
||||
try:
|
||||
os.mkdir(path, mode=0o755)
|
||||
except OSError:
|
||||
fatal("Unable to mkdir %s" % path)
|
||||
|
||||
|
||||
def initctl(cmd, svc):
|
||||
output = runcmdj(['initctl', '-j', 'status'])
|
||||
for ctl in output:
|
||||
if ctl["identity"] == svc:
|
||||
return runcmd(['initctl', cmd, svc])
|
||||
return False
|
||||
|
||||
|
||||
def get_path(index):
|
||||
for modem in runcmdj(['/usr/libexec/modemd/modem-info']):
|
||||
if modem["index"] == index:
|
||||
return modem["path"]
|
||||
|
||||
|
||||
def get_device_info(index):
|
||||
path = get_path(index)
|
||||
if path is None:
|
||||
return None
|
||||
|
||||
output = runcmdj(['mmcli', '-J', '-m', path])
|
||||
if not output:
|
||||
return None
|
||||
|
||||
gen = output["modem"]["generic"]
|
||||
info = {
|
||||
"index": index,
|
||||
"vendor": gen["manufacturer"],
|
||||
"model": gen["model"],
|
||||
"state": gen["state"],
|
||||
"qmidev": [],
|
||||
"atdev": []
|
||||
}
|
||||
for port in gen["ports"]:
|
||||
if "(qmi)" in port:
|
||||
info["qmidev"].append(port.split()[0])
|
||||
elif "(at)" in port:
|
||||
info["atdev"].append(port.split()[0])
|
||||
|
||||
return info
|
||||
|
||||
|
||||
def wait_for_device(dev, appear):
|
||||
if appear:
|
||||
mode = "appear"
|
||||
else:
|
||||
mode = "disappear"
|
||||
|
||||
info("Waiting for modem%d to %s" % (dev["index"], mode))
|
||||
|
||||
path = "/dev/%s" % dev["qmidev"][0]
|
||||
timeout = 30
|
||||
while timeout > 0:
|
||||
if mode == "appear":
|
||||
if exists(path):
|
||||
return True
|
||||
elif mode == "disappear":
|
||||
if not exists(path):
|
||||
return True
|
||||
time.sleep(1)
|
||||
timeout -= 1
|
||||
return False
|
||||
|
||||
|
||||
def qmiupdate(dev):
|
||||
if len(dev["qmidev"]) == 0:
|
||||
err("No qmi device found")
|
||||
return False
|
||||
|
||||
qmidev = "/dev/%s" % dev["qmidev"][0]
|
||||
cwefile = None
|
||||
nvufile = None
|
||||
|
||||
for f in os.scandir(tempdir):
|
||||
if f.name.endswith('cwe'):
|
||||
cwefile = "%s/%s" % (tempdir, f.name)
|
||||
if f.name.endswith('nvu'):
|
||||
nvufile = "%s/%s" % (tempdir, f.name)
|
||||
|
||||
if not cwefile:
|
||||
err("No CWE file found")
|
||||
return False
|
||||
if not nvufile:
|
||||
err("No NVU file found")
|
||||
return False
|
||||
|
||||
info("Resetting modem")
|
||||
if not runcmd(['qmicli', '-d', qmidev,
|
||||
'--dms-set-operating-mode=offline']):
|
||||
err("Unable to set modem offline")
|
||||
return False
|
||||
if not runcmd(['qmicli', '-d', qmidev,
|
||||
'--dms-set-operating-mode=reset']):
|
||||
err("Unable to reset modem")
|
||||
return False
|
||||
|
||||
if not wait_for_device(dev, 0):
|
||||
err("Device has not disappeared")
|
||||
return False
|
||||
if not wait_for_device(dev, 1):
|
||||
err("Device has not appeared")
|
||||
return False
|
||||
|
||||
info("Running qmi-firmware-update, please stand by...")
|
||||
|
||||
cmd = ['qmi-firmware-update', '-v', '--override-download',
|
||||
'-w', qmidev, '-u', cwefile, nvufile]
|
||||
return runcmd(cmd)
|
||||
|
||||
|
||||
def firmware_update_sierra(dev, url, checksum):
|
||||
if not download(url, checksum):
|
||||
err("Unable to prepare firmware")
|
||||
return False
|
||||
|
||||
info("Stopping modem services")
|
||||
initctl("stop", "modemd")
|
||||
initctl("stop", "modem-manager")
|
||||
|
||||
time.sleep(3)
|
||||
ret = qmiupdate(dev)
|
||||
|
||||
info("Starting modem services")
|
||||
initctl("restart", "modem-manager")
|
||||
initctl("restart", "modemd")
|
||||
|
||||
return ret
|
||||
|
||||
|
||||
def atcmd(dev, command, expect=None, timeout=None):
|
||||
device = "/dev/%s" % dev["atdev"][0]
|
||||
|
||||
cmd = ['modem-command', '-d', device, command]
|
||||
if expect:
|
||||
cmd += ['-e', expect]
|
||||
if timeout:
|
||||
cmd += ['-t', str(timeout)]
|
||||
|
||||
return runcmd(cmd)
|
||||
|
||||
|
||||
def firmware_update_telit(dev, url, checksum):
|
||||
if 'LN920' not in dev["model"]:
|
||||
err("Not supported")
|
||||
return False
|
||||
|
||||
if len(dev["atdev"]) == 0:
|
||||
err("No AT device found")
|
||||
return False
|
||||
|
||||
if dev["state"] != "connected":
|
||||
err("Modem is not connected")
|
||||
return False
|
||||
|
||||
u = urllib.parse.urlparse(url)
|
||||
if not u:
|
||||
err("Unable to parse URL")
|
||||
return False
|
||||
if u.scheme != "ftp":
|
||||
err("Not an FTP URL")
|
||||
return False
|
||||
|
||||
hostname = ""
|
||||
username = ""
|
||||
password = ""
|
||||
port = 21
|
||||
|
||||
if u.hostname:
|
||||
hostname = u.hostname
|
||||
if u.username:
|
||||
username = u.username
|
||||
if u.password:
|
||||
password = u.password
|
||||
if u.port:
|
||||
port = int(u.port)
|
||||
if u.path:
|
||||
path = u.path
|
||||
|
||||
if hostname == "":
|
||||
err("No hostname in URL")
|
||||
return False
|
||||
if path == "":
|
||||
err("No path in URL")
|
||||
return False
|
||||
|
||||
parms = (hostname, port, path, username, password)
|
||||
cmd = 'AT#FTPGETOTAENH=%s,%d,%s,%s,%s' % parms
|
||||
exp = '#DREL'
|
||||
if not atcmd(dev, cmd, exp, 60):
|
||||
err("Unable to download firmware")
|
||||
return False
|
||||
|
||||
if not atcmd(dev, 'AT#OTAUP=2'):
|
||||
err("Unable to update firmware")
|
||||
return False
|
||||
|
||||
if not atcmd(dev, 'AT#ENHRST=1,3'):
|
||||
err("Unable to reboot modem")
|
||||
return False
|
||||
|
||||
time.sleep(5)
|
||||
info("Restarting modem services")
|
||||
initctl("restart", "modem-manager")
|
||||
initctl("restart", "modemd")
|
||||
return True
|
||||
|
||||
|
||||
def verify(path, checksum):
|
||||
if not checksum or checksum == "any":
|
||||
return True
|
||||
|
||||
sha256 = hashlib.sha256()
|
||||
with open(path, 'rb') as f:
|
||||
while True:
|
||||
data = f.read(1024)
|
||||
if not data:
|
||||
break
|
||||
sha256.update(data)
|
||||
|
||||
calculated = sha256.hexdigest()
|
||||
|
||||
if checksum != calculated:
|
||||
err("Checksum mismatch (%s vs. %s)" % (calculated, checksum))
|
||||
return False
|
||||
else:
|
||||
info("Verified checksum")
|
||||
return True
|
||||
|
||||
|
||||
def download(url, checksum):
|
||||
path = "%s/firmware.zip" % tempdir
|
||||
|
||||
info("Downloading %s" % url)
|
||||
|
||||
if not runcmd(['curl', '-s', '-L', url, '-o', path]):
|
||||
err("Unable to download firmware")
|
||||
return False
|
||||
|
||||
if not verify(path, checksum):
|
||||
err("Checksum verification failed")
|
||||
return False
|
||||
|
||||
if not runcmd(['unzip', '-d', tempdir, path]):
|
||||
err("Unable to unpack firmware")
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
|
||||
def main():
|
||||
global debug, index
|
||||
syslog.openlog(logoption=syslog.LOG_PID, facility=syslog.LOG_DAEMON)
|
||||
|
||||
parser = argparse.ArgumentParser(prog='modem-firmware-update')
|
||||
parser.add_argument("-i", "--index", default=None, help="Modem index")
|
||||
parser.add_argument("-u", "--url", default=None, help="URL to firmware")
|
||||
parser.add_argument("-c", "--checksum", default=None,
|
||||
help="SHA256 checksum of firmware")
|
||||
parser.add_argument('-d', action='store_true')
|
||||
|
||||
args = parser.parse_args()
|
||||
if args.d:
|
||||
debug = True
|
||||
if args.index:
|
||||
index = int(args.index)
|
||||
if not args.url:
|
||||
fatal("No firmware URL specified")
|
||||
|
||||
rmdir(tempdir)
|
||||
mkdir(tempdir)
|
||||
|
||||
dev = get_device_info(index)
|
||||
if dev is None:
|
||||
fatal("No device found for modem%d" % index)
|
||||
|
||||
info("Starting firmware update for %s" % dev["model"])
|
||||
|
||||
if "Sierra" in dev["vendor"]:
|
||||
ret = firmware_update_sierra(dev, args.url, args.checksum)
|
||||
elif "Telit" in dev["vendor"]:
|
||||
ret = firmware_update_telit(dev, args.url, args.checksum)
|
||||
else:
|
||||
fatal("Unsupported vendor")
|
||||
|
||||
if ret:
|
||||
info("Firmware update succeeded")
|
||||
else:
|
||||
fatal("Firmware update failed")
|
||||
|
||||
rmdir(tempdir)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,75 @@
|
||||
import argparse
|
||||
import subprocess
|
||||
import sys
|
||||
|
||||
verbose = False
|
||||
|
||||
|
||||
def runcmd(cmd):
|
||||
if verbose:
|
||||
print("Running: %s" % " ".join(cmd))
|
||||
|
||||
ret = None
|
||||
try:
|
||||
res = subprocess.run(cmd, capture_output=True, text=True)
|
||||
if verbose:
|
||||
print(res.stdout)
|
||||
if res.returncode == 0:
|
||||
if res.stdout:
|
||||
ret = res.stdout.strip()
|
||||
else:
|
||||
ret = True
|
||||
except subprocess.CalledProcessError:
|
||||
return None
|
||||
finally:
|
||||
return ret
|
||||
|
||||
|
||||
def ussd(index, code):
|
||||
cmd = ['/sbin/modem-command',
|
||||
'--index', str(index),
|
||||
'--expect', '+CUSD',
|
||||
'--timeout', '60',
|
||||
'AT+CUSD=1,"%s",15' % code]
|
||||
|
||||
if verbose:
|
||||
cmd.append('--verbose')
|
||||
|
||||
output = runcmd(cmd)
|
||||
if output is None:
|
||||
sys.stderr.write("ERROR: Command failed\n")
|
||||
return False
|
||||
|
||||
resp = output.split('"')
|
||||
if len(resp) == 3:
|
||||
print(resp[1])
|
||||
|
||||
return True
|
||||
|
||||
|
||||
def main():
|
||||
global verbose
|
||||
parser = argparse.ArgumentParser(prog='modem-ussd')
|
||||
parser.add_argument("-i", "--index", default=0, help="Modem index")
|
||||
parser.add_argument("-c", "--code", default=None, help="USSD codec")
|
||||
parser.add_argument("-v", "--verbose", action="store_true")
|
||||
args = parser.parse_args()
|
||||
|
||||
if args.verbose:
|
||||
verbose = True
|
||||
|
||||
index = 0
|
||||
if args.index:
|
||||
index = int(args.index)
|
||||
|
||||
if not args.code:
|
||||
sys.stderr.write("Error: need USSD code\n")
|
||||
else:
|
||||
if ussd(index, args.code):
|
||||
sys.exit(0)
|
||||
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,28 @@
|
||||
[tool.poetry]
|
||||
name = "modemd"
|
||||
version = "1.0"
|
||||
description = "Infix modem daemon and utilities"
|
||||
license = "BSD-3-Clause"
|
||||
packages = [
|
||||
{ include = "modemd" }
|
||||
]
|
||||
authors = [
|
||||
"KernelKit developers"
|
||||
]
|
||||
|
||||
[build-system]
|
||||
requires = ["poetry-core>=1.0.0"]
|
||||
build-backend = "poetry.core.masonry.api"
|
||||
|
||||
[tool.poetry.scripts]
|
||||
modemd = "modemd:main"
|
||||
modem-info = "modemd.info:main"
|
||||
modem-carrier = "modemd.carrier:main"
|
||||
modem-rpc = "modemd.rpc:main"
|
||||
modem-sms = "modemd.sms:main"
|
||||
modem-scan-networks = "modemd.scan_networks:main"
|
||||
modem-update-firmware = "modemd.update_firmware:main"
|
||||
modem-ussd = "modemd.ussd:main"
|
||||
modem-power = "modemd.power:main"
|
||||
modem-udev = "modemd.udev:main"
|
||||
sim-setup = "modemd.sim_setup:main"
|
||||
@@ -0,0 +1,7 @@
|
||||
# Bind USB modem devices whose IDs are not yet upstream in qmi_wwan.
|
||||
# Required until the IDs are accepted into the kernel driver.
|
||||
ACTION!="add", GOTO="qmi_wwan_ids_end"
|
||||
SUBSYSTEM!="usb", GOTO="qmi_wwan_ids_end"
|
||||
ENV{DEVTYPE}!="usb_device", GOTO="qmi_wwan_ids_end"
|
||||
|
||||
LABEL="qmi_wwan_ids_end"
|
||||
@@ -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,276 @@ 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(ms):
|
||||
parts = []
|
||||
sq = ms.get("signal-quality")
|
||||
if sq is not None:
|
||||
recency = "" if ms.get("signal-quality-recent", True) else " (stale)"
|
||||
word = signal_quality_to_status(sq)
|
||||
head = f"Quality: {sq}% {word}" if word else f"Quality: {sq}%"
|
||||
parts.append(f"{head}{recency}")
|
||||
for key, label, unit in (
|
||||
("signal-rssi", "RSSI", "dBm"),
|
||||
("signal-rsrp", "RSRP", "dBm"),
|
||||
("signal-rsrq", "RSRQ", "dB"),
|
||||
("signal-sinr", "SINR", "dB")):
|
||||
if key in ms:
|
||||
parts.append(f"{label}: {ms[key]} {unit}")
|
||||
return " ".join(parts)
|
||||
|
||||
|
||||
def _bearers_for_modem(json, modem_name):
|
||||
"""Return [(ifname, bearer_state, wwan_dict), ...] for wwan interfaces
|
||||
that reference 'modem_name', preserving sysrepo list order."""
|
||||
interfaces = get_json_data([], json, "ietf-interfaces:interfaces",
|
||||
"interface")
|
||||
out = []
|
||||
for iface in interfaces:
|
||||
if iface.get("type") != "infix-if-type:modem":
|
||||
continue
|
||||
wwan = iface.get("infix-interfaces:wwan") or {}
|
||||
if wwan.get("modem") != modem_name:
|
||||
continue
|
||||
bearer_state = wwan.get("bearer-state") or {}
|
||||
out.append((iface.get("name", "?"), bearer_state, wwan))
|
||||
return out
|
||||
|
||||
|
||||
def show_modem_detail(json, ref):
|
||||
components = get_json_data([], json, "ietf-hardware:hardware", "component")
|
||||
modem = next((c for c in components
|
||||
if c.get("class") == "infix-hardware:modem"
|
||||
and c.get("name") == ref), None)
|
||||
if modem is None:
|
||||
print(f"Modem '{ref}' not found.")
|
||||
return
|
||||
|
||||
ms = modem.get("infix-hardware:modem-state", {})
|
||||
cell = ms.get("cellular", {})
|
||||
location = ms.get("location-state", {})
|
||||
|
||||
sim_name = ref.replace("modem", "sim", 1)
|
||||
sim = next((c for c in components
|
||||
if c.get("class") == "infix-hardware:sim"
|
||||
and c.get("name") == sim_name), None)
|
||||
sim_state = sim.get("infix-hardware:sim-state", {}) if sim else {}
|
||||
|
||||
bearers = _bearers_for_modem(json, ref)
|
||||
|
||||
width = 62
|
||||
print(Decore.invert(f"{'MODEM: ' + ref:<{width}}"))
|
||||
|
||||
Decore.title("Hardware Information", width)
|
||||
for label, val in (
|
||||
("Manufacturer", ms.get("manufacturer", "")),
|
||||
("Model", ms.get("model", "")),
|
||||
("Hardware Revision", ms.get("hardware-revision", "")),
|
||||
("Firmware Version", ms.get("firmware-version", "")),
|
||||
("Serial Number", ms.get("serial-number", "")),
|
||||
("IMSI", ms.get("imsi", "")),
|
||||
("ICCID", ms.get("iccid", "")),
|
||||
):
|
||||
if val:
|
||||
print(f" {label:<20}: {val}")
|
||||
phone = ms.get("phone-number", [])
|
||||
if phone:
|
||||
print(f" {'Phone Number':<20}: {', '.join(phone)}")
|
||||
carriers = ms.get("supported-carrier", [])
|
||||
if carriers:
|
||||
print(f" {'Supported Carriers':<20}: {', '.join(carriers)}")
|
||||
selected = ms.get("selected-carrier", "")
|
||||
if selected:
|
||||
print(f" {'Selected Carrier':<20}: {selected}")
|
||||
|
||||
Decore.title("Status", width)
|
||||
state = ms.get("state", "unknown")
|
||||
print(f" {'State':<20}: {modem_state_to_status(state)}")
|
||||
if state == "failed":
|
||||
reason = ms.get("state-failed-reason")
|
||||
if reason and reason != "none":
|
||||
print(f" {'Failed Reason':<20}: {Decore.red(reason)}")
|
||||
power = ms.get("power-state")
|
||||
if power and power != "unknown":
|
||||
print(f" {'Power State':<20}: {power}")
|
||||
locks = ms.get("enabled-locks") or []
|
||||
if locks:
|
||||
print(f" {'Locks':<20}: {', '.join(locks)}")
|
||||
sig_str = _modem_signal_str(ms)
|
||||
if sig_str:
|
||||
print(f" {'Signal':<20}: {sig_str}")
|
||||
last_change = ms.get("last-change")
|
||||
if last_change:
|
||||
print(f" {'Last Update':<20}: {last_change}")
|
||||
|
||||
if cell:
|
||||
Decore.title("Cellular", width)
|
||||
for label, key in (
|
||||
("Registration", "registration-state"),
|
||||
("Service State", "service-state"),
|
||||
("Operator", "operator-name"),
|
||||
("Operator ID", "operator-id"),
|
||||
("Network Type", "network-type"),
|
||||
):
|
||||
val = cell.get(key, "")
|
||||
if val:
|
||||
print(f" {label:<20}: {val}")
|
||||
|
||||
# SIM Card section is now always rendered (sim-state is always emitted
|
||||
# by yanger). When the slot is empty, sim_state.state == 'not-inserted'
|
||||
# tells Andrew's "where's my missing SIM" story directly.
|
||||
Decore.title("SIM Card", width)
|
||||
print(f" {'Name':<20}: {sim.get('name', '') if sim else sim_name}")
|
||||
slot_val = sim_state.get("slot")
|
||||
if slot_val is not None:
|
||||
print(f" {'Slot':<20}: {slot_val}")
|
||||
lock_state = sim_state.get("state")
|
||||
if lock_state:
|
||||
print(f" {'Lock State':<20}: {sim_lock_to_status(lock_state)}")
|
||||
sim_op = sim_state.get("operator-name")
|
||||
if sim_op:
|
||||
print(f" {'Operator':<20}: {sim_op}")
|
||||
|
||||
if bearers:
|
||||
Decore.title("Bearers", width)
|
||||
for ifname, bstate, _wwan in bearers:
|
||||
connected = bstate.get("connected", False)
|
||||
print(f" {ifname} ({'connected' if connected else 'disconnected'})")
|
||||
if not connected:
|
||||
reason = bstate.get("connection-failed-reason", "")
|
||||
if reason:
|
||||
print(f" {'Error':<18}: {reason}")
|
||||
v4 = bstate.get("ipv4-address")
|
||||
v4pfx = bstate.get("ipv4-prefix-length", 0)
|
||||
if v4 and v4pfx:
|
||||
print(f" {'IPv4':<18}: {v4}/{v4pfx}")
|
||||
v6 = bstate.get("ipv6-address")
|
||||
v6pfx = bstate.get("ipv6-prefix-length", 0)
|
||||
if v6 and v6pfx:
|
||||
print(f" {'IPv6':<18}: {v6}/{v6pfx}")
|
||||
rx = bstate.get("in-bytes", 0)
|
||||
tx = bstate.get("out-bytes", 0)
|
||||
try:
|
||||
rxi = int(rx)
|
||||
txi = int(tx)
|
||||
if rxi or txi:
|
||||
print(f" {'Traffic':<18}: RX {rxi} B TX {txi} B")
|
||||
except (TypeError, ValueError):
|
||||
pass
|
||||
total_rx = bstate.get("total-in-bytes", 0)
|
||||
total_tx = bstate.get("total-out-bytes", 0)
|
||||
duration = bstate.get("total-duration", 0)
|
||||
try:
|
||||
trxi = int(total_rx)
|
||||
ttxi = int(total_tx)
|
||||
duri = int(duration)
|
||||
if trxi or ttxi:
|
||||
print(f" {'Total Traffic':<18}: "
|
||||
f"RX {trxi} B TX {ttxi} B ({duri} s)")
|
||||
except (TypeError, ValueError):
|
||||
pass
|
||||
|
||||
if location:
|
||||
Decore.title("Location", width)
|
||||
src = location.get("source")
|
||||
if src:
|
||||
print(f" {'Source':<20}: {src}")
|
||||
lat = location.get("latitude")
|
||||
lon = location.get("longitude")
|
||||
alt = location.get("altitude")
|
||||
if lat is not None and lon is not None:
|
||||
pos = f"{lat}, {lon}"
|
||||
if alt is not None:
|
||||
pos += f" Alt: {alt} m"
|
||||
print(f" {'GPS':<20}: {pos}")
|
||||
cid = location.get("cell-id")
|
||||
if cid is not None:
|
||||
cell_id = f"CID {cid}"
|
||||
lac = location.get("lac")
|
||||
tac = location.get("tac")
|
||||
if lac is not None:
|
||||
cell_id += f" LAC {lac}"
|
||||
if tac is not None:
|
||||
cell_id += f" TAC {tac}"
|
||||
mcc = location.get("mcc")
|
||||
mnc = location.get("mnc")
|
||||
if mcc and mnc:
|
||||
cell_id += f" MCC {mcc} MNC {mnc}"
|
||||
print(f" {'Cell ID':<20}: {cell_id}")
|
||||
loc_change = location.get("last-change")
|
||||
if loc_change:
|
||||
print(f" {'Last Update':<20}: {loc_change}")
|
||||
|
||||
|
||||
def show_hardware(json):
|
||||
if not json.get("ietf-hardware:hardware"):
|
||||
print("Error, top level \"ietf-hardware:component\" missing")
|
||||
@@ -2257,8 +2581,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 +2721,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 +2764,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 +2772,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 +6333,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 +6413,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":
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
}
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import re
|
||||
|
||||
from ..host import HOST
|
||||
from . import common
|
||||
|
||||
@@ -5,6 +7,7 @@ from . import bridge
|
||||
from . import ethernet
|
||||
from . import ip
|
||||
from . import lag
|
||||
from . import modem
|
||||
from . import tun
|
||||
from . import veth
|
||||
from . import vlan
|
||||
@@ -12,6 +15,9 @@ from . import wifi
|
||||
from . import wireguard
|
||||
|
||||
|
||||
_WWAN_RE = re.compile(r"^wwan\d+$")
|
||||
|
||||
|
||||
def statistics(iplink):
|
||||
statistics = {}
|
||||
|
||||
@@ -29,6 +35,9 @@ def statistics(iplink):
|
||||
def iplink2yang_type(iplink):
|
||||
ifname=iplink["ifname"]
|
||||
|
||||
if _WWAN_RE.match(ifname):
|
||||
return "infix-if-type:modem"
|
||||
|
||||
match iplink["link_type"]:
|
||||
case "loopback":
|
||||
return "infix-if-type:loopback"
|
||||
@@ -182,6 +191,9 @@ def interface(iplink, ipaddr, systemjson=None):
|
||||
case "infix-if-type:wireguard":
|
||||
if wg := wireguard.wireguard(iplink):
|
||||
interface["infix-interfaces:wireguard"] = wg
|
||||
case "infix-if-type:modem":
|
||||
if wwan := modem.wwan(iplink["ifname"]):
|
||||
interface["infix-interfaces:wwan"] = wwan
|
||||
|
||||
match iplink2yang_lower(iplink):
|
||||
case "infix-interfaces:bridge-port":
|
||||
|
||||
@@ -0,0 +1,103 @@
|
||||
"""
|
||||
Modem (wwan) interface operational state.
|
||||
|
||||
Populates `infix-interfaces:wwan/bearer-state` from modemd's modem-info
|
||||
JSON output. Yanger calls wwan(ifname) from link.interface() when an
|
||||
interface is classified as 'infix-if-type:modem'.
|
||||
|
||||
The mapping from wwan interface to bearer:
|
||||
|
||||
- modem-info reports a list of bearers per modem. Each bearer dict
|
||||
has an 'interface' field set to the kernel device name (e.g.
|
||||
'wwan0') when the bearer is connected; absent when disconnected.
|
||||
- For a connected bearer we match by 'interface' directly.
|
||||
- For disconnected bearers we fall back to modem index: bearers on
|
||||
'modem{N}' default to 'wwan{N}' when no better hint is available.
|
||||
|
||||
That matches modemd's own naming convention for the single-bearer
|
||||
case, which is the only case the codebase exercises today. Multi-
|
||||
bearer setups will need a richer cross-reference (e.g. APN matching
|
||||
against sysrepo config) — a follow-on once multi-bearer is in
|
||||
serious use.
|
||||
"""
|
||||
|
||||
from ..host import HOST
|
||||
|
||||
|
||||
# modem-info bearer key -> bearer-state YANG leaf. Keys absent from
|
||||
# the source dict are simply not emitted.
|
||||
_BEARER_KEY_MAP = {
|
||||
"path": "path",
|
||||
"connected": "connected",
|
||||
"connection-failed-reason": "connection-failed-reason",
|
||||
"interface": "interface",
|
||||
"ipv4-address": "ipv4-address",
|
||||
"ipv4-prefix": "ipv4-prefix-length",
|
||||
"ipv6-address": "ipv6-address",
|
||||
"ipv6-prefix": "ipv6-prefix-length",
|
||||
"in-bytes": "in-bytes",
|
||||
"out-bytes": "out-bytes",
|
||||
"total-in-bytes": "total-in-bytes",
|
||||
"total-out-bytes": "total-out-bytes",
|
||||
"total-duration": "total-duration",
|
||||
}
|
||||
|
||||
|
||||
def _bearer_state(bearer):
|
||||
state = {}
|
||||
for src, dst in _BEARER_KEY_MAP.items():
|
||||
if src not in bearer:
|
||||
continue
|
||||
val = bearer[src]
|
||||
if val in (None, "", "--"):
|
||||
continue
|
||||
if dst in ("ipv4-prefix-length", "ipv6-prefix-length"):
|
||||
try:
|
||||
val = int(val)
|
||||
except (TypeError, ValueError):
|
||||
continue
|
||||
if val == 0:
|
||||
continue
|
||||
state[dst] = val
|
||||
return state or None
|
||||
|
||||
|
||||
def _find_bearer(modems, ifname):
|
||||
"""Return the bearer dict matching ifname, or None.
|
||||
|
||||
Prefers an exact 'interface' match (connected case), falls back to
|
||||
the bearer list of modem{N} when ifname is wwan{N}.
|
||||
"""
|
||||
for modem in modems:
|
||||
bearers = (modem.get("status") or {}).get("bearer") or []
|
||||
for b in bearers:
|
||||
if b.get("interface") == ifname:
|
||||
return b
|
||||
|
||||
# No connected match — fall back to wwan{N} → modem{N}.
|
||||
if ifname.startswith("wwan"):
|
||||
try:
|
||||
idx = int(ifname[4:])
|
||||
except ValueError:
|
||||
return None
|
||||
for modem in modems:
|
||||
if modem.get("index") == idx:
|
||||
bearers = (modem.get("status") or {}).get("bearer") or []
|
||||
if bearers:
|
||||
return bearers[0]
|
||||
return None
|
||||
|
||||
|
||||
def wwan(ifname):
|
||||
modems = HOST.run_json(['/usr/libexec/modemd/modem-info'], [])
|
||||
if not modems:
|
||||
return None
|
||||
|
||||
bearer = _find_bearer(modems, ifname)
|
||||
if not bearer:
|
||||
return None
|
||||
|
||||
state = _bearer_state(bearer)
|
||||
if not state:
|
||||
return None
|
||||
return {"bearer-state": state}
|
||||
@@ -134,11 +134,10 @@ def add_dns(out):
|
||||
|
||||
try:
|
||||
ipaddress.ip_address(ip)
|
||||
servers.append({
|
||||
"address": ip,
|
||||
"origin": "dhcp",
|
||||
"interface": iface
|
||||
})
|
||||
entry = {"address": ip, "origin": "dhcp"}
|
||||
if iface:
|
||||
entry["interface"] = iface
|
||||
servers.append(entry)
|
||||
except ValueError:
|
||||
continue
|
||||
|
||||
@@ -146,9 +145,12 @@ def add_dns(out):
|
||||
parts = line.split('#', 1)
|
||||
search.extend(parts[0].split()[1:])
|
||||
|
||||
insert(out, "infix-system:dns-resolver", "options", options)
|
||||
insert(out, "infix-system:dns-resolver", "server", servers)
|
||||
insert(out, "infix-system:dns-resolver", "search", search)
|
||||
if options:
|
||||
insert(out, "infix-system:dns-resolver", "options", options)
|
||||
if servers:
|
||||
insert(out, "infix-system:dns-resolver", "server", servers)
|
||||
if search:
|
||||
insert(out, "infix-system:dns-resolver", "search", search)
|
||||
|
||||
def add_software_slots(out, data):
|
||||
slots = []
|
||||
@@ -357,7 +359,8 @@ def add_users(out):
|
||||
|
||||
users.append(user)
|
||||
|
||||
insert(out, "authentication", "user", users)
|
||||
if users:
|
||||
insert(out, "authentication", "user", users)
|
||||
|
||||
def add_clock(out):
|
||||
clock = {}
|
||||
|
||||
@@ -0,0 +1,232 @@
|
||||
from .common import LOG
|
||||
from .host import HOST
|
||||
|
||||
|
||||
RUNDIR = "/run/modemd"
|
||||
|
||||
|
||||
# YANG modem-state.state values that map to ietf-hardware oper-state
|
||||
# 'enabled' (resource is partially or fully operable). Everything in
|
||||
# the "failed" / "disabled" buckets maps to oper-state 'disabled';
|
||||
# everything else is reported as 'unknown'.
|
||||
_MODEM_OPER_ENABLED = {
|
||||
"enabling", "enabled",
|
||||
"searching", "registered",
|
||||
"connecting", "connected",
|
||||
"disconnecting",
|
||||
}
|
||||
_MODEM_OPER_DISABLED = {
|
||||
"failed", "disabled", "disabling", "locked",
|
||||
}
|
||||
|
||||
# state-failed-reason values that warrant an alarm-state 'major' bit
|
||||
# (i.e. the operator can resolve them by intervention — insert SIM,
|
||||
# enter PIN, …).
|
||||
_SIM_CLASS_FAILED_REASONS = {
|
||||
"sim-missing", "sim-error", "sim-wrong", "sim-busy",
|
||||
"unlock-required",
|
||||
}
|
||||
|
||||
# A 'warning' alarm-state bit is raised when registered but signal
|
||||
# quality drops below this percentage. Recoverable, not load-bearing.
|
||||
_WEAK_SIGNAL_THRESHOLD = 20
|
||||
|
||||
|
||||
def _location_state(idx):
|
||||
data = HOST.read_json("%s/modem%d/location/data.json" % (RUNDIR, idx),
|
||||
default={})
|
||||
if not data:
|
||||
return None
|
||||
|
||||
state = {}
|
||||
for key in ("source", "latitude", "longitude", "altitude",
|
||||
"cell-id", "lac", "tac", "mcc", "mnc", "last-change"):
|
||||
if key in data and data[key] is not None:
|
||||
state[key] = data[key]
|
||||
return state or None
|
||||
|
||||
|
||||
def _last_change(idx):
|
||||
"""modem-state/last-change — freshness, written by modemd each poll."""
|
||||
data = HOST.read_json("%s/modem%d/state.json" % (RUNDIR, idx), default={})
|
||||
return data.get("last-change") if data else None
|
||||
|
||||
|
||||
def _state_last_changed(idx):
|
||||
"""component/state/state-last-changed — RFC 4268, transition-driven.
|
||||
|
||||
Written by modemd when admin/oper/alarm state actually changes.
|
||||
"""
|
||||
data = HOST.read_json("%s/modem%d/state.json" % (RUNDIR, idx), default={})
|
||||
return data.get("state-last-changed") if data else None
|
||||
|
||||
|
||||
def _modem_oper_state(modem_state):
|
||||
if not modem_state:
|
||||
return "unknown"
|
||||
if modem_state in _MODEM_OPER_ENABLED:
|
||||
return "enabled"
|
||||
if modem_state in _MODEM_OPER_DISABLED:
|
||||
return "disabled"
|
||||
return "unknown"
|
||||
|
||||
|
||||
def _modem_alarm_state(modem_state, failed_reason, signal_quality):
|
||||
bits = []
|
||||
if modem_state == "failed" and failed_reason in _SIM_CLASS_FAILED_REASONS:
|
||||
bits.append("major")
|
||||
elif modem_state == "failed":
|
||||
bits.append("minor")
|
||||
elif modem_state in ("registered", "connected") \
|
||||
and isinstance(signal_quality, int) \
|
||||
and signal_quality < _WEAK_SIGNAL_THRESHOLD:
|
||||
bits.append("warning")
|
||||
return " ".join(bits) if bits else None
|
||||
|
||||
|
||||
def _sim_oper_state(sim_state):
|
||||
if sim_state == "not-inserted":
|
||||
return "disabled"
|
||||
if sim_state in ("unlocked", "pin-required", "puk-required",
|
||||
"permanently-blocked"):
|
||||
return "enabled"
|
||||
return "unknown"
|
||||
|
||||
|
||||
def _modem_to_hw_state(modem):
|
||||
info = modem.get("info", {})
|
||||
status = modem.get("status", {})
|
||||
cellular = status.get("cellular", {})
|
||||
|
||||
state = {}
|
||||
|
||||
# Hardware information
|
||||
for src_key in ("manufacturer", "model", "hardware-revision",
|
||||
"firmware-version", "serial-number", "imsi", "iccid",
|
||||
"selected-carrier"):
|
||||
if info.get(src_key):
|
||||
state[src_key] = info[src_key]
|
||||
elif status.get(src_key):
|
||||
state[src_key] = status[src_key]
|
||||
|
||||
for leaflist_key in ("phone-number", "supported-carrier"):
|
||||
values = info.get(leaflist_key) or []
|
||||
values = [v for v in values if v]
|
||||
if values:
|
||||
state[leaflist_key] = values
|
||||
|
||||
# Status
|
||||
if status.get("state"):
|
||||
state["state"] = status["state"]
|
||||
if status.get("state-failed-reason"):
|
||||
state["state-failed-reason"] = status["state-failed-reason"]
|
||||
if status.get("power-state"):
|
||||
state["power-state"] = status["power-state"]
|
||||
locks = status.get("enabled-locks") or []
|
||||
if locks:
|
||||
state["enabled-locks"] = locks
|
||||
|
||||
if "signal-quality" in status:
|
||||
state["signal-quality"] = status["signal-quality"]
|
||||
if "signal-quality-recent" in status:
|
||||
state["signal-quality-recent"] = status["signal-quality-recent"]
|
||||
for sig in ("signal-rssi", "signal-rsrp", "signal-rsrq", "signal-sinr"):
|
||||
if sig in status:
|
||||
state[sig] = status[sig]
|
||||
|
||||
# Cellular
|
||||
if cellular:
|
||||
cell = {}
|
||||
for key in ("registration-state", "service-state",
|
||||
"operator-name", "operator-id", "network-type"):
|
||||
v = cellular.get(key)
|
||||
if v:
|
||||
cell[key] = v
|
||||
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
|
||||
status = modem.get("status", {})
|
||||
modem_state = status.get("state")
|
||||
failed_reason = status.get("state-failed-reason")
|
||||
signal_quality = status.get("signal-quality")
|
||||
|
||||
hw_state = _modem_to_hw_state(modem)
|
||||
|
||||
location_state = _location_state(idx)
|
||||
if location_state:
|
||||
hw_state["location-state"] = location_state
|
||||
|
||||
last_change = _last_change(idx)
|
||||
if last_change:
|
||||
hw_state["last-change"] = last_change
|
||||
|
||||
component = {
|
||||
"name": name,
|
||||
"class": "infix-hardware:modem",
|
||||
}
|
||||
|
||||
# Standard ietf-hardware state container — drives 'show hardware'
|
||||
# and gives generic NETCONF clients a meaningful view of the
|
||||
# modem without needing modem-specific YANG awareness.
|
||||
std_state = {
|
||||
"oper-state": _modem_oper_state(modem_state),
|
||||
}
|
||||
alarm = _modem_alarm_state(modem_state, failed_reason, signal_quality)
|
||||
if alarm:
|
||||
std_state["alarm-state"] = alarm
|
||||
slc = _state_last_changed(idx)
|
||||
if slc:
|
||||
std_state["state-last-changed"] = slc
|
||||
component["state"] = std_state
|
||||
|
||||
if hw_state:
|
||||
component["infix-hardware:modem-state"] = hw_state
|
||||
hw_components.append(component)
|
||||
|
||||
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)
|
||||
sim_component = {
|
||||
"name": sim_name,
|
||||
"class": "infix-hardware:sim",
|
||||
"state": {
|
||||
"oper-state": _sim_oper_state(sim_raw.get("state")),
|
||||
},
|
||||
}
|
||||
if slc:
|
||||
sim_component["state"]["state-last-changed"] = slc
|
||||
if sim_hw_state:
|
||||
sim_component["infix-hardware:sim-state"] = sim_hw_state
|
||||
hw_components.append(sim_component)
|
||||
|
||||
if not hw_components:
|
||||
return {}
|
||||
|
||||
return {
|
||||
"ietf-hardware:hardware": {
|
||||
"component": hw_components
|
||||
}
|
||||
}
|
||||
+3
-2
@@ -112,7 +112,8 @@ static int ly_add_yanger_data(const struct ly_ctx *ctx, struct lyd_node **parent
|
||||
|
||||
err = lyd_parse_data_fd(ctx, fd, LYD_JSON, LYD_PARSE_ONLY, 0, parent);
|
||||
if (err)
|
||||
ERROR("Error, parsing yanger data (%d): %s", err, ly_errmsg(ctx));
|
||||
ERROR("Error, parsing yanger data (%d) for model '%s': %s", err,
|
||||
yanger_args[1] ?: "?", ly_errmsg(ctx));
|
||||
|
||||
fclose(stream);
|
||||
/* Note: fclose() already closes the underlying fd from fdopen() */
|
||||
@@ -227,7 +228,7 @@ static int sr_generic_cb(sr_session_ctx_t *session, uint32_t, const char *model,
|
||||
|
||||
err = ly_add_yanger_data(ctx, parent, yanger_args);
|
||||
if (err)
|
||||
ERROR("Error adding yanger data");
|
||||
ERROR("Error adding yanger data for xpath: %s", xpath);
|
||||
|
||||
sr_release_context(con);
|
||||
|
||||
|
||||
@@ -31,12 +31,6 @@ check()
|
||||
|
||||
echo "1..$total"
|
||||
for defconfig in "$@"; do
|
||||
# Skip UNIX backup files
|
||||
case "$defconfig" in
|
||||
*~|*.bak|'#'*'#'|.#*)
|
||||
continue
|
||||
;;
|
||||
esac
|
||||
base=$(basename "$defconfig")
|
||||
if whitelist "$base"; then
|
||||
echo "ok $num - $base is exempted # skip"
|
||||
@@ -50,6 +44,9 @@ check()
|
||||
}
|
||||
|
||||
# shellcheck disable=SC2046
|
||||
check $(find "$CONFIGS" -maxdepth 1 -type f | LC_ALL=C sort) || exit 1
|
||||
check $(find "$CONFIGS" -maxdepth 1 -type f \
|
||||
! -name '*~' ! -name '*.bak' \
|
||||
! -name '#*#' ! -name '.#*' \
|
||||
| LC_ALL=C sort) || exit 1
|
||||
|
||||
exit 0
|
||||
|
||||
@@ -110,15 +110,13 @@
|
||||
}
|
||||
},
|
||||
"infix-system:dns-resolver": {
|
||||
"options": {},
|
||||
"server": [
|
||||
{
|
||||
"address": "192.168.2.1",
|
||||
"origin": "dhcp",
|
||||
"interface": "e7"
|
||||
}
|
||||
],
|
||||
"search": []
|
||||
]
|
||||
},
|
||||
"clock": {
|
||||
"boot-datetime": "2026-01-02T20:42:11+00:00",
|
||||
|
||||
Reference in New Issue
Block a user