--- # Configures the embedded BroadMobi BM806C LTE modem for QMI use. # # Two workarounds are applied here for documented bugs in this modem's firmware # (M1.2.0_E1.0.1_A1.1.8, no public updates available): # # 1. raw-ip framing. The modem advertises 802.3 support but the 802.3 firmware # path is buggy — downlink frames don't reach the host. raw-ip works. # See bmork's kernel commit "net: qmi_wwan: support 'raw IP' mode": # 'newer generations of QMI hardware and firmware have moved towards # defaulting to raw IP mode instead, followed by an increasing number of # bugs in the already buggy 802.3 firmware implementation'. # qmi.sh hardcodes 802.3; we patch it in-place to use raw-ip. # # 2. --profile instead of --apn on --start-network. With --apn, the modem # establishes a phantom bearer that has no working downlink data path. # With --profile referencing a pre-configured NVRAM profile, data flows. # See https://forum.openwrt.org/t/problem-with-bm806u-e1-dwr-921-c3/130094 # and https://github.com/openwrt/openwrt/issues/6295 (FS#1363). # qmi.sh already supports the 'profile' UCI option; we set it in the # wwan interface config (profile=1 for IPv4, v6profile=2 for IPv6). # qmi.sh's --modify-profile call writes profile 1; profile 2 is bootstrapped # here for the IPv6-specific APN. # Patch qmi.sh to request raw-ip framing instead of 802.3. Two distinct uqmi # calls in the upstream proto handler request the data format — both must be # patched for the readback to return raw-ip and trigger the kernel driver's # sysfs raw_ip=Y flip. Idempotent: lineinfile is a no-op once the patterns # no longer match. # Diff output is suppressed for these tasks: the full qmi.sh file (~13 KB) is # included in both before/after diff payloads, and ansible's SSH stdout parser # truncates the resulting JSON, causing 'Module result deserialization failed: # No end of json char found'. The change is self-evident from the task name. - name: Patch qmi.sh — set-data-format to raw-ip community.openwrt.lineinfile: path: /lib/netifd/proto/qmi.sh regex: '^(\s*uqmi .* --set-data-format) 802\.3(.*)$' line: '\1 raw-ip\2' backrefs: true diff: false - name: Patch qmi.sh — wda-set-data-format to raw-ip community.openwrt.lineinfile: path: /lib/netifd/proto/qmi.sh regex: '^(\s*uqmi .* --wda-set-data-format) 802\.3(.*)$' line: '\1 raw-ip\2' backrefs: true diff: false # The kernel rejects writes to /sys/class/net/wwan0/qmi/raw_ip while the netdev # is up: 'Cannot change a running device' (-EBUSY). qmi.sh tries to flip it # while the interface is up — this works in 802.3 mode (no-op when already N), # but with our raw-ip patch above, the flip is mandatory and must succeed. # We bracket the sysfs write with ip link down/up. - name: Patch qmi.sh — bracket raw_ip sysfs write with ip link down/up community.openwrt.lineinfile: path: /lib/netifd/proto/qmi.sh regex: '^(\s*)echo "Y" > /sys/class/net/\$ifname/qmi/raw_ip$' line: '\1ip link set $ifname down; echo "Y" > /sys/class/net/$ifname/qmi/raw_ip; ip link set $ifname up' backrefs: true diff: false # Profile 2 in modem NVRAM holds the IPv6 APN. qmi.sh only manages profile 1 # (the v4 APN via --modify-profile, line 314); profile 2 is our responsibility. # These steps are skipped if the modem isn't enumerated yet (fresh boot before # usb-modeswitch completes, or modem in a fault state). - name: Check if QMI device is available community.openwrt.stat: path: /dev/cdc-wdm0 register: wwan_cdc_wdm - name: Query QMI profile list community.openwrt.command: cmd: uqmi -t 3000 -d /dev/cdc-wdm0 --get-profile-list 3gpp register: wwan_profile_list changed_when: false failed_when: false when: wwan_cdc_wdm.stat.exists | default(false) - name: Configure IPv6 APN profile 2 when: - wwan_cdc_wdm.stat.exists | default(false) - wwan_profile_list.rc | default(1) == 0 - wwan_profile_list.stdout | default('') | trim | length > 0 - wwan_profile_list.stdout | default('') | trim is match('^\\{') block: - name: Parse profile indexes ansible.builtin.set_fact: wwan_profile_indexes: >- {{ (wwan_profile_list.stdout | from_json).profiles | default([]) | map(attribute='index') | list }} - name: Create profile 2 for IPv6 APN if missing community.openwrt.command: cmd: uqmi -t 3000 -d /dev/cdc-wdm0 --create-profile 3gpp --apn internetipv6 --pdp-type ipv6 when: 2 not in wwan_profile_indexes # --modify-profile is idempotent at the modem level. We can't detect # whether values changed (uqmi doesn't return diff info), so we always # report 'ok' (changed_when: false) to keep play output clean. The cost # of always calling this is one QMI roundtrip. - name: Ensure profile 2 settings are current community.openwrt.command: cmd: uqmi -t 3000 -d /dev/cdc-wdm0 --modify-profile 3gpp,2 --apn internetipv6 --pdp-type ipv6 changed_when: false # On cold boot the BM806C's UIM (SIM) QMI service comes up permanently # broken: --uim-get-sim-state returns {}, --get-imsi returns # "UIM uninitialized", AT+CPIN? returns +CME ERROR: SIM busy, and the # modem never converges (verified at uptime 21 min with no intervention). # CTL/NAS/WDS do come up after ~5 min of warmup, but UIM does not. # # A single USB re-enumeration of the device (authorized=0 / authorized=1) # forces the modem to redo its internal QMI service init from scratch. # After this, UIM comes up within ~1 s and ifup wwan succeeds normally. # # We use authorized=0/1 rather than usb/unbind+bind because the former # keeps qmi_wwan in the bound-drivers list and the kernel re-runs its # bind machinery for us; the latter detaches and re-attaches drivers # explicitly. Both work; authorized is cleaner. # # Full investigation, ruled-out hypotheses, and reproduction steps: # /root/wwan-diag/boot-wedge-investigation.md on the router. - name: Install wwan-bringup worker script community.openwrt.copy: dest: /usr/libexec/wwan-bringup mode: '0755' owner: root group: root content: | #!/bin/sh # Force-clean BM806C cold-boot UIM wedge by re-enumerating the USB # device once, then bring up wwan. Called by /etc/init.d/wwan-bringup # as a procd service. DEV=/dev/cdc-wdm0 IFACE=wwan USB_PORT=1-1 log() { logger -t wwan-bringup "$1" } # Wait for cold-boot enumeration of cdc-wdm0 (<=60s). waited=0 while [ ! -e "$DEV" ]; do sleep 1 waited=$((waited + 1)) [ $waited -ge 60 ] && break done if [ ! -e "$DEV" ]; then log "$DEV never appeared within 60s; giving up" exit 1 fi # Force-clean re-enumeration. The BM806C's UIM QMI service never # comes up on cold boot without this. log "BM806C cold-boot UIM workaround: re-authorizing $USB_PORT" echo 0 > /sys/bus/usb/devices/$USB_PORT/authorized sleep 3 echo 1 > /sys/bus/usb/devices/$USB_PORT/authorized # Wait for cdc-wdm0 to return after re-enumeration (<=30s). waited=0 while [ ! -e "$DEV" ]; do sleep 1 waited=$((waited + 1)) [ $waited -ge 30 ] && break done if [ ! -e "$DEV" ]; then log "$DEV did not return after re-auth; giving up" exit 1 fi # qmi.sh's own SIM-init and network-registration loops handle the # small remaining warmup (~5-30s) gracefully now that UIM is healthy. log "bringing up $IFACE" ifup "$IFACE" # qmi.sh installs an IPv6 default route with a source-specific prefix # constraint (`default from 2a00:f44:.../64 ...`). This means only # traffic sourced from the wwan IPv6 prefix uses it — forwarded traffic # from internal subnets fails routing lookup with "net unreachable" # before masquerade can rewrite the source. Add a non-source-specific # default at a higher metric so forwarded traffic has a valid route, # gets routed out wwan0, then masqueraded by fw4. # # Wait up to 90s for qmi.sh to install its source-specific default, # then derive the gateway and add a regular default route. waited=0 while [ $waited -lt 90 ]; do gw6=$(ip -6 route show default dev wwan0 2>/dev/null | awk '/^default from/ {print $5; exit}') if [ -n "$gw6" ]; then if ip -6 route show default dev wwan0 | grep -qE "^default via "; then log "non-source-specific IPv6 default already present" else log "adding non-source-specific IPv6 default via $gw6" ip -6 route add default via "$gw6" dev wwan0 metric 1024 fi break fi sleep 3 waited=$((waited + 3)) done [ -z "$gw6" ] && log "warning: wwan IPv6 gateway never appeared, skipping default route" - name: Install wwan-bringup init script community.openwrt.copy: dest: /etc/init.d/wwan-bringup mode: '0755' owner: root group: root content: | #!/bin/sh /etc/rc.common # Starts the wwan-bringup worker which re-enumerates the BM806C USB # device once to clear the cold-boot UIM wedge, then triggers # `ifup wwan`. See /usr/libexec/wwan-bringup. START=99 USE_PROCD=1 # One-shot script: launch the worker directly without procd_open_instance # so procd does not respawn it after successful exit. PIDFILE=/var/run/wwan-bringup.pid start_service() { /usr/libexec/wwan-bringup & echo $! > $PIDFILE } stop_service() { [ -f $PIDFILE ] && kill "$(cat $PIDFILE)" 2>/dev/null rm -f $PIDFILE } - name: Enable and start wwan-bringup service community.openwrt.service: name: wwan-bringup enabled: true state: started