🔥 Sync 2026-08-18 23:43:20
Merge-upstream / merge (push) Canceled after 0s

This commit is contained in:
github-actions[bot]
2026-08-18 23:43:20 +08:00
parent b71e04d39a
commit c4ac5442bc
11 changed files with 857 additions and 32 deletions
+59
View File
@@ -0,0 +1,59 @@
#
# LuCI status & settings app for the wwand modem daemon: the modem status page,
# the Modems editor, live settings, and the shared resources (wwand.bands /
# wwand.modemopts / wwand.simlist) reused by the proto handler.
#
# Sources live in https://github.com/ddimension/luci-app-wwand — bump
# PKG_SOURCE_VERSION (and PKG_SOURCE_DATE) to pick up new source commits.
#
# NOTE: built from a git PKG_SOURCE, so it does NOT use luci.mk — luci.mk's
# htdocs/root install is gated on those dirs living next to the Makefile
# (in-tree), which yields an EMPTY package for a git-fetched source. We install
# explicitly from $(PKG_BUILD_DIR), like the wwand package in this feed.
#
include $(TOPDIR)/rules.mk
PKG_NAME:=luci-app-wwand
PKG_RELEASE:=1
PKG_SOURCE_PROTO:=git
PKG_SOURCE_URL:=https://github.com/ddimension/luci-app-wwand.git
PKG_SOURCE_VERSION:=1c03a023715367e8ce36f9c50ac6ebdaa1e49459
PKG_SOURCE_DATE:=2026-08-13
PKG_MIRROR_HASH:=skip
PKG_LICENSE:=GPL-2.0-only
PKG_MAINTAINER:=
include $(INCLUDE_DIR)/package.mk
define Package/luci-app-wwand
SECTION:=luci
CATEGORY:=LuCI
SUBMENU:=3. Applications
TITLE:=wwand cellular modem status & settings
DEPENDS:=+luci-base +wwand
PKGARCH:=all
endef
define Package/luci-app-wwand/description
LuCI status and configuration app for the wwand cellular modem daemon.
endef
define Build/Compile
endef
define Package/luci-app-wwand/install
$(INSTALL_DIR) $(1)/www/luci-static/resources
$(CP) $(PKG_BUILD_DIR)/htdocs/luci-static/resources/* \
$(1)/www/luci-static/resources/
$(INSTALL_DIR) $(1)/usr/share/luci/menu.d
$(INSTALL_DATA) $(PKG_BUILD_DIR)/root/usr/share/luci/menu.d/*.json \
$(1)/usr/share/luci/menu.d/
$(INSTALL_DIR) $(1)/usr/share/rpcd/acl.d
$(INSTALL_DATA) $(PKG_BUILD_DIR)/root/usr/share/rpcd/acl.d/*.json \
$(1)/usr/share/rpcd/acl.d/
endef
$(eval $(call BuildPackage,luci-app-wwand))
+62
View File
@@ -0,0 +1,62 @@
#
# LuCI network-manager protocol handler for the wwand modem daemon.
# Registers only the 'wwand' protocol UI (protocol/wwand.js). It deliberately
# does NOT ship a 'qmi' alias: `proto qmi` stays with luci-proto-qmi/uqmi (no
# file collision, good-citizen coexistence), and interfaces are moved to
# `proto wwand` via the migration list in the LuCI modem page.
#
# Sources live in https://github.com/ddimension/luci-proto-wwand — bump
# PKG_SOURCE_VERSION (and PKG_SOURCE_DATE) to pick up new source commits.
#
# NOTE: this is built from a git PKG_SOURCE, so it does NOT use luci.mk. luci.mk
# gates its htdocs/root install on those directories living NEXT TO the Makefile
# (in-tree); for a git-fetched source they only exist under $(PKG_BUILD_DIR), so
# luci.mk produces an EMPTY package. We install explicitly from $(PKG_BUILD_DIR),
# exactly like the wwand package in this feed.
#
include $(TOPDIR)/rules.mk
PKG_NAME:=luci-proto-wwand
PKG_RELEASE:=1
PKG_SOURCE_PROTO:=git
PKG_SOURCE_URL:=https://github.com/ddimension/luci-proto-wwand.git
PKG_SOURCE_VERSION:=7d9ac0fe15257ac6469d9e2c0228506789200229
PKG_SOURCE_DATE:=2026-08-11
PKG_MIRROR_HASH:=skip
PKG_LICENSE:=GPL-2.0-only
PKG_MAINTAINER:=
include $(INCLUDE_DIR)/package.mk
define Package/luci-proto-wwand
SECTION:=luci
CATEGORY:=LuCI
SUBMENU:=4. Protocols
TITLE:=Support for QMI/5G cellular modems (wwand)
# luci-app-wwand provides the shared wwand.bands resource (band/frequency
# helpers) used by this handler; depending on it keeps a single source of truth.
DEPENDS:=+luci-base +wwand +luci-app-wwand
PKGARCH:=all
endef
define Package/luci-proto-wwand/description
LuCI protocol handler for the wwand cellular modem daemon: the network-native
'wwand' proto plus the legacy 'qmi' alias.
endef
define Build/Compile
endef
define Package/luci-proto-wwand/install
$(INSTALL_DIR) $(1)/www/luci-static/resources/protocol
$(INSTALL_DATA) $(PKG_BUILD_DIR)/htdocs/luci-static/resources/protocol/*.js \
$(1)/www/luci-static/resources/protocol/
$(INSTALL_DIR) $(1)/usr/share/rpcd/acl.d
$(INSTALL_DATA) $(PKG_BUILD_DIR)/root/usr/share/rpcd/acl.d/*.json \
$(1)/usr/share/rpcd/acl.d/
endef
$(eval $(call BuildPackage,luci-proto-wwand))
+1 -1
View File
@@ -17,7 +17,7 @@ LUCI_NAME:=luci-theme-footstrap
FOOTSTRAP_VERSION?=
ifneq ($(FOOTSTRAP_VERSION),)
PKG_VERSION:=$(FOOTSTRAP_VERSION)
PKG_RELEASE:=24
PKG_RELEASE:=25
endif
LUCI_TITLE:=Footstrap Theme
@@ -154,6 +154,7 @@ const GEOM_DFLT = { contentMin: 500, sidebarW: 224, railW: 68, contentPad: 56 };
let _geom = null, _geomDensity = null, _geomWarned = false;
function shellGeometry() {
const density = document.documentElement.getAttribute('data-density') || '';
/* the gutter is re-asked even on the memo hit: it moves with the WIDTH, not with the density */
if (_geom && _geomDensity === density) return _geom;
_geomDensity = density;
const px = (name, dflt) => resolveLen(name, dflt);
@@ -161,7 +162,9 @@ function shellGeometry() {
contentMin: px('--fs-content-min', GEOM_DFLT.contentMin),
sidebarW: px('--fs-sidebar-w', GEOM_DFLT.sidebarW),
railW: px('--fs-rail-w', GEOM_DFLT.railW),
/* the token is ONE side's padding; the column loses it twice */
/* the token is ONE side's padding; the column loses it twice. Kept as `tokenPad` because it
* is only the FALLBACK — the gutter that counts is measured, see measureShell() */
tokenPad: px('--fs-content-pad', GEOM_DFLT.contentPad / 2) * 2,
contentPad: px('--fs-content-pad', GEOM_DFLT.contentPad / 2) * 2
};
/* Plausibility, and it costs one comparison: the rail IS the sidebar collapsed, so
@@ -183,21 +186,67 @@ function shellGeometry() {
+ 'renamed, or a foreign sheet is reaching the measurement probe.');
}
_geom = sane ? g : Object.assign({}, GEOM_DFLT);
if (_geom.tokenPad === undefined) _geom.tokenPad = _geom.contentPad;
/* the token is only the fallback; the gutter that counts is the one measureShell() read */
if (_shellPad != null) _geom.contentPad = _shellPad;
return _geom;
}
/* THE GUTTER IS MEASURED WHERE IT IS APPLIED, not read off the token that usually supplies it, and
* SO IS THE WINDOW — but both are read HERE, from a fitter, and nowhere else.
*
* `--fs-content-pad` is what `.fs-content` uses at desktop widths, and `theme/20-shell.css` gives
* the same element `padding: var(--fs-space-4)` below 767px: the token says 28px a side while the
* column's gutter is 16px there. That is a 24px error in a model this file exists to keep honest,
* and `live-audit` found it on every page at 320, 390 and 568 — the same trap the alert-message rule
* under that media query carries a paragraph about, repeated in the JS. The breakpoint itself may
* NOT come here: a width literal copied into JS is exactly what these reads exist to avoid, and a
* media query has no `data-*` to key on either. Asking the element what it actually got is the only
* form that cannot drift.
*
* WHY IT IS A SEPARATE FUNCTION AND WHY ONLY A FITTER CALLS IT. `clientWidth` is a LAYOUT read and
* `getComputedStyle` resolves style; `contentWidth()` below is called mid-scroll, by the one pass in
* this theme that must answer without reading either (fs-select's, for a table the poll brought in
* under the reader's thumb). So the numbers are taken when a fitter runs — which is on every resize
* and every content mutation, and never during a flick, because `fitChrome()` defers exactly like
* every other measuring pass — and everything after that reads what was stored. Nothing they
* describe can change without a resize, and a resize schedules a fit.
*
* Unlike the token probe above, a hostile app's declaration is not a threat here: `.fs-content`
* carries no chrome mark, so an app CAN restyle it, and if it did then that padding IS the column's
* real gutter. What the probe must not report is a foreign answer for OUR layout; this reports the
* layout as it stands. Before any fitter has run — the login page has no `.fs-content` at all — the
* token stands in. */
let _shellOuter = 0, _shellPad = null;
function measureShell() {
_shellOuter = document.documentElement.clientWidth;
const host = document.querySelector('.fs-content');
const cs = host ? getComputedStyle(host) : null;
const v = cs ? parseFloat(cs.paddingLeft) + parseFloat(cs.paddingRight) : NaN;
if (Number.isFinite(v) && v >= 0) {
_shellPad = v;
if (_geom) _geom.contentPad = v;
}
}
function columnWidth(g, state) {
/* narrow OR top: in both the chrome is above the content, not beside it */
const cut = (state.narrow || state.top) ? 0 : (state.rail ? g.railW : g.sidebarW);
return Math.max(0, state.outerW - cut - g.contentPad);
}
function fitShell() {
const root = document.documentElement;
/* the one place the window and the gutter are read — see measureShell(). It runs in BOTH
* branches: the bar layout decides nothing here, but `contentWidth()` still answers in it. */
measureShell();
if (prefs.currentLayout() === 'top') { /* no sidebar, no cut, nothing to decide */
root.removeAttribute('data-narrow');
return;
}
const g = shellGeometry();
const cut = prefs.currentRail() ? g.railW : g.sidebarW;
/* clientWidth, not innerWidth: the column the content actually gets excludes a classic
* scrollbar, and innerWidth includes it — 15-17px of phantom room on Linux/Windows, i.e. the
* 500px floor really fired at ~484. */
const content = document.documentElement.clientWidth - cut - g.contentPad;
/* asked UNCOLLAPSED — this is the measurement that decides `data-narrow`, so it may not read it */
const content = columnWidth(g, { outerW: _shellOuter, rail: prefs.currentRail() });
/* toggleAttribute, NOT setAttribute: a same-value setAttribute still QUEUES a mutation record
* (measured in Chromium: 5 identical setAttribute('data-narrow','') -> 5 records; toggleAttribute
* on an already-present attribute -> 0). fitShell runs from fitChrome, which fs-fit calls on every
@@ -476,15 +525,30 @@ return baseclass.extend({
* a known amount of the window and the shell adds a known padding, and all three are already
* memoised against the density attribute for `fitShell()`. It is exported because a pass that
* must answer mid-scroll — fs-select's, for a table the poll has just brought in — otherwise has
* only `window.innerWidth`, which in the sidebar layout is the wrong number by exactly the
* only the window's own width, which in the sidebar layout is the wrong number by exactly the
* sidebar: at an 800px window the column is 520px, so the cheap judgement said "plenty of room"
* for a table that then overflowed and was clipped. */
* for a table that then overflowed and was clipped.
*
* The arithmetic itself is columnWidth()'s, shared with fitShell — see there. All this adds is
* the page's current state, which fitShell is in the middle of deciding and this one reads. */
contentWidth() {
const g = shellGeometry();
const narrow = document.documentElement.hasAttribute('data-narrow');
/* in the bar layout the chrome is above the content, not beside it */
const cut = narrow ? 0 : (prefs.currentRail() ? g.railW : g.sidebarW);
return Math.max(0, window.innerWidth - cut - (g.contentPad * 2));
/* NO LAYOUT READ, and that is the whole point of this export: the window's width and the
* column's gutter are whatever the last fitter measured (measureShell), and the three
* attributes below are style, not layout. The bootstrap read is for a caller that arrives
* before any fitter has run, which cannot happen mid-scroll. */
if (!_shellOuter) measureShell();
const root = document.documentElement;
return columnWidth(shellGeometry(), {
outerW: _shellOuter,
narrow: root.hasAttribute('data-narrow'),
top: prefs.isTopLayout(),
rail: prefs.currentRail()
});
},
/* exported for the unit suite (tests/chrome-geometry.test.mjs): the cut is pure arithmetic over
* four measured numbers, and driving it directly is the only way to hold every combination of
* layout, rail and width without a browser */
columnWidth,
wireRail
});
@@ -37,25 +37,38 @@ const _viewIntervals = (window.__fsViewIntervals || (window.__fsViewIntervals =
if (window.__fsIntervalsHooked) return;
window.__fsIntervalsHooked = true;
const _si = window.setInterval, _ci = window.clearInterval;
/* THE ID THE CALLER GOT IS THE ID IT KEEPS, whatever the pause below does underneath. The map is
* keyed by that first id and the entry carries `live`, the id the platform has armed RIGHT NOW
* (null while paused) — so a view holding its handle can still stop its own poller after a trip
* through a hidden tab, which is exactly what re-arming under a fresh id took away from it.
* The arguments are kept for the same reason: a `setInterval` id carries none of them back. */
window.setInterval = function (fn, ms) {
const id = _si.apply(window, arguments);
/* the arguments are kept, not just the id: a paused timer has to be re-armed with what it
* was armed with, and a `setInterval` id carries none of that back */
_viewIntervals.set(id, { fn, ms, rest: Array.prototype.slice.call(arguments, 2) });
_viewIntervals.set(id, { fn, ms, rest: Array.prototype.slice.call(arguments, 2), live: id });
return id;
};
window.clearInterval = function (id) {
const spec = _viewIntervals.get(id);
_viewIntervals.delete(id);
/* A PAUSED TIMER IS ALREADY DISARMED, and its number is the platform's to hand out again —
* clearing it here would stop whatever timer holds it now. Untracked ids fall through
* unchanged: the hook must stay a pass-through for everything it did not arm. */
if (spec) return (spec.live == null) ? undefined : _ci.call(window, spec.live);
return _ci.apply(window, arguments);
};
/* A HIDDEN TAB MUST NOT KEEP CALLING THE ROUTER. `wireVisibility()` below stops LuCI's own poll
* when the tab goes away, which is most of the traffic — but a view is free to run a plain
* `setInterval` of its own (luci-app-podkop's log tailer does), and those kept hammering ubus in
* a background tab for as long as it stayed open. The registry that navigation already uses to
* clear them is enough to pause them too: cleared on hide, re-armed on show with the same
* clear them is enough to pause them too: disarmed on hide, re-armed on show with the same
* callback and period, so a view that was polling every 3 s is polling every 3 s again and one
* that was cleared meanwhile stays cleared. */
let _paused = [];
* that was cleared meanwhile stays cleared.
*
* A PAUSED TIMER STAYS IN THE REGISTRY, armed on nothing. It was carried in a private list
* beside it, and a list is not what the navigation sweep reads: hide the tab while a navigation
* is in flight (a click, then straight to another tab), and coming back re-armed the timers of
* the page that navigation had already replaced — the sweep had run while they were somewhere
* it could not see. In the map, `clearViewIntervals()` takes them like any other. */
document.addEventListener('visibilitychange', () => {
if (document.hidden) {
/* LuCI'S OWN TICK IS NOT OURS TO PAUSE, and taking it made the page poll FASTER the
@@ -75,18 +88,22 @@ const _viewIntervals = (window.__fsViewIntervals || (window.__fsViewIntervals =
* costs a doubling that never stops. Same judgement as clearViewIntervals(). */
const keep = pollTickId();
if (keep === false) return;
_paused = [];
for (const [ id, spec ] of _viewIntervals) {
if (id === keep) continue;
_paused.push(spec);
_ci.call(window, id);
_viewIntervals.delete(id);
if (id === keep || spec.live == null) continue;
_ci.call(window, spec.live);
spec.live = null;
}
}
else {
const back = _paused;
_paused = [];
for (const spec of back) window.setInterval(spec.fn, spec.ms, ...spec.rest);
for (const spec of _viewIntervals.values()) {
if (spec.live != null) continue;
/* `_si`, not the hook: this timer is already in the registry under the id its
* caller is holding, and re-registering it would key a second entry to a number
* nobody has. The fresh id lives in `spec.live` alone and is never a KEY, so it
* cannot collide with an entry — and the platform hands ids out in sequence, so a
* re-arm cannot be given a number some earlier caller is still holding either. */
spec.live = _si.call(window, spec.fn, spec.ms, ...spec.rest);
}
}
});
})();
@@ -306,6 +323,22 @@ function markExpired() {
* ended, and the very next click sends the user to a login form. */
console.warn('footstrap: the LuCI session is gone — every navigation from here is a full load.');
}
/* AND THE VERDICT IS NOT A LATCH. It was, and the shape was wrong for a signal read off somebody
* else's reply: an interceptor sees `msg` only once the transport succeeded and the body parsed
* (rpc.js rejects before that), so a missing frame is not a network flap — but it IS a captive
* portal's page, a proxy's error body, one truncated reply. Any of those took the client router off
* for the rest of the document while the session was alive throughout, explained by one console
* line nobody reads until later.
*
* A clean `session.access` is the same call the failing one was, so it is evidence in the other
* direction and it is taken as such. If the session really has ended no clean one arrives, because
* every ubus call carries the same dead sid — the router stays off exactly as long as it should. */
function markAlive() {
if (!_expired) return;
_expired = false;
console.warn('footstrap: the LuCI session answers again — client navigation is back on.');
}
function sessionExpired() { return _expired; }
function watchSession() {
if (_sessionWired) return;
_sessionWired = true;
@@ -325,9 +358,22 @@ function watchSession() {
rpc.addInterceptor((msg, r) => {
try {
if (!r || r.object !== 'session' || r.method !== 'access') return;
if (!msg || msg.jsonrpc !== '2.0' ||
(msg.error && msg.error.code && msg.error.message))
markExpired();
if (!msg || msg.jsonrpc !== '2.0') return;
/* an `error` carrying both a code and a message is what handleCallReply() rejects
* on, and a rejected session probe is the signal. A frame that is not JSON-RPC 2.0
* is rejected there too, but it says nothing about the SESSION. */
if (msg.error && msg.error.code && msg.error.message) { markExpired(); return; }
/* AND ONLY `access: true` SAYS THE SESSION IS THERE — measured on the stands, because
* this is the one place where guessing costs the whole fix. A dead sid does not make
* this call fail: `session.access` answers `[0, {access:false}]` with HTTP 200 and no
* error frame at all (the `-32002` arrives on the ORDINARY call, which is what makes
* luci-base fire this probe in the first place). So "the reply parsed" would have
* been read as "the session is back", and the verdict a 403 had just reached would
* be cleared by the very probe that confirms it. `access:false` stays out of BOTH
* answers: an ACL denial for a restricted user looks exactly the same, which is why
* it may not expire a session either. */
if (Array.isArray(msg.result) && msg.result[1] && msg.result[1].access === true)
markAlive();
}
catch (e) { /* ditto */ }
});
@@ -1503,6 +1549,11 @@ return baseclass.extend({
/* exported for the unit suite (tests/router-contract.test.mjs), which drives it against a
* hand-broken `L` — the one way to see the OFF branch without a router that ships one */
contractBreaks,
/* likewise: tests/interval-pause.test.mjs drives the navigation sweep around a visibilitychange,
* and tests/session-expiry.test.mjs reads the verdict the interceptors reached. navigate() is
* the real caller of the first and `_expired` gates the second — nothing else may call either. */
clearViewIntervals,
sessionExpired,
/* fs-search warms the pages this admin actually uses (its recents) and the arrow-key-highlighted
* result, both of which the pointer/focus triggers above cannot see. The edge points that way
* round — search → router — because the router must keep no dependency on the palette. */
@@ -651,6 +651,34 @@ function fitTables() {
return;
}
/* AND THE TABLE ITSELF HAS TO BE MEASURABLE, WHICH ON ITS FIRST PASS IT IS NOT because
* THIS FILE'S OWN GATE is what hides it. `theme/30-tables.css` holds a `.fs-dt` out of the
* layout until something marks it `.fs-fitted`, so the very first pass over a fresh table
* reads `scrollWidth: 0` a `display: none` box has no content width and 0 overflows
* nothing. The pass therefore concluded "it fits", wrote `.fs-fitted`, and CACHED that
* answer against the slot; the table then appeared at its natural width, 777px inside a
* 712px column, columns cut off by `.fs-main { overflow-x: clip }` with no scrollbar and
* nothing to say so.
*
* It usually corrected itself, which is what made it a report rather than a gate failure: a
* second pass ~60 ms later measured the now-visible table and broke its widest column. But
* that pass only exists if something mutates `#view` again, and Status Processes renders
* once and then stands still. Measured on the stand at 768px, entering the page directly:
* 2 of 8 arrivals on 0.13.1 and 3 of 8 here left the table past the column, and one
* mutation of any kind fixed it instantly.
*
* So a zero measurement is not an answer to anything: lift the gate so the next frame can
* see the table, ask for that frame, and write NOTHING to the slot. The schedule is asked
* for once on the pass that lifts the gate so a table hidden for somebody else's reason
* (a closed section, a tab pane) cannot turn this into a frame loop. */
if (t.scrollWidth === 0) {
if (!t.classList.contains('fs-fitted')) {
t.classList.add('fs-fitted');
fit.schedule();
}
return;
}
/* the answer this slot already has, for a table of the same shape in the same room UNLESS the
* table has since outgrown it. Room and column count are not the whole input: a poll tick can
* put longer values into the same columns, and then a table that fitted a second ago does not.
@@ -9,7 +9,13 @@
-#}
{% if (dispatcher.lookup("admin/logout")): %}
<a class="fs-logout" href="{{ dispatcher.build_url('admin/logout') }}" title="{{ _('Log out') }}" aria-label="{{ _('Log out') }}">
{% include('themes/footstrap/partials/icon', { body: '<path d="M14 4h4a2 2 0 0 1 2 2v12a2 2 0 0 1-2 2h-4"/><path d="M10 12H3m0 0 3-3m-3 3 3 3"/>' }) %}
{#
MIRRORED ABOUT x=12, i.e. the glyph reads LEAVING rather than entering: the doorway sits on
the left and the arrow points out of it, to the right. Every path is the same path reflected
(each x becomes 24-x, and each arc's sweep flag flips with it), so the stroke widths, the
corner radii and the 24x24 box are untouched — nothing here is a redrawn icon.
-#}
{% include('themes/footstrap/partials/icon', { body: '<path d="M10 4H6a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h4"/><path d="M14 12h7m0 0-3-3m3 3-3 3"/>' }) %}
<span class="fs-label">{{ _('Log out') }}</span>
</a>
{% endif %}
+170
View File
@@ -0,0 +1,170 @@
# Self-contained static build of lpac for wwand-esim.
#
# lpac's ES9+ HTTPS leg needs a TLS stack with the full GSMA curve/algorithm
# set. OpenWrt's mbedtls is feature-stripped (fails the SM-DP+ TLS) and its
# libcurl+OpenSSL drags in ~6.4 MB of libcrypto/libssl. Instead we bundle a
# minimal static wolfSSL + a minimal static libcurl straight into the lpac
# binary: one ~1 MB self-contained executable, no libcurl/libssl .so at all.
#
# Proven on the RG650E: full SGP.22 RSP download over the wwand stdio bridge.
include $(TOPDIR)/rules.mk
PKG_NAME:=wwand-lpac
PKG_VERSION:=2.3.0
PKG_RELEASE:=1
PKG_LICENSE:=AGPL-3.0-only GPL-3.0-or-later curl
PKG_MAINTAINER:=
# lpac (primary source)
PKG_SOURCE:=lpac-$(PKG_VERSION).tar.gz
PKG_SOURCE_URL:=https://codeload.github.com/estkme-group/lpac/tar.gz/refs/tags/v$(PKG_VERSION)?
PKG_HASH:=skip
# unique build dir (the lpac tarball's top dir is lpac-2.3.0, same as the stock
# lpac package — extract it ourselves with --strip-components into our own dir)
PKG_BUILD_DIR:=$(BUILD_DIR)/wwand-lpac-$(PKG_VERSION)
# bundled, statically-linked dependencies
WOLFSSL_VERSION:=5.9.1-stable
CURL_VERSION:=8.21.0
PKG_BUILD_PARALLEL:=1
# wolfSSL's SP math (sp_int.c, WOLFSSL_SP_MIPS) carries inline assembly that
# MIPS16 cannot encode ("operand 3 must be an immediate expression `addu ...'")
PKG_BUILD_FLAGS:=no-mips16
include $(INCLUDE_DIR)/package.mk
define Download/wolfssl
FILE:=wolfssl-$(WOLFSSL_VERSION).tar.gz
URL:=https://github.com/wolfSSL/wolfssl/archive/v$(WOLFSSL_VERSION)
HASH:=skip
endef
$(eval $(call Download,wolfssl))
define Download/curl
FILE:=curl-$(CURL_VERSION).tar.xz
URL:=https://github.com/curl/curl/releases/download/curl-8_21_0
HASH:=skip
endef
$(eval $(call Download,curl))
define Package/wwand-lpac
SECTION:=net
CATEGORY:=Network
SUBMENU:=WWAN
TITLE:=lpac eSIM LPA — self-contained static build (wolfSSL)
URL:=https://github.com/estkme-group/lpac
# PROVIDES:=lpac so wwand-esim's `+lpac` dependency resolves to this
# self-contained build. Do NOT also CONFLICTS:=lpac — when the real `lpac`
# package is present in the feed set (CI), CONFLICTS emits a
# `depends on PACKAGE_lpac` (scripts/package-metadata.pl mconf_conflicts)
# which, together with the PROVIDES `select`, is a Kconfig recursive
# dependency that breaks `make defconfig` for every feed consumer. The
# vpackage provider model keeps the two `lpac` providers mutually exclusive.
PROVIDES:=lpac
endef
define Package/wwand-lpac/description
A single self-contained lpac binary for wwand-esim: minimal static wolfSSL
(TLS 1.2/1.3, ECDHE, ECDSA+RSA, NIST + Brainpool curves, x25519, AES-GCM,
SHA-2; built with WOLFSSL_NO_ASN_STRICT so the GSMA id-rspRole critical
extension is accepted) and a minimal static libcurl (HTTPS only) are linked
straight into lpac. No libcurl/libssl/libcrypto .so — ~1 MB total instead of
the ~6.4 MB that libcurl+OpenSSL pulls in. Only the stdio + curl drivers are
built (the wwand stdio bridge relays the eUICC APDUs through wwand's own
modem_apdu channel). Drop-in replacement for the stock lpac package.
Includes patch 110 (backport of upstream #399, the v2.3.0 stdio-driver fix).
endef
PREFIX:=$(PKG_BUILD_DIR)/.bundle
WOLFSSL_DIR:=$(PKG_BUILD_DIR)/wolfssl-$(WOLFSSL_VERSION)
CURL_DIR:=$(PKG_BUILD_DIR)/curl-$(CURL_VERSION)
LPAC_BUILD:=$(PKG_BUILD_DIR)/.lpac-build
# LTO across the whole bundle (wolfSSL + libcurl + lpac). Fat LTO objects keep
# real machine code alongside the IR, so the sub-libs' configure link-tests
# (plain, no -flto) still resolve symbols while the final lpac link (-flto) still
# optimises across all three. gcc-ar/gcc-ranlib are the LTO-aware archivers.
LTO_CFLAGS:=-flto=auto -ffat-lto-objects
BUNDLE_AR:=AR="$(TARGET_CROSS)gcc-ar" RANLIB="$(TARGET_CROSS)gcc-ranlib"
# extract lpac (strip its top dir into our unique build dir), patch it, then
# unpack the bundled wolfSSL + curl sources alongside it
define Build/Prepare
$(INSTALL_DIR) $(PKG_BUILD_DIR)
$(TAR) -C $(PKG_BUILD_DIR) --strip-components=1 -xzf $(DL_DIR)/$(PKG_SOURCE)
$(Build/Patch)
$(TAR) -C $(PKG_BUILD_DIR) -xzf $(DL_DIR)/wolfssl-$(WOLFSSL_VERSION).tar.gz
$(TAR) -C $(PKG_BUILD_DIR) -xJf $(DL_DIR)/curl-$(CURL_VERSION).tar.xz
endef
# nothing to autoconf for the top-level (lpac is CMake); the sub-libs are
# configured inside Build/Compile
Build/Configure:=
define Build/Compile
# 1) minimal static wolfSSL (GSMA-capable)
( cd $(WOLFSSL_DIR) && ./autogen.sh )
( cd $(WOLFSSL_DIR) && ./configure --host=$(GNU_TARGET_NAME) --prefix=$(PREFIX) \
--enable-static --disable-shared \
--enable-curl --enable-tls13 --enable-brainpool --enable-ecccustcurves \
--enable-curve25519 --disable-dtls --disable-oldtls \
--disable-examples --disable-crypttests \
CC="$(TARGET_CC)" $(BUNDLE_AR) \
CFLAGS="$(TARGET_CFLAGS) $(LTO_CFLAGS) -ffunction-sections -fdata-sections -DWOLFSSL_NO_ASN_STRICT" )
+$(MAKE) -C $(WOLFSSL_DIR)
$(MAKE) -C $(WOLFSSL_DIR) install
# 2) minimal static libcurl (HTTPS only) against the bundled wolfSSL
# -DSIZEOF_LONG_LONG=8: on 32-bit targets curl_config.h defines
# SIZEOF_LONG=4 and nothing else; wolfSSL's CTC_SETTINGS enum (types.h)
# then hits "#error bad math long / long long settings". 64-bit targets
# take the SIZEOF_LONG==8 branch and never look at it.
( cd $(CURL_DIR) && \
PKG_CONFIG_PATH="$(PREFIX)/lib/pkgconfig" PKG_CONFIG_LIBDIR="$(PREFIX)/lib/pkgconfig" \
./configure --host=$(GNU_TARGET_NAME) --prefix=$(PREFIX) --with-wolfssl=$(PREFIX) \
--enable-static --disable-shared \
--disable-ftp --disable-file --disable-ldap --disable-ldaps --disable-rtsp \
--disable-dict --disable-telnet --disable-tftp --disable-pop3 --disable-imap \
--disable-smtp --disable-gopher --disable-mqtt --disable-smb --disable-ntlm \
--disable-manual --disable-unix-sockets --disable-progress-meter \
--without-libpsl --without-zlib --without-nghttp2 --without-brotli \
--without-zstd --without-libidn2 --without-librtmp --without-ca-path \
--with-ca-bundle=/etc/ssl/certs/ca-certificates.crt \
CC="$(TARGET_CC)" $(BUNDLE_AR) \
CFLAGS="$(TARGET_CFLAGS) $(LTO_CFLAGS) -ffunction-sections -fdata-sections -DSIZEOF_LONG_LONG=8" \
CPPFLAGS="-I$(PREFIX)/include" LDFLAGS="-L$(PREFIX)/lib" LIBS="-lm" )
+$(MAKE) -C $(CURL_DIR)
$(MAKE) -C $(CURL_DIR) install
# 3) lpac, statically linked against the two bundled .a
rm -rf $(LPAC_BUILD)
mkdir -p $(LPAC_BUILD)
( cd $(LPAC_BUILD) && PKG_CONFIG_PATH="$(PREFIX)/lib/pkgconfig" cmake $(PKG_BUILD_DIR) \
-DCMAKE_SYSTEM_NAME=Linux -DCMAKE_SYSTEM_PROCESSOR=$(ARCH) \
-DCMAKE_C_COMPILER="$(TARGET_CROSS)gcc" \
-DCMAKE_AR="$(TARGET_CROSS)gcc-ar" -DCMAKE_RANLIB="$(TARGET_CROSS)gcc-ranlib" \
-DCMAKE_FIND_ROOT_PATH="$(PREFIX)" -DCMAKE_PREFIX_PATH="$(PREFIX)" \
-DCMAKE_C_FLAGS="$(TARGET_CFLAGS) $(LTO_CFLAGS) -ffunction-sections -fdata-sections -I$(PREFIX)/include" \
-DCMAKE_EXE_LINKER_FLAGS="$(TARGET_LDFLAGS) $(LTO_CFLAGS) -Wl,--gc-sections -L$(PREFIX)/lib" \
-DCMAKE_C_STANDARD_LIBRARIES="-L$(PREFIX)/lib -lwolfssl -lm" \
-DLPAC_DYNAMIC_LIBEUICC=OFF \
-DLPAC_WITH_APDU_PCSC=OFF -DLPAC_WITH_APDU_AT=OFF \
-DLPAC_WITH_APDU_QMI=OFF -DLPAC_WITH_APDU_MBIM=OFF \
-DLPAC_WITH_HTTP_CURL=ON \
-DCURL_INCLUDE_DIR="$(PREFIX)/include" -DCURL_LIBRARY="$(PREFIX)/lib/libcurl.a" )
+$(MAKE) -C $(LPAC_BUILD)
endef
define Package/wwand-lpac/install
$(INSTALL_DIR) $(1)/usr/lib
$(INSTALL_BIN) $(LPAC_BUILD)/output/lpac $(1)/usr/lib/lpac
$(INSTALL_DIR) $(1)/usr/bin
$(INSTALL_BIN) ./files/lpac.sh $(1)/usr/bin/lpac
endef
$(eval $(call BuildPackage,wwand-lpac))
+3
View File
@@ -0,0 +1,3 @@
#!/bin/sh
# thin wrapper so `lpac` on PATH resolves to the self-contained static binary
exec /usr/lib/lpac "$@"
@@ -0,0 +1,36 @@
Backport of upstream lpac PR #399 ("fix(driver): 'json_request' interface was
changed in b23f0e4", commit 977c324, merged 2025-11-02) to the v2.3.0 release.
json_print()/json_request() were changed to return true on success, but
apdu/stdio.c's connect/logic_channel_open/transmit still checked
`if (json_request(...)) return -1;` — bailing with -1 the moment a request is
sent successfully, so euicc_init dies at connect before reading any reply.
Drop this patch once lpac ships a release later than v2.3.0 that includes #399.
--- a/driver/apdu/stdio.c
+++ b/driver/apdu/stdio.c
@@ -160,7 +160,7 @@
static int apdu_interface_connect(struct euicc_ctx *ctx) {
int ecode;
- if (json_request("connect", NULL, 0)) {
+ if (!json_request("connect", NULL, 0)) {
return -1;
}
@@ -183,7 +183,7 @@
static int apdu_interface_logic_channel_open(struct euicc_ctx *ctx, const uint8_t *aid, uint8_t aid_len) {
int ecode;
- if (json_request("logic_channel_open", aid, aid_len)) {
+ if (!json_request("logic_channel_open", aid, aid_len)) {
return -1;
}
@@ -209,7 +209,7 @@
uint32_t tx_len) {
int ecode;
- if (json_request("transmit", tx, tx_len)) {
+ if (!json_request("transmit", tx, tx_len)) {
return -1;
}
+346
View File
@@ -0,0 +1,346 @@
# OpenWrt package definitions for the wwand connection manager.
# One source package (git: ddimension/wwand); the ucode side is split into a
# backend-neutral base plus per-backend packages so an install carries only the
# control protocols it needs:
# wwand - daemon + shared core + codec/client + native wwand_io.so
# transport module (no backend on its own)
# wwand-qmi - QMI backend (DEPENDS wwand)
# wwand-mbim - MBIM backend (DEPENDS wwand-qmi: the MBIM
# QMI-over-MBIM passthrough reuses
# qmi_backend)
# wwand-ncm - NCM/ECM backend (DEPENDS wwand)
# wwand-mhi - PCIe/MHI transport + drivers (DEPENDS wwand; add a backend)
# wwand-esim - eSIM management (DEPENDS wwand-qmi + wwand-lpac)
# A typical QMI router installs `wwand-qmi` (which pulls in `wwand`); add
# `wwand-mbim` / `wwand-ncm` for those modems, `wwand-mhi` for a PCIe/MHI modem.
# The ucode tree ships precompiled to bytecode by default — built by the
# repo-root CMakeLists.txt alongside wwand_io.so (dev opt-out:
# CONFIG_WWAND_UCODE_SOURCE).
#
# NOTE: the split needs the ucode source that lazy-loads every backend
# (qmi_lazy.uc + the backend-neutral daemon). Bump PKG_SOURCE_VERSION (and
# PKG_SOURCE_DATE / PKG_MIRROR_HASH) to a wwand commit that includes them.
include $(TOPDIR)/rules.mk
PKG_NAME:=wwand
PKG_RELEASE:=1
PKG_SOURCE_PROTO:=git
PKG_SOURCE_URL:=https://github.com/ddimension/wwand.git
PKG_SOURCE_VERSION:=61f4c2dc728d70e490c94b3e1d8cb2262c262ebb
PKG_SOURCE_DATE:=2026-08-17
PKG_MIRROR_HASH:=skip
PKG_LICENSE:=GPL-2.0-only
PKG_MAINTAINER:=
# host ucode is the bytecode compiler for the precompile step (below). It is
# built from the same ucode revision as the on-device runtime, so the bytecode
# it emits always matches the target interpreter.
PKG_BUILD_DEPENDS:=ucode/host
include $(INCLUDE_DIR)/package.mk
include $(INCLUDE_DIR)/cmake.mk
# One cmake build (repo-root CMakeLists.txt) produces both the native
# wwand_io.so (io/ subdir) and — by default — the ucode tree compiled to
# bytecode, one compiler invocation per file with explicit source lists (the
# whole mechanism, incl. the invariants it enforces, is documented in the
# wwand repo's CMakeLists.txt). The bytecode compiler is the SDK's host ucode
# (PKG_BUILD_DEPENDS:=ucode/host) — same ucode revision as the target runtime,
# which bytecode is locked to.
CMAKE_BINARY_SUBDIR:=build
CMAKE_OPTIONS += \
-DUCODE_COMPILER=$(STAGING_DIR_HOSTPKG)/bin/ucode \
-DUCODE_PRECOMPILE=$(if $(CONFIG_WWAND_UCODE_SOURCE),OFF,ON)
# every install section takes the ucode tree from here: the bytecode output of
# the cmake build, or the plain sources when CONFIG_WWAND_UCODE_SOURCE is set.
WWAND_UCODE=$(if $(CONFIG_WWAND_UCODE_SOURCE),$(PKG_BUILD_DIR)/src-ucode,$(PKG_BUILD_DIR)/$(CMAKE_BINARY_SUBDIR)/ucode/wwand)
UCDIR:=/usr/share/ucode/wwand
# Base-package ucode set, listed explicitly per install dir. No glob-then-`rm`:
# every module is owned by exactly one package, so a new backend file can never
# silently ship in the base too. The per-backend install lists below carry the
# QMI/MBIM/NCM/eSIM modules; main.uc/wwandctl.uc install as executables.
WWAND_BASE_UC:=apndb.uc atcmd.uc atcmd_parse.uc atport.uc backend.uc board.uc \
client.uc config.uc config_check.uc context_common.uc context_monitor_qmi.uc \
ctx_settings.uc daemon.uc datapath_qmi.uc discovery.uc hwops.uc log.uc \
modem_common.uc modem_init_qmi.uc modem_quirks.uc modeswitch.uc ncm_vendors.uc \
netlink.uc netsel_ops.uc protocol_switch.uc reconnect.uc recovery.uc regdetail.uc \
sim.uc sim_plmn.uc simops.uc sms.uc sms_pdu.uc telemetry_mbim.uc telemetry_ncm.uc \
telemetry_qmi.uc transport.uc ubus.uc
WWAND_BASE_CODEC:=arfcn_bands.uc hex.uc qmux.uc tlv.uc
WWAND_BASE_SCHEMA:=ctl.uc dms.uc dsd.uc loc.uc loc_lazy.uc merge.uc nas.uc rat.uc \
uim.uc wda.uc wds.uc wms.uc wms_lazy.uc
# DEVELOPERS: enable CONFIG_WWAND_UCODE_SOURCE (menuconfig, under this package)
# to ship readable .uc source instead of bytecode — for editing modules live
# under /usr/share/ucode/wwand and for source-line tracebacks; bytecode
# tracebacks report offsets only.
PKG_CONFIG_DEPENDS:=CONFIG_WWAND_UCODE_SOURCE
define Package/wwand/config
config WWAND_UCODE_SOURCE
bool "Ship ucode sources instead of precompiled bytecode"
depends on PACKAGE_wwand
default n
help
wwand normally ships its ucode tree precompiled to bytecode
(faster daemon start, no parse step on the device). Enable this
for development builds: the readable sources are installed
instead, runtime tracebacks carry source line numbers and the
modules can be edited in place on the device.
endef
# ---------------------------------------------------------------------------
# base: daemon + framework + codec/client + shared core. No backend on its own;
# the daemon loads whichever wwand-qmi/-mbim/-ncm package is present.
# ---------------------------------------------------------------------------
define Package/wwand
SECTION:=net
CATEGORY:=Network
SUBMENU:=WWAN
TITLE:=Event-driven cellular connection manager (backend-neutral core)
DEPENDS:=+ucode +ucode-mod-fs +ucode-mod-struct +ucode-mod-uloop \
+ucode-mod-ubus +ucode-mod-uci +ucode-mod-rtnl
# the native wwand_io transport module ships inside this package (it is
# wwand-private, always version-locked to the ucode side). PROVIDES keeps
# configs/upgrades from the era of the separate ucode-mod-wwand-io working.
PROVIDES:=ucode-mod-wwand-io
endef
define Package/wwand/description
wwand is an event-driven connection manager daemon for cellular modems,
supporting multiple modems and multiple parallel PDP contexts. This base
package carries the daemon, the netifd proto shim, the QMI codec (qmux/tlv +
service schemas), the generic service client, the SIM/APDU layer and the
shared modem/context core. Install a backend package (wwand-qmi, wwand-mbim
or wwand-ncm) for the control protocols your modems use.
endef
define Package/wwand/install
# native transport module (built by the cmake step from io/)
$(INSTALL_DIR) $(1)/usr/lib/ucode
$(INSTALL_BIN) $(CMAKE_BINARY_DIR)/io/wwand_io.so $(1)/usr/lib/ucode/
# base ucode tree — explicit per-file lists (see WWAND_BASE_* above), so the
# base package owns exactly its own modules and nothing a backend ships.
$(INSTALL_DIR) $(1)$(UCDIR)/codec/schema
$(INSTALL_DATA) $(addprefix $(WWAND_UCODE)/,$(WWAND_BASE_UC)) $(1)$(UCDIR)/
$(INSTALL_DATA) $(addprefix $(WWAND_UCODE)/codec/,$(WWAND_BASE_CODEC)) $(1)$(UCDIR)/codec/
$(INSTALL_DATA) $(addprefix $(WWAND_UCODE)/codec/schema/,$(WWAND_BASE_SCHEMA)) $(1)$(UCDIR)/codec/schema/
# daemon + CLI as executables (not under $(UCDIR))
$(INSTALL_DIR) $(1)/usr/sbin
$(INSTALL_BIN) $(WWAND_UCODE)/main.uc $(1)/usr/sbin/wwand
# wwandctl: the human-friendly CLI front-end over the ubus API
$(INSTALL_DIR) $(1)/usr/bin
$(INSTALL_BIN) $(WWAND_UCODE)/wwandctl.uc $(1)/usr/bin/wwandctl
$(INSTALL_DIR) $(1)/lib/netifd/proto
# the shim registers `wwand`; the legacy `qmi` alias only when the global
# `option takeover` is set (default off, so uqmi keeps `proto qmi`). Install
# under the current name — the historical qmi.sh belonged to uqmi
$(INSTALL_BIN) $(PKG_BUILD_DIR)/files/wwand-proto.sh $(1)/lib/netifd/proto/wwand.sh
$(INSTALL_DIR) $(1)/usr/libexec/wwand
$(INSTALL_BIN) $(PKG_BUILD_DIR)/files/wwand-migrate $(1)/usr/libexec/wwand/migrate
# migration is user-triggered (LuCI modem list / the migrate CLI); the
# uci-defaults hook auto-migrates on install/upgrade ONLY when the global
# `option takeover` is set (default off — wwand does not touch existing configs)
$(INSTALL_DIR) $(1)/etc/uci-defaults
$(INSTALL_BIN) $(PKG_BUILD_DIR)/files/wwand.defaults $(1)/etc/uci-defaults/99-wwand-migrate
$(INSTALL_DIR) $(1)/etc/init.d
$(INSTALL_BIN) $(PKG_BUILD_DIR)/files/wwand.init $(1)/etc/init.d/wwand
# no /etc/config/wwand for new installs — all config lives in
# /etc/config/network now (existing files survive upgrade and are still read).
# files/wwand.config is kept in the source tree only as a documented example.
$(INSTALL_DIR) $(1)/etc/hotplug.d/usbmisc
$(INSTALL_DATA) $(PKG_BUILD_DIR)/files/wwand.hotplug $(1)/etc/hotplug.d/usbmisc/20-wwand
# net hotplug: NCM modems (no cdc-wdm) + re-enumeration after a mode switch
$(INSTALL_DIR) $(1)/etc/hotplug.d/net
$(INSTALL_DATA) $(PKG_BUILD_DIR)/files/wwand.hotplug.net $(1)/etc/hotplug.d/net/20-wwand
# tty hotplug: AT ports appearing after the datapath netdev (vendor-serial
# new_id bind / late kmodloader) re-kick a modem parked in no_at_port backoff
$(INSTALL_DIR) $(1)/etc/hotplug.d/tty
$(INSTALL_DATA) $(PKG_BUILD_DIR)/files/wwand.hotplug.tty $(1)/etc/hotplug.d/tty/20-wwand
# NOTE: the kernel-wwan-subsystem hotplug (/etc/hotplug.d/wwan/20-wwand) is
# NOT installed here — it belongs to wwand-mhi, which also pulls the MHI
# drivers that create /sys/class/wwan in the first place (procd only arms a
# subsystem whose hotplug dir exists). USB modems never need it.
endef
# ---------------------------------------------------------------------------
# wwand-qmi: the QMI backend (native qmux over /dev/cdc-wdmX).
# ---------------------------------------------------------------------------
define Package/wwand-qmi
SECTION:=net
CATEGORY:=Network
SUBMENU:=WWAN
TITLE:=QMI backend for wwand
DEPENDS:=+wwand +kmod-usb-net-qmi-wwan +kmod-rmnet
# coexists with the stock OpenWrt QMI stack (uqmi):
# by default wwand claims only `proto wwand` interfaces, so both can be
# installed. Set the global `option takeover '1'` (or migrate interfaces from
# the LuCI modem list) to hand `proto qmi` interfaces to wwand.
endef
define Package/wwand-qmi/description
QMI control backend for wwand: talks QMI natively over /dev/cdc-wdmX (qmi_wwan
driver) with QMAP multiplexing (rmnet), without spawning uqmi/qmicli. This is
the common case for most cellular routers.
endef
define Package/wwand-qmi/install
$(INSTALL_DIR) $(1)$(UCDIR)
$(INSTALL_DATA) $(WWAND_UCODE)/modem.uc $(1)$(UCDIR)/
$(INSTALL_DATA) $(WWAND_UCODE)/context.uc $(1)$(UCDIR)/
$(INSTALL_DATA) $(WWAND_UCODE)/qmi_backend.uc $(1)$(UCDIR)/
$(INSTALL_DATA) $(WWAND_UCODE)/callend.uc $(1)$(UCDIR)/
$(INSTALL_DATA) $(WWAND_UCODE)/qmi_lazy.uc $(1)$(UCDIR)/
endef
# ---------------------------------------------------------------------------
# wwand-mbim: the MBIM backend. Depends on wwand-qmi because the QMI-over-MBIM
# passthrough runs the QMI stack (qmi_backend) over the open MBIM channel.
# ---------------------------------------------------------------------------
define Package/wwand-mbim
SECTION:=net
CATEGORY:=Network
SUBMENU:=WWAN
TITLE:=MBIM backend for wwand
DEPENDS:=+wwand-qmi +kmod-usb-net-cdc-mbim
# coexists with the stock OpenWrt MBIM stack (netifd `mbim` proto, package
# umbim): wwand manages a cdc_mbim modem only once its interface has been
# migrated to `proto wwand` (LuCI modem list / migrate CLI), so both stacks can
# be installed side by side.
endef
define Package/wwand-mbim/description
MBIM control backend for wwand (cdc_mbim driver): native MS Basic Connect (+
Extensions v2/v3) plus a QMI-over-MBIM passthrough that tunnels the whole QMI
stack over the open MBIM channel for full CDC-level telemetry/config. The
passthrough reuses qmi_backend, hence the dependency on wwand-qmi.
endef
define Package/wwand-mbim/install
$(INSTALL_DIR) $(1)$(UCDIR)/codec/mbim_schema
$(INSTALL_DATA) $(WWAND_UCODE)/modem_mbim.uc $(1)$(UCDIR)/
$(INSTALL_DATA) $(WWAND_UCODE)/context_mbim.uc $(1)$(UCDIR)/
$(INSTALL_DATA) $(WWAND_UCODE)/mbim_backend.uc $(1)$(UCDIR)/
$(INSTALL_DATA) $(WWAND_UCODE)/mbim_client.uc $(1)$(UCDIR)/
$(INSTALL_DATA) $(WWAND_UCODE)/qmi_over_mbim.uc $(1)$(UCDIR)/
$(INSTALL_DATA) $(WWAND_UCODE)/mbim_lazy.uc $(1)$(UCDIR)/
$(INSTALL_DATA) $(WWAND_UCODE)/codec/mbim.uc $(1)$(UCDIR)/codec/
$(INSTALL_DATA) $(WWAND_UCODE)/codec/mbim_schema/*.uc $(1)$(UCDIR)/codec/mbim_schema/
endef
# ---------------------------------------------------------------------------
# wwand-ncm: the NCM/ECM backend (cdc_ncm / cdc_ether, AT-controlled).
# ---------------------------------------------------------------------------
define Package/wwand-ncm
SECTION:=net
CATEGORY:=Network
SUBMENU:=WWAN
TITLE:=NCM/ECM backend for wwand
DEPENDS:=+wwand +kmod-usb-net-cdc-ncm +kmod-usb-net-cdc-ether
# coexists with the stock OpenWrt NCM stack (netifd `ncm` proto, package
# comgt-ncm): wwand drives an AT/NCM modem only once its interface has been
# migrated to `proto wwand` (LuCI modem list / migrate CLI), so both stacks can
# be installed side by side.
endef
define Package/wwand-ncm/description
NCM/ECM control backend for wwand: an AT-controlled datapath over a plain
cdc_ncm / cdc_ether netdev, for modems that expose no cdc-wdm control device.
endef
define Package/wwand-ncm/install
$(INSTALL_DIR) $(1)$(UCDIR)
$(INSTALL_DATA) $(WWAND_UCODE)/modem_ncm.uc $(1)$(UCDIR)/
$(INSTALL_DATA) $(WWAND_UCODE)/context_ncm.uc $(1)$(UCDIR)/
$(INSTALL_DATA) $(WWAND_UCODE)/ncm_lazy.uc $(1)$(UCDIR)/
endef
# ---------------------------------------------------------------------------
# wwand-mhi: PCIe/MHI transport bundle. Modems on the kernel `wwan` subsystem
# (Foxconn T99W175/SDX, Quectel RM5xx-AE, Telit FN990, …) expose their QMI/MBIM
# control port under /sys/class/wwan — not usbmisc/cdc-wdm — and need the MHI
# driver stack instead of the USB kmods. This package pulls those drivers and
# ships the wwan-subsystem hotplug that binds such a modem on cold-plug. It is
# transport-only and backend-neutral: install ALONGSIDE wwand-qmi (QMI over MHI,
# rmnet data) or wwand-mbim (MBIM over MHI) for the control protocol.
# ---------------------------------------------------------------------------
define Package/wwand-mhi
SECTION:=net
CATEGORY:=Network
SUBMENU:=WWAN
TITLE:=PCIe/MHI transport + drivers for wwand
DEPENDS:=+wwand +kmod-mhi-pci-generic +kmod-mhi-wwan-ctrl \
+kmod-mhi-wwan-mbim +kmod-mhi-net
endef
define Package/wwand-mhi/description
Support for cellular modems attached over PCIe/MHI (Foxconn T99W175 and
similar), whose control ports appear on the kernel wwan subsystem. Pulls the
MHI bus/PCI/control/data drivers and installs the wwan-subsystem hotplug so
wwand binds these modems on boot. Install together with a control backend
(wwand-qmi or wwand-mbim).
endef
define Package/wwand-mhi/install
# wwan-subsystem hotplug: MHI modems expose their control port under
# /sys/class/wwan (/dev/wwanXqmiN|mbimN). procd only dispatches the `wwan`
# subsystem once this directory exists — so it ships with the MHI drivers,
# not with the base package.
$(INSTALL_DIR) $(1)/etc/hotplug.d/wwan
$(INSTALL_DATA) $(PKG_BUILD_DIR)/files/wwand.hotplug.wwan $(1)/etc/hotplug.d/wwan/20-wwand
endef
# ---------------------------------------------------------------------------
# wwand-esim: eSIM (eUICC) profile management. Needs a QMI UIM/APDU channel.
# ---------------------------------------------------------------------------
define Package/wwand-esim
SECTION:=net
CATEGORY:=Network
SUBMENU:=WWAN
TITLE:=eSIM (eUICC) profile management for wwand
# Needs an lpac binary at /usr/bin/lpac: either the generic openwrt-packages
# lpac, or our self-contained static wwand-lpac (bundled wolfSSL+libcurl, no
# OpenSSL/mbedtls .so — it PROVIDES lpac, so it satisfies +lpac too). lpac
# drives the SM-DP+ download/notification (ES9+ HTTPS on the router); the
# daemon bridges its stdio APDU protocol inline. APDU access uses the modem
# UIM channel, so it also needs the QMI backend.
DEPENDS:=+wwand-qmi +lpac
endef
define Package/wwand-esim/description
Optional ES10c (SGP.22) profile management for wwand: EID, profile list,
enable/disable/delete over the modem UIM APDU channel. SM-DP+ profile
downloads and notification acknowledgements are driven by lpac: its ES10
APDUs are relayed through wwand's own APDU channel (ubus modem_apdu) by the
stdio bridge, so wwand stays the sole owner of the modem — no separate AT
port. lpac runs the ES9+ HTTPS on the router; no dedicated modem APN is
required.
The download is handled by an lpac binary (the +lpac dependency): either the
generic openwrt-packages lpac, or the self-contained wwand-lpac (which PROVIDES
lpac) — a single ~0.9 MB binary with a minimal static wolfSSL + libcurl linked
in, needing no libcurl/libssl/libcrypto .so (vs the ~6.4 MB stock libcurl+
OpenSSL pulls in) and carrying the GSMA-capable TLS itself. Either works via
/usr/bin/lpac; the stdio APDU bridge needs lpac >= 2.3.0 (upstream PR #399).
See wwand-lpac/Makefile.
endef
define Package/wwand-esim/install
$(INSTALL_DIR) $(1)$(UCDIR)
$(INSTALL_DATA) $(WWAND_UCODE)/esim.uc $(1)$(UCDIR)/
$(INSTALL_DATA) $(WWAND_UCODE)/esim_bridge.uc $(1)$(UCDIR)/
endef
$(eval $(call BuildPackage,wwand))
$(eval $(call BuildPackage,wwand-qmi))
$(eval $(call BuildPackage,wwand-mbim))
$(eval $(call BuildPackage,wwand-ncm))
$(eval $(call BuildPackage,wwand-mhi))
$(eval $(call BuildPackage,wwand-esim))