Skip to content

luci-app-wwand + luci-proto-wwand: add LuCI support for the wwand connection manager - #8917

Open
ddimension wants to merge 2 commits into
openwrt:masterfrom
ddimension:wwand-luci
Open

luci-app-wwand + luci-proto-wwand: add LuCI support for the wwand connection manager#8917
ddimension wants to merge 2 commits into
openwrt:masterfrom
ddimension:wwand-luci

Conversation

@ddimension

@ddimension ddimension commented Aug 7, 2026

Copy link
Copy Markdown

LuCI companion packages for the wwand cellular connection manager (submitted to openwrt/packages as net/wwand, upstream repo https://github.com/ddimension/wwand).

  • luci-app-wwand — Status → Modem pages: live signal + cell environment (serving/neighbour cells, LTE & NR5G, peak-hold for antenna alignment), connection details, SIM/eSIM management (slots, PIN/PUK unlock, SGP.22 profiles), SMS, operator scan/selection and a modem settings editor (modes/bands, guarded modem-reset). Includes a "Migratable interfaces" list that converts an existing proto qmi/mbim/ncm interface in place to proto wwand (user-triggered). Ships the shared wwand.bands band/frequency resource.
  • luci-proto-wwand — protocol handler for proto 'wwand' (Network → Interfaces): modem selection, APN / PDP type / auth, per-interface options.

Good-citizen coexistence. luci-proto-wwand ships only protocol/wwand.js — it does not install a protocol/qmi.js alias and does not conflict with luci-proto-qmi. proto qmi/mbim/ncm interfaces stay with the stock LuCI proto handlers; wwand manages only proto wwand, and moving an interface across is the explicit "Migrate" action above. Pairs with openwrt/packages#30185 (wwand 1.6.4).

🤖 Generated with Claude Code

@openwrt openwrt Bot added the add package Introduces a new package Makefile build script label Aug 7, 2026

@openwrt-ai openwrt-ai left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed 2 new commits (both messages match their diffs). 12 inline comments, the main ones being an rpc expect unwrapping bug in wwand.modemopts, two ACL gaps (modem_scan_start/modem_scan_status, file list /dev/), the undeclared protocol/qmi.js file collision with luci-proto-qmi, and a poller that outlives the interface-edit modal.


Generated by Claude Code

Comment thread applications/luci-app-wwand/htdocs/luci-static/resources/wwand/modemopts.js Outdated
Comment thread applications/luci-app-wwand/htdocs/luci-static/resources/wwand/modemopts.js Outdated
Comment thread applications/luci-app-wwand/root/usr/share/rpcd/acl.d/luci-app-wwand.json Outdated
Comment thread protocols/luci-proto-wwand/Makefile
Comment thread applications/luci-app-wwand/htdocs/luci-static/resources/view/wwand/settings.js Outdated
Comment thread applications/luci-app-wwand/htdocs/luci-static/resources/wwand/bands.js Outdated
Comment thread applications/luci-app-wwand/htdocs/luci-static/resources/view/wwand/settings.js Outdated
Comment thread protocols/luci-proto-wwand/htdocs/luci-static/resources/protocol/wwand.js Outdated
@openwrt openwrt Bot added the not following guidelines Pull request does not follow formatting guidelines label Aug 8, 2026
@ddimension

Copy link
Copy Markdown
Author

Updated to the current sources, addressing the inline review:

  • Dropped the legacy qmi proto alias (protocol/qmi.js) — it collided with luci-proto-qmi (same path) and presented proto qmi as wwand-managed even when it isn't. proto qmi now stays with luci-proto-qmi/uqmi; interfaces are moved to proto wwand from a new Migratable interfaces list in the modem page (in line with wwand becoming opt-in / coexisting with the stock stack — see wwand: add cellular connection manager packages#30185).
  • SIM-slot dropdown: unwrap the statusRaw reply's .modems (was Object.keys on the whole reply) and read sl.physical (was sl.slot).
  • Live-field poller is removed when its modal closes (was a permanent 5 s poll issuing status/signal/cells forever).
  • status.js cell headers use literal _() for the i18n scanner; settings.js uses dom.content(); ACL grants for the async scan; callGpioList/callRepower dead decls dropped.

Still on the list (follow-ups, not blockers): a modem selector on the settings page instead of hard-binding names[0].

@ddimension
ddimension force-pushed the wwand-luci branch 2 times, most recently from e9a073a to f52b465 Compare August 8, 2026 21:23
@openwrt openwrt Bot removed the not following guidelines Pull request does not follow formatting guidelines label Aug 8, 2026

@openwrt-ai openwrt-ai left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed 2 new commits (343ab75, f52b465). The earlier findings are addressed: the protocol/qmi.js path collision is gone, the statusRaw/.modems unwrap and sl.physical are fixed, the async-scan ACL grants are in, status.js uses literal _() headers, and the interface-edit poller now unregisters itself. 4 new inline comments — the notable one is a modem_reset ACL gap plus a section_id/modem-name mixup, both fallout from the modem_repowermodem_reset rename in the shared wwand.modemopts module.

Commit checks

  • 343ab75 "luci-app-wwand: sync modem UI to current sources" — the two bullets cover the Modems-page additions and the review fixes, but not the largest parts of the diff. Undocumented: the Status → Modem detail page is deleted outright (view/wwand/detail.js, −306 lines, plus its admin/status/wwand-detail menu entry); view/wwand/settings.js is split into five new shared resources (wwand.rpc / format / esim / netsel / modemsid, −729 lines there); the _repower action and its wwand.modem_repower ubus call become _reset / wwand.modem_reset; the Modem Tools menu node loses its title; and a new fcc_auth modem option is added. Removing a user-visible page and swapping a ubus method are the kind of thing worth naming in the message — the ACL fallout of that swap is the subject of one of the inline comments.

Generated by Claude Code

Comment thread applications/luci-app-wwand/htdocs/luci-static/resources/wwand/modemopts.js Outdated
Comment thread protocols/luci-proto-wwand/root/usr/share/rpcd/acl.d/luci-proto-wwand.json Outdated
Comment thread applications/luci-app-wwand/htdocs/luci-static/resources/wwand/netsel.js Outdated
ddimension pushed a commit to ddimension/wwand that referenced this pull request Aug 9, 2026
Package-G artefacts for the openwrt/packages#30185 + openwrt/luci#8917 push:
- upstream/dossier-packages-30185.md — maturity/coexistence dossier to post as a
  PR comment (ModemManager precedent, independent-verification story, HW/test
  matrix, audit changelog, device-ownership answer, re-review request).
- upstream/rfc-openwrt-devel.md — the architecture RFC (improve-existing-stack
  vs. a separate opt-in proto) for openwrt-devel.
- upstream/checklist.md — the push/tag/PR/feed action sequence.

Drafts for author review; sending/posting is manual.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@openwrt openwrt Bot added the not following guidelines Pull request does not follow formatting guidelines label Aug 14, 2026
@ddimension

Copy link
Copy Markdown
Author

Synced to the wwand 1.2.0 sources (paired with openwrt/packages#30185).

  • SIM unlock dialog on the modem list — PUK entry (modem_sim_puk, with remaining-attempts + a wrong-PUK safeguard) and the manual PIN release for PIN-class blocks; ACL grants for both.
  • "Configure modem" persists immediately (fixes a lost path binding); Save-SIM + Reattach actions; SMS-send ACL.
  • Status/format/netsel updates for the unified telemetry (mode / RAT / caps), an MCC/MNC operator-name resource, per-operator scan RAT.
  • Proto: hard_reconnect_on_ip_change flag.

All JS node --check-clean.

@openwrt openwrt Bot removed the not following guidelines Pull request does not follow formatting guidelines label Aug 14, 2026

@openwrt-ai openwrt-ai left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed 1 new commit (5da1788). 5 inline comments — the blocking one is a var plmn redeclaration in status.js that both kills the new NAS panel and throws on an unregistered modem; the rest are an unreachable plmnEditor() in settings.js and three items in the reworked ACL file.

Checked the new UCI surface (wwand_plmnlist / plmn_list / hard_reconnect_on_ip_change) against the backend-coupling rule: wwand is not in the packages feed yet, but the PR body and the follow-up comment already name openwrt/packages#30185 as the pairing, so not flagged. CI on the head SHA is green.

Commit checks

  • 5da1788 "luci-app-wwand, luci-proto-wwand: sync to wwand 1.2.0 sources" — the four bullets describe real changes, but they leave out the largest one. Undocumented: the whole preferred-PLMN manager (renderPlmnManager, ~200 new lines in settings.js), the new modem_plmn_set / modem_plmn_restore ubus methods, a new UCI section type wwand_plmnlist (type + plmn list) and the plmn_list options it adds to both wwand.modemopts and wwand.simlist. Also unmentioned: the new Datapath & muxing status panel (modem_datapath), the Preferred networks (NAS) panel, the cachedCall/slowCache rework that moves cells/slots/datapath/PLMN off the 1 s poll onto per-call TTLs, the fmt.term() tooltip pass across status.js, adding modemmanager to the migration list, and the tab→space reindent of the ACL file. Introducing a new config section type and a new poll-caching model are the kind of thing worth naming in the message.

Generated by Claude Code

Comment thread applications/luci-app-wwand/htdocs/luci-static/resources/view/wwand/status.js Outdated
Comment thread applications/luci-app-wwand/htdocs/luci-static/resources/view/wwand/settings.js Outdated
Comment thread applications/luci-app-wwand/root/usr/share/rpcd/acl.d/luci-app-wwand.json Outdated
Comment thread applications/luci-app-wwand/root/usr/share/rpcd/acl.d/luci-app-wwand.json Outdated
Comment thread applications/luci-app-wwand/root/usr/share/rpcd/acl.d/luci-app-wwand.json Outdated

@openwrt-ai openwrt-ai left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed 2 new commits (12ef4b1, d7bb9ee). These replace the single 5da1788 I reviewed last round — git diff 5da1788..d7bb9ee is empty, so this is a pure re-split of that commit into an app half and a proto half (plus the dropped Co-Authored-By trailer). The split itself is clean: 12ef4b1 touches only applications/luci-app-wwand/, d7bb9ee only protocols/luci-proto-wwand/.

Because the tree is unchanged, the five findings from the previous round are all still open, including the blocking one: the var plmn redeclaration in status.js, which leaves the new "Preferred networks (NAS)" panel permanently unrendered and throws on an unregistered modem. The modem_repower leftover grant and its mirror in luci-proto-wwand.json (missing modem_reset) are likewise untouched. One new nit inline, on the only hunk in this round's diff that I had not already commented on. CI on the head SHA is green (all three FormalityCheck runs pass).

Commit checks

  • 12ef4b1 "luci-app-wwand: sync to wwand 1.2.0 sources" — the message is 5da1788's verbatim minus the proto bullet, so the omissions I flagged there carry over unchanged. Still undocumented in a 913-line diff: the whole preferred-PLMN manager (renderPlmnManager, ~200 lines in settings.js), the new modem_plmn_set / modem_plmn_restore ubus methods, a new UCI section type wwand_plmnlist (type + plmn list) with the plmn_list options it adds to both wwand.modemopts and wwand.simlist, the Datapath & muxing panel (modem_datapath), the Preferred networks (NAS) panel, the cachedCall/slowCache rework that moves cells/slots/datapath/PLMN off the 1 s poll onto per-call TTLs, the fmt.term() tooltip pass over status.js, modemmanager joining the migration list, and the tab→space reindent of the ACL file. A new config section type and a new poll-caching model are worth naming in the message — and the ACL reindent is what hid the two grant bugs.
  • d7bb9ee "luci-proto-wwand: sync to wwand 1.2.0 sources" — "add the hard_reconnect_on_ip_change option" covers 4 of the 10 added lines; the other three hunks add help text to pre-existing password, metric and peerdns options. Minor, but a "- plus help text for the PAP/CHAP password, gateway metric and peer-DNS options" bullet would make the diff match the body.

Generated by Claude Code

Comment thread protocols/luci-proto-wwand/htdocs/luci-static/resources/protocol/wwand.js Outdated
ddimension pushed a commit to ddimension/luci-app-wwand that referenced this pull request Aug 14, 2026
- status.js: fix the `var plmn` redeclaration. `plmn` was declared three
  times in one function scope (`res[5]` for the PLMN lists, then `reg.plmn`
  for the serving PLMN); by the NAS panel line `plmn` was the serving object,
  so `plmn.nas` was always undefined — the "Preferred networks (NAS)" panel
  never rendered and threw on an unregistered modem (swallowed by the status
  poller's error handler → the page silently stopped refreshing). Rename the
  lists reply to `plmnLists`.
- settings.js: drop the unreachable `plmnEditor()` (~62 dead lines; the only
  render path is `renderPlmnManager`). It also carried a latent `list.map`
  index-as-noRat bug.
- ACL: remove the dead `modem_repower` grant (the ubus method was renamed to
  `modem_reset`) and the `modem_sms_send` grant (no caller in the app — the
  SMS UI only lists/deletes). Restore tab indentation + inline single-element
  arrays to match the tree convention (100/104 acl.d files use tabs), which
  keeps the ACL delta reviewable.
- format/esim/netsel: move the shared `.wwe-*` stylesheet into wwand.format
  and inject it once from the settings view. Previously esim.js and netsel.js
  each emitted their own <style>, so the settings page (rendering both) got it
  twice, and netsel depended on esim purely for that string.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
ddimension pushed a commit to ddimension/luci-proto-wwand that referenced this pull request Aug 14, 2026
- ACL: the write grant still named the removed `modem_repower` and did not
  grant `modem_reset`, so the Reset button rendered from the shared
  wwand.modemopts on the interface-edit page hit access-denied. Grant
  `modem_reset` instead.
- protocol/wwand.js: drop the redundant `o.default = '0'` on the
  hard_reconnect_on_ip_change flag — form.Flag already defaults to disabled,
  and it was the only literal '0'/'1' default in the file (the flags around it
  use the symbolic `o.enabled`/`o.disabled`).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

@openwrt-ai openwrt-ai left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed 2 new commits (7e1c0ec, 442ba27). Both messages match their diffs, so no commit checks this round — and this pair closes every finding from the last two rounds. One nit inline; CI on the head SHA is green (all three FormalityCheck runs).

What I verified:

  • status.js scope bug — fixed properly. plmnLists is now the only name bound to res[5], and var plmn = reg.plmn at :405 is the sole plmn left in renderLive (used only at :427, guarded by plmn ? …). renderNasList() already returns null for a missing/empty list, so an unregistered modem no longer throws into the poller's error handler.
  • plmnEditor() removal — no dangling references. plmnEditRow survives because renderPlmnManager still uses it (:358, :437) with an explicit wrapper rather than a bare list.map, and callPlmnSet is still reached at :407. No require in settings.js became unused by the deletion.
  • ACL — both files now match the actual call surface exactly. Every method wwand.rpc declares is granted and has at least one caller; conversely nothing is granted without a caller. modem_sms_send is correctly dropped (sms_send appears nowhere in either package), and luci-proto-wwand.json's modem_reset closes the access-denied on the Reset button that wwand.modemopts renders on the interface-edit Modem & SIM tab. modemopts' other calls from that page (status, modem_sim_slots, modem_probe, file list /sys/class/gpio/) are all covered by the proto grant too. Tab indentation matches the tree convention. Both files still parse as JSON.
  • CSS move — safe. wwand.esim and wwand.netsel are required only by view/wwand/settings.js, which is exactly where fmt.injectStyle() now runs, and it sits in the same returned tree as netsel.render() (:806) and esim.render() (:807), so neither panel loses its .wwe-* rules. No ESIM_CSS / esim.CSS references remain anywhere, and dropping the wwand.esim require from netsel.js leaves all of its remaining requires in use.
  • o.default = '0' — confirmed a no-op: form.js:5036-5037 sets this.disabled = '0'; this.default = this.disabled;.

Generated by Claude Code

@openwrt-ai openwrt-ai left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed 1 new commit (eba976f). git diff 442ba27..eba976f is a single blank line removed from wwand/esim.js — the message matches the diff exactly, so no commit checks this round. Everything closed in the previous two rounds stays closed; nothing in this diff reopens a prior finding. CI on the head SHA is green (all three FormalityCheck runs).

The one nit inline is about where the commit sits in the series rather than what it does — the blank line it removes was introduced by 7e1c0ec earlier in this same series, so it reads as squash material.


Generated by Claude Code

@ddimension

Copy link
Copy Markdown
Author

Rebased onto current master and squashed from nine commits to two — one per package — and the sources are re-synced from upstream, which they had drifted from (the branch was still at the "wwand 1.2.0 sources" state).

Six files changed in the re-sync, the two substantive ones both being fixes found in the field since:

  • view/wwand/settings.js — the RAT mode picker rebuilt mode_preference from the four checkboxes it renders (GSM/UMTS/LTE/NR5G), so every save silently cleared CDMA (0x01), HDR/EVDO (0x02) and TD-SCDMA (0x20). Seen on a Quectel RG650E, where one save turned 0x7F into 0x5C. The mask now starts from the bits of the reported value that the picker does not render.
  • view/wwand/settings.js — a failing modem_set_settings printed only error ("qmi") and dropped detail, which made a firmware-level rejection undiagnosable from the GUI. It now appends the qmi result/code.

The rest are the status/modems/format/rpc/ACL updates that go with them. All JS passes node --check.

No takeover references remain: wwand no longer registers the qmi proto alias at all, so these packages only ever address proto wwand. That change is in the companion packages PR (openwrt/packages#30185), which now points at wwand v1.4.1.

@openwrt-ai openwrt-ai left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed the rebase: the nine-commit series is now two commits, one per package, and both messages match their diffs exactly (d41c76c touches only protocols/luci-proto-wwand/, b0f6e4e only applications/luci-app-wwand/) — so no commit checks this round.

The content delta against the previous head (eba976f) is the six files the PR comment names: git diff eba976f..b0f6e4e -- applications/luci-app-wwand protocols/luci-proto-wwand is +124/−6. The two fixes called out in that comment both check out — MODE_BITS_MASK correctly preserves the 0x23 (CDMA/EVDO/TD-SCDMA) bits the picker does not render, and describeError() recovers the detail.result/detail.code that a QMI-level rejection carries. Everything closed in the previous rounds stays closed; nothing here reopens a prior finding. CI on the head SHA is green (all three FormalityCheck runs).

Six inline comments, all on the new hunks. The two worth a second look are the control-protocol switch, whose comment claims a "supports both" gate that the code does not implement (and which the daemon documents as a Quectel-only AT command, broken on the RG650E for MBIM), and "Reset to defaults", which still sends DEFAULTS.mode_preference = 0x50 verbatim and so re-clobbers exactly the unrendered bits collect() now preserves.

I also re-checked the backend coupling for the two new ubus methods rather than assuming: modem_repower and modem_set_protocol both exist in the daemon (src-ucode/ubus.uc, src-ucode/hwops.uc, src-ucode/daemon.uc), both are called only from luci-app-wwand pages, and both are granted under write in luci-app-wwand.json — so no proto-side ACL gap this time, and no frontend-only option.


Generated by Claude Code

Comment thread applications/luci-app-wwand/htdocs/luci-static/resources/view/wwand/settings.js Outdated
Comment thread applications/luci-app-wwand/htdocs/luci-static/resources/view/wwand/settings.js Outdated
Comment thread applications/luci-app-wwand/htdocs/luci-static/resources/view/wwand/settings.js Outdated
Comment thread applications/luci-app-wwand/htdocs/luci-static/resources/view/wwand/modems.js Outdated
Comment thread applications/luci-app-wwand/htdocs/luci-static/resources/view/wwand/status.js Outdated
Comment thread applications/luci-app-wwand/htdocs/luci-static/resources/wwand/format.js Outdated
@ddimension

Copy link
Copy Markdown
Author

All six addressed. Five changed the code; one did not, and I want to be explicit about which.

Control-protocol switch. You were right that the comment promised a gate the code did not implement — but the underlying capability was already there and simply unexposed, so the fix is a real gate rather than a reworded comment. protocol_switch.uc in the daemon carries per-vendor AT recipes (Quectel AT+QCFG="usbnet", Fibocom, Sierra) and deliberately withholds the hardware-unverified ones, because a bad composition write can cost access to the modem. protocol_switch_supported() existed on the modem object but was never surfaced, so no UI could gate on it. The daemon now reports it as proto_switch in status(), and this section gates on that.

One correction to the review: the switch is not Quectel-specific. recipe_for() matches ^(RG|EG|EM|EC|BG|AG)[0-9] (Quectel), ^(FM|FG|NL)[0-9] (Fibocom) and ^MC[0-9] (Sierra).

Your RG650E point survives the gate, though, and it is the sharper half: that model has a recipe, so proto_switch is true for it, yet its firmware rejects MBIM_OPEN. A capability flag cannot express "the switch works but the destination does not". The MBIM direction now says so in the confirmation instead of promising the connections come back on their own.

"Reset to defaults". Correct, and it was worse than the bug it sat next to: 0x7F → 0x50 versus the 0x5C the Apply path produced. A reset now means "defaults for what is on screen" and keeps the bits the picker cannot render.

Protocol-switch failure path. Fixed — it now uses describeError(), which was indeed added in the same diff for exactly this shape.

Duplicated lock formatting. Moved to fmt.fmtLocks(). While it moved I took your second point: it renders the daemon shape ({ lte: { enabled, values: [earfcn, pci, …] } }) in the colon spelling the lock editor accepts, instead of JSON.stringify output. One behaviour change worth naming — a lock that is present but disarmed is no longer listed under "Locked to", which both call sites previously did.

in a block comment. Fixed.

modem_repower — not changed, checked. Every success path in hwops.uc returns { ok: true, action: … }: the reset-GPIO branch, the power-cycle branch, both. Failures return { error: … } with no action. So requiring res.ok is correct here and the Reboot buttons extra res.resetting tolerance is not needed — that field exists because modem_reset genuinely sets it. If the asymmetry is worth removing it belongs on the Reboot side, not here.

Note the gate needs the wwand release carrying the proto_switch status field; the packages PR (openwrt/packages#30185) will point at it. Until then the section simply does not render, which is the safe direction.

@openwrt-ai openwrt-ai left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed the amended head. d41c76c is byte-identical to the previous round; ad0894b replaces b0f6e4e, so the new content is git diff b0f6e4e..ad0894b — +88/−28 across five files, still scoped to applications/luci-app-wwand/ only, and the message matches the diff. No commit checks this round. CI on the head SHA is green (all three FormalityCheck runs).

All six findings from the last round are closed, and the follow-up comment's account of them checks out:

  • Duplicated lock formattingfmt.fmtLocks() is now the single implementation, and both call sites use it (status.js:409, settings.js:616). It renders the earfcn:pci / pci:arfcn:scs:band spelling the lock editor accepts, matching the placeholders in renderCellLock(), instead of JSON.stringify output. The disarmed-lock omission you called out is visible in the code (l.enabled === false skips the group), and unknown keys still fall through to the JSON branch, so nothing the daemon grows later is silently dropped.
  • "Reset to defaults" — now seeds from ~MODE_BITS_MASK exactly like collect(), and Object.assign keeps DEFAULTS itself unmutated across repeated presses. One question inline about which bits "defaults" should mean.
  • Control-protocol switch — gated on data.info.proto_switch, which comes off the same per-modem status object status.js reads caps.rats from, and fails closed when the daemon predates the field. The MBIM direction now warns about the MBIM_OPEN rejection; the QMI direction keeps the old wording. Failure path uses describeError(), so a QMI-level rejection reports result/code instead of bare "qmi".
  • NR band ordering — the narrower-before-broader invariant now holds at every overlap I could check: n14 before n28, n18 and n5 before the superset n26, n12 before n85, n261 before the superset n257. One nit inline on the new tie-break paragraph.
  • Em dash in the format.js block comment — literal now.
  • admin/network/wwand-tools — title and order restored, and load() falls back to the first modem when no ?modem= is present, so the menu entry lands on a usable page rather than an empty one.

Generated by Claude Code

Comment thread applications/luci-app-wwand/htdocs/luci-static/resources/wwand/bands.js Outdated

@openwrt-ai openwrt-ai left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed the amended head (9a6dc44 replaces b0f6e4e; d41c76c is unchanged). The commit message is byte-identical to the one I reviewed last round and still matches the package it adds, so no commit checks this round. git diff b0f6e4e..9a6dc44 is +100/−31 across five files, all in luci-app-wwand, and it is exactly the follow-up the previous round's comments asked for. CI on the head SHA is green (all three FormalityCheck runs).

What I verified closed:

  • Reset to defaults re-clobbering the unrendered RAT bits — fixed on both halves. DEFAULTS.mode_preference is now derived from MODE_BITS_MASK instead of the hardcoded 0x50, and the button ORs in the & ~MODE_BITS_MASK remainder of the reported value, so a reset can no longer drop CDMA/EVDO/TD-SCDMA. Object.assign({}, DEFAULTS) keeps the shared band arrays unmutated.
  • Control-protocol switch gate — the section now actually gates on a capability (data.info.proto_switch) rather than on "the modem currently speaks QMI or MBIM", and the MBIM direction no longer promises the connections come back on their own. The dependency on a not-yet-released daemon field is called out in your comment, and the failure direction is safe (section simply does not render), so not flagged.
  • Protocol-switch failure path — now routed through describeError(), which is in scope at :90.
  • Duplicated lock formattingfmt.fmtLocks() is a genuine single source: the widths it groups by (2 for LTE, 4 for NR5G) match the spellings the cell-lock editor accepts (earfcn:pci, pci:arfcn:scs:band, placeholders at settings.js :585/:587), and both former call sites are gone. No lockRows/liveLocks leftovers, and fmt is required in both files.
  • Modem Tools menu title — restored, with order: 60 after Modems at 55. No collision with the core admin/network/* orders (10–50). The JSON parses and all four changed JS files pass node --check.

Three inline comments. The one worth a look is in bands.js: the reorder pass fixed n14, n18/n5 and n261, but n74 is still listed behind n50/n75, which leaves it reachable only for a 1 MHz sliver — the same class of bug this hunk was written to remove.


Generated by Claude Code

Comment thread applications/luci-app-wwand/htdocs/luci-static/resources/wwand/bands.js Outdated
Comment thread applications/luci-app-wwand/htdocs/luci-static/resources/wwand/format.js Outdated
Comment thread applications/luci-app-wwand/htdocs/luci-static/resources/wwand/format.js Outdated

@openwrt-ai openwrt-ai left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed the amended head. 1a66040 is byte-identical to the d406f26 I reviewed last round; 46fb9e1 replaces 0e41f7c with the same message, and it is still the single "add the package" commit for luci-app-wwand, so no commit checks this round.

git diff 0e41f7c..46fb9e1 is +19/−1 in one file — the group() payload widening from the last open thread. CI on the head SHA is green (all three FormalityCheck runs).

The widening does what your table says for every shape it lists — I ran the function rather than reading it: {enabled:true,values:[1300,246,5230,118]}LTE 1300:246, 5230:118, 'n78'band_lock n78, {values:'n41'}band_lock n41, trueband_lock armed, {enabled:false,values:[7]} omitted, and the known keys still pair by width. Nothing from the previous rounds reopens, and no new UCI option or ubus method here, so nothing new for the backend-coupling rule.

Two inline comments, both on the new branch chain. The one worth a look is a shape the chain does not cover: a bare array ({ lte: [1300, 246] }) reaches the l.values != null test, and since Array.prototype.values is a function that test passes — the row renders as LTE function values() { [native code] }.


Generated by Claude Code

Comment thread applications/luci-app-wwand/htdocs/luci-static/resources/wwand/format.js Outdated
ddimension pushed a commit to ddimension/luci-app-wwand that referenced this pull request Aug 25, 2026
…ing k=v

Two follow-up findings on the previous fix, both from review on
openwrt/luci#8917 and both real.

A bare array — { lte: [1300, 246] }, the most obvious shape after
{ values: [...] } — fell into the `l.values != null` branch, because
Array.prototype.values is a real function on every array. v became
[ Array.prototype.values ] and the row rendered as

    LTE function values() { [native code] }

An explicit Array.isArray(l) branch ahead of the l.values tests fixes it and
restores the colon pairing for that shape (LTE 1300:246).

And the k=v branch fed its strings through the width pairing, so the same
payload rendered differently depending on the key it arrived under:
{ lte: { earfcn: 1300, pci: 246 } } gave "LTE earfcn=1300:pci=246" while
{ band_lock: {...} } (width 0) gave "band_lock earfcn=1300, pci=246". Colon
pairing only means something for a positional values array, so that branch
clears the width.

Checked every shape against the real function, not by reading it.
@ddimension

Copy link
Copy Markdown
Author

Both correct, both fixed — the array one is a good catch.

Array.prototype.values being a real function is exactly why { lte: [1300, 246] } slipped through: Array.isArray(l.values) is false, l.values != null
is true, and the row rendered the native-code string. An explicit
Array.isArray(l) branch ahead of the l.values tests handles it, and the
colon pairing comes back with it.

The width point is right too, and it was inconsistent in a way that would have
been confusing to debug: the same payload rendered differently depending on the
key it arrived under. That branch now clears the width, since colon pairing only
means anything for a positional array.

Ran every shape through the real function again:

input width output
{ enabled: true, values: [1300,246,5230,118] } 2 LTE 1300:246, 5230:118
[1300, 246] 2 LTE 1300:246
[242, 431070, 1, 78] 4 NR5G 242:431070:1:78
{ earfcn: 1300, pci: 246 } 2 LTE earfcn=1300, pci=246
{ earfcn: 1300, pci: 246 } 0 band_lock earfcn=1300, pci=246
'n78' 0 band_lock n78
{ values: 'n41' } 0 band_lock n41
true 0 band_lock armed
{ enabled: false, values: [7] } 2 omitted

The PR body is updated to v1.4.7 as well — you were right that it had fallen
behind the pin again, and for the second time. I have written that down as a
step in the release routine rather than promising to remember it: tag the
commit (not the annotated tag object), bump the feed pin and recompute the
mirror hash, repoint the PR, and edit the body in the same pass.

@openwrt-ai openwrt-ai left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed the amended head. 1a66040 is byte-identical to the proto commit I reviewed last round; bc1667f replaces 46fb9e1 with a byte-identical message and is still the single "add the package" commit scoped to applications/luci-app-wwand/ — so no commit checks this round.

git diff 46fb9e1..bc1667f is +5/−1 in one file, exactly the two threads left open from the last round, and both close:

  • Bare-array payload. The explicit Array.isArray(l) branch now sits ahead of the l.values tests, so Array.prototype.values can no longer be mistaken for a payload. I ran the function rather than reading it: {lte:[1300,246]}LTE 1300:246 and {nr5g:[242,431070,1,78]}NR5G 242:431070:1:78, both with the colon pairing restored, in place of the function values() { [native code] } string. Branch order is right — l === true is still tested first, so the "armed, no detail" spelling is unaffected, and typeof l != 'object' still catches scalars, since an array never reaches it.
  • Width on the k=v branch. width = 0 there makes the same payload render identically under a known and an unknown key: {lte:{earfcn:1300,pci:246}}LTE earfcn=1300, pci=246, matching {band_lock:{earfcn:1300,pci:246}}band_lock earfcn=1300, pci=246. Previously the first produced LTE earfcn=1300:pci=246. Reassigning the parameter is local to the call, so the widths passed at the three call sites are untouched.

Every row of the table in your comment reproduces against the real function, including the ones this round did not change ({enabled:false,values:[7]} omitted, 'n78'band_lock n78, {values:'n41'}band_lock n41, trueband_lock armed, and the two known keys pairing by width). Nothing from the previous rounds reopens.

No new UCI option or ubus method in this diff, so nothing new to check against the backend-coupling rule. All 13 JS files pass node --check and all three JSON files parse. CI on the head SHA is green (all three FormalityCheck runs).

One nit inline, and it is about the PR description rather than the code.


Generated by Claude Code

Comment thread applications/luci-app-wwand/Makefile
@ddimension
ddimension force-pushed the wwand-luci branch 2 times, most recently from a5c7fba to b001972 Compare August 30, 2026 09:22

@openwrt-ai openwrt-ai left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed the rebased head. The branch moved onto current master, so the content delta against my last review is git diff bc1667f..b001972 -- applications/luci-app-wwand protocols/luci-proto-wwand — +279/−56 across seven files. Both commits are new objects (f9a395c, b001972) but the messages are byte-identical to the ones I reviewed last round and each is still the single "add the package" commit scoped to one package (f9a395cprotocols/luci-proto-wwand/ only, 3 files; b001972applications/luci-app-wwand/ only, 15 files) — so no commit checks this round. CI on the head SHA is green (all three FormalityCheck runs).

Most of the delta is one hardening pass plus three functional changes, and I checked the pass rather than taking the comments' word for it:

  • Bare string → one-element array. The premise is right: dom.append() turns an array element into createTextNode but assigns a bare string through innerHTML, and an element passed either way is appended as itself. So wrapping is a real escape, not cosmetics. I looked specifically for the failure mode the change could introduce — a row whose value is already an array, which [ r[1] ] would nest and createTextNode would stringify with commas — and there is none: every one of the 58 row entries reaching fmt.tbl()`` from status.js and the proto handler is a string or a single element; the multi-node ones (`capsBadges`, the PCI lock cells at `wwand.js:219` / `:270`) are already wrapped in their own `E('span', …)`. `renderWarnings`, `simSlotRow` and the netsel/esim cells were array-valued to begin with.
  • The GridSection status columns on the Modems page are not affected, in case that was the worry — col()'s DummyValues go through AbstractValue.textvalue(), which %h-escapes before renderTextValue hands the string to E().
  • renderWidget overrides. Both claims in the comments hold. getUIElement() resolves through map.findElement('id', …) over the whole map and data-field lives on the container render() builds, not on the widget node — so wrapping the AT2 flag in a <div> is safe. this.super('renderWidget', …) from an own-property override also resolves correctly: Class.super() starts its walk at Object.getPrototypeOf(this), skipping own properties. The keylist swap works because transformChoices() reads keylist/vallist synchronously inside Value.renderWidget, and neither filter can empty the list (auto has proto: null, '0' is always in slotsOf), so it can never silently degrade the Combobox to a plain Textfield.
  • tx_aggregation un-nesting is correct — p and kidTx are computed above the rx_aggregation guard, so moving the uplink row out of it does not take anything out of scope.
  • Actions-column flex wrap. renderRowActions really does return E('td', { class: '… nowrap cbi-section-actions' }, E('div')), so td.firstElementChild is the button wrapper and not a button — the styles land where intended, and white-space: nowrap on the parent td does not govern flex line breaking, so the wrap does take effect.

Three inline comments. The one worth a look is in wwand.modemopts: the new per-row load() hooks index status().modems by the raw section_id, which is the modem name only on the Modems page — on the interface-edit surface it is the interface section, and the SIM-slot dropdown loses its slot list there entirely.

No new UCI option or ubus method in this delta (globals.datapaths, qmap_version, dp.extra and at2_released are all new status fields, read-only), so nothing new to check against the backend-coupling rule; the ACL files are unchanged.


Generated by Claude Code

Comment on lines +331 to +335
return L.resolveDefault(callStatus(), {}).then(function(reply) {
if (!((reply && reply.modems) || {})[section_id])
return self.super('load', [section_id]);

return Promise.all([ L.resolveDefault(callSlots(section_id), {}) ])

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The comment above says "the section id is the modem name", but that only holds on the Modems page. addModemSim is also called from the proto handler at protocol/wwand.js:509, where `section_id` is the *interface* section and the modem name has to be resolved through [`modemsid.modemSid()`](https://github.com/openwrt/luci/blob/b001972ef879061e33273e5a5c51d3daf26f9085/applications/luci-app-wwand/htdocs/luci-static/resources/wwand/modemsid.js#L18-L22)`` — which this file already does for the Reset button at modemopts.js:158`` (modemsid.modemSid(section_id) || section_id).

So on Network → Interfaces → Modem & SIM, reply.modems[section_id] is always undefined, the early return fires, callSlots() is never issued and the SIM-slot field is left with just 0 — leave as-is. The previous sweep-all-modems version did populate it there, so this is a regression on that surface rather than only a narrowing.

Suggested change
return L.resolveDefault(callStatus(), {}).then(function(reply) {
if (!((reply && reply.modems) || {})[section_id])
return self.super('load', [section_id]);
return Promise.all([ L.resolveDefault(callSlots(section_id), {}) ])
return L.resolveDefault(callStatus(), {}).then(function(reply) {
var modem = modemsid.modemSid(section_id) || section_id;
if (!((reply && reply.modems) || {})[modem])
return self.super('load', [section_id]);
return Promise.all([ L.resolveDefault(callSlots(modem), {}) ])

Same raw-section_id lookup in the two other new hooks in this commit, both of which also silently no-op on the interface page (they fail open, so the effect is milder — the AT2 hint never appears, and the mux list is never filtered):

  • line 193 — var m = ((res || {}).modems || {})[section_id];
  • lines 250-251 — self.dpProto[section_id] = ((reply || {}).modems || {})[section_id] ? … .protocol : null;

Keying slotsOf / at2Port / dpProto by section_id is right — it is only the lookup into the status reply that needs the resolved modem name.

(Minor, while here: with a single-element array the Promise.all([…]).then(function(results) { results.forEach(…) }) wrapper is a leftover from the sweep and could just be .then(function(r) { … }).)


Generated by Claude Code

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Confirmed, and it was a regression exactly as you describe — fixed.

The proto handler calls modemopts.addModemSim(s, 'modem', bindModem) with the
interface section (protocol/wwand.js), so reply.modems[section_id] was
always undefined on Network → Interfaces → Modem & SIM, the early return fired
and the SIM-slot field kept only 0 — leave as-is.

What made this worth more than a one-line patch: the Reset button in the same
file already resolved through modemsid.modemSid(section_id) || section_id and
carried a comment spelling out the trap. Repeating that inline a fourth time
would have left the fifth to be found by someone else, so the resolution is now
one modemOf() helper and every lookup into a status reply goes through it —
including the two you flagged at lines 193 and 250-251. The per-row caches stay
keyed by section_id, which was the right key.

Checked that the fallback cannot misfire on the Modems page: option modem
belongs to the interface section and a wwand_modem section never carries it
(the daemon's config parser reads it only on interfaces), so modemSid()
returns null there and the raw section id is used, as before.

The single-element Promise.all is gone too.

how 'raw-ip' gets written, and the daemon canonicalises it */
if (value == null || value === '' || /^[a-z][a-z0-9_-]*$/.test(value))
return true;
return _('A datapath name: lowercase letters, digits and underscore.');

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: the error message contradicts the regex two lines up — the pattern deliberately accepts - (per the comment, so raw-ip can be typed), but the message tells the user only letters, digits and underscore are allowed. Someone who types raw-ip, gets it rejected for an unrelated reason and reads this message will conclude the hyphen was the problem.

Suggested change
return _('A datapath name: lowercase letters, digits and underscore.');
return _('A datapath name: lowercase letters, digits, underscore and hyphen.');

Generated by Claude Code

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Applied as suggested. The regex accepts the hyphen deliberately, and the message
now says so — someone typing raw-ip and hitting an unrelated rejection would
have drawn exactly the wrong conclusion.

Comment on lines +281 to +282
rows.push([ k.replace(/_/g, ' ').replace(/^./, function(c){ return c.toUpperCase(); }),
'' + v ]);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: the block comment names "whether the NSS shim was loaded" as one of the keys this loop is expected to render, i.e. a boolean — and '' + v turns that into a bare, untranslated true / false sitting next to translated labels like "Parent device" and "Uplink NTB". The _size branch already special-cases one type, so a boolean branch fits the same shape:

Suggested change
rows.push([ k.replace(/_/g, ' ').replace(/^./, function(c){ return c.toUpperCase(); }),
'' + v ]);
if (typeof v == 'boolean')
v = v ? _('yes') : _('no');
rows.push([ k.replace(/_/g, ' ').replace(/^./, function(c){ return c.toUpperCase(); }),
'' + v ]);

(false is deliberately still rendered — the v == null || v === '' guard above lets it through, and "NSS shim: no" is the interesting answer.)


Generated by Claude Code

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Applied, though the premise does not hold for the code as it stands — worth
recording so the reasoning survives.

No datapath in tree sends a boolean here. The NSS one reports
nss_shim: 'loaded' or 'absent' — a string, so nothing untranslated reaches
the page today. The block comment describes that key in prose ("whether the NSS
shim was loaded"), which reads like a boolean but is not one.

The suggestion is still right, as hardening rather than a fix: the contract in
the daemon's datapath interface says the keys are shown as given, and
docs/extending.md explicitly invites out-of-tree datapaths, so a boolean is a
value this loop should be able to render. Taken with your reasoning on false
kept in the comment, since that is the non-obvious half.

ddimension pushed a commit to ddimension/luci-app-wwand that referenced this pull request Sep 5, 2026
modemopts.js is shared by two surfaces and `section_id` does not mean the same
thing on both: on Network → Modems it IS the wwand_modem name, on Network →
Interfaces → Modem & SIM it is the interface section. Status replies are keyed
by modem name, so three lookups missed entirely on the interface page — and
missed quietly, reading as "this modem has nothing to report":

  - addModemSim took the early return every time, so callSlots() was never
    issued and the SIM-slot field stayed at "0 — leave as-is". The sweep-all
    version it replaced did populate it there, so this was a regression on that
    surface, not just a narrowing.
  - the AT2 hint never appeared, and the datapath list was never filtered to
    the modem's protocol. Those fail open, so they only lost information.

The Reset button in the same file already resolved through modemsid.modemSid()
and carried a comment explaining exactly this trap. Rather than repeat it a
fourth time, it is now one modemOf() helper that every status-reply lookup goes
through; the per-row caches stay keyed by section_id, which was right. A
wwand_modem section never carries `option modem` (config.uc reads it only on
the interface), so the fallback leaves the Modems page unchanged.

Also drops a single-element Promise.all left over from the sweep, and two nits
found in review: the datapath validator's message denied the hyphen its own
regex deliberately accepts (so `raw-ip` can be typed), and a datapath's extra
status rows now render a boolean as a translated yes/no. No datapath in tree
sends one — both send strings — but docs/extending.md invites out-of-tree ones
and the contract promises the keys are shown as given.

Reported by openwrt-ai on openwrt/luci#8917.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

@openwrt-ai openwrt-ai left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed 2 new commits; both messages match their diffs.


Generated by Claude Code

acting = false;

if (el)
el.click();

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

createHandlerFn puts the spinner and disabled on ev.currentTarget (ui.js:5681-5684) — here a node that is never inserted into the page, so Reattach/Reboot/Repower now run with no visible busy state and the dropdown stays selectable throughout. The comment at :350-353 claims the spinner keeps working; it does not. Put the busy state on ddNode for the duration of the returned promise instead.


Generated by Claude Code

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fixed, thanks


Generated by Claude Code

var st = d.st.state || d.cfg.state || '?';
var rows = [
[ _('Interface'), d.cfg.interface + (d.cfg.mux_id ? ' · mux %d'.format(d.cfg.mux_id) : '') ],
[ _('State'), E('strong', { 'style': 'color:%s'.format(st == 'CONNECTED' ? '#3c3' : '#da3') }, st) ]

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missed by this round's array-wrapping pass: st is d.st.state || d.cfg.state, i.e. daemon/uci text, and a bare string child is assigned through innerHTML (luci.js:1395). This is the last one I can find in either package.

Suggested change
[ _('State'), E('strong', { 'style': 'color:%s'.format(st == 'CONNECTED' ? '#3c3' : '#da3') }, st) ]
[ _('State'), E('strong', { 'style': 'color:%s'.format(st == 'CONNECTED' ? '#3c3' : '#da3') }, [ st ]) ]

Generated by Claude Code

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fixed, thanks


Generated by Claude Code

recognise is honoured, but hardware recovery then stays disarmed for
that modem, because an AT port answers on QMI and MBIM modems too and
so proves nothing about the pin. */
o = s.taboption(tab, form.ListValue, 'protocol', _('Control protocol'),

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: the Tools page carries a second control with this exact label (settings.js:933),`` but it issues modem_set_protocol — a firmware switch — and leaves this option untouched. Using it on a modem that has `protocol` pinned here produces exactly the "pin contradicts the driver wwand recognises" state whose consequence (hardware recovery disarmed) this help text warns about. Should the Tools switch also rewrite `option protocol`, or should the two be named apart?


Generated by Claude Code

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Both, in the end — the two halves turned out not to be alternatives.

The Tools switch now clears the pin. modem_set_protocol wraps its callback so a successful firmware switch drops a option protocol that named the old protocol (daemon.uc:2466-2467 calling the clear_protocol_pin dep from main.uc:339). That is the part that mattered: a pin the driver contradicts is not inert — recovery.revoke_arming withdraws permission to touch hardware for that modem, persistently, so the modem would come back on the new protocol with its recovery ladder disabled. Leaving the stale pin in place was the actual bug behind your question.

And the two controls are named apart. This page's heading is now Firmware protocol mode, with the reason recorded at the site so it does not get "tidied" back:

/* NOT "Control protocol": that is the name of the uci option
   ... */
E('h3', {}, _('Firmware protocol mode')),

Both confirm texts and the description say what happens to the pin, e.g.

A "Control protocol" pinned on the modem page is cleared back to "detect", because a pin the driver contradicts disables hardware recovery for this modem.

so the modem page keeps Control protocol as the name of the uci option, and the Tools page no longer claims that name for a different operation.

The rename is in this PR; the daemon side is in wwand v1.6.3, which the companion openwrt/packages#30185 pins.

@ddimension
ddimension force-pushed the wwand-luci branch 2 times, most recently from 7fc1d13 to ec3f279 Compare September 8, 2026 18:27
ddimension pushed a commit to ddimension/wwand that referenced this pull request Sep 8, 2026
`option protocol` names the protocol wwand uses to drive a modem. A pin that
contradicts the driver wwand recognises is not inert: recovery.revoke_arming
withdraws the permission to touch that modem's hardware, and persists it. So a
modem pinned to `qmi` and then switched to MBIM came back with its reset and
power-cycle rungs silently disabled, and nothing saying why. The path there is
ordinary — the pin exists because detection failed once, the switch happens on
the Tools page months later.

Raised in review on openwrt/luci#8917, where the observation was that the two
controls carry the same label. The label is worth fixing, but it is the smaller
half: renaming makes the collision less likely and leaves the trap.

CLEARED, not rewritten to the target. The option exists for a control device
wwand cannot classify — its own help says leave it on detect — and the reason
this one was pinned, detection failing on the OLD protocol, usually does not
survive the switch. Writing the target would leave a pin that outlives its cause
and needs cleaning up by hand. If detection is broken on the new protocol too,
the daemon already says so with the remedy; a stated failure beats a silent one.

In the daemon rather than in LuCI, for three reasons: only here is it known
whether the switch actually succeeded (a failed one leaves the pin alone, and a
test fails if that changes); it covers the ubus/CLI path too; and the daemon
already owns three uci write-backs, so there is a place for it.

Deliberately not gated on auto_correct_config: that gate is for the daemon
correcting config on its own initiative, and this runs only because an operator
asked for the switch.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
ddimension pushed a commit to ddimension/luci-app-wwand that referenced this pull request Sep 8, 2026
… what it clears

Two controls were both labelled "Control protocol" while doing different things:
the one on the modem page sets `option protocol` (which protocol wwand uses to
drive the modem), this one rewrites the modem's FIRMWARE mode and resets the
device. That is how someone ends up with one pinned and the other switched —
which disables hardware recovery for that modem, persistently
(recovery.revoke_arming). Raised in review on openwrt/luci#8917.

This one is now "Firmware protocol mode". The daemon clears a contradicting pin
when the switch succeeds, so the confirmation says so rather than changing the
operator's configuration silently.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

@openwrt-ai openwrt-ai left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed 2 new commits; both messages match their diffs.


Generated by Claude Code

Comment on lines +44 to +51
o.boundTo = {};

o.cfgvalue = function(sid) {
var opt = this.ucioption || this.option;
var msid = modemSid(sid);
this.boundTo[sid] = msid;
return uci.get('network', msid || sid, opt);
};

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Stamping inside cfgvalue() makes the guard a tautology: parse() calls this.cfgvalue(section_id) and then this.remove(section_id) in the same synchronous block (form.js:2149-2154), so boundTo[sid] is refreshed with the post-save modem reference microseconds before msid is recomputed from it — this.boundTo[sid] === msid can never be false, and the RUTC50 uci del network.wwmodem_auto.path still happens. Stamp what the form read at render time instead; parse() never calls render().

Suggested change
o.boundTo = {};
o.cfgvalue = function(sid) {
var opt = this.ucioption || this.option;
var msid = modemSid(sid);
this.boundTo[sid] = msid;
return uci.get('network', msid || sid, opt);
};
o.boundTo = {};
/* Stamped from render(), NOT from cfgvalue(): parse() calls cfgvalue()
immediately before remove() (form.js:2149-2154), so a stamp written
there always agrees with the modemSid() remove() computes and the
guard below could never fire. render() runs only on the load path. */
var baseRender = o.render;
o.render = function(option_index, section_id, in_table) {
this.boundTo[section_id] = modemSid(section_id);
return baseRender.apply(this, arguments);
};
o.cfgvalue = function(sid) {
var opt = this.ucioption || this.option;
var msid = modemSid(sid);
return uci.get('network', msid || sid, opt);
};

Generated by Claude Code

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Confirmed, and the reporter proved it on a shipped build before I read this —
reset_gpio went too, so it was every modem-bound field
(ddimension/luci-app-wwand#7).

Your diagnosis is exact: cfgvalue() and remove() run in the same
synchronous block inside save(), so the stamp always agrees with the
modemSid() that remove() computes. I had assumed cfgvalue was a
render-time call.

I went further than the suggestion rather than moving the stamp to render(),
for two reasons:

  • an option that is not rendered still reaches remove()form.js:2167
    removes an inactive option with cfgvalue never called at all — so a
    render-time stamp is absent exactly when it would have to decide, and absent
    reads the same as "never showed a value";
  • more fundamentally, no record of when something ran is worth trusting for
    this. Two attempts have now died on that.

So the redirect no longer deletes on the wwand_modem section at all. It
belongs to the interface form, which only references a modem; the section is
the hardware's and is shared with every other interface on it. Clearing a
modem-level option is the Modems page's job, where the section is the
wwand_modem and the binding is a pass-through — there remove() reaches it
directly and means what it says. What this one still does is drop a legacy
inline copy from the interface, which touches nothing shared.

Checked against a model of the real save path this time, not by reading: with
the old guard it reproduces the reporter's two uci del lines exactly; with the
fix the modem section keeps both options, in the active and the inactive path.

Pushed, along with the two nits from this round — the unguarded mnc in
fmtOperator's sibling ('%02d'.format(null) really is "00", verified in
cbi.js:753) and the stale qmi alias comment.

/* same two shapes as fmtOperator; prefer the name, then either
spelling of the numeric id, and only then the generic word */
var op = (reg.plmn && (reg.plmn.description ||
(reg.plmn.mcc != null ? '%d/%02d'.format(reg.plmn.mcc, reg.plmn.mnc) : null) ||

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: only mcc is guarded, but '%02d'.format(null) is Math.floor(+null || 0)"00" (cbi.js:753-754), so a half-populated plmn renders a real-looking 260/00 here instead of falling through to plmn.id. fmtOperator in this same hunk guards both halves.

Suggested change
(reg.plmn.mcc != null ? '%d/%02d'.format(reg.plmn.mcc, reg.plmn.mnc) : null) ||
((reg.plmn.mcc != null && reg.plmn.mnc != null) ? '%d/%02d'.format(reg.plmn.mcc, reg.plmn.mnc) : null) ||

Generated by Claude Code

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fixed, thanks


Generated by Claude Code

Comment on lines +371 to +372
/* One protocol descriptor, registered under both the current name `wwand`
and the historical `qmi` alias — see the tail of this file. */

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: stale — the tail of the file (:585-588) and the Makefile say the qmi alias was dropped and only wwand is registered.

Suggested change
/* One protocol descriptor, registered under both the current name `wwand`
and the historical `qmi` alias see the tail of this file. */
/* One protocol descriptor, registered under the single name `wwand` see the
tail of this file. */

Generated by Claude Code

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fixed, thanks


Generated by Claude Code

ddimension pushed a commit to ddimension/luci-app-wwand that referenced this pull request Sep 9, 2026
'%02d'.format(null) is Math.floor(+null || 0) -> "00" (cbi.js:753-754), so a
half-populated plmn rendered a real-looking 260/00 instead of falling through
to plmn.id. fmtOperator in the same file already guarded both halves; this
site did not — the same defect the mcc/mnc formatter was written to end, one
caller further on.

Raised in review on openwrt/luci#8917.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
ddimension pushed a commit to ddimension/luci-proto-wwand that referenced this pull request Sep 9, 2026
The comment said the descriptor is registered under both `wwand` and a
historical `qmi` alias; the registration 236 lines below has said `wwand` and
nothing else since the alias was dropped. netifd sources every handler in
/lib/netifd/proto, so two claiming `qmi` would be settled by load order, which
no package can control.

Raised in review on openwrt/luci#8917.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

@openwrt-ai openwrt-ai left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed 2 new commits; both messages match their diffs.


Generated by Claude Code

});
}) }, _('Switch protocol')),
E('div', { 'class': 'cbi-value-description' },
_('Flips the control protocol (QMI ↔ MBIM). The modem resets; use only when the other protocol is known to work on this module.')),

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The description under the renamed heading still calls this "the control protocol" — the exact term the h3 and both confirm texts were moved away from, and the one the modem page's option owns.

Suggested change
_('Flips the control protocol (QMI ↔ MBIM). The modem resets; use only when the other protocol is known to work on this module.')),
_('Rewrites the modem firmware mode (QMI ↔ MBIM). The modem resets; use only when the other mode is known to work on this module.')),

Generated by Claude Code

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

solved by renaming this control's wording to "firmware mode" and quoting "Control protocol" only as the modem-page option, rather than dropping the sentence — thanks.


Generated by Claude Code

Comment on lines +962 to +963
? _('Switch the modem firmware to MBIM? The modem resets and re-enumerates. Some firmwares reject MBIM even though the switch itself succeeds — if the modem does not come back, switch it to QMI again. A "Control protocol" pinned on the modem page is cleared back to "detect", because a pin the driver contradicts disables hardware recovery for this modem.')
: _('Switch the modem firmware to %s? The modem resets and re-enumerates — its connections come back on their own. A "Control protocol" pinned on the modem page is cleared back to "detect", because a pin the driver contradicts disables hardware recovery for this modem.').format(target.toUpperCase()))))

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Both texts promise the user that a pinned protocol is cleared back to "detect", but nothing in either package writes it: wrpc.setProtocol() at :965 is the only call and no uci.unset('network', …, 'protocol') follows it, so the promise can only be kept by the daemon's modem_set_protocol. Does it unset the uci pin, and from which wwand version — otherwise the dialog states an outcome that does not happen and the modem is left in the pin-contradicts-driver state that modemopts.js:184`` warns about.


Generated by Claude Code

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good question to ask rather than assume, and the answer is yes — but not in
either of these packages, which is exactly why it was worth checking.

wrpc.setProtocol() reaches the daemon's modem_set_protocol, and that clears
the pin: on a successful switch it removes option protocol from the
wwand_modem section when the pin no longer names what was switched to. A
failed switch leaves it alone — the pin still describes reality then — and a
test fails if that changes.

It is cleared, not rewritten to the target. The option exists for a control
device wwand cannot classify (its own help says leave it on detect), and the
reason a pin was set — detection failing on the OLD protocol — usually does not
survive the switch. Writing the target would leave a pin that outlives its
cause; clearing returns the modem to the recommended state, and if detection is
broken on the new protocol too the daemon says so and names the option.

Version: ddimension/wwand@3469e73, released in v1.6.3, which is what this
PR series pins. So for the combination these packages are built against, the
promise holds.

You are right that the page was making a promise it cannot itself keep, though.
The description now attributes the action — "the daemon clears a contradicting
Control protocol pin when the switch succeeds" — rather than stating it in the
page's own voice, so someone running an older daemon reads a description of the
system rather than a guarantee from the dialog.

Fixed along with the description still borrowing "control protocol" from the
option page, and the trailing tabs on my comment separators.

var opt = this.ucioption || this.option;

/* NEVER delete on the wwand_modem section from here.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: trailing tabs on this comment's separator line, same at :67 and :79 — the only trailing whitespace in either package.


Generated by Claude Code

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fixed, thanks


Generated by Claude Code

ddimension pushed a commit to ddimension/luci-app-wwand that referenced this pull request Sep 9, 2026
…g tabs

The heading and both confirmation texts were moved off "control protocol" —
that term belongs to the option on the modem page — but the description under
them still used it, which puts the borrowed word back where the confusion
started. It now says what the control does and names the daemon as the actor
that clears a contradicting pin, rather than promising it in the page's own
voice.

Also strips trailing tabs from three blank separator lines inside a block
comment I added; they were the only trailing whitespace in either package.

Both raised in review on openwrt/luci#8917.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

@openwrt-ai openwrt-ai left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed 1 new commit; the message matches its diff.


Generated by Claude Code

});
}) }, _('Switch protocol')),
E('div', { 'class': 'cbi-value-description' },
_('Rewrites the modem firmware mode (QMI ↔ MBIM). The modem resets; use only when the other mode is known to work on this module. The daemon clears a contradicting "Control protocol" pin when the switch succeeds.')),

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: the PR body still says the series pairs with "wwand 1.6.0", but the daemon behaviour this sentence now attributes only exists from v1.6.3 per your reply. Bump the body so the pin and the text agree.


Generated by Claude Code

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fixed, thanks — the body now reads "Pairs with openwrt/packages#30185 (wwand 1.6.3)", so it agrees with what that PR pins (PKG_VERSION:=1.6.3) and with the daemon behaviour this description attributes.

The other version mentions in the body are historical rather than stale — "Since v1.6.0 the QMI mux datapath is an interface…" describes when that change landed, not what is pinned — so I left those as they are.

Netifd protocol handler for `proto wwand`, the cellular connection manager
proposed in openwrt/packages#30185. It renders the connection options the
daemon consumes — APN, PDP type, authentication, MUX id, MTU handling — and
resolves the modem an interface is bound to.

wwand registers `proto wwand` only; the historical `qmi` proto name stays
uqmi's, so this handler never competes with the stock one for an interface.

Signed-off-by: André Valentin <avalentin@marcant.net>

@openwrt-ai openwrt-ai left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed 2 new commits; both messages match their diffs.


Generated by Claude Code

msgstr ""

#: htdocs/luci-static/resources/view/wwand/status.js:770
msgid "Reset peak"

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The template was not regenerated for this round, in both directions: this msgid and Peak: RSRP %s dBm · SINR %s dB, Signal — aim the antenna for the highest RSRP/SINR and the three Reference Signal Received … descriptions no longer exist in any source, while none of the new strings are present — Signal strength, Reference signal quality (RSRQ), 3G signal quality (Ec/Io), the seven RSRP … / RSSI … / SINR … / Ec/Io 3G series labels, excellent/good/fair, avg, peak, not reported, Signal graphs unavailable: …, registered — this modem reports no signal detail and the rewritten page description. wwand/graph.js is not referenced anywhere in the file. Regenerate it against the current sources before merge.


Generated by Claude Code

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fixed, thanks — regenerated with build/i18n-scan.pl from the package root, so the paths stay package-relative.

Both directions confirmed gone/present: Reset peak, Peak: RSRP %s dBm · SINR %s dB and Signal — aim the antenna for the highest RSRP/SINR are out; Signal strength, Reference signal quality (RSRQ), 3G signal quality (Ec/Io), the seven series labels, excellent/good/fair, not reported, Signal graphs unavailable: …, registered — this modem reports no signal detail and the rewritten page description are in, and graph.js is now referenced 34 times.

One correction to the list: of the "Reference Signal Received …" descriptions, the panel's three are gone, but two remain on purpose — they are the cell-table column headers at status.js:50-51, which still exist and still describe the RSRP/RSRQ columns of the carrier-aggregation and neighbour tables.

Comment on lines +644 to +655
load: function() {
return Promise.all([
L.resolveDefault(callStatus(), {}),
/* the graph canvas: threshold rules only, series drawn by graph.js */
request.get(L.resource('wwand/signal.svg')).then(function(r) {
return r.ok ? r.text() : null;
}).catch(function() { return null; }),
]);
},

render: function(loaded) {
var svgText = loaded[1];

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: loaded[0] is never read — refresh() issues its own callStatus() a moment later — so this spends a throwaway ubus call on every page load, and the Promise.all exists only to carry it.

Suggested change
load: function() {
return Promise.all([
L.resolveDefault(callStatus(), {}),
/* the graph canvas: threshold rules only, series drawn by graph.js */
request.get(L.resource('wwand/signal.svg')).then(function(r) {
return r.ok ? r.text() : null;
}).catch(function() { return null; }),
]);
},
render: function(loaded) {
var svgText = loaded[1];
load: function() {
/* the graph canvas: threshold rules only, series drawn by graph.js */
return request.get(L.resource('wwand/signal.svg')).then(function(r) {
return r.ok ? r.text() : null;
}).catch(function() { return null; });
},
render: function(svgText) {

Generated by Claude Code

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fixed, thanks — and it was worse than a wasted call: nothing read loaded[0] at all, so the round trip was pure loss on every page load. load() now returns the canvas alone and render() takes it directly, as suggested.

Comment on lines +400 to +403
dom.content(ctx.legend[i], [ (cur != null)
? '%.1f %s (%s %.1f, %s %.1f)'.format(cur, s.unit,
_('avg'), avg, _('peak'), Math.max.apply(null, seen))
: _('not reported') ]);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: avg and peak reach the catalogue as two bare words with no context, and the sentence that frames them is not translatable at all, so its word order and punctuation are fixed for every language. The code this replaces shipped the whole thing as one msgid (Peak: RSRP %s dBm · SINR %s dB).

Suggested change
dom.content(ctx.legend[i], [ (cur != null)
? '%.1f %s (%s %.1f, %s %.1f)'.format(cur, s.unit,
_('avg'), avg, _('peak'), Math.max.apply(null, seen))
: _('not reported') ]);
dom.content(ctx.legend[i], [ (cur != null)
? _('%.1f %s (avg %.1f, peak %.1f)').format(cur, s.unit,
avg, Math.max.apply(null, seen))
: _('not reported') ]);

Generated by Claude Code

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fixed, thanks — one msgid now, as the code it replaced had.

Taking your suggestion surfaced a second defect in the same line, which is worth recording: my literal had two spaces before the bracket, and i18n-scan collapses whitespace when it builds the catalogue (build/i18n-scan.pl:43, applied at :85). The generated msgid could therefore never have matched the runtime string, and the translation would have silently never applied — no shipped template in the tree contains a double-space msgid. It is a single space now, so the entry the scanner emits is the string the page asks for.

ddimension pushed a commit to ddimension/luci-app-wwand that referenced this pull request Sep 11, 2026
From openwrt/luci#8917 review:

- The translation template was stale in both directions — it still carried
  "Reset peak", "Peak: RSRP %s dBm · SINR %s dB" and the removed panel's three
  Reference-Signal-Received descriptions, and none of the new strings, with
  graph.js not referenced at all. Regenerated with build/i18n-scan.pl from the
  package root. (The two "Reference Signal Received" entries that remain are
  the cell-table column headers at status.js:50-51, which are still live — not
  leftovers.)

- load() fetched the status and threw the result away: nothing read loaded[0],
  and refresh() issues its own callStatus() the moment the view renders. One
  ubus round trip per page load, for nothing. load() now returns the canvas
  alone.

- The legend line was assembled from three msgids, so "avg" and "peak" reached
  the catalogue as context-free words and the sentence around them was not
  translatable at all — its word order, spacing and punctuation fixed for every
  language. One msgid now, as the code it replaced had.

And the one the regeneration surfaced: that msgid had TWO spaces before the
bracket, and i18n-scan collapses whitespace when it builds the catalogue
(build/i18n-scan.pl:43,85). The generated msgid could therefore never match the
runtime string, and the translation would have silently never applied — no
shipped template in the tree contains a double-space msgid. One space now.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Status and management UI for the wwand cellular connection manager proposed in
openwrt/packages#30185: modem overview, per-modem tools (radio technologies,
band selection, network selection and scan, cell lock), SIM and eSIM profile
management, SMS, and the per-interface migration entry point that converts a
stock cellular interface to `proto wwand` in place.

The status page graphs the signal live — one canvas per quantity with its
published thresholds, one series per radio technology — so a modem dropping from
5G to LTE, or a hand on the antenna, is visible rather than inferred. The
history is kept in the browser: nothing is buffered on the router and nothing is
sampled while the page is closed.

Everything is driven over the daemon's ubus API; the ACL file lists exactly the
methods the pages call.

Signed-off-by: André Valentin <avalentin@marcant.net>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

add package Introduces a new package Makefile build script

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants