From 285933d22c340524563ef9404d118f70c46114e4 Mon Sep 17 00:00:00 2001 From: Sergei Poljanski Date: Tue, 20 Oct 2026 20:37:11 +0400 Subject: [PATCH] networking: recover the WWAN modem after a cold boot After a cold boot the EM160R-GL often drops off the PCIe link 10-20s after probe ("Unable to change power state from D3cold to D0", config space reads all-ones) and never brings up its wwan ports. Until now only a hibernate/resume cycle brought it back. An ACPI _RST through the PCI reset attribute does the same at runtime. The rescan after it catches the firmware mid-boot, under the generic Qualcomm ID 17cb:0304, which has no FCC unlock script, so ModemManager fails with "Couldn't set operating mode". A second rescan once the firmware is up gets 1eac:100d back and the modem enables normally. The service does nothing if wwan ports show up within 60s of boot. The recovery sequence was verified by hand on 7.2.11; the service has not yet run it on a real cold boot. Assisted-by: Claude:opus-5-5 --- modules/networking.nix | 47 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 47 insertions(+) diff --git a/modules/networking.nix b/modules/networking.nix index 4d06947..6e3026a 100644 --- a/modules/networking.nix +++ b/modules/networking.nix @@ -56,6 +56,53 @@ ACTION=="add", SUBSYSTEM=="mhi", KERNEL=="mhi[0-9]*", ATTR{power/wakeup}="disabled" ''; + # EM160R-GL cold-boot recovery. + # Why: after a cold boot the modem often drops off the PCIe link ~10-20s + # after probe ("Unable to change power state from D3cold to D0", config + # space reads all-ones) and never brings up its wwan ports; only a platform + # power cycle (hibernate/resume) recovered it. An ACPI _RST via the PCI + # reset attribute does the same live. The first rescan catches the firmware + # mid-boot with the generic Qualcomm ID 17cb:0304, which has no FCC unlock + # script (ModemManager then fails with "Couldn't set operating mode"), so + # once the firmware is up, re-enumerate again to get 1eac:100d back. + # Verified live 2026-10-20 on kernel 7.2.11. + systemd.services.wwan-coldboot-recover = { + description = "Recover EM160R-GL WWAN modem dead after cold boot"; + wantedBy = [ "multi-user.target" ]; + after = [ "systemd-udev-trigger.service" ]; + serviceConfig.Type = "exec"; + script = '' + dev= + for d in /sys/bus/pci/devices/*; do + [ "$(cat $d/vendor)" = 0x1eac ] && [ "$(cat $d/device)" = 0x100d ] && dev=$d + done + [ -n "$dev" ] || { echo "modem not found"; exit 0; } + addr=$(basename $dev) + + wait_ports() { + for _ in $(seq "$1"); do [ -e /sys/class/wwan/wwan0 ] && return 0; sleep 1; done + return 1 + } + rescan() { + echo 1 > /sys/bus/pci/devices/$addr/remove + echo 1 > /sys/bus/pci/rescan + } + + wait_ports 60 && { echo "modem up, nothing to do"; exit 0; } + + echo "no wwan ports after 60s, resetting $addr via ACPI _RST" + [ -e $dev/driver ] && echo $addr > $dev/driver/unbind + echo acpi > $dev/reset_method + echo 1 > $dev/reset + rescan + wait_ports 90 || { echo "modem still down after reset"; exit 1; } + sleep 30 + rescan + wait_ports 60 || { echo "modem lost after re-enumeration"; exit 1; } + echo "modem recovered as $(cat /sys/bus/pci/devices/$addr/vendor):$(cat /sys/bus/pci/devices/$addr/device)" + ''; + }; + # Ensure ModemManager starts before NetworkManager systemd.services.ModemManager = { wantedBy = [ "multi-user.target" ];