From c4ac5442bccf491f314ee5a7f0cc18a1fdc9d1bd Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Tue, 18 Aug 2026 23:43:20 +0800 Subject: [PATCH] =?UTF-8?q?=F0=9F=94=A5=20Sync=202026-08-18=2023:43:20?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- luci-app-wwand/Makefile | 59 +++ luci-proto-wwand/Makefile | 62 ++++ luci-theme-footstrap/Makefile | 2 +- .../htdocs/luci-static/resources/fs-chrome.js | 90 ++++- .../htdocs/luci-static/resources/fs-router.js | 85 ++++- .../htdocs/luci-static/resources/fs-select.js | 28 ++ .../themes/footstrap/partials/logout.ut | 8 +- wwand-lpac/Makefile | 170 +++++++++ wwand-lpac/files/lpac.sh | 3 + .../110-stdio-apdu-request-check.patch | 36 ++ wwand/Makefile | 346 ++++++++++++++++++ 11 files changed, 857 insertions(+), 32 deletions(-) create mode 100644 luci-app-wwand/Makefile create mode 100644 luci-proto-wwand/Makefile create mode 100644 wwand-lpac/Makefile create mode 100644 wwand-lpac/files/lpac.sh create mode 100644 wwand-lpac/patches/110-stdio-apdu-request-check.patch create mode 100644 wwand/Makefile diff --git a/luci-app-wwand/Makefile b/luci-app-wwand/Makefile new file mode 100644 index 00000000..e1ee68e4 --- /dev/null +++ b/luci-app-wwand/Makefile @@ -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)) diff --git a/luci-proto-wwand/Makefile b/luci-proto-wwand/Makefile new file mode 100644 index 00000000..32594648 --- /dev/null +++ b/luci-proto-wwand/Makefile @@ -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)) diff --git a/luci-theme-footstrap/Makefile b/luci-theme-footstrap/Makefile index fbaa2839..ad711fd9 100644 --- a/luci-theme-footstrap/Makefile +++ b/luci-theme-footstrap/Makefile @@ -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 diff --git a/luci-theme-footstrap/htdocs/luci-static/resources/fs-chrome.js b/luci-theme-footstrap/htdocs/luci-static/resources/fs-chrome.js index e5396551..05354ef6 100644 --- a/luci-theme-footstrap/htdocs/luci-static/resources/fs-chrome.js +++ b/luci-theme-footstrap/htdocs/luci-static/resources/fs-chrome.js @@ -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 }); diff --git a/luci-theme-footstrap/htdocs/luci-static/resources/fs-router.js b/luci-theme-footstrap/htdocs/luci-static/resources/fs-router.js index 092c2df6..8a6c4fd1 100644 --- a/luci-theme-footstrap/htdocs/luci-static/resources/fs-router.js +++ b/luci-theme-footstrap/htdocs/luci-static/resources/fs-router.js @@ -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. */ diff --git a/luci-theme-footstrap/htdocs/luci-static/resources/fs-select.js b/luci-theme-footstrap/htdocs/luci-static/resources/fs-select.js index c9d92b89..39df4ba9 100644 --- a/luci-theme-footstrap/htdocs/luci-static/resources/fs-select.js +++ b/luci-theme-footstrap/htdocs/luci-static/resources/fs-select.js @@ -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. diff --git a/luci-theme-footstrap/ucode/template/themes/footstrap/partials/logout.ut b/luci-theme-footstrap/ucode/template/themes/footstrap/partials/logout.ut index 88a32001..19efa5a6 100644 --- a/luci-theme-footstrap/ucode/template/themes/footstrap/partials/logout.ut +++ b/luci-theme-footstrap/ucode/template/themes/footstrap/partials/logout.ut @@ -9,7 +9,13 @@ -#} {% if (dispatcher.lookup("admin/logout")): %} - {% include('themes/footstrap/partials/icon', { body: '' }) %} + {# + 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: '' }) %} {{ _('Log out') }} {% endif %} diff --git a/wwand-lpac/Makefile b/wwand-lpac/Makefile new file mode 100644 index 00000000..268dec6c --- /dev/null +++ b/wwand-lpac/Makefile @@ -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)) diff --git a/wwand-lpac/files/lpac.sh b/wwand-lpac/files/lpac.sh new file mode 100644 index 00000000..98b9c13a --- /dev/null +++ b/wwand-lpac/files/lpac.sh @@ -0,0 +1,3 @@ +#!/bin/sh +# thin wrapper so `lpac` on PATH resolves to the self-contained static binary +exec /usr/lib/lpac "$@" diff --git a/wwand-lpac/patches/110-stdio-apdu-request-check.patch b/wwand-lpac/patches/110-stdio-apdu-request-check.patch new file mode 100644 index 00000000..1412d671 --- /dev/null +++ b/wwand-lpac/patches/110-stdio-apdu-request-check.patch @@ -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; + } diff --git a/wwand/Makefile b/wwand/Makefile new file mode 100644 index 00000000..60558451 --- /dev/null +++ b/wwand/Makefile @@ -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))