Wwand - new mobile network connection manager with eSIM support

Hi all,

I've been running the MikroTik Chateau 5G R17 ax (qualcommax/ipq60xx, Quectel RG650E-EU) on OpenWrt and want to share wwand โ€” a small connection manager I wrote to replace the old bash qmi-advanced dialer.

:backhand_index_pointing_right: https://github.com/ddimension/wwand

It's an event-driven daemon written entirely in ucode that talks QMI, MBIM and NCM/AT natively over /dev/cdc-wdmX (or the tty) โ€” no uqmi, qmicli, libqmi, umbim or glib. It drives netifd directly (no per-interface monitor process), so a daemon restart or a transient signal loss doesn't bounce your WAN (IPv6-PD/VRF survive). On the Chateau it does 5G NSA with two parallel PDP contexts (IPv4 + IPv6) over QMAP multiplexing, ~3 MB RSS, no polling.

It installs alongside the stock stack, it does not replace it. There is no CONFLICTS on uqmi/umbim/comgt-ncm, and the netifd shim registers proto wwand and nothing else โ€” your existing proto qmi interfaces stay with uqmi until you decide otherwise. Moving one across is always an explicit act (see Configuration below).

Features

  • One daemon, three modem backends: QMI, native MBIM and NCM/ECM (AT-driven) โ€” one proto 'wwand' for all of them
  • Multiple modems + multiple parallel PDP contexts (QMAP: rmnet / qmimux, auto-selected), with the QMAP header version negotiated down a ladder (v5 โ†’ v4 โ†’ v1)
  • The mux datapath is a plug-in interface, not a hard-coded branch: an out-of-tree package can add its own without patching wwand. Two exist for the vendor QMAP drivers (qmi_wwan_q, pcie_mhi) so Qualcomm NSS offload survives
  • PCIe/MHI transport (wwand-mhi) for modems whose control port appears under /sys/class/wwan, incl. AT over the MBIM channel for modems with no DUN channel
  • IPv4 / IPv6 / dual-stack per context; IPv6 prefix delegation
  • Recovery ladder: retry โ†’ op-mode cycle โ†’ modem reset โ†’ board power-cycle / reset-GPIO โ†’ reboot, plus a zero-rx watchdog
  • Board profiles (MikroTik Chateau, Zyxel LTE33xx / NR7101): modem power/reset GPIOs and status LEDs (5-bar signal graph) โ€” replaces the vendor helper scripts
  • SIM: PIN, multi-slot switching, PIN-lock, per-SIM overrides by ICCID โ€” and eSIM/eUICC: native ES10c profile management (list/enable/disable/EID) plus SM-DP+ provisioning (profile download) via a bundled lpac; the HTTPS runs on the router over your existing WAN, no dedicated provisioning APN. The eSIM APDU transport is auto-chosen (QMI UIM โ†’ native MBIM UICC โ†’ AT), so it also works on MBIM modems without an AT port
  • SMS: receive/list/read/delete with a full GSM 03.40 PDU decoder (7-bit incl. umlauts, UCS2, multipart merge) + LuCI inbox
  • Registration diagnostics: EMM reject cause + limited-service flag over ubus/log (handy when an attach is rejected, e.g. IPv4-only โ†’ cause 33); attach profile is programmed from config before registration to avoid exactly that
  • Telemetry: serving + neighbour cells, signal, operator, data-system mode (LTE/NSA/SA) โ€” uniform across all backends
  • Quectel cell locking (4G anchor / 5G SA), network scan/selection, QMI LOC/GPS
  • Zero-config autosetup: a modem on an unconfigured box gets an interface and an APN filled in from ICCID/IMSI, copied once into uci and then yours to edit
  • Stable modem binding by path / serial / IMEI (wireless-style option path), so nothing depends on enumeration order
  • LuCI integration (proto handler + Status โ†’ Modem page + settings/eSIM/SMS UI)
  • Configuration lives entirely in /etc/config/network (WireGuard-style wwand_modem sections)

Installation

Option 1: prebuilt packages (no buildroot needed)

CI publishes signed binary repositories for snapshot, 25.12 and 24.10 across seven architectures (aarch64_cortex-a53/-a72, arm_cortex-a7/-a15, mips_24kc, mipsel_24kc, x86_64):

https://ddimension.github.io/openwrt-repo/<release>/<arch>/

apk (snapshot / 25.12), example for a Chateau on snapshot:

wget -O /etc/apk/keys/wwand.pem https://ddimension.github.io/openwrt-repo/keys/public-key.pem
echo "https://ddimension.github.io/openwrt-repo/snapshot/aarch64_cortex-a53/packages.adb" > /etc/apk/repositories.d/wwand.list
apk update
apk add wwand-qmi luci-proto-wwand luci-app-wwand
apk add wwand-esim wwand-lpac   # optional: eSIM management + provisioning

Option 2: build it yourself

The package definitions live in a separate feed repo (the wwand repo itself is the pure source tree):

echo "src-git wwand https://github.com/ddimension/openwrt-repo.git" >> feeds.conf.default
./scripts/feeds update wwand
./scripts/feeds install -a -p wwand

make menuconfig
  Network โ†’ WWAN โ†’ wwand-qmi          # the common case; pulls in the wwand base
  #                wwand-mbim         # for MBIM modems (includes the QMI passthrough)
  #                wwand-ncm          # for NCM/ECM (AT-driven) modems
  #                wwand-mhi          # PCIe/MHI transport; pair with -qmi or -mbim
  LuCI โ†’ Protocols    โ†’ luci-proto-wwand
  LuCI โ†’ Applications โ†’ luci-app-wwand
  # optional eSIM: wwand-esim + wwand-lpac

make

Pick the backend package(s) matching your modems โ€” the wwand base carries no backend on its own and is pulled in automatically. Nothing here removes or conflicts with uqmi, umbim, comgt-ncm or luci-proto-qmi; they can stay installed.

Configuration

Create a proto 'wwand' interface, or hand an existing one over. Migration is always user-triggered and rewrites the interface in place: the Migratable interfaces list on the LuCI modem page, the migrate ubus method, or /usr/libexec/wwand/migrate --apply. Nothing is installed under /etc/uci-defaults, so installing or upgrading wwand never rewrites a configuration of yours; an example hook ships inert under /usr/share/wwand/examples/.

Full config/ubus reference, quirk handling and an FAQ: https://github.com/ddimension/wwand/blob/main/docs/reference.md

Tested

MikroTik Chateau 5G R17 ax (RG650E, 5G NSA) ยท Zyxel NR7101 (RG502Q) ยท Zyxel LTE3301-Plus (EG06) ยท GL.iNet GL-X3000 (RM520N-GL, MBIM) ยท Cudy LT300 v3 (MeiG SLM770A, NCM) ยท Fibocom FM350-GL (RNDIS/NCM).

3268 host-side checks across 50 suites run without hardware (cd tests && sh run_tests.sh).

Both halves are up for review upstream: openwrt/packages#30185 and openwrt/luci#8917.

Feedback / testers on other modems very welcome!

Cheers!

3 Likes

Hi avalentin,

This looks like a great replacement for the old bash qmi-advanced approach โ€” thanks for sharing it. I've been going through the code (atcmd.uc, modem_mbim.uc, netlink.uc, atport.uc) to see whether it'd fit my setup and have a few questions, since my hardware isn't on the tested list yet.

My setup: an LTE5398-M904 board with a Quectel EG18-EA module (Bus 002 Device 002: ID 2c7c:0512), currently running over MBIM with a custom bash watchdog (AT-based recovery ladder with a pre-escalation diagnostic step and a GPIO power-cycle rung instead of USB re-power).

  1. MBIM registration diagnostics โ€” in modem_mbim.uc, _update_register() only tracks REGISTER_STATE (home/roaming/partner); I don't see an equivalent of the QMI backend's reg_detail (EMM reject cause + limited-service via AT+CEER). Is that intentionally QMI-only for now, or is there a path to get reject-cause info on MBIM too? Since step_at() already opens an AT side channel for quirks/cell-lock on MBIM modems, would hooking AT+CEER in there be the right place, or is there a reason it was left out?

  2. settings_poll behavior on a mid-session IP change โ€” if settings_poll detects that the network pushed a different IP/DNS/MTU without an actual deregistration in between, does that trigger a renew on the context (in-place update, no teardown), or does it force a full down/up cycle? Trying to understand whether IPv4/6-PD/VRF survive that kind of change the same way they do for a transient signal-loss renew.

Happy to test on the EG18-EA and report back once I have a sense of where these gaps are best addressed. Thanks again for open-sourcing this!

Hi again,

I've seen the recent changes to the code (the lazy QMI/MBIM/NCM packaging, the QMI-over-MBIM passthrough bringing reg_detail to MBIM too โ€” answers my question 1 above, thanks!). While going through recovery.uc I ran into something I'd like to ask about.

Is it possible to disable the final system reboot, replacing or supplementing it with a GPIO reset instead? The goal is to keep the router itself up so logs and recovery counters survive, which makes debugging a stuck modem much easier than after a full reboot wipes everything.

A few specifics from reading recovery.uc:

  • failreboot=0 disables the whole ladder (not just the reboot rung), so it can't be used to keep opmode_cycle/modem_reset/usb_repower while dropping only the final reboot.
  • Even with failreboot set very high, PROTO_ERROR_LIMIT (25) is hardcoded and triggers reboot unconditionally, independent of failreboot.
  • There's no GPIO-based action in the ladder today, only opmode_cycle, modem_reset, usb_repower and reboot.

For reference, here's the GPIO reset function I use for the EG18-EA today in my own watchdog, in case it's useful as a starting point for a gpio_reset rung:

gpio_lte_power_link="/sys/class/gpio/lte_power"
gpio_lte_power_value="${gpio_lte_power_link}/value"

# Hardware GPIO reset for Quectel EG18-EA
gpio_modem_reset() {
    # Check GPIO access
    if [ ! -w "$gpio_lte_power_value" ]; then
        logger -t "$logheader" "escalation: GPIO reset FAILED - not writable"
        return 1
    fi
    # Pull LOW (modem powered off)
    echo 0 > "$gpio_lte_power_value"
    sleep 3
    # Pull HIGH (modem powered on)
    echo 1 > "$gpio_lte_power_value"
    return 0
}

Would you be open to a gpio_reset rung (config-selectable, board/GPIO path supplied via config) sitting between usb_repower and reboot, plus making PROTO_ERROR_LIMIT configurable so it can be raised/disabled independently of failreboot? Happy to help test or draft a patch if that's a direction you'd consider.

@ncompact โ€” thanks, this is exactly the kind of feedback that makes a project
better. Both of your posts were spot-on; here's where each point stands.

MBIM registration diagnostics. Good news: modem_mbim now surfaces the
reject cause the same way QMI does. It reads the native MBIM register state
first, falls back to the QMI-over-MBIM passthrough NAS system-info, and tops the
numeric cause up from AT+CEER when the firmware leaves it blank โ€” all merged
into one registration_detail (EMM reject cause + limited-service flag) that
shows up over ubus and in the log. So on your EG18-EA you should get the reject
cause without the bash side-channel.

IP changes mid-session. settings_poll detecting a network-pushed
IP/DNS/MTU triggers an in-place renew, not a down/up โ€” same path as a
transient signal-loss reconnect. The interface stays IFS_UP (no-proto-task),
so IPv4/IPv6-PD and the VRF/ip4table/ip6table binding survive it, bounded by
hold_max before it gives up and goes down.

GPIO reset in the recovery ladder. This is in now. There's a hardware rung
that pulses a reset_gpio (or does a board power-cycle), and โ€” importantly
for you โ€” a per-modem option reset_gpio '<n>' works without a board
profile, so you can wire your EG18 power/reset line straight into the ladder.

And your two sharp observations were both correct, so I just fixed them:

  • failreboot=0 was all-or-nothing. It now disables only the final
    reboot rung. The cheaper hardware recovery (op-mode cycle โ†’ modem reset โ†’ GPIO
    reset / repower) still runs on its thresholds, and past the last rung the
    ladder just keeps retrying forever โ€” the router stays up for logging/debugging,
    which is exactly what you asked for.
  • PROTO_ERROR_LIMIT was hardcoded at 25 and rebooted unconditionally. It's
    now a config option proto_error_limit (default 25) and is gated by
    failreboot the same way, so with failreboot 0 a protocol-error storm no
    longer cycles the box.

So your headless setup is now: option failreboot '0' + option reset_gpio '<your-gpio>' on the modem โ†’ escalates through GPIO reset and never reboots.

If you share the LTE5398-M904 GPIO map (power/reset lines, active level, and the
status-LED names from /etc/board.json), I'll add a built-in board profile so it
works out of the box. And I'd very much welcome an EG18-EA test โ€” that's a module
I don't have on the bench.

1 Like

That's fantastic โ€” thank you for turning both around so quickly, and for the detailed explanation of each fix.

Here's the GPIO map from /etc/board.json on the LTE5398-M904:

"led": {
    "internet": {
        "name": "LTE/3G Internet Activity",
        "sysfs": "green:internet",
        "type": "netdev",
        "device": "wwan0",
        "mode": "rx tx"
    }
},
"gpioswitch": {
    "usb_power": {
        "name": "Power USB Port",
        "pin": "usb_power",
        "default": 1
    },
    "lte_power": {
        "name": "Power LTE modem",
        "pin": "lte_power",
        "default": 1
    }
}

And the raw GPIO side, from /sys/kernel/debug/gpio (gpiochip0, parent 1e000600.gpio):

gpio-518 (reset)          in  hi IRQ ACTIVE LOW
gpio-525 (orange:lte)     out lo
gpio-526 (green:lte)      out lo
gpio-527 (red:lte)        out lo
gpio-531 (reset)          out hi ACTIVE LOW
gpio-539 (lte_power)      out lo ACTIVE LOW

Notes:

  • lte_power is wired ACTIVE LOW at the hardware level, but /sys/class/gpio/lte_power/active_low reads 1 โ€” confirmed with a working connection โ€” so the sysfs value node is already logical (kernel-corrected): echo 0 = off, echo 1 = on, exactly what my script does. No inversion to worry about.

  • It's a power-enable line, not a dedicated reset โ€” power-cycle only (pull low, sleep 3s, pull high).

  • There's a separate reset line at gpio-531 (out hi ACTIVE LOW) that board.json doesn't expose at all โ€” unconfirmed what it's wired to (could be a real hardware reset distinct from power-enable, worth investigating if useful).

  • There's also a 3-color LTE status LED (orange:lte / green:lte / red:lte, gpio 525-527) that board.json doesn't surface either โ€” only the generic green:internet (traffic) one is mapped. Could be useful if you ever want richer state feedback (e.g. red on registration failure) beyond traffic-only. (I personally don't use any status LEDs to understand the underlying behavior)

  • usb_power is a separate line that (in theory) power-cycles the whole USB port rather than just the modem โ€” I haven't tested this one myself, so I can't confirm it actually works as expected on this board. Might be worth having as a fallback rung above GPIO reset if the modem-only power line ever isn't enough, similar in spirit to your existing usb_repower, but treat it as unverified on my end for now.

I'll pull the latest feed to pick up the failreboot/proto_error_limit/reset_gpio changes before building, then set option failreboot '0' + option reset_gpio pointing at lte_power and get the EG18-EA on the bench with wwand-mbim. Will report back with how discovery/registration/reg_detail behave and whether the passthrough or native MBIM path answers first. Thanks again!

@ncompact โ€” perfect, that GPIO dump is exactly what I needed. I've added a
built-in board profile for the LTE5398-M904, so this works out of the box rather
than needing manual config.

What the profile does

  • Power / recovery โ€” it drives lte_power (gpio-539) as the modem power line.
    You spotted the tricky bit yourself: it's active-low in HW but
    /sys/class/gpio/lte_power/active_low reads 1, so the sysfs value is already
    the logical level (1 = on, matching the gpioswitch default). wwand therefore
    power-cycles it the normal way โ€” value 0, wait, value 1 โ€” which is the sysfs
    equivalent of your "pull low, then high". The recovery ladder uses this
    automatically at the hardware rung, and it's also on the LuCI "Reset modem"
    button.
  • Status LEDs โ€” I mapped red/green/orange:lte (gpio-525/526/527) to the
    mobile panel: green solid when registered, red blinking while searching, orange
    for roaming. (green:internet stays a netdev LED owned by the OS for wwan0
    activity โ€” untouched.)

Your planned test (option failreboot '0' + option reset_gpio 'lte_power')
is spot-on and will work. With the board profile you don't even need the
reset_gpio line โ€” recovery will power-cycle lte_power on its own โ€” but setting
it does no harm. And just to recap the earlier fix: failreboot 0 now disables
only the reboot, so the ladder still escalates through op-mode cycle โ†’ modem
reset โ†’ lte_power cycle and then keeps retrying, exactly the headless behaviour
you asked for.

Two things I couldn't verify without the board โ€” could you check?

  1. The exact model id: ubus call system board | jsonfilter -e '@.board_name'
    (or .model.id in /etc/board.json). I keyed the profile on
    zyxel,lte5398-m904; if yours differs, it's a one-line fix (a mismatched id
    just falls back to a safe no-op, so nothing breaks meanwhile).
  2. Whether those three LEDs are real /sys/class/leds entries:
    ls /sys/class/leds | grep lte. If they show up as green:lte etc. the LED
    panel will just work; if they're raw GPIOs only, I'll wire them differently.

It's on the wwand main branch now (feed bump pending). Thanks again for doing
the legwork โ€” and I'm still very keen to hear how the EG18-EA fares on discovery +
registration once you pull it.

One more thing, unrelated to the modem itself: the install instructions in your first post point feeds.conf.default at the wwand.git source repo:

echo "src-git wwand https://github.com/ddimension/wwand.git" >> feeds.conf.default

I just cloned wwand.git to look at the source and noticed it has no Makefile anywhere โ€” only the ucode/C source, tests and docs. The actual OpenWrt package definitions (the multi-package split into wwand/wwand-qmi/wwand-mbim/wwand-ncm/wwand-esim/ucode-mod-wwand-io, plus luci-proto-wwand/luci-app-wwand) live in a separate feed repo, ddimension/openwrt-repo. Pointing feeds.conf.default at wwand.git as written would give an empty feed.

Probably just leftover from before the feed/source split โ€” would you mind either editing the first post, or sharing the exact steps to build the package? Just want to make sure I (and anyone else following the thread) end up building it the right way.

Here is the data:

ubus call system board | jsonfilter -e '@.board_name'
zyxel,lte5398-m904

ls /sys/class/leds | grep lte
green:lte
orange:lte
red:lte

cat /sys/kernel/debug/gpio 
gpiochip0: GPIOs 512-543, parent: platform/1e000600.gpio, 1e000600.gpio-bank0:
 gpio-512 (                    |regulator-vbus      ) out hi 
 gpio-518 (                    |reset               ) in  hi IRQ ACTIVE LOW
 gpio-520 (                    |wps                 ) in  hi IRQ ACTIVE LOW
 gpio-525 (                    |orange:lte          ) out lo 
 gpio-526 (                    |green:lte           ) out lo 
 gpio-527 (                    |red:lte             ) out lo 
 gpio-528 (                    |green:power         ) out lo 
 gpio-531 (                    |reset               ) out hi ACTIVE LOW
 gpio-534 (                    |blue:power          ) out lo 
 gpio-535 (                    |green:internet      ) out lo 
 gpio-536 (                    |green:wifi          ) out lo 
 gpio-537 (                    |green:voice         ) out lo 
 gpio-539 (                    |lte_power           ) out lo ACTIVE LOW

gpiochip1: GPIOs 544-575, parent: platform/1e000600.gpio, 1e000600.gpio-bank1:

gpiochip2: GPIOs 576-607, parent: platform/1e000600.gpio, 1e000600.gpio-bank2:

You're right about the source. Add this source repo to as feed:

Then enable package wwand and build wwand and wwand-qmi.
Detailed steps will follow.
If everything goes fine, you should also find there binary packages later, when github finishes :slight_smile:

Build CI was successfull on github, please see initial posting for binary package repo URL.

Current apks are now available on:

For integration take a look here:

Thanks, I've updated docs, should fit now.

All gpios have been added before. Please try if it works as expected or if you need sth special (I.e. special LED handling)

That's great news on the repo index โ€” much easier for others to pick up than compiling from source.

On the GPIO/LED side: no special handling needed on my end, the profile you already added (lte_power power-cycle + the red/green/orange:lte trio) matches the board exactly โ€” that part's solid.

I could also verify eSIM / eUICC Support on routers which do not natively support this. I tested on several devices with an eUICC from https://shop.sysmocom.de/sysmoEUICC1-eUICC-for-consumer-eSIM-RSP/sysmoEUICC1

Big week: a maintainability overhaul plus a wave of features and backend-parity work across the QMI / MBIM / NCM backends.

New features

  • modem_reattach โ€” force a network deregister + re-attach (fixes a modem stuck on a previously-selected PLMN) without a full modem reset. QMI opmode-bounce, MBIM passthrough-DMS, AT+COPS fallback.
  • wwandctl โ€” a human-friendly CLI over the ubus API (status, up/down, reattach, scan, select, slots, pin/puk, sms, reset, at, migrate, --json). No more hand-writing JSON.
  • PUK handling end-to-end โ€” LuCI unlock dialog + wwandctl puk + ubus, with remaining-attempts and a wrong-PUK safeguard.
  • SMS send (was receive-only), SIM slots on MBIM, PCIe/MHI modem discovery (kernel wwan subsystem), stable L3 device names (wwand0โ€ฆ), FCC RF unlock for laptop-SKU modems, eSIM switch via lpac, and config migration from stock proto qmi/mbim/ncm and proto modemmanager.

Backend parity (QMI โ†” MBIM โ†” NCM)

  • Unified telemetry: mode / manufacturer / firmware / RAT / temperature in status on all backends.
  • Network scan reports RAT per operator; named PLMN lists (NAS + user + forbidden/FPLMN) with pre-radio restore.
  • Recovery ladder, live IP-settings refresh, and last_error now work on MBIM/NCM too.

Correctness & robustness

  • Program the LTE attach APN before registration (fixes carriers rejecting IPv4-only attach); surface registration reject causes immediately; PUK-locked SIM is now a clean terminal block on MBIM (no more reset-looping); Huawei CHECK_PERSONALIZATION no longer wedges a good SIM.
  • Native-module hardening: out-of-bounds netlink reads, missed-ACK-as-success, spawn exit-status race; several QMAP mux/datapath fixes.
  • Good-citizen coexistence: opt-in takeover, user-triggered migration โ€” wwand only manages proto wwand by default.

Under the hood

  • Decomposed the mega-modules (modem/daemon/context/sim) into focused units, added CI (host test suite + shellcheck), SPDX/GPL headers, many new test suites (~1050 checks green), and a five-round code audit that de-duplicated ~1250 lines.
  • Upstream prep: openwrt-devel RFC draft + a ModemManager feature comparison.

Hi @avalentin, would it be possible to add support for Fibocom FM350-GL modems? Currently, they need out-of-tree packages when connected via USB, which is the only way possible if they are used, e.g., in a Huasifei WH3000 Pro.

Hi. Thanks for the possibility, Fibocom FM350-GL supported added.

1 Like

Hi!
PCI MHI Modems are now fully supported, inlcuding usage of hidden AT Channel (MBIM OC) if serial is missing.
Feed is updated.

Hi, after a full fail with the Mikrotik R11e-LTE7 I'm looking for a suitable and affordable eSIM-capable 4G/5G-modem with frequency-band support for Germany as a backup to my VDSL internet connection on x86_64-OpenWRT 25.12. I have a Mikrotik RB14eU and sysmoEUICC-C2G eSIMs.

Can you recommend a 4G/5G modem with integrated eSIM or compatibility with sysmoEUICC-C2G from experience? It should have external antenna connectors for pigtails.

Thanx for any hint! :slight_smile: