diff --git a/package/base-files/files/etc/banner b/package/base-files/files/etc/banner index f3af3c014f20f1..12b5065a045f07 100644 --- a/package/base-files/files/etc/banner +++ b/package/base-files/files/etc/banner @@ -4,5 +4,7 @@ |_______|| __|_____|__|__||________||__| |____| |__| W I R E L E S S F R E E D O M ----------------------------------------------------- - %D %V, %C + YYH2913-jjcszxh-openwrt-xr1710g-6.18.38-integration-test-sync-20260707 0.0.8, 202607072019 + ----------------------------------------------------- + 本固件完全免费开源,遇到奸商售卖固件请退款,唯一可信下载渠道https://github.com/jjcszxh/openwrt-XR1710G ----------------------------------------------------- diff --git a/package/boot/uboot-tools/uboot-envtools/files/airoha_an7581 b/package/boot/uboot-tools/uboot-envtools/files/airoha_an7581 index c0a5c9fb444b24..73443bef1827df 100644 --- a/package/boot/uboot-tools/uboot-envtools/files/airoha_an7581 +++ b/package/boot/uboot-tools/uboot-envtools/files/airoha_an7581 @@ -2,17 +2,33 @@ # Copyright (C) 2025 OpenWrt.org # -[ -e /etc/config/ubootenv ] && exit 0 +. /lib/functions.sh + +board=$(board_name) + +case "$board" in +gemtek,w1700k-ubi|\ +econet,xr1710g-ubi|\ +nokia,xg-040g-md-ubi) + if [ -e /etc/config/ubootenv ] && [ -e /etc/fw_env.config ] && + grep -Eq '^/dev/ubi[0-9]+_[0-9]+[[:space:]]' /etc/fw_env.config; then + exit 0 + fi + + rm -f /etc/config/ubootenv /etc/fw_env.config /etc/fw_sys.config + ;; +*) + [ -e /etc/config/ubootenv ] && exit 0 + ;; +esac touch /etc/config/ubootenv . /lib/uboot-envtools.sh -. /lib/functions.sh - -board=$(board_name) case "$board" in gemtek,w1700k-ubi|\ +econet,xr1710g-ubi|\ nokia,xg-040g-md-ubi) ubootenv_add_ubi_default ;; diff --git a/package/boot/uboot-tools/uboot-envtools/files/fw_loadenv b/package/boot/uboot-tools/uboot-envtools/files/fw_loadenv index 9fe302ce892315..46894abdc5b763 100644 --- a/package/boot/uboot-tools/uboot-envtools/files/fw_loadenv +++ b/package/boot/uboot-tools/uboot-envtools/files/fw_loadenv @@ -11,6 +11,9 @@ if (fs.lsdir(path)) { exit(0); } +if (!fs.stat('/etc/fw_env.config')) + exit(0); + let fp = fs.popen('fw_printenv'); let raw = fp.read('all'); fp.close(); diff --git a/package/boot/uboot-tools/uboot-envtools/files/uboot-envtools.sh b/package/boot/uboot-tools/uboot-envtools/files/uboot-envtools.sh index 7fb995fa823495..7f580e9e480573 100644 --- a/package/boot/uboot-tools/uboot-envtools/files/uboot-envtools.sh +++ b/package/boot/uboot-tools/uboot-envtools/files/uboot-envtools.sh @@ -66,8 +66,17 @@ ubootenv_add_mmc() { ubootenv_add_ubi_default() { . /lib/upgrade/nand.sh local envubi=$(nand_find_ubi ubi) - local envdev=/dev/$(nand_find_volume $envubi ubootenv) - local envdev2=/dev/$(nand_find_volume $envubi ubootenv2) + local envvol envvol2 envdev envdev2 + + [ -n "$envubi" ] || return 0 + + envvol="$(nand_find_volume $envubi ubootenv)" + envvol2="$(nand_find_volume $envubi ubootenv2)" + [ -n "$envvol" ] || return 0 + [ -n "$envvol2" ] || return 0 + + envdev="/dev/$envvol" + envdev2="/dev/$envvol2" ubootenv_add_uci_config "$envdev" "0x0" "0x1f000" "0x1f000" "1" ubootenv_add_uci_config "$envdev2" "0x0" "0x1f000" "0x1f000" "1" } diff --git a/package/firmware/wireless-regdb/patches/600-custom-change-txpower-and-dfs.patch b/package/firmware/wireless-regdb/patches/600-custom-change-txpower-and-dfs.patch new file mode 100644 index 00000000000000..8f657488e8d4c3 --- /dev/null +++ b/package/firmware/wireless-regdb/patches/600-custom-change-txpower-and-dfs.patch @@ -0,0 +1,31 @@ +--- a/db.txt ++++ b/db.txt +@@ -469,9 +469,8 @@ country CL: DFS-JP + # https://www.miit.gov.cn/cms_files/filemanager/1226211233/attach/20236/d1dc19424d5a4cfe90d631adeee8dd58.pdf + # Note: The transmit power for 5250-5350MHz bands can be raised by 3dBm when TPC is implemented + country CN: DFS-FCC +- (2400 - 2483.5 @ 40), (20) +- (5150 - 5250 @ 80), (23), NO-OUTDOOR, AUTO-BW +- (5250 - 5350 @ 80), (20), NO-OUTDOOR, DFS, AUTO-BW ++ (2400 - 2483.5 @ 40), (30) ++ (5150 - 5350 @ 160), (30) + (5725 - 5850 @ 80), (33) + # 60 GHz band channels 1,4: 28dBm, channels 2,3: 44dBm + # ref: http://www.miit.gov.cn/n11293472/n11505629/n11506593/n11960250/n11960606/n11960700/n12330791.files/n12330790.pdf +@@ -2081,14 +2080,12 @@ country US: DFS-FCC + (920 - 928 @ 8), (30) + (2400 - 2472 @ 40), (30) + # 5.15 ~ 5.25 GHz: 30 dBm for master mode, 23 dBm for clients +- (5150 - 5250 @ 80), (23), AUTO-BW +- (5250 - 5350 @ 80), (24), DFS, AUTO-BW ++ (5150 - 5350 @ 160), (30) + # This range ends at 5725 MHz, but channel 144 extends to 5730 MHz. + # Since 5725 ~ 5730 MHz belongs to the next range which has looser + # requirements, we can extend the range by 5 MHz to make the kernel + # happy and be able to use channel 144. +- (5470 - 5730 @ 160), (24), DFS +- (5730 - 5850 @ 80), (30), AUTO-BW ++ (5470 - 5850 @ 160), (30) + # https://www.federalregister.gov/documents/2021/05/03/2021-08802/use-of-the-5850-5925-ghz-band + # max. 33 dBm AP @ 20MHz, 36 dBm AP @ 40Mhz+, 6 dB less for clients + (5850 - 5895 @ 40), (27), NO-OUTDOOR, AUTO-BW, NO-IR diff --git a/package/firmware/wireless-regdb/patches/601-custom-6g-unlock.patch b/package/firmware/wireless-regdb/patches/601-custom-6g-unlock.patch new file mode 100644 index 00000000000000..3643c7cd12d06f --- /dev/null +++ b/package/firmware/wireless-regdb/patches/601-custom-6g-unlock.patch @@ -0,0 +1,11 @@ +--- a/db.txt ++++ b/db.txt +@@ -2091,7 +2091,7 @@ + (5850 - 5895 @ 40), (27), NO-OUTDOOR, AUTO-BW, NO-IR + # 6g band + # https://www.federalregister.gov/documents/2020/05/26/2020-11236/unlicensed-use-of-the-6ghz-band +- (5925 - 7125 @ 320), (12), NO-OUTDOOR, NO-IR ++ (5925 - 7125 @ 320), (30) + # 60g band + # reference: section IV-D https://docs.fcc.gov/public/attachments/FCC-16-89A1.pdf + # channels 1-6 EIRP=40dBm(43dBm peak) diff --git a/package/kernel/linux/modules/crypto.mk b/package/kernel/linux/modules/crypto.mk index b5d065cb909845..0e4365f465fae2 100644 --- a/package/kernel/linux/modules/crypto.mk +++ b/package/kernel/linux/modules/crypto.mk @@ -522,8 +522,8 @@ endef $(eval $(call KernelPackage,crypto-hw-talitos)) define KernelPackage/crypto-hw-eip93 - TITLE:=MTK EIP93 crypto module - DEPENDS:=@(TARGET_ramips_mt7621||TARGET_airoha) \ + TITLE:=SafeXcel EIP-93 Crypto Engine module + DEPENDS:=@(TARGET_airoha||TARGET_econet||TARGET_ramips_mt7621) \ +kmod-crypto-authenc \ +kmod-crypto-des \ +kmod-crypto-md5 \ @@ -540,10 +540,10 @@ define KernelPackage/crypto-hw-eip93 endef define KernelPackage/crypto-hw-eip93/description -Kernel module to enable EIP-93 Crypto engine as found -in Mediatek MT7621 and Airoha SoCs. -It enables DES/3DES/AES ECB/CBC/CTR and -IPSEC offload with authenc(hmac(sha1/sha256), aes/cbc/rfc3686) +Kernel module to enable EIP-93 Crypto engine as +found in the Airoha / EcoNet / Mediatek MT7621 SoC. +It enables DES/3DES/AES ECB/CBC/CTR and IPSEC offload +with authenc(hmac(sha1/sha256), aes/cbc/rfc3686) endef $(eval $(call KernelPackage,crypto-hw-eip93)) @@ -1167,7 +1167,9 @@ $(eval $(call KernelPackage,crypto-sha512)) define KernelPackage/crypto-test TITLE:=Test CryptoAPI module - KCONFIG:=CONFIG_CRYPTO_TEST + KCONFIG:= \ + CONFIG_CRYPTO_TEST \ + CONFIG_CRYPTO_BENCHMARK FILES:=$(LINUX_DIR)/crypto/tcrypt.ko $(call AddDepends/crypto,+kmod-crypto-manager) endef diff --git a/package/kernel/linux/modules/netdevices.mk b/package/kernel/linux/modules/netdevices.mk index a4d16a2dfbe72b..9586288662d124 100644 --- a/package/kernel/linux/modules/netdevices.mk +++ b/package/kernel/linux/modules/netdevices.mk @@ -657,6 +657,81 @@ endef $(eval $(call KernelPackage,phy-airoha-en8811h)) +define KernelPackage/airoha-npu + SUBMENU:=$(NETWORK_DEVICES_MENU) + TITLE:=Airoha Network Processor Unit support + DEPENDS:=@TARGET_airoha +airoha-en7581-mt7996-npu-firmware + KCONFIG:=CONFIG_NET_AIROHA_NPU + FILES:=$(LINUX_DIR)/drivers/net/ethernet/airoha/airoha_npu.ko + AUTOLOAD:=$(call AutoLoad,40,airoha_npu,1) +endef + +define KernelPackage/airoha-npu/description + Kernel module for Airoha Network Processor Unit (NPU). + Required for PPE hardware flow offloading and WiFi WED offloading. +endef + +$(eval $(call KernelPackage,airoha-npu)) + + +# SOE is linked into airoha-eth rather than shipped as a standalone module +# because it shares the Ethernet driver's FE/QDMA rings and netdev lifetime. +define KernelPackage/airoha-eth + SUBMENU:=$(NETWORK_DEVICES_MENU) + TITLE:=Airoha SoC Gigabit Ethernet support + DEPENDS:=@TARGET_airoha +kmod-airoha-npu +kmod-of-mdio \ + +kmod-ipsec4-offload \ + +IPV6:kmod-ipsec6-offload + KCONFIG:= \ + CONFIG_NET_AIROHA \ + CONFIG_NET_AIROHA_SOE + FILES:=$(LINUX_DIR)/drivers/net/ethernet/airoha/airoha-eth.ko + AUTOLOAD:=$(call AutoLoad,41,airoha-eth,1) +endef + +define KernelPackage/airoha-eth/description + Kernel module for Airoha SoC Gigabit Ethernet. + Includes PPE and SOE support for hardware flow and ESP offloading. +endef + +$(eval $(call KernelPackage,airoha-eth)) + + +define KernelPackage/dsa-mt7530 + SUBMENU:=$(NETWORK_DEVICES_MENU) + TITLE:=MediaTek MT7530/MT7531 DSA switch + DEPENDS:=@TARGET_airoha + KCONFIG:=CONFIG_NET_DSA_MT7530 + FILES:=$(LINUX_DIR)/drivers/net/dsa/mt7530-dsa.ko + AUTOLOAD:=$(call AutoLoad,42,mt7530-dsa,1) +endef + +define KernelPackage/dsa-mt7530/description + Kernel module for MediaTek MT7530/MT7531 Gigabit switch. + This is the core switch driver. +endef + +$(eval $(call KernelPackage,dsa-mt7530)) + + +define KernelPackage/dsa-mt7530-mmio + SUBMENU:=$(NETWORK_DEVICES_MENU) + TITLE:=MediaTek MT7530 MMIO interface driver + DEPENDS:=@TARGET_airoha +kmod-dsa-mt7530 +kmod-airoha-eth + KCONFIG:=CONFIG_NET_DSA_MT7530_MMIO + FILES:=$(LINUX_DIR)/drivers/net/dsa/mt7530-mmio.ko + AUTOLOAD:=$(call AutoLoad,43,mt7530-mmio,1) + DEFAULT:=y if TARGET_airoha_an7581 +endef + +define KernelPackage/dsa-mt7530-mmio/description + Kernel module for MediaTek MT7530 DSA switch with MMIO interface. + Used on Airoha EN7581 SoC with built-in switch. +endef + +$(eval $(call KernelPackage,dsa-mt7530-mmio)) + + define KernelPackage/phy-aquantia SUBMENU:=$(NETWORK_DEVICES_MENU) TITLE:=Aquantia Ethernet PHYs diff --git a/package/kernel/linux/modules/netsupport.mk b/package/kernel/linux/modules/netsupport.mk index c4ab8dc2fe7336..d447b349af6ebf 100644 --- a/package/kernel/linux/modules/netsupport.mk +++ b/package/kernel/linux/modules/netsupport.mk @@ -246,8 +246,7 @@ define KernelPackage/ipsec4 CONFIG_INET_AH \ CONFIG_INET_ESP \ CONFIG_INET_IPCOMP \ - CONFIG_INET_XFRM_TUNNEL \ - CONFIG_INET_ESP_OFFLOAD=n + CONFIG_INET_XFRM_TUNNEL FILES:=$(foreach mod,$(IPSEC4-m),$(LINUX_DIR)/net/$(mod).ko) AUTOLOAD:=$(call AutoLoad,32,$(notdir $(IPSEC4-m))) endef @@ -264,6 +263,24 @@ endef $(eval $(call KernelPackage,ipsec4)) +define KernelPackage/ipsec4-offload + SUBMENU:=$(NETWORK_SUPPORT_MENU) + TITLE:=IPsec ESP offload support (IPv4) + DEPENDS:=+kmod-ipsec4 + KCONFIG:= \ + CONFIG_XFRM_OFFLOAD=y \ + CONFIG_INET_ESP_OFFLOAD + FILES:=$(LINUX_DIR)/net/ipv4/esp4_offload.ko + AUTOLOAD:=$(call AutoLoad,33,esp4_offload) +endef + +define KernelPackage/ipsec4-offload/description + Kernel module for IPv4 ESP transformation offload support. +endef + +$(eval $(call KernelPackage,ipsec4-offload)) + + IPSEC6-m = \ ipv6/ah6 \ ipv6/esp6 \ @@ -278,8 +295,7 @@ define KernelPackage/ipsec6 CONFIG_INET6_AH \ CONFIG_INET6_ESP \ CONFIG_INET6_IPCOMP \ - CONFIG_INET6_XFRM_TUNNEL \ - CONFIG_INET6_ESP_OFFLOAD=n + CONFIG_INET6_XFRM_TUNNEL FILES:=$(foreach mod,$(IPSEC6-m),$(LINUX_DIR)/net/$(mod).ko) AUTOLOAD:=$(call AutoLoad,32,$(notdir $(IPSEC6-m))) endef @@ -296,6 +312,24 @@ endef $(eval $(call KernelPackage,ipsec6)) +define KernelPackage/ipsec6-offload + SUBMENU:=$(NETWORK_SUPPORT_MENU) + TITLE:=IPsec ESP offload support (IPv6) + DEPENDS:=@IPV6 +kmod-ipsec6 + KCONFIG:= \ + CONFIG_XFRM_OFFLOAD=y \ + CONFIG_INET6_ESP_OFFLOAD + FILES:=$(LINUX_DIR)/net/ipv6/esp6_offload.ko + AUTOLOAD:=$(call AutoLoad,33,esp6_offload) +endef + +define KernelPackage/ipsec6-offload/description + Kernel module for IPv6 ESP transformation offload support. +endef + +$(eval $(call KernelPackage,ipsec6-offload)) + + define KernelPackage/iptunnel SUBMENU:=$(NETWORK_SUPPORT_MENU) TITLE:=IP tunnel support diff --git a/package/kernel/mac80211/patches/subsys/372-wifi-cfg80211-fix-definition-of-EHT-MCS-15-in-MRU.patch b/package/kernel/mac80211/patches/subsys/372-wifi-cfg80211-fix-definition-of-EHT-MCS-15-in-MRU.patch new file mode 100644 index 00000000000000..f7b95f7eb5f79d --- /dev/null +++ b/package/kernel/mac80211/patches/subsys/372-wifi-cfg80211-fix-definition-of-EHT-MCS-15-in-MRU.patch @@ -0,0 +1,33 @@ +From linux-wireless Fri Mar 13 06:21:49 2026 +From: Shayne Chen +Date: Fri, 13 Mar 2026 06:21:49 +0000 +Subject: [PATCH] wifi: ieee80211: fix definition of EHT-MCS 15 in MRU + +According to IEEE Std 802.11be-2024 Table 9-417r, the bits indicate +support for EHT-MCS 15 in: +- B0: 52+26-tone and 106+26-tone MRUs +- B1: a 484+242-tone MRU if 80 MHz is supported +- B2: a 996+484-tone and a 996+484+242-tone MRU if 160 MHz is supported +- B3: a 3x996-tone MRU if 320 MHz is supported + +Fixes: 6239da18d2f9 ("wifi: mac80211: adjust EHT capa when lowering bandwidth") +Signed-off-by: Shayne Chen +--- + include/linux/ieee80211.h | 4 ++-- + 1 file changed, 2 insertions(+), 2 deletions(-) + +diff --git a/include/linux/ieee80211.h b/include/linux/ieee80211.h +index f8e9f5d36d2a..a97b1d01f3ac 100644 +--- a/include/linux/ieee80211.h ++++ b/include/linux/ieee80211.h +@@ -3192,8 +3192,8 @@ struct ieee80211_eht_operation_info { + #define IEEE80211_EHT_PHY_CAP5_SUPP_EXTRA_EHT_LTF 0x40 + #define IEEE80211_EHT_PHY_CAP6_MAX_NUM_SUPP_EHT_LTF_MASK 0x07 + +-#define IEEE80211_EHT_PHY_CAP6_MCS15_SUPP_80MHZ 0x08 +-#define IEEE80211_EHT_PHY_CAP6_MCS15_SUPP_160MHZ 0x30 ++#define IEEE80211_EHT_PHY_CAP6_MCS15_SUPP_80MHZ 0x10 ++#define IEEE80211_EHT_PHY_CAP6_MCS15_SUPP_160MHZ 0x20 + #define IEEE80211_EHT_PHY_CAP6_MCS15_SUPP_320MHZ 0x40 + #define IEEE80211_EHT_PHY_CAP6_MCS15_SUPP_MASK 0x78 + #define IEEE80211_EHT_PHY_CAP6_EHT_DUP_6GHZ_SUPP 0x80 diff --git a/package/kernel/mac80211/patches/subsys/373-wifi-mac80211-fix-missing-ieee80211_eml_params-member-init.patch b/package/kernel/mac80211/patches/subsys/373-wifi-mac80211-fix-missing-ieee80211_eml_params-member-init.patch new file mode 100644 index 00000000000000..4bad6b5ea3041c --- /dev/null +++ b/package/kernel/mac80211/patches/subsys/373-wifi-mac80211-fix-missing-ieee80211_eml_params-member-init.patch @@ -0,0 +1,26 @@ +From: MeiChia Chiu +Date: Tue, 10 Mar 2026 00:00:00 +0000 +Subject: [PATCH] wifi: mac80211: fix missing ieee80211_eml_params member initialization + +The missing initialization causes the driver to misinterpret the EML +control bitmap, resulting in incorrect link bitmap handling. + +Fixes: 0d95280a2d54e ("wifi: mac80211: Add eMLSR/eMLMR action frame parsing support") +Signed-off-by: MeiChia Chiu +Acked-by: Lorenzo Bianconi +--- + net/mac80211/eht.c | 1 + + 1 file changed, 1 insertion(+) + +diff --git a/net/mac80211/eht.c b/net/mac80211/eht.c +index 75096b2195d2..078e1e23d8d1 100644 +--- a/net/mac80211/eht.c ++++ b/net/mac80211/eht.c +@@ -154,6 +154,7 @@ void ieee80211_rx_eml_op_mode_notif(struct ieee80211_sub_if_data *sdata, + u8 *ptr = mgmt->u.action.u.eml_omn.variable; + struct ieee80211_eml_params eml_params = { + .link_id = status->link_id, ++ .control = control, + }; + struct sta_info *sta; + int opt_len = 0; diff --git a/package/kernel/mac80211/patches/subsys/381-wifi-cfg80211-split-wiphy-radio-dump.patch b/package/kernel/mac80211/patches/subsys/381-wifi-cfg80211-split-wiphy-radio-dump.patch new file mode 100644 index 00000000000000..68f70493adecf8 --- /dev/null +++ b/package/kernel/mac80211/patches/subsys/381-wifi-cfg80211-split-wiphy-radio-dump.patch @@ -0,0 +1,76 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: Yanghan Ye +Date: Sun, 10 May 2026 03:09:44 +0800 +Subject: [PATCH] wifi: cfg80211: keep wiphy radios in one split dump fragment + +Large multi-radio wiphys can overflow a single split dump fragment while exporting radio and interface-combination data. Keep NL80211_ATTR_WIPHY_RADIOS complete in a single split fragment because hostapd parses that attribute as a complete radio list per message. + +Signed-off-by: Yanghan Ye +--- + net/wireless/nl80211.c | 22 ++++++++++++++++++---- + 1 file changed, 18 insertions(+), 4 deletions(-) + +diff --git a/net/wireless/nl80211.c b/net/wireless/nl80211.c +index 432085f..b67b1e0 100644 +--- a/net/wireless/nl80211.c ++++ b/net/wireless/nl80211.c +@@ -2570,7 +2570,8 @@ nla_put_failure: + return -ENOBUFS; + } + +-static int nl80211_put_radios(struct wiphy *wiphy, struct sk_buff *msg) ++static int nl80211_put_radios(struct wiphy *wiphy, struct sk_buff *msg, ++ bool split) + { + struct nlattr *radios; + int i; +@@ -2582,12 +2583,16 @@ static int nl80211_put_radios(struct wiphy *wiphy, struct sk_buff *msg) + if (!radios) + return -ENOBUFS; + +- for (i = 0; i < wiphy->n_radio; i++) ++ for (i = 0; i < wiphy->n_radio; i++) { + if (nl80211_put_radio(wiphy, msg, i)) + goto fail; ++ } + + nla_nest_end(msg, radios); + ++ if (split) ++ return 0; ++ + if (nl80211_put_iface_combinations(wiphy, msg, + NL80211_ATTR_WIPHY_INTERFACE_COMBINATIONS, + -1, true, NLA_F_NESTED)) +@@ -3284,12 +3289,21 @@ static int nl80211_send_wiphy(struct cfg80211_registered_device *rdev, + state->split_start++; + break; + case 17: +- if (nl80211_put_radios(&rdev->wiphy, msg)) ++ if (nl80211_put_radios(&rdev->wiphy, msg, state->split)) + goto nla_put_failure; + + state->split_start++; + break; + case 18: ++ if (state->split && rdev->wiphy.n_radio && ++ nl80211_put_iface_combinations(&rdev->wiphy, msg, ++ NL80211_ATTR_WIPHY_INTERFACE_COMBINATIONS, ++ -1, true, NLA_F_NESTED)) ++ goto nla_put_failure; ++ ++ state->split_start++; ++ break; ++ case 19: + if (nl80211_put_nan_capa(&rdev->wiphy, msg)) + goto nla_put_failure; + +@@ -3442,7 +3456,7 @@ static int nl80211_get_wiphy(struct sk_buff *skb, struct genl_info *info) + struct cfg80211_registered_device *rdev = info->user_ptr[0]; + struct nl80211_dump_wiphy_state state = {}; + +- msg = nlmsg_new(4096, GFP_KERNEL); ++ msg = nlmsg_new(NLMSG_DEFAULT_SIZE, GFP_KERNEL); + if (!msg) + return -ENOMEM; + diff --git a/package/kernel/mac80211/patches/subsys/391-mtk-cfg80211-fix-early-return-in-cfg80211_stop_backg.patch b/package/kernel/mac80211/patches/subsys/391-mtk-cfg80211-fix-early-return-in-cfg80211_stop_backg.patch new file mode 100644 index 00000000000000..778402b92ea4c0 --- /dev/null +++ b/package/kernel/mac80211/patches/subsys/391-mtk-cfg80211-fix-early-return-in-cfg80211_stop_backg.patch @@ -0,0 +1,31 @@ +From 02aa7aa1907075e19de9c18e11619fd76aaff4af Mon Sep 17 00:00:00 2001 +From: StanleyYP Wang +Date: Thu, 27 Jul 2023 10:25:59 +0800 +Subject: [PATCH 025/137] mtk: cfg80211: fix early return in + cfg80211_stop_background_radar_detection + +Signed-off-by: StanleyYP Wang +--- + net/wireless/mlme.c | 2 +- + 1 file changed, 1 insertion(+), 1 deletion(-) + +diff --git a/net/wireless/mlme.c b/net/wireless/mlme.c +index 206e78c1..519e4ea2 100644 +--- a/net/wireless/mlme.c ++++ b/net/wireless/mlme.c +@@ -1331,11 +1331,11 @@ void cfg80211_stop_background_radar_detection(struct wireless_dev *wdev) + return; + + rdev_set_radar_background(rdev, NULL); +- rdev->background_radar_wdev = NULL; /* Release offchain ownership */ + + __cfg80211_background_cac_event(rdev, wdev, + &rdev->background_radar_chandef, + NL80211_RADAR_CAC_ABORTED); ++ rdev->background_radar_wdev = NULL; /* Release offchain ownership */ + } + + int cfg80211_assoc_ml_reconf(struct cfg80211_registered_device *rdev, +-- +2.45.2 + diff --git a/package/kernel/mac80211/patches/subsys/392-mtk-mac80211-Fix-SMPS-action-frame-cap-check.patch b/package/kernel/mac80211/patches/subsys/392-mtk-mac80211-Fix-SMPS-action-frame-cap-check.patch new file mode 100644 index 00000000000000..f538e44e8c375e --- /dev/null +++ b/package/kernel/mac80211/patches/subsys/392-mtk-mac80211-Fix-SMPS-action-frame-cap-check.patch @@ -0,0 +1,33 @@ +From 66aa48703ff78c308097871c84ad3946bc2ac6d4 Mon Sep 17 00:00:00 2001 +From: "Allen.Ye" +Date: Thu, 30 Nov 2023 14:01:29 +0800 +Subject: [PATCH 037/137] mtk: mac80211: Fix SMPS action frame cap check + +Fix SMPS action frame cap check. +Due to 6G band doesn't have HT cap, we change cap check into each action +frame section. + +Signed-off-by: Allen.Ye +--- + net/mac80211/rx.c | 5 +++++ + 1 file changed, 5 insertions(+) + +diff --git a/net/mac80211/rx.c b/net/mac80211/rx.c +index 7f4bb2cd..44ed0111 100644 +--- a/net/mac80211/rx.c ++++ b/net/mac80211/rx.c +@@ -3654,6 +3654,11 @@ ieee80211_rx_h_action(struct ieee80211_rx_data *rx) + enum ieee80211_smps_mode smps_mode; + struct sta_opmode_info sta_opmode = {}; + ++ if (rx->link_sta->pub->he_cap.has_he && ++ !(rx->link_sta->pub->he_cap.he_cap_elem.mac_cap_info[5] & ++ IEEE80211_HE_MAC_CAP5_HE_DYNAMIC_SM_PS)) ++ goto invalid; ++ + if (sdata->vif.type != NL80211_IFTYPE_AP && + sdata->vif.type != NL80211_IFTYPE_AP_VLAN) + goto handled; +-- +2.45.2 + diff --git a/package/kernel/mac80211/patches/subsys/393-mtk-mac80211-fix-radar-required-of-link-issue-in-res.patch b/package/kernel/mac80211/patches/subsys/393-mtk-mac80211-fix-radar-required-of-link-issue-in-res.patch new file mode 100644 index 00000000000000..8cc543fc515230 --- /dev/null +++ b/package/kernel/mac80211/patches/subsys/393-mtk-mac80211-fix-radar-required-of-link-issue-in-res.patch @@ -0,0 +1,39 @@ +From 4e28388aca77d5dc33d1c34d55a2317c75580612 Mon Sep 17 00:00:00 2001 +From: StanleyYP Wang +Date: Thu, 1 Feb 2024 17:46:49 +0800 +Subject: [PATCH 045/137] mtk: mac80211: fix radar required of link issue in + reserve_reassign and reserve_assign + +link->radar_required is not updated in +ieee80211_link_use_reserved_assign & ieee80211_link_use_reserved_reassign +This will lead to DFS RDD init incomplete (RDD_CAC_START, RDD_CAC_END & +RDD_DET_MODE is not set to fw) + +Signed-off-by: StanleyYP Wang +--- + net/mac80211/chan.c | 2 ++ + 1 file changed, 2 insertions(+) + +diff --git a/net/mac80211/chan.c b/net/mac80211/chan.c +index fffba3d2..a7ec951c 100644 +--- a/net/mac80211/chan.c ++++ b/net/mac80211/chan.c +@@ -1496,6 +1496,7 @@ ieee80211_link_use_reserved_reassign(struct ieee80211_link_data *link) + if (link_conf->chanreq.oper.width != link->reserved.oper.width) + changed = BSS_CHANGED_BANDWIDTH; + ++ link->radar_required = link->reserved_radar_required; + ieee80211_link_update_chanreq(link, &link->reserved); + + _ieee80211_change_chanctx(local, new_ctx, old_ctx, chanreq, link); +@@ -1575,6 +1576,7 @@ ieee80211_link_use_reserved_assign(struct ieee80211_link_data *link) + + link->reserved_chanctx = NULL; + ++ link->radar_required = link->reserved_radar_required; + err = ieee80211_assign_link_chanctx(link, new_ctx, false); + if (err) { + if (ieee80211_chanctx_refcount(local, new_ctx) == 0) +-- +2.45.2 + diff --git a/package/kernel/mac80211/patches/subsys/394-mtk-mac80211-fix-AP-mgmt-not-encrypted-in-WDS-mode-w.patch b/package/kernel/mac80211/patches/subsys/394-mtk-mac80211-fix-AP-mgmt-not-encrypted-in-WDS-mode-w.patch new file mode 100644 index 00000000000000..1bad8c1085223a --- /dev/null +++ b/package/kernel/mac80211/patches/subsys/394-mtk-mac80211-fix-AP-mgmt-not-encrypted-in-WDS-mode-w.patch @@ -0,0 +1,37 @@ +From a84590cb11fa1b679251c5d61247c1702e68386f Mon Sep 17 00:00:00 2001 +From: Michael-CY Lee +Date: Thu, 25 Jan 2024 14:07:23 +0800 +Subject: [PATCH 048/137] mtk: mac80211: fix AP mgmt not encrypted in WDS mode + with PMF on + +In ieee80211_tx_prepare(), if tx->sta is still NULL after calling +sta_info_get(), the skb might be mgmt for WDS peer, so sta_info_get_bss() +if called to find sta from AP_VLAN, and then interface type & 4-addr +using is checked. + +Signed-off-by: Michael-CY Lee +--- + net/mac80211/tx.c | 7 +++++++ + 1 file changed, 7 insertions(+) + +diff --git a/net/mac80211/tx.c b/net/mac80211/tx.c +index d50969e6..794f0d83 100644 +--- a/net/mac80211/tx.c ++++ b/net/mac80211/tx.c +@@ -1247,6 +1247,13 @@ ieee80211_tx_prepare(struct ieee80211_sub_if_data *sdata, + if (!tx->sta && !is_multicast_ether_addr(hdr->addr1)) { + tx->sta = sta_info_get(sdata, hdr->addr1); + aggr_check = true; ++ ++ if (!tx->sta) { ++ tx->sta = sta_info_get_bss(sdata, hdr->addr1); ++ if (!tx->sta || !tx->sta->sdata->wdev.use_4addr || ++ tx->sta->sdata->vif.type != NL80211_IFTYPE_AP_VLAN) ++ tx->sta = NULL; ++ } + } + } + +-- +2.45.2 + diff --git a/package/kernel/mac80211/patches/subsys/395-mtk-mac80211-fix-ieee80211_ht_cap_ie_to_sta_ht_cap-w.patch b/package/kernel/mac80211/patches/subsys/395-mtk-mac80211-fix-ieee80211_ht_cap_ie_to_sta_ht_cap-w.patch new file mode 100644 index 00000000000000..8cb9bd532244c2 --- /dev/null +++ b/package/kernel/mac80211/patches/subsys/395-mtk-mac80211-fix-ieee80211_ht_cap_ie_to_sta_ht_cap-w.patch @@ -0,0 +1,33 @@ +From 18fa782bec4763b92e68a69a0445859feb405287 Mon Sep 17 00:00:00 2001 +From: StanleyYP Wang +Date: Thu, 15 Feb 2024 14:30:02 +0800 +Subject: [PATCH 049/137] mtk: mac80211: fix ieee80211_ht_cap_ie_to_sta_ht_cap + warn on + +Fix ieee80211_ht_cap_ie_to_sta_ht_cap warning. +For MLD with a 2/5G primary link, auth/assoc is done in the 2G or 5G link. +Therefore, 6G link will enter ieee80211_ht_cap_ie_to_sta_ht_cap, as elems->ht_cap_elem of 2/5G is NOT NULL. +This should be avoided; otherwise, if 6G is bw 320, then the warning will be triggered. + +Signed-off-by: StanleyYP Wang +--- + net/mac80211/mlme.c | 3 ++- + 1 file changed, 2 insertions(+), 1 deletion(-) + +diff --git a/net/mac80211/mlme.c b/net/mac80211/mlme.c +index 29ece163..5c169411 100644 +--- a/net/mac80211/mlme.c ++++ b/net/mac80211/mlme.c +@@ -5526,7 +5526,8 @@ static bool ieee80211_assoc_config_link(struct ieee80211_link_data *link, + sband = local->hw.wiphy->bands[link->conf->chanreq.oper.chan->band]; + + /* Set up internal HT/VHT capabilities */ +- if (elems->ht_cap_elem && link->u.mgd.conn.mode >= IEEE80211_CONN_MODE_HT) ++ if (elems->ht_cap_elem && link->u.mgd.conn.mode >= IEEE80211_CONN_MODE_HT && ++ !is_6ghz) + ieee80211_ht_cap_ie_to_sta_ht_cap(sdata, sband, + elems->ht_cap_elem, + link_sta); +-- +2.45.2 + diff --git a/package/kernel/mac80211/patches/subsys/396-mtk-mac80211-fix-incorrect-VIF-assignment-for-IEEE-8.patch b/package/kernel/mac80211/patches/subsys/396-mtk-mac80211-fix-incorrect-VIF-assignment-for-IEEE-8.patch new file mode 100644 index 00000000000000..d628e73233d93f --- /dev/null +++ b/package/kernel/mac80211/patches/subsys/396-mtk-mac80211-fix-incorrect-VIF-assignment-for-IEEE-8.patch @@ -0,0 +1,36 @@ +From 71e895f36259440a412548dac7378de77277d4d0 Mon Sep 17 00:00:00 2001 +From: Benjamin Lin +Date: Fri, 12 Jul 2024 15:44:03 +0800 +Subject: [PATCH 067/137] mtk: mac80211: fix incorrect VIF assignment for IEEE + 802.11 fragments + +In WDS mode, first fragment is obtained from ieee80211_local->fq, so its VIF is later changed from AP_VLAN to AP VIF. +On the other hand, subsequent fragments are obtained from txq_info->frags, so its VIF remains AP_VLAN. +Inconsistency in VIFs of fragments results in transmission failure. +Therefore, VIF assignment for non-first fragments is added. + +Signed-off-by: Benjamin Lin +--- + net/mac80211/tx.c | 5 ++++- + 1 file changed, 4 insertions(+), 1 deletion(-) + +diff --git a/net/mac80211/tx.c b/net/mac80211/tx.c +index 688e448b..65a73221 100644 +--- a/net/mac80211/tx.c ++++ b/net/mac80211/tx.c +@@ -3857,8 +3857,11 @@ begin: + skb = __skb_dequeue(&txqi->frags); + if (unlikely(skb)) { + if (!(IEEE80211_SKB_CB(skb)->control.flags & +- IEEE80211_TX_INTCFL_NEED_TXPROCESSING)) ++ IEEE80211_TX_INTCFL_NEED_TXPROCESSING)) { ++ // TODO: report airtime of non-first fragments. ++ IEEE80211_SKB_CB(skb)->control.vif = vif; + goto out; ++ } + IEEE80211_SKB_CB(skb)->control.flags &= + ~IEEE80211_TX_INTCFL_NEED_TXPROCESSING; + } else { +-- +2.45.2 + diff --git a/package/kernel/mac80211/patches/subsys/397-mtk-mac80211-Fix-channel-switch-punct-bitmap-would-b.patch b/package/kernel/mac80211/patches/subsys/397-mtk-mac80211-Fix-channel-switch-punct-bitmap-would-b.patch new file mode 100644 index 00000000000000..329a53e53438b9 --- /dev/null +++ b/package/kernel/mac80211/patches/subsys/397-mtk-mac80211-Fix-channel-switch-punct-bitmap-would-b.patch @@ -0,0 +1,46 @@ +From e40b712abc05146cca3a1c3461085b2531838e85 Mon Sep 17 00:00:00 2001 +From: Allen Ye +Date: Wed, 10 Jul 2024 17:45:05 +0800 +Subject: [PATCH 069/137] mtk: mac80211: Fix channel switch punct bitmap would + be clean bug in STA mode + +The patch add back the puncture bitmap when mac80211 create a new channel +definition in ieee80211_chandef_ht_oper for legacy cap check. + +Signed-off-by: Allen Ye +--- + net/mac80211/spectmgmt.c | 4 ++++ + 1 file changed, 4 insertions(+) + +diff --git a/net/mac80211/spectmgmt.c b/net/mac80211/spectmgmt.c +index 7422888d..1fab5017 100644 +--- a/net/mac80211/spectmgmt.c ++++ b/net/mac80211/spectmgmt.c +@@ -76,6 +76,7 @@ validate_chandef_by_ht_vht_oper(struct ieee80211_sub_if_data *sdata, + struct cfg80211_chan_def *chandef) + { + u32 control_freq, center_freq1, center_freq2; ++ u16 punct_bitmap; + enum nl80211_chan_width chan_width; + struct ieee80211_ht_operation ht_oper; + struct ieee80211_vht_operation vht_oper; +@@ -90,6 +91,7 @@ validate_chandef_by_ht_vht_oper(struct ieee80211_sub_if_data *sdata, + center_freq1 = chandef->center_freq1; + center_freq2 = chandef->center_freq2; + chan_width = chandef->width; ++ punct_bitmap = chandef->punctured; + + ht_oper.primary_chan = ieee80211_frequency_to_channel(control_freq); + if (control_freq != center_freq1) +@@ -101,6 +103,8 @@ validate_chandef_by_ht_vht_oper(struct ieee80211_sub_if_data *sdata, + + ieee80211_chandef_ht_oper(&ht_oper, chandef); + ++ chandef->punctured = punct_bitmap; ++ + if (conn->mode < IEEE80211_CONN_MODE_VHT) + return; + +-- +2.45.2 + diff --git a/package/kernel/mac80211/patches/subsys/398-mtk-mac80211-fix-ieee80211_mgmt_tx.patch b/package/kernel/mac80211/patches/subsys/398-mtk-mac80211-fix-ieee80211_mgmt_tx.patch new file mode 100644 index 00000000000000..36a19e08fa117e --- /dev/null +++ b/package/kernel/mac80211/patches/subsys/398-mtk-mac80211-fix-ieee80211_mgmt_tx.patch @@ -0,0 +1,107 @@ +From ccf0fc9e2efebdacccd153558d1642650dea6832 Mon Sep 17 00:00:00 2001 +From: Howard Hsu +Date: Thu, 15 Aug 2024 14:06:20 +0800 +Subject: [PATCH 074/137] mtk: mac80211: fix ieee80211_mgmt_tx + +Without this commit, sending an action frame will unexpectedly trigger +remain on channel. When determining which link to send the management +frame to, compare the SA of the management frame with the MLD address +first. If the SA of the management frame is not the MLD address, then +proceed to determine which link address to use. + +Signed-off-by: Howard Hsu +--- + net/mac80211/offchannel.c | 68 ++++++++++++++++++++++++--------------- + 1 file changed, 42 insertions(+), 26 deletions(-) + +diff --git a/net/mac80211/offchannel.c b/net/mac80211/offchannel.c +index bcf6c6c6..6937a94f 100644 +--- a/net/mac80211/offchannel.c ++++ b/net/mac80211/offchannel.c +@@ -916,41 +916,57 @@ int ieee80211_mgmt_tx(struct wiphy *wiphy, struct wireless_dev *wdev, + } else if (!need_offchan && !(ieee80211_vif_is_mld(&sdata->vif) && + is_multicast_ether_addr(mgmt->da))) { + struct ieee80211_chanctx_conf *chanctx_conf = NULL; +- int i; ++ struct ieee80211_bss_conf *conf; ++ unsigned int link; + + rcu_read_lock(); +- /* Check all the links first */ +- for (i = 0; i < ARRAY_SIZE(sdata->vif.link_conf); i++) { +- struct ieee80211_bss_conf *conf; + +- conf = rcu_dereference(sdata->vif.link_conf[i]); +- if (!conf) +- continue; ++ if (ieee80211_vif_is_mld(&sdata->vif) && mlo_sta && ++ ether_addr_equal(sdata->vif.addr, mgmt->sa)) { ++ unsigned long links = sdata->vif.active_links; + +- chanctx_conf = rcu_dereference(conf->chanctx_conf); +- if (!chanctx_conf) +- continue; ++ for_each_set_bit(link, &links, IEEE80211_MLD_MAX_NUM_LINKS) { ++ conf = rcu_dereference(sdata->vif.link_conf[link]); ++ if (!conf) ++ continue; + +- if (mlo_sta && params->chan == chanctx_conf->def.chan && +- ether_addr_equal(sdata->vif.addr, mgmt->sa)) { +- link_id = i; +- break; +- } ++ chanctx_conf = rcu_dereference(conf->chanctx_conf); ++ if (!chanctx_conf) ++ continue; + +- if (ether_addr_equal(conf->addr, mgmt->sa)) { +- /* If userspace requested Tx on a specific link +- * use the same link id if the link bss is matching +- * the requested chan. +- */ +- if (sdata->vif.valid_links && +- params->link_id >= 0 && params->link_id == i && +- params->chan == chanctx_conf->def.chan) +- link_id = i; ++ if (params->chan == chanctx_conf->def.chan) { ++ link_id = link; ++ break; ++ } + +- break; ++ chanctx_conf = NULL; + } ++ } else { ++ for (link = 0; link < ARRAY_SIZE(sdata->vif.link_conf); link++) { ++ conf = rcu_dereference(sdata->vif.link_conf[link]); ++ if (!conf) ++ continue; ++ ++ chanctx_conf = rcu_dereference(conf->chanctx_conf); ++ if (!chanctx_conf) ++ continue; ++ ++ if (ether_addr_equal(conf->addr, mgmt->sa)) { ++ /* If userspace requested Tx on a specific link ++ * use the same link id if the link bss is matching ++ * the requested chan. ++ */ ++ if (sdata->vif.valid_links && ++ params->link_id >= 0 && ++ params->link_id == link && ++ params->chan == chanctx_conf->def.chan) ++ link_id = link; ++ ++ break; ++ } + +- chanctx_conf = NULL; ++ chanctx_conf = NULL; ++ } + } + + if (chanctx_conf) { +-- +2.45.2 + diff --git a/package/kernel/mac80211/patches/subsys/399-mtk-mac80211-Fix-CAC-event-call-trace.patch b/package/kernel/mac80211/patches/subsys/399-mtk-mac80211-Fix-CAC-event-call-trace.patch new file mode 100644 index 00000000000000..97a6bc39e413a1 --- /dev/null +++ b/package/kernel/mac80211/patches/subsys/399-mtk-mac80211-Fix-CAC-event-call-trace.patch @@ -0,0 +1,46 @@ +From b2182f40c934dab42ae7936dc1fb5dac52924a87 Mon Sep 17 00:00:00 2001 +From: StanleyYP Wang +Date: Wed, 30 Oct 2024 10:49:44 +0800 +Subject: [PATCH 080/137] mtk: mac80211: Fix CAC event call trace + +A CAC abort event call trace will occur when deleting the interface +during bootup CAC. +In cfg80211_remove_link, cfg80211_stop_ap will be called to stop the AP, +then the CAC link will be cleared in wdev->valid_links, +and finally ieee80211_link_stop will be called to free the +link-related data. +For cases of deleting the interface during 'runtime' CAC, the CAC abort +event will be handled in cfg80211_stop_ap. +However, for bootup CAC, since the beacon of the link has not been set yet, cfg80211_stop_ap will directly return. +Therefore, the CAC abort event should be handled in ieee80211_link_stop. +When the CAC abort event is handled in ieee80211_link_stop, since the +CAC link is already cleared in wdev->valid_links, the valid_link check +in cfg80211_cac_event will not pass, resulting in a WARN ON and leaving +the cac_started flag set. +To prevent this situation, this patch allows bypassing the valid_link +check for CAC abortion. +The following code flow of cfg80211_cac_event for CAC abortion works +fine when the wdev->valid_link of the CAC abort link truly does not exist. + +Signed-off-by: StanleyYP Wang +--- + net/wireless/mlme.c | 3 ++- + 1 file changed, 2 insertions(+), 1 deletion(-) + +diff --git a/net/wireless/mlme.c b/net/wireless/mlme.c +index e336dd00..83a30cf9 100644 +--- a/net/wireless/mlme.c ++++ b/net/wireless/mlme.c +@@ -1138,7 +1138,8 @@ void cfg80211_cac_event(struct net_device *netdev, + struct cfg80211_registered_device *rdev = wiphy_to_rdev(wiphy); + unsigned long timeout; + +- if (WARN_ON(wdev->valid_links && ++ if (WARN_ON(event != NL80211_RADAR_CAC_ABORTED && ++ wdev->valid_links && + !(wdev->valid_links & BIT(link_id)))) + return; + +-- +2.45.2 + diff --git a/package/kernel/mac80211/patches/subsys/400-mtk-mac80211-fix-non-TX-BSS-checking-in-ieee802_11_p.patch b/package/kernel/mac80211/patches/subsys/400-mtk-mac80211-fix-non-TX-BSS-checking-in-ieee802_11_p.patch new file mode 100644 index 00000000000000..c1001757874dd8 --- /dev/null +++ b/package/kernel/mac80211/patches/subsys/400-mtk-mac80211-fix-non-TX-BSS-checking-in-ieee802_11_p.patch @@ -0,0 +1,27 @@ +From 19cf1bbd3c1d207f89e9e6bcdb770406b1b3153f Mon Sep 17 00:00:00 2001 +From: Shayne Chen +Date: Wed, 16 Apr 2025 16:47:12 +0800 +Subject: [PATCH 091/137] mtk: mac80211: fix non-TX BSS checking in + ieee802_11_parse_elems_full() + +Signed-off-by: Shayne Chen +--- + net/mac80211/parse.c | 2 +- + 1 file changed, 1 insertion(+), 1 deletion(-) + +diff --git a/net/mac80211/parse.c b/net/mac80211/parse.c +index 8260f6bd..59451764 100644 +--- a/net/mac80211/parse.c ++++ b/net/mac80211/parse.c +@@ -1072,7 +1072,7 @@ ieee802_11_parse_elems_full(struct ieee80211_elems_parse_params *params) + * In any case, we only look for one at a time, as encoded by + * the WARN_ON above. + */ +- if (params->bss) { ++ if (params->bss && params->bss->transmitted_bss) { + int nontx_len = + ieee802_11_find_bssid_profile(params->start, + params->len, +-- +2.45.2 + diff --git a/package/kernel/mac80211/patches/subsys/401-mtk-mac80211-fixup-sanity-check-in-ieee80211_build_h.patch b/package/kernel/mac80211/patches/subsys/401-mtk-mac80211-fixup-sanity-check-in-ieee80211_build_h.patch new file mode 100644 index 00000000000000..4d06101458e76b --- /dev/null +++ b/package/kernel/mac80211/patches/subsys/401-mtk-mac80211-fixup-sanity-check-in-ieee80211_build_h.patch @@ -0,0 +1,46 @@ +From c66a89c6c7820be363d534f61270b41e35a75cac Mon Sep 17 00:00:00 2001 +From: Michael-CY Lee +Date: Tue, 25 Mar 2025 16:09:24 +0800 +Subject: [PATCH 092/137] mtk: mac80211: fixup sanity check in + ieee80211_build_hdr() + +This might be a typo from upstream. + +There were two conditions in the if-clause: +1. pre_conf_link_id != link_id +2. link_id != IEEE80211_LINK_UNSPECIFIED +and if the two conditions are true at the same time, the frame is +dropped. + +The link_id is originally set to pre_conf_link_id and might be changed +to sta->deflink.link_id if sdata is an AP interface. In other word, the +only case that link_id is not equal to pre_conf_link is link_id becomes +sta->deflink.link_id, which is nerver IEEE80211_LINK_UNSPECIFIED. +In sammary, if the condition 1 is satisfied, the condition 2 must also +be satisfied and therefore becomes redundant. + +The condition 2 might be a check on pre_conf_link_id not being +IEEE80211_LINK_UNSPECIFIED to prevent dropping the frame when then +link id is not specified originally. + +Signed-off-by: Michael-CY Lee +--- + net/mac80211/tx.c | 2 +- + 1 file changed, 1 insertion(+), 1 deletion(-) + +diff --git a/net/mac80211/tx.c b/net/mac80211/tx.c +index 65a73221..ac1bf1ca 100644 +--- a/net/mac80211/tx.c ++++ b/net/mac80211/tx.c +@@ -2994,7 +2994,7 @@ static struct sk_buff *ieee80211_build_hdr(struct ieee80211_sub_if_data *sdata, + IEEE80211_TX_CTRL_MLO_LINK); + + if (pre_conf_link_id != link_id && +- link_id != IEEE80211_LINK_UNSPECIFIED) { ++ pre_conf_link_id != IEEE80211_LINK_UNSPECIFIED) { + #ifdef CPTCFG_MAC80211_VERBOSE_DEBUG + net_info_ratelimited("%s: dropped frame to %pM with bad link ID request (%d vs. %d)\n", + sdata->name, hdr.addr1, +-- +2.45.2 + diff --git a/package/kernel/mac80211/patches/subsys/402-mtk-mac80211-fix-radio-idx-missing-after-replace-ctx.patch b/package/kernel/mac80211/patches/subsys/402-mtk-mac80211-fix-radio-idx-missing-after-replace-ctx.patch new file mode 100644 index 00000000000000..86490f61ba2261 --- /dev/null +++ b/package/kernel/mac80211/patches/subsys/402-mtk-mac80211-fix-radio-idx-missing-after-replace-ctx.patch @@ -0,0 +1,69 @@ +From da9ff958d35a11476500cf60ac71c404d690727b Mon Sep 17 00:00:00 2001 +From: StanleyYP Wang +Date: Fri, 25 Apr 2025 15:32:05 +0800 +Subject: [PATCH 093/137] mtk: mac80211: fix radio idx missing after replace + ctx + +The radio idx of the new ctx would become -1 after channel switch. +This leads to using ieee80211_link_use_reserved_reassign for the next +channel switch while the chanctx list is actually full, especially for +mt7992 and mt7990, so ieee80211_vif_use_reserved_switch should be used instead. + +Signed-off-by: StanleyYP Wang +--- + net/mac80211/chan.c | 21 ++++++++++++++++++--- + 1 file changed, 18 insertions(+), 3 deletions(-) + +diff --git a/net/mac80211/chan.c b/net/mac80211/chan.c +index 9af49683..edeb88e5 100644 +--- a/net/mac80211/chan.c ++++ b/net/mac80211/chan.c +@@ -1268,11 +1268,13 @@ static struct ieee80211_chanctx * + ieee80211_replace_chanctx(struct ieee80211_local *local, + const struct ieee80211_chan_req *chanreq, + enum ieee80211_chanctx_mode mode, +- struct ieee80211_chanctx *curr_ctx) ++ struct ieee80211_chanctx *curr_ctx, ++ u32 radio_mask) + { + struct ieee80211_chanctx *new_ctx, *ctx; + struct wiphy *wiphy = local->hw.wiphy; + const struct wiphy_radio *radio; ++ int i, radio_idx = -1; + + if (!curr_ctx || + curr_ctx->replace_state == IEEE80211_CHANCTX_WILL_BE_REPLACED || +@@ -1322,7 +1324,19 @@ ieee80211_replace_chanctx(struct ieee80211_local *local, + ieee80211_chanctx_num_reserved(local, curr_ctx) != 0) + return ERR_PTR(-EBUSY); + +- new_ctx = ieee80211_alloc_chanctx(local, chanreq, mode, -1); ++ for (i = 0; i < wiphy->n_radio; i++) { ++ if (!(radio_mask & BIT(i))) ++ continue; ++ ++ radio = &wiphy->radio[i]; ++ if (!cfg80211_radio_chandef_valid(radio, &chanreq->oper)) ++ continue; ++ ++ radio_idx = i; ++ break; ++ } ++ ++ new_ctx = ieee80211_alloc_chanctx(local, chanreq, mode, radio_idx); + if (!new_ctx) + return ERR_PTR(-ENOMEM); + +@@ -1394,7 +1408,8 @@ int ieee80211_link_reserve_chanctx(struct ieee80211_link_data *link, + false, radio_idx); + else + new_ctx = ieee80211_replace_chanctx(local, chanreq, +- mode, curr_ctx); ++ mode, curr_ctx, ++ sdata->wdev.radio_mask); + if (IS_ERR(new_ctx)) + return PTR_ERR(new_ctx); + } +-- +2.45.2 + diff --git a/package/kernel/mac80211/patches/subsys/403-mtk-mac80211-fix-addba-buffer-size.patch b/package/kernel/mac80211/patches/subsys/403-mtk-mac80211-fix-addba-buffer-size.patch new file mode 100644 index 00000000000000..ea1c6a6224960e --- /dev/null +++ b/package/kernel/mac80211/patches/subsys/403-mtk-mac80211-fix-addba-buffer-size.patch @@ -0,0 +1,31 @@ +From b83a2412cb367af30d63a7a0a28e1790de967609 Mon Sep 17 00:00:00 2001 +From: Peter Chiu +Date: Fri, 9 May 2025 17:35:43 +0800 +Subject: [PATCH 096/137] mtk: mac80211: fix addba buffer size + +When a HE station connect to an EHT MLD ap, the sta->sta.valid_links +would not be zero and ap would send ba request that HE station cannot +understand. +Use sta->sta.mlo to check if station is using mlo. + +Signed-off-by: Peter Chiu +--- + net/mac80211/agg-tx.c | 2 +- + 1 file changed, 1 insertion(+), 1 deletion(-) + +diff --git a/net/mac80211/agg-tx.c b/net/mac80211/agg-tx.c +index 9592f681..ab7c388a 100644 +--- a/net/mac80211/agg-tx.c ++++ b/net/mac80211/agg-tx.c +@@ -473,7 +473,7 @@ static void ieee80211_send_addba_with_timeout(struct sta_info *sta, + sta->ampdu_mlme.addba_req_num[tid]++; + spin_unlock_bh(&sta->lock); + +- if (sta->sta.valid_links || ++ if (sta->sta.mlo || + sta->sta.deflink.eht_cap.has_eht || + ieee80211_hw_check(&local->hw, STRICT)) { + buf_size = local->hw.max_tx_aggregation_subframes; +-- +2.45.2 + diff --git a/package/kernel/mac80211/patches/subsys/404-mtk-mac80211-fix-kernel-crash-during-reboot-cfg80211.patch b/package/kernel/mac80211/patches/subsys/404-mtk-mac80211-fix-kernel-crash-during-reboot-cfg80211.patch new file mode 100644 index 00000000000000..bd8dc4529398eb --- /dev/null +++ b/package/kernel/mac80211/patches/subsys/404-mtk-mac80211-fix-kernel-crash-during-reboot-cfg80211.patch @@ -0,0 +1,95 @@ +From 74a4029d3e01160059f6b3276b9060a11974dc8b Mon Sep 17 00:00:00 2001 +From: StanleyYP Wang +Date: Tue, 24 Jun 2025 12:06:51 +0800 +Subject: [PATCH 100/137] mtk: mac80211: fix kernel crash during reboot & + cfg80211_autodisconnect_wk + +When cfg80211_autodisconnect_wk is executed during the reboot process, a race +condition might occur, causing the link->conf to be NULL in +__ieee80211_sta_cap_rx_bw + + Unable to handle kernel read from unreadable memory at virtual address 0000000000000088 + Mem abort info: + ESR = 0x0000000096000005 + EC = 0x25: DABT (current EL), IL = 32 bits + SET = 0, FnV = 0 + EA = 0, S1PTW = 0 + FSC = 0x05: level 1 translation fault + Data abort info: + ISV = 0, ISS = 0x00000005, ISS2 = 0x00000000 + CM = 0, WnR = 0, TnD = 0, TagAccess = 0 + GCS = 0, Overlay = 0, DirtyBit = 0, Xs = 0 + user pgtable: 4k pages, 39-bit VAs, pgdp=0000000042059000 + [0000000000000088] pgd=0000000000000000, p4d=0000000000000000, pud=0000000000000000 + Internal error: Oops: 0000000096000005 [#1] SMP + Modules linked in: pppoe ppp_async nft_fib_inet l2tp_ppp pptp pppox ppp_mppe ppp_generic nft_reject_ipv6 nft_reject_ipv4 nft_reject_inet nft_reject nft_redir nft_quota nft_numgen nft_nat nft_masq nft_log nft_limit nft_hash nft_fib_ipv6 nft_fib_ipv4 nft_fib nft_ct nft_chain_nat nf_flow_table_netlink nft_flow_offload nf_flow_table_inet nf_tables mt7996e(O) mt76_connac_lib(O) mt76(O) mac80211(O) iptable_nat ipt_REJECT cfg80211(O) xt_time xt_tcpudp xt_tcpmss xt_statistic xt_state xt_recent xt_quota xt_pkttype xt_owner xt_nat xt_multiport xt_mark xt_mac xt_limit xt_length xt_hl xt_helper xt_ecn xt_dscp xt_conntrack xt_connmark xt_connlimit xt_connbytes xt_comment xt_cgroup xt_addrtype xt_TCPMSS xt_REDIRECT xt_MASQUERADE xt_LOG xt_HL xt_FLOWOFFLOAD xt_DSCP xt_CLASSIFY ums_usbat ums_sddr55 ums_sddr09 ums_karma ums_jumpshot ums_isd200 ums_freecom ums_datafab ums_cypress ums_alauda spidev slhc sfp rtc_pcf8563 nfnetlink nf_reject_ipv4 nf_nat nf_log_syslog nf_conncount mdio_i2c libcrc32c iptable_mangle iptable_filter + ipt_ECN ip_tables compat(O) br_netfilter at24 mt76qos(O) crypto_safexcel ntfs3 sg pwm_fan i2c_gpio i2c_algo_bit i2c_mux_pca954x i2c_mux ip6table_mangle ip6table_filter ip6_tables ip6t_REJECT x_tables nf_reject_ipv6 nls_ucs2_utils cifs_arc4 asn1_decoder nfsd msdos ip_gre gre l2tp_netlink l2tp_core udp_tunnel ip6_udp_tunnel ip_tunnel rpcsec_gss_krb5 auth_rpcgss oid_registry lockd sunrpc grace autofs4 nls_utf8 nls_iso8859_1 nls_cp437 marvell10g marvell crypto_user algif_skcipher algif_rng algif_hash algif_aead af_alg sha512_arm64 sha1_ce sha1_generic seqiv md5 geniv des_generic libdes cts cbc authencesn authenc arc4 uas usb_storage leds_gpio xhci_plat_hcd xhci_pci xhci_mtk_hcd xhci_hcd gpio_button_hotplug(O) vfat fat exfat usbcore usb_common aquantia an8801 [last unloaded: ksmbd] + CPU: 2 PID: 250 Comm: kworker/2:2 Tainted: G O 6.6.92 #0 + Hardware name: MediaTek MT7987A RFB (DT) + Workqueue: events cfg80211_autodisconnect_wk [cfg80211] + pstate: 60400005 (nZCv daif +PAN -UAO -TCO -DIT -SSBS BTYPE=--) + pc : __ieee80211_sta_cap_rx_bw+0xb4/0xcc [mac80211] + lr : _ieee80211_sta_cap_rx_bw+0x10/0x24 [mac80211] + sp : ffffffc0817838e0 + x29: ffffffc0817838e0 x28: 0000000000000000 x27: 0000000000000000 + x26: ffffff8006c7e678 x25: 0000000000000000 x24: ffffff8010951ad0 + x23: ffffff8010950900 x22: ffffff800a378600 x21: ffffff8010950900 + x20: ffffff8006c7e000 x19: ffffff8010951a58 x18: 0000000000000468 + x17: 662030206b6e696c x16: 2065766f6d657220 x15: ffffffc081783af0 + x14: ffffff8010835c18 x13: 0000000000000468 x12: 00000000ffffffea + x11: 00000000ffffefff x10: ffffffc080e1d890 x9 : ffffffc080dc5838 + x8 : ffffffc081783b70 x7 : 0000000000000000 x6 : 0000000000000000 + x5 : 0000000000000fff x4 : ffffff8006c7e678 x3 : 00000000000015f0 + x2 : ffffff8006c7eb60 x1 : ffffff8010844900 x0 : 0000000000000000 + Call trace: + __ieee80211_sta_cap_rx_bw+0xb4/0xcc [mac80211] + _ieee80211_recalc_chanctx_min_def+0x1a8/0x3ec [mac80211] + ieee80211_recalc_chanctx_min_def+0x1c/0x6c [mac80211] + _ieee80211_change_chanctx+0x2cc/0x394 [mac80211] + ieee80211_recalc_chanctx_chantype+0x180/0x1dc [mac80211] + ieee80211_assign_link_chanctx+0x27c/0x4b4 [mac80211] + __ieee80211_link_release_channel+0x60/0x164 [mac80211] + ieee80211_link_release_channel+0x1c/0x28 [mac80211] + ieee80211_stop_ap+0x1d4/0x378 [mac80211] + ___cfg80211_stop_ap+0xf4/0x334 [cfg80211] + cfg80211_stop_ap+0x54/0xd4 [cfg80211] + cfg80211_autodisconnect_wk+0xc0/0x128 [cfg80211] + process_one_work+0x17c/0x3bc + worker_thread+0x2e8/0x4d0 + kthread+0xd8/0xdc + ret_from_fork+0x10/0x20 + Code: d282be03 8b030000 8b000020 f9400800 (f9404400) + ---[ end trace 0000000000000000 ]--- + pstore: backend (ramoops) writing error (-28) + Kernel panic - not syncing: Oops: Fatal exception + SMP: stopping secondary CPUs + Kernel Offset: disabled + CPU features: 0x0,00000000,00000000,1000400b + Memory Limit: none + Starting Memory dump SMCC + Memory dump SMCC failed + Rebooting in 3 seconds.. + +Signed-off-by: StanleyYP Wang +--- + net/mac80211/vht.c | 4 ++++ + 1 file changed, 4 insertions(+) + +diff --git a/net/mac80211/vht.c b/net/mac80211/vht.c +index a3464c49..befc6f10 100644 +--- a/net/mac80211/vht.c ++++ b/net/mac80211/vht.c +@@ -372,6 +372,10 @@ __ieee80211_sta_cap_rx_bw(struct link_sta_info *link_sta, + + rcu_read_lock(); + link_conf = rcu_dereference(sdata->vif.link_conf[link_id]); ++ if (unlikely(!link_conf)) { ++ rcu_read_unlock(); ++ return IEEE80211_STA_RX_BW_20; ++ } + band = link_conf->chanreq.oper.chan->band; + rcu_read_unlock(); + } +-- +2.45.2 + diff --git a/package/kernel/mac80211/patches/subsys/405-mtk-mac80211-handle-csa-finalize-process-for-non-tx-.patch b/package/kernel/mac80211/patches/subsys/405-mtk-mac80211-handle-csa-finalize-process-for-non-tx-.patch new file mode 100644 index 00000000000000..3eae1c2011dda3 --- /dev/null +++ b/package/kernel/mac80211/patches/subsys/405-mtk-mac80211-handle-csa-finalize-process-for-non-tx-.patch @@ -0,0 +1,47 @@ +From 0a11ae852172f583c61f50e826087a1bdc4f8fc2 Mon Sep 17 00:00:00 2001 +From: StanleyYP Wang +Date: Thu, 26 Jun 2025 10:52:23 +0800 +Subject: [PATCH 101/137] mtk: mac80211: handle csa finalize process for non-tx + bss + +In our design, during a channel switch, the partner links of the affiliated MLD +AP for the channel switching link should also perform CSA finalization. + +Signed-off-by: StanleyYP Wang +--- + net/mac80211/cfg.c | 13 ++++++++++--- + 1 file changed, 10 insertions(+), 3 deletions(-) + +diff --git a/net/mac80211/cfg.c b/net/mac80211/cfg.c +index d86d1d3b..666db3a4 100644 +--- a/net/mac80211/cfg.c ++++ b/net/mac80211/cfg.c +@@ -4061,15 +4061,22 @@ void ieee80211_csa_finish(struct ieee80211_vif *vif, unsigned int link_id) + * interfaces when channel switch is received on + * transmitting interface + */ +- struct ieee80211_link_data *iter; ++ struct ieee80211_link_data *iter, *iter_tmp; ++ unsigned int link; + + for_each_sdata_link_rcu(local, iter) { + if (iter->sdata == sdata || + rcu_access_pointer(iter->conf->tx_bss_conf) != tx_bss_conf) + continue; + +- wiphy_work_queue(iter->sdata->local->hw.wiphy, +- &iter->csa.finalize_work); ++ /* For non-tx bss, finalize the csa process for both ++ * its partner links and itself. ++ */ ++ for_each_valid_link(&iter->sdata->wdev, link) { ++ iter_tmp = rcu_dereference(iter->sdata->link[link]); ++ wiphy_work_queue(iter->sdata->local->hw.wiphy, ++ &iter_tmp->csa.finalize_work); ++ } + } + } + +-- +2.45.2 + diff --git a/package/kernel/mac80211/patches/subsys/406-wifi-mac80211-do-not-update-channel-if-csa-ie-exists.patch b/package/kernel/mac80211/patches/subsys/406-wifi-mac80211-do-not-update-channel-if-csa-ie-exists.patch new file mode 100644 index 00000000000000..5e07b093bdb93e --- /dev/null +++ b/package/kernel/mac80211/patches/subsys/406-wifi-mac80211-do-not-update-channel-if-csa-ie-exists.patch @@ -0,0 +1,32 @@ +From fd78898a15a776dbcee104cabf3111a777da1901 Mon Sep 17 00:00:00 2001 +From: Peter Chiu +Date: Mon, 30 Jun 2025 13:55:14 +0800 +Subject: [PATCH 103/137] wifi: mac80211: do not update channel if csa ie + exists + +Station may receive beacon ie with old channel after channel switch. +Do not update channel when CSA ie exists to prevent station +switch back to old channel and leads to disconnection. + +Signed-off-by: Peter Chiu +--- + net/mac80211/mlme.c | 3 ++- + 1 file changed, 2 insertions(+), 1 deletion(-) + +diff --git a/net/mac80211/mlme.c b/net/mac80211/mlme.c +index c1eefae5..b9e8e2f9 100644 +--- a/net/mac80211/mlme.c ++++ b/net/mac80211/mlme.c +@@ -7918,7 +7918,8 @@ static void ieee80211_rx_mgmt_beacon(struct ieee80211_link_data *link, + + changed |= ieee80211_recalc_twt_req(sdata, sband, link, link_sta, elems); + +- if (ieee80211_config_bw(link, elems, true, &changed, ++ if (!elems->ch_switch_ie && ++ ieee80211_config_bw(link, elems, true, &changed, + IEEE80211_STYPE_BEACON)) { + ieee80211_set_disassoc(sdata, IEEE80211_STYPE_DEAUTH, + WLAN_REASON_DEAUTH_LEAVING, +-- +2.45.2 + diff --git a/package/kernel/mac80211/patches/subsys/407-mtk-mac80211-fix-direct-csa-issues.patch b/package/kernel/mac80211/patches/subsys/407-mtk-mac80211-fix-direct-csa-issues.patch new file mode 100644 index 00000000000000..dbff2aad8d580e --- /dev/null +++ b/package/kernel/mac80211/patches/subsys/407-mtk-mac80211-fix-direct-csa-issues.patch @@ -0,0 +1,68 @@ +From 27c1c2bb44e18f2b34bf1814467ac287a3a403b4 Mon Sep 17 00:00:00 2001 +From: StanleyYP Wang +Date: Fri, 4 Jul 2025 15:36:36 +0800 +Subject: [PATCH 107/137] mtk: mac80211: fix direct csa issues + +When csa count is set to 0, then AP will trigger the channel switch +directly, which might cause some issues. +1. For mbss case, the csa finalize process should wait until all the +interface are ready to use reserved chanctx. Otherwise, the channel +switch will fail. +2. For mld, the partner links of the affiliated mld of the channel +swicth link should also perform csa finalize in order to update the +RNR IE in their beacons. + +Signed-off-by: StanleyYP Wang +--- + net/mac80211/cfg.c | 35 +++++++++++++++++++++++++++++++++-- + 1 file changed, 33 insertions(+), 2 deletions(-) + +diff --git a/net/mac80211/cfg.c b/net/mac80211/cfg.c +index f733e379..88f9da95 100644 +--- a/net/mac80211/cfg.c ++++ b/net/mac80211/cfg.c +@@ -4535,8 +4535,39 @@ __ieee80211_channel_switch(struct wiphy *wiphy, struct net_device *dev, + ieee80211_link_info_change_notify(sdata, link_data, changed); + drv_channel_switch_beacon(sdata, &link_data->csa.chanreq.oper); + } else { +- /* if the beacon didn't change, we can finalize immediately */ +- ieee80211_csa_finalize(link_data); ++ struct ieee80211_link_data *link; ++ int n_assigned = 0, n_reserved = 0; ++ unsigned int link_id; ++ ++ /* if the beacon didn't change, we can finalize after ++ * all assigned links are ready. ++ */ ++ for_each_sdata_link(local, link) { ++ if (rcu_access_pointer(link->conf->chanctx_conf) == &chanctx->conf) ++ n_assigned++; ++ if (link->reserved_chanctx == chanctx) ++ n_reserved++; ++ } ++ ++ /* Wait for all interfaces to be ready */ ++ if (n_assigned != n_reserved) { ++ link_data->reserved_ready = true; ++ err = 0; ++ goto out; ++ } ++ ++ for_each_sdata_link(local, link) { ++ struct ieee80211_link_data *tmp; ++ ++ for_each_valid_link(&link->sdata->wdev, link_id) { ++ tmp = sdata_dereference(link->sdata->link[link_id], ++ link->sdata); ++ if (!tmp) ++ continue; ++ wiphy_work_queue(link->sdata->local->hw.wiphy, ++ &tmp->csa.finalize_work); ++ } ++ } + } + + out: +-- +2.45.2 + diff --git a/package/kernel/mac80211/patches/subsys/408-mtk-cfg80211-fix-crash-in-cfg80211_sta_set_mld_sinfo.patch b/package/kernel/mac80211/patches/subsys/408-mtk-cfg80211-fix-crash-in-cfg80211_sta_set_mld_sinfo.patch new file mode 100644 index 00000000000000..4df6a6d538eeaf --- /dev/null +++ b/package/kernel/mac80211/patches/subsys/408-mtk-cfg80211-fix-crash-in-cfg80211_sta_set_mld_sinfo.patch @@ -0,0 +1,26 @@ +From 621a2d729f12399eb1a2aa8b8b02651b4139b943 Mon Sep 17 00:00:00 2001 +From: Shayne Chen +Date: Wed, 16 Jul 2025 20:58:16 +0800 +Subject: [PATCH 109/137] mtk: cfg80211: fix crash in + cfg80211_sta_set_mld_sinfo() + +--- + net/wireless/nl80211.c | 2 +- + 1 file changed, 1 insertion(+), 1 deletion(-) + +diff --git a/net/wireless/nl80211.c b/net/wireless/nl80211.c +index 29d8b48a..8fd78f35 100644 +--- a/net/wireless/nl80211.c ++++ b/net/wireless/nl80211.c +@@ -7966,7 +7966,7 @@ static void cfg80211_sta_set_mld_sinfo(struct station_info *sinfo) + init++; + + /* pertid stats accumulate for rx/tx fields */ +- if (sinfo->pertid) { ++ if (sinfo->pertid && link_sinfo->pertid) { + sinfo->pertid->rx_msdu += + link_sinfo->pertid->rx_msdu; + sinfo->pertid->tx_msdu += +-- +2.45.2 + diff --git a/package/kernel/mac80211/patches/subsys/409-mtk-mac80211-fix-AQL-for-BMC-packets-in-MLD-interfac.patch b/package/kernel/mac80211/patches/subsys/409-mtk-mac80211-fix-AQL-for-BMC-packets-in-MLD-interfac.patch new file mode 100644 index 00000000000000..d7529bb91bd304 --- /dev/null +++ b/package/kernel/mac80211/patches/subsys/409-mtk-mac80211-fix-AQL-for-BMC-packets-in-MLD-interfac.patch @@ -0,0 +1,64 @@ +From 54a414f331da33fcc7a684c2fb796733c6336793 Mon Sep 17 00:00:00 2001 +From: Peter Chiu +Date: Thu, 24 Jul 2025 15:06:18 +0800 +Subject: [PATCH 110/137] mtk: mac80211: fix AQL for BMC packets in MLD + interface + +The vif->bss_conf is only used by legacy interface. +Choose a link instead of using vif->bss_conf directly. + +Signed-off-by: Peter Chiu +--- + net/mac80211/airtime.c | 22 ++++++++++++++++++---- + 1 file changed, 18 insertions(+), 4 deletions(-) + +diff --git a/net/mac80211/airtime.c b/net/mac80211/airtime.c +index c61df637..f9cad05b 100644 +--- a/net/mac80211/airtime.c ++++ b/net/mac80211/airtime.c +@@ -759,7 +759,8 @@ u32 ieee80211_calc_expected_tx_airtime(struct ieee80211_hw *hw, + int len, bool ampdu) + { + struct ieee80211_supported_band *sband; +- struct ieee80211_chanctx_conf *conf; ++ struct ieee80211_bss_conf *link_conf = NULL; ++ struct ieee80211_chanctx_conf *conf = NULL; + int rateidx; + bool cck, short_pream; + u32 basic_rates; +@@ -768,7 +769,20 @@ u32 ieee80211_calc_expected_tx_airtime(struct ieee80211_hw *hw, + + len += 38; /* Ethernet header length */ + +- conf = rcu_dereference(vif->bss_conf.chanctx_conf); ++ if (ieee80211_vif_is_mld(vif)) { ++ unsigned int link; ++ ++ for (link = 0; link < IEEE80211_MLD_MAX_NUM_LINKS ; link++) { ++ link_conf = rcu_dereference(vif->link_conf[link]); ++ if (link_conf) ++ break; ++ } ++ } else ++ link_conf = &vif->bss_conf; ++ ++ if (link_conf) ++ conf = rcu_dereference(link_conf->chanctx_conf); ++ + if (conf) + band = conf->def.chan->band; + +@@ -826,8 +840,8 @@ u32 ieee80211_calc_expected_tx_airtime(struct ieee80211_hw *hw, + */ + sband = hw->wiphy->bands[band]; + +- basic_rates = vif->bss_conf.basic_rates; +- short_pream = vif->bss_conf.use_short_preamble; ++ basic_rates = link_conf->basic_rates; ++ short_pream = link_conf->use_short_preamble; + + rateidx = basic_rates ? ffs(basic_rates) - 1 : 0; + rate = sband->bitrates[rateidx].bitrate; +-- +2.45.2 + diff --git a/package/kernel/mac80211/patches/subsys/410-mtk-mac80211-prevent-RTS-from-being-unexpectedly-set.patch b/package/kernel/mac80211/patches/subsys/410-mtk-mac80211-prevent-RTS-from-being-unexpectedly-set.patch new file mode 100644 index 00000000000000..a483ef798dd367 --- /dev/null +++ b/package/kernel/mac80211/patches/subsys/410-mtk-mac80211-prevent-RTS-from-being-unexpectedly-set.patch @@ -0,0 +1,61 @@ +From f83d1f785a49af2295afc1372dc01d4818f77cf6 Mon Sep 17 00:00:00 2001 +From: Michael-CY Lee +Date: Tue, 3 Mar 2026 14:33:56 +0800 +Subject: [PATCH 128/137] mtk: mac80211: prevent RTS from being unexpectedly + set to 0 + +The RTS value will be set to 0 if NL80211_ATTR_WIPHY_RADIO_INDEX is +included but NL80211_ATTR_WIPHY_RTS_THRESHOLD is not included in the +function nl80211_set_wiphy(). + +Refactor the function nl80211_set_wiphy_radio() to follow the format in +nl80211_set_wiphy() and fix the above problem. + +Fixes: 264637941cf4 ("wifi: cfg80211: Add Support to Set RTS Threshold for each Radio") +Signed-off-by: Michael-CY Lee + +Fix set txpower failed. + +Signed-off-by: Allen Ye +--- + net/wireless/nl80211.c | 15 +++++++++------ + 1 file changed, 9 insertions(+), 6 deletions(-) + +diff --git a/net/wireless/nl80211.c b/net/wireless/nl80211.c +index fc3b18a5..d711a963 100644 +--- a/net/wireless/nl80211.c ++++ b/net/wireless/nl80211.c +@@ -3878,7 +3878,7 @@ static int nl80211_set_wiphy_radio(struct genl_info *info, + { + const struct wiphy_radio *radio; + u32 rts_threshold = 0, old_rts, changed = 0; +- int result; ++ int result = 0; + + if (!rdev->ops->set_wiphy_params) + return -EOPNOTSUPP; +@@ -3894,13 +3894,16 @@ static int nl80211_set_wiphy_radio(struct genl_info *info, + changed |= WIPHY_PARAM_RTS_THRESHOLD; + } + +- old_rts = rdev->wiphy.radio_cfg[radio_idx].rts_threshold; ++ if (changed) { ++ old_rts = rdev->wiphy.radio_cfg[radio_idx].rts_threshold; ++ if (changed & WIPHY_PARAM_RTS_THRESHOLD) ++ rdev->wiphy.radio_cfg[radio_idx].rts_threshold = rts_threshold; + +- rdev->wiphy.radio_cfg[radio_idx].rts_threshold = rts_threshold; ++ result = rdev_set_wiphy_params(rdev, radio_idx, changed); + +- result = rdev_set_wiphy_params(rdev, radio_idx, changed); +- if (result) +- rdev->wiphy.radio_cfg[radio_idx].rts_threshold = old_rts; ++ if (result) ++ rdev->wiphy.radio_cfg[radio_idx].rts_threshold = old_rts; ++ } + + if (info->attrs[NL80211_ATTR_WIPHY_ANTENNA_TX] && + info->attrs[NL80211_ATTR_WIPHY_ANTENNA_RX]) { +-- +2.45.2 + diff --git a/package/kernel/mt76/Makefile b/package/kernel/mt76/Makefile index d880b9c104220c..0bf31c5c458bf0 100644 --- a/package/kernel/mt76/Makefile +++ b/package/kernel/mt76/Makefile @@ -1,7 +1,7 @@ include $(TOPDIR)/rules.mk PKG_NAME:=mt76 -PKG_RELEASE=1 +PKG_RELEASE=3 PKG_LICENSE:=BSD-3-Clause-Clear PKG_LICENSE_FILES:= diff --git a/package/kernel/mt76/patches/0001-wifi-mt76-mt7996-add-token-and-NPU-debugfs-counters.patch b/package/kernel/mt76/patches/0001-wifi-mt76-mt7996-add-token-and-NPU-debugfs-counters.patch new file mode 100644 index 00000000000000..9e352540ed7b2d --- /dev/null +++ b/package/kernel/mt76/patches/0001-wifi-mt76-mt7996-add-token-and-NPU-debugfs-counters.patch @@ -0,0 +1,100 @@ +From: Ryan Chen +Date: Thu, 06 Mar 2026 21:00:00 -0600 +Subject: [PATCH] wifi: mt76: mt7996: add token and NPU debugfs counters + +Add a read-only "token_info" debugfs file that exposes token pool usage +and NPU queue state for runtime diagnostics. This is computed only on +read and adds no overhead during normal operation. + +The file shows: +- SW token count vs pool size (in-flight DMA buffers) +- HW/SW token range split when NPU offload is active +- NPU RX queue depths (packets received from NPU firmware) +- Per-band TX queue depths with NPU vs DMA path indication + +Useful for diagnosing token pool starvation between NPU-offloaded +bands (2.4GHz/6GHz) and DMA-path bands (5GHz). + +Signed-off-by: Ryan Chen +Index: mt76-2026.03.05~9f95baf9/mt7996/debugfs.c +=================================================================== +--- mt76-2026.03.05~9f95baf9.orig/mt7996/debugfs.c ++++ mt76-2026.03.05~9f95baf9/mt7996/debugfs.c +@@ -851,6 +851,68 @@ mt7996_rf_regval_set(void *data, u64 val + DEFINE_DEBUGFS_ATTRIBUTE(fops_rf_regval, mt7996_rf_regval_get, + mt7996_rf_regval_set, "0x%08llx\n"); + ++static int mt7996_token_info_read(struct seq_file *s, void *data) ++{ ++ struct mt7996_dev *dev = dev_get_drvdata(s->private); ++ struct mt76_dev *mdev = &dev->mt76; ++ bool npu_active = mt76_npu_device_active(mdev); ++ int i; ++ ++ seq_puts(s, "=== Token Pool ===\n"); ++ ++ spin_lock_bh(&mdev->token_lock); ++ seq_printf(s, " token_count: %u / %u (SW in-flight / pool size)\n", ++ mdev->token_count, mdev->token_size); ++ seq_printf(s, " token_start: %u\n", mdev->token_start); ++ seq_printf(s, " wed_token_count: %u\n", mdev->wed_token_count); ++ spin_unlock_bh(&mdev->token_lock); ++ ++ spin_lock_bh(&mdev->rx_token_lock); ++ seq_printf(s, " rx_token_size: %u\n", mdev->rx_token_size); ++ spin_unlock_bh(&mdev->rx_token_lock); ++ ++ if (npu_active) { ++ seq_printf(s, "\n=== NPU Offload ===\n"); ++ seq_printf(s, " status: active\n"); ++ seq_printf(s, " hw_token_range: 0 - %u (NPU firmware)\n", ++ mdev->token_start - 1); ++ seq_printf(s, " sw_token_range: %u - %u (host driver)\n", ++ mdev->token_start, ++ mdev->token_start + mdev->token_size - 1); ++ } ++ ++ seq_printf(s, "\n=== NPU Queues ===\n"); ++ seq_puts(s, " queue | ndesc | queued | head | tail |\n"); ++ for (i = MT_RXQ_NPU0; i <= MT_RXQ_NPU1; i++) { ++ struct mt76_queue *q = &mdev->q_rx[i]; ++ ++ if (!q->ndesc) ++ continue; ++ ++ seq_printf(s, " npu_rx%d | %6d | %6d | %6d | %6d |\n", ++ i - MT_RXQ_NPU0, q->ndesc, q->queued, ++ q->head, q->tail); ++ } ++ ++ seq_printf(s, "\n=== TX Queues (per-band) ===\n"); ++ seq_puts(s, " band | type | ndesc | queued | head | tail |\n"); ++ for (i = 0; i < __MT_MAX_BAND; i++) { ++ struct mt76_phy *phy = mdev->phys[i]; ++ struct mt76_queue *q; ++ const char *type; ++ ++ if (!phy || !phy->q_tx[0]) ++ continue; ++ ++ q = phy->q_tx[0]; ++ type = mt76_queue_is_npu_tx(q) ? "npu" : "dma"; ++ seq_printf(s, " band %d | %6s | %6d | %6d | %6d | %6d |\n", ++ i, type, q->ndesc, q->queued, q->head, q->tail); ++ } ++ ++ return 0; ++} ++ + int mt7996_init_debugfs(struct mt7996_dev *dev) + { + struct dentry *dir; +@@ -883,6 +945,8 @@ int mt7996_init_debugfs(struct mt7996_de + &fops_radar_trigger); + debugfs_create_devm_seqfile(dev->mt76.dev, "rdd_monitor", dir, + mt7996_rdd_monitor); ++ debugfs_create_devm_seqfile(dev->mt76.dev, "token_info", dir, ++ mt7996_token_info_read); + + dev->debugfs_dir = dir; + diff --git a/package/kernel/mt76/patches/0003-wifi-mt76-mt7996-replace-direct-WTBL-access-with-MCU-for-station-statistics.patch b/package/kernel/mt76/patches/0003-wifi-mt76-mt7996-replace-direct-WTBL-access-with-MCU-for-station-statistics.patch new file mode 100644 index 00000000000000..bd5d62417ce5cd --- /dev/null +++ b/package/kernel/mt76/patches/0003-wifi-mt76-mt7996-replace-direct-WTBL-access-with-MCU-for-station-statistics.patch @@ -0,0 +1,571 @@ +From patchwork Tue Apr 7 10:45:55 2026 +Content-Type: text/plain; charset="utf-8" +MIME-Version: 1.0 +Content-Transfer-Encoding: 7bit +X-Patchwork-Submitter: Joshua Klinesmith +X-Patchwork-Id: 14514179 +X-Patchwork-Delegate: nbd@nbd.name +Received: from mail-yw1-f182.google.com (mail-yw1-f182.google.com + [209.85.128.182]) + (using TLSv1.2 with cipher ECDHE-RSA-AES128-GCM-SHA256 (128/128 bits)) + (No client certificate requested) + by smtp.subspace.kernel.org (Postfix) with ESMTPS id 3DED839D6DA + for ; Tue, 7 Apr 2026 10:46:00 +0000 (UTC) +Authentication-Results: smtp.subspace.kernel.org; + arc=none smtp.client-ip=209.85.128.182 +ARC-Seal: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; + t=1775558762; cv=none; + b=bax3pIGGlllC5hHoQWgv1ndeCO6KVUE7ufBMFeoBeBo0qvlOWii2Y244NpL5X8vAjwkVx3F6Te2di9vK3mkRc3otrEyLAhF30PbciJYkKhK4R/SiKEGVs/zPReaUmHMtGGjC9XvOHi4h0i7/pc/0UR1fitc/n52xcSzjAaQngUE= +ARC-Message-Signature: i=1; a=rsa-sha256; d=subspace.kernel.org; + s=arc-20240116; t=1775558762; c=relaxed/simple; + bh=KzCWgqU+IhleKRUt2GYZfDqGAIlgsdtbDD9bYFUtl2Y=; + h=From:To:Cc:Subject:Date:Message-ID:MIME-Version; + b=sr7dadTE23qzY0u2Lv1W6J1EHl3DB6QaqCkrRMZHVl3WOg2EUtdoMw9lZRfZtPjfx3Kf/JRk8sEs1hti4c0zcohmJ/pp8vPLX43Vn3gW5qq0gQbRVTHHUko4w2pErMnEUn3PFwVq+oK6+/qeXIviWRfgBcJVUQvMGUgAwvGAwjI= +ARC-Authentication-Results: i=1; smtp.subspace.kernel.org; + dmarc=pass (p=none dis=none) header.from=gmail.com; + spf=pass smtp.mailfrom=gmail.com; + dkim=pass (2048-bit key) header.d=gmail.com header.i=@gmail.com + header.b=cz2WTd2Y; arc=none smtp.client-ip=209.85.128.182 +Authentication-Results: smtp.subspace.kernel.org; + dmarc=pass (p=none dis=none) header.from=gmail.com +Authentication-Results: smtp.subspace.kernel.org; + spf=pass smtp.mailfrom=gmail.com +Authentication-Results: smtp.subspace.kernel.org; + dkim=pass (2048-bit key) header.d=gmail.com header.i=@gmail.com + header.b="cz2WTd2Y" +Received: by mail-yw1-f182.google.com with SMTP id + 00721157ae682-79ea87af213so85006647b3.0 + for ; + Tue, 07 Apr 2026 03:46:00 -0700 (PDT) +DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; + d=gmail.com; s=20251104; t=1775558759; x=1776163559; + darn=vger.kernel.org; + h=content-transfer-encoding:mime-version:message-id:date:subject:cc + :to:from:from:to:cc:subject:date:message-id:reply-to; + bh=TFu7/65ERhIDpydkOjgi0fOTCwEEMyQDActZ2NxpxtA=; + b=cz2WTd2Y9ElFnmjR+NfbTNIY703aa3ho8qHl1z1Fg6RjMUFfXYRO6erbMOH3KFOrm7 + bKd56LnwdO86nXmmqGBZTJOh0hlYjSo1za20X2AJ4gw3zZk7ag9ky1V8BCw8sQM1a/NM + hdgYcJqvfe0IgUrzI2c/MDc9wKW6475XCZFvlddahOWmDoCXU7NV0xl2F3f+Z6aMhbyX + ttyr/H/TM0L/MUJSbJ5lR39yQh7NIqya/6gXoNsAkSgj7wGS4tKDTMraN4UwJYgnkcTA + PhgewlaxDemmuOqgYD3VkPcD8SRROu9kVNfDWn6kRd5bIxulcoHuP5uQCawuq+zyMhOk + u2Yw== +X-Google-DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; + d=1e100.net; s=20251104; t=1775558759; x=1776163559; + h=content-transfer-encoding:mime-version:message-id:date:subject:cc + :to:from:x-gm-gg:x-gm-message-state:from:to:cc:subject:date + :message-id:reply-to; + bh=TFu7/65ERhIDpydkOjgi0fOTCwEEMyQDActZ2NxpxtA=; + b=qzHblcFygl5g5n552FOH2gkoW7XqgeG4Ukso7KoF4uXXK5J55wBs/vDFycQCny5CZD + HmyEob9bm8RdYmOBXe/g7yzzENZ8j3NX1fG1MgArZ8aEdtg2YYxq87JzZ8V4xNecU+dm + KKsKtwgX9UrU+R3Am5N/BDtBVNx3E37cS4838Rz3+GVBCx3+VVdrdkUF/kMT/YJfpXPl + b/hRznSY0xGX5X/lTx79BNcGrU/RG7MPd8/talUc64YiliEPOguiA9HDgr2sCKV3eu+0 + Aq3X8BirucjMhPuTHRLxlILqpQym8/Z19j9grctBs+x5YzMueGvkYT4bbh0Gx5M+R7aV + Jzlw== +X-Gm-Message-State: AOJu0YzPTaWuaU+JskBjhVb6S6OJBeSYrATxPNr0yIpz0jRt3hRmJP2r + +krH1OfMkxQcMU1DV/Pv+T57A7+JDYUYcHzbryWM0SFFdKyVRSv4UmQvlKDTnPeK +X-Gm-Gg: AeBDietFVj2BZ7qTDRtymZU7BDMLsUJJr/zQykupDDacMUqfFUrYz2cCpMBZBtFTbu4 + peX2CmNOiocj1HTtX1u1NurOprs0nUAiOvmW5+BWI3C/PifV83rGtDqMPz0eEqvT85LNc4U23F6 + 1gJleOS/pjZFDjjg/XAhqC9dDqz5tcyUSPHE1NK2Z9Qe5OJXCe4X5wz1JH6ECku5JJCiQzAm3mS + jQX5XBSFOeDCr1aBPP7CMiy/XT7zzEcmytNiMmAy2NgAVNSkdsotcdKHhcsifXz/Vdnp+zSEOLs + ZHVMgaNpqF67b29jDgX3VCOs+VOs2ip331im4UldsnOVXAdziezlHtxZ2NFYqd3Z5AeQ+IRIAyd + S2Na+Q3WGIkJ3Ndnj4dQ5wbDXZCdp4YNiWVOCkYgCd2jgLBZ2ElheDE1113qujzCK+UiNUrEaK4 + TdXx+LULzcN2bIvLM1ddSHbl+tfbJymHYUWYxIKQ2zMvYL0ts97pk549ZNN7YSqqYt05UNhwY= +X-Received: by 2002:a05:690c:3684:b0:79e:c253:f723 with SMTP id + 00721157ae682-7a3bee2ddecmr146542007b3.34.1775558758737; + Tue, 07 Apr 2026 03:45:58 -0700 (PDT) +Received: from DEV.lan (c-75-74-152-49.hsd1.fl.comcast.net. [75.74.152.49]) + by smtp.gmail.com with ESMTPSA id + 00721157ae682-7a3711eebfesm68209177b3.43.2026.04.07.03.45.56 + (version=TLS1_3 cipher=TLS_AES_256_GCM_SHA384 bits=256/256); + Tue, 07 Apr 2026 03:45:58 -0700 (PDT) +From: Joshua Klinesmith +To: linux-wireless@vger.kernel.org +Cc: nbd@nbd.name, + lorenzo@kernel.org, + ryder.lee@mediatek.com, + shayne.chen@mediatek.com, + sean.wang@mediatek.com, + Joshua Klinesmith +Subject: [PATCH wireless v3] wifi: mt76: mt7996: replace direct WTBL access + with MCU for station statistics +Date: Tue, 7 Apr 2026 06:45:55 -0400 +Message-ID: <20260407104555.4342-1-joshuaklinesmith@gmail.com> +X-Mailer: git-send-email 2.43.0 +Precedence: bulk +X-Mailing-List: linux-wireless@vger.kernel.org +List-Id: +List-Subscribe: +List-Unsubscribe: +MIME-Version: 1.0 + +Direct MMIO access to WTBL entries for airtime and RSSI statistics in +mt7996_mac_sta_poll() races with firmware, causing warnings at +mt7996_mac_wtbl_lmac_addr, MCU message timeouts, and firmware +communication breakdown. The function was called from +mt7996_mac_tx_free() on every TX-Free-Done event, compounding the +issue with heavy CPU overhead. + +Replace the direct WTBL polling with firmware MCU queries: +- Airtime: UNI_ALL_STA_TXRX_AIR_TIME via the existing all_sta_info + MCU command, with a new handler in mt7996_mcu_rx_all_sta_info_event() +- RSSI: UNI_PER_STA_RSSI via new mt7996_mcu_get_per_sta_info() using + MCU_WM_UNI_CMD(PER_STA_INFO) + +Both queries run from mt7996_mac_work() every 5th tick under dev mutex, +matching the pattern already used for TX rate, admission stats, and +MSDU count reporting. + +Remove mt7996_mac_sta_poll() and its airtime_ac tracking array entirely, +along with the now-orphaned mt76_wcid_add_poll() producers in the RX +status, TX free, and TX status paths that fed the removed consumer. + +The per-sta-info response handler validates the firmware-returned entry +count against what was requested and against the skb payload length +before walking the response array, and uses bounds-checked +mt76_wcid_ptr() for WLAN ID lookup. RSSI polling is batched in groups +of PER_STA_INFO_MAX_NUM to cover the full WTBL capacity. + +Vendor driver analysis (mt_wifi.ko from Xiaomi AX3000T MT7981 firmware) +confirms the RCPI-to-RSSI conversion formula (rcpi - 220) / 2 and that +the vendor never performs direct WTBL reads for statistics. + +Fixes: 98686cd21624 ("wifi: mt76: mt7996: add driver for MediaTek Wi-Fi 7 (802.11be) devices") +Link: https://github.com/openwrt/openwrt/issues/21177 +Signed-off-by: Joshua Klinesmith +--- + .../mt76_connac_mcu.h | 7 + + .../mt7996/mac.c | 144 +----------------- + .../mt7996/mcu.c | 133 ++++++++++++++++ + .../mt7996/mcu.h | 25 +++ + .../mt7996/mt7996.h | 2 +- + 5 files changed, 168 insertions(+), 143 deletions(-) + +diff --git a/mt76_connac_mcu.h b/mt76_connac_mcu.h +index 8d59cf43f0e2..14d3ee7defa1 100644 +--- a/mt76_connac_mcu.h ++++ b/mt76_connac_mcu.h +@@ -1392,6 +1392,13 @@ enum { + UNI_OFFLOAD_OFFLOAD_BMC_RPY_DETECT, + }; + ++#define PER_STA_INFO_MAX_NUM 90 ++ ++enum UNI_PER_STA_INFO_TAG { ++ UNI_PER_STA_RSSI, ++ UNI_PER_STA_MAX_NUM ++}; ++ + enum UNI_ALL_STA_INFO_TAG { + UNI_ALL_STA_TXRX_RATE, + UNI_ALL_STA_TX_STAT, +diff --git a/mt7996/mac.c b/mt7996/mac.c +index d4f3ee943b47..ad8377375265 100644 +--- a/mt7996/mac.c ++++ b/mt7996/mac.c +@@ -111,119 +111,6 @@ u32 mt7996_mac_wtbl_lmac_addr(struct mt7996_dev *dev, u16 wcid, u8 dw) + return MT_WTBL_LMAC_OFFS(wcid, dw); + } + +-static void mt7996_mac_sta_poll(struct mt7996_dev *dev) +-{ +- static const u8 ac_to_tid[] = { +- [IEEE80211_AC_BE] = 0, +- [IEEE80211_AC_BK] = 1, +- [IEEE80211_AC_VI] = 4, +- [IEEE80211_AC_VO] = 6 +- }; +- struct mt7996_sta_link *msta_link; +- struct mt76_vif_link *mlink; +- struct ieee80211_sta *sta; +- struct mt7996_sta *msta; +- u32 tx_time[IEEE80211_NUM_ACS], rx_time[IEEE80211_NUM_ACS]; +- LIST_HEAD(sta_poll_list); +- struct mt76_wcid *wcid; +- int i; +- +- spin_lock_bh(&dev->mt76.sta_poll_lock); +- list_splice_init(&dev->mt76.sta_poll_list, &sta_poll_list); +- spin_unlock_bh(&dev->mt76.sta_poll_lock); +- +- rcu_read_lock(); +- +- while (true) { +- bool clear = false; +- u32 addr, val; +- u16 idx; +- s8 rssi[4]; +- +- spin_lock_bh(&dev->mt76.sta_poll_lock); +- if (list_empty(&sta_poll_list)) { +- spin_unlock_bh(&dev->mt76.sta_poll_lock); +- break; +- } +- msta_link = list_first_entry(&sta_poll_list, +- struct mt7996_sta_link, +- wcid.poll_list); +- msta = msta_link->sta; +- wcid = &msta_link->wcid; +- list_del_init(&wcid->poll_list); +- spin_unlock_bh(&dev->mt76.sta_poll_lock); +- +- idx = wcid->idx; +- +- /* refresh peer's airtime reporting */ +- addr = mt7996_mac_wtbl_lmac_addr(dev, idx, 20); +- +- for (i = 0; i < IEEE80211_NUM_ACS; i++) { +- u32 tx_last = msta_link->airtime_ac[i]; +- u32 rx_last = msta_link->airtime_ac[i + 4]; +- +- msta_link->airtime_ac[i] = mt76_rr(dev, addr); +- msta_link->airtime_ac[i + 4] = mt76_rr(dev, addr + 4); +- +- tx_time[i] = msta_link->airtime_ac[i] - tx_last; +- rx_time[i] = msta_link->airtime_ac[i + 4] - rx_last; +- +- if ((tx_last | rx_last) & BIT(30)) +- clear = true; +- +- addr += 8; +- } +- +- if (clear) { +- mt7996_mac_wtbl_update(dev, idx, +- MT_WTBL_UPDATE_ADM_COUNT_CLEAR); +- memset(msta_link->airtime_ac, 0, +- sizeof(msta_link->airtime_ac)); +- } +- +- if (!wcid->sta) +- continue; +- +- sta = container_of((void *)msta, struct ieee80211_sta, +- drv_priv); +- for (i = 0; i < IEEE80211_NUM_ACS; i++) { +- u8 q = mt76_connac_lmac_mapping(i); +- u32 tx_cur = tx_time[q]; +- u32 rx_cur = rx_time[q]; +- u8 tid = ac_to_tid[i]; +- +- if (!tx_cur && !rx_cur) +- continue; +- +- ieee80211_sta_register_airtime(sta, tid, tx_cur, rx_cur); +- } +- +- /* get signal strength of resp frames (CTS/BA/ACK) */ +- addr = mt7996_mac_wtbl_lmac_addr(dev, idx, 34); +- val = mt76_rr(dev, addr); +- +- rssi[0] = to_rssi(GENMASK(7, 0), val); +- rssi[1] = to_rssi(GENMASK(15, 8), val); +- rssi[2] = to_rssi(GENMASK(23, 16), val); +- rssi[3] = to_rssi(GENMASK(31, 14), val); +- +- mlink = rcu_dereference(msta->vif->mt76.link[wcid->link_id]); +- if (mlink) { +- struct mt76_phy *mphy = mt76_vif_link_phy(mlink); +- +- if (mphy) +- msta_link->ack_signal = +- mt76_rx_signal(mphy->antenna_mask, +- rssi); +- } +- +- ewma_avg_signal_add(&msta_link->avg_ack_signal, +- -msta_link->ack_signal); +- } +- +- rcu_read_unlock(); +-} +- + /* The HW does not translate the mac header to 802.3 for mesh point */ + static int mt7996_reverse_frag0_hdr_trans(struct sk_buff *skb, u16 hdr_gap) + { +@@ -508,7 +395,6 @@ mt7996_mac_fill_rx(struct mt7996_dev *dev, enum mt76_rxq_id q, + msta_link = container_of(status->wcid, struct mt7996_sta_link, + wcid); + msta = msta_link->sta; +- mt76_wcid_add_poll(&dev->mt76, &msta_link->wcid); + } + + status->freq = mphy->chandef.chan->center_freq; +@@ -1355,9 +1241,6 @@ mt7996_mac_tx_free(struct mt7996_dev *dev, void *data, int len) + info = le32_to_cpu(*cur_info); + if (info & MT_TXFREE_INFO_PAIR) { + struct ieee80211_sta *sta; +- unsigned long valid_links; +- struct mt7996_sta *msta; +- unsigned int id; + u16 idx; + + idx = FIELD_GET(MT_TXFREE_INFO_WLAN_ID, info); +@@ -1371,22 +1254,6 @@ mt7996_mac_tx_free(struct mt7996_dev *dev, void *data, int len) + link_sta = rcu_dereference(sta->link[wcid->link_id]); + if (!link_sta) + goto next; +- +- msta = (struct mt7996_sta *)sta->drv_priv; +- valid_links = sta->valid_links ?: BIT(0); +- +- /* For MLD STA, add all link's wcid to sta_poll_list */ +- for_each_set_bit(id, &valid_links, +- IEEE80211_MLD_MAX_NUM_LINKS) { +- struct mt7996_sta_link *msta_link; +- +- msta_link = mt7996_sta_link(msta, id); +- if (!msta_link) +- continue; +- +- mt76_wcid_add_poll(&dev->mt76, +- &msta_link->wcid); +- } + next: + /* ver 7 has a new DW with pair = 1, skip it */ + if (ver == 7 && ((void *)(cur_info + 1) < end) && +@@ -1424,8 +1291,6 @@ mt7996_mac_tx_free(struct mt7996_dev *dev, void *data, int len) + } + } + +- mt7996_mac_sta_poll(dev); +- + if (wake) + mt76_set_tx_blocked(&dev->mt76, false); + +@@ -1590,7 +1455,6 @@ mt7996_mac_add_txs_skb(struct mt7996_dev *dev, struct mt76_wcid *wcid, + + static void mt7996_mac_add_txs(struct mt7996_dev *dev, void *data) + { +- struct mt7996_sta_link *msta_link; + struct mt76_wcid *wcid; + __le32 *txs_data = data; + u16 wcidx; +@@ -1610,12 +1474,6 @@ static void mt7996_mac_add_txs(struct mt7996_dev *dev, void *data) + + mt7996_mac_add_txs_skb(dev, wcid, pid, txs_data); + +- if (!wcid->sta) +- goto out; +- +- msta_link = container_of(wcid, struct mt7996_sta_link, wcid); +- mt76_wcid_add_poll(&dev->mt76, &msta_link->wcid); +- + out: + rcu_read_unlock(); + } +@@ -2947,6 +2805,8 @@ void mt7996_mac_work(struct work_struct *work) + mt7996_mac_update_stats(phy); + + mt7996_mcu_get_all_sta_info(phy, UNI_ALL_STA_TXRX_RATE); ++ mt7996_mcu_get_all_sta_info(phy, UNI_ALL_STA_TXRX_AIR_TIME); ++ mt7996_mcu_get_per_sta_info(phy, UNI_PER_STA_RSSI); + if (mtk_wed_device_active(&phy->dev->mt76.mmio.wed)) { + mt7996_mcu_get_all_sta_info(phy, UNI_ALL_STA_TXRX_ADM_STAT); + mt7996_mcu_get_all_sta_info(phy, UNI_ALL_STA_TXRX_MSDU_COUNT); +diff --git a/mt7996/mcu.c b/mt7996/mcu.c +index c0c042de477b..ffe30c914678 100644 +--- a/mt7996/mcu.c ++++ b/mt7996/mcu.c +@@ -616,6 +616,33 @@ mt7996_mcu_rx_all_sta_info_event(struct mt7996_dev *dev, struct sk_buff *skb) + wcid->stats.rx_packets += + le32_to_cpu(res->msdu_cnt[i].rx_msdu_cnt); + break; ++ case UNI_ALL_STA_TXRX_AIR_TIME: { ++ static const u8 ac_to_tid[] = { ++ [IEEE80211_AC_BE] = 0, ++ [IEEE80211_AC_BK] = 1, ++ [IEEE80211_AC_VI] = 4, ++ [IEEE80211_AC_VO] = 6 ++ }; ++ struct ieee80211_sta *sta; ++ ++ wlan_idx = le16_to_cpu(res->airtime[i].wlan_idx); ++ wcid = mt76_wcid_ptr(dev, wlan_idx); ++ sta = wcid_to_sta(wcid); ++ if (!sta) ++ break; ++ ++ for (ac = 0; ac < IEEE80211_NUM_ACS; ac++) { ++ u8 lmac_ac = mt76_connac_lmac_mapping(ac); ++ u32 tx_cur = le32_to_cpu(res->airtime[i].tx[lmac_ac]); ++ u32 rx_cur = le32_to_cpu(res->airtime[i].rx[lmac_ac]); ++ ++ if (!tx_cur && !rx_cur) ++ continue; ++ ++ ieee80211_sta_register_airtime(sta, ac_to_tid[ac], tx_cur, rx_cur); ++ } ++ break; ++ } + default: + break; + } +@@ -4755,6 +4782,112 @@ int mt7996_mcu_get_all_sta_info(struct mt7996_phy *phy, u16 tag) + &req, sizeof(req), false); + } + ++int mt7996_mcu_get_per_sta_info(struct mt7996_phy *phy, u16 tag) ++{ ++ struct mt7996_dev *dev = phy->dev; ++ struct mt7996_mcu_per_sta_info_event *res; ++ struct mt76_wcid *wcid; ++ struct sk_buff *skb; ++ int i, ret, sta_num, resp_sta_num; ++ int wcid_idx = 0; ++ struct { ++ u8 _rsv1; ++ u8 unsolicit; ++ u8 _rsv2[2]; ++ ++ __le16 tag; ++ __le16 len; ++ __le16 sta_num; ++ u8 _rsv3[2]; ++ __le16 wlan_idx[PER_STA_INFO_MAX_NUM]; ++ } __packed req = { ++ .tag = cpu_to_le16(tag), ++ .len = cpu_to_le16(sizeof(req) - 4), ++ }; ++ ++ while (wcid_idx < mt7996_wtbl_size(dev)) { ++ sta_num = 0; ++ ++ rcu_read_lock(); ++ for (i = wcid_idx; ++ i < mt7996_wtbl_size(dev) && sta_num < PER_STA_INFO_MAX_NUM; ++ i++) { ++ wcid = rcu_dereference(dev->mt76.wcid[i]); ++ if (!wcid || !wcid->sta) ++ continue; ++ req.wlan_idx[sta_num++] = cpu_to_le16(i); ++ } ++ rcu_read_unlock(); ++ wcid_idx = i; ++ ++ if (!sta_num) ++ continue; ++ ++ req.sta_num = cpu_to_le16(sta_num); ++ ++ ret = mt76_mcu_send_and_get_msg(&dev->mt76, ++ MCU_WM_UNI_CMD(PER_STA_INFO), ++ &req, sizeof(req), true, &skb); ++ if (ret) ++ return ret; ++ ++ res = (struct mt7996_mcu_per_sta_info_event *)skb->data; ++ ++ resp_sta_num = le16_to_cpu(res->sta_num); ++ if (resp_sta_num > sta_num || ++ skb->len < struct_size(res, rssi, resp_sta_num)) { ++ dev_kfree_skb(skb); ++ return -EINVAL; ++ } ++ ++ rcu_read_lock(); ++ for (i = 0; i < resp_sta_num; i++) { ++ struct mt7996_sta_link *msta_link; ++ struct mt76_vif_data *mvif; ++ struct mt76_vif_link *mlink; ++ struct mt76_phy *mphy; ++ u16 wlan_idx; ++ s8 rssi[4]; ++ ++ switch (tag) { ++ case UNI_PER_STA_RSSI: ++ wlan_idx = le16_to_cpu(res->rssi[i].wlan_idx); ++ wcid = mt76_wcid_ptr(dev, wlan_idx); ++ if (!wcid || !wcid->sta) ++ break; ++ ++ msta_link = container_of(wcid, ++ struct mt7996_sta_link, ++ wcid); ++ ++ rssi[0] = (res->rssi[i].rcpi[0] - 220) / 2; ++ rssi[1] = (res->rssi[i].rcpi[1] - 220) / 2; ++ rssi[2] = (res->rssi[i].rcpi[2] - 220) / 2; ++ rssi[3] = (res->rssi[i].rcpi[3] - 220) / 2; ++ ++ mvif = &msta_link->sta->vif->mt76; ++ mlink = rcu_dereference(mvif->link[wcid->link_id]); ++ if (mlink) { ++ mphy = mt76_vif_link_phy(mlink); ++ if (mphy) ++ msta_link->ack_signal = ++ mt76_rx_signal(mphy->antenna_mask, ++ rssi); ++ } ++ ++ ewma_avg_signal_add(&msta_link->avg_ack_signal, ++ -msta_link->ack_signal); ++ break; ++ } ++ } ++ rcu_read_unlock(); ++ ++ dev_kfree_skb(skb); ++ } ++ ++ return 0; ++} ++ + int mt7996_mcu_wed_rro_reset_sessions(struct mt7996_dev *dev, u16 id) + { + struct { +diff --git a/mt7996/mcu.h b/mt7996/mcu.h +index e0b83ac9f5e2..b5bad9a76c49 100644 +--- a/mt7996/mcu.h ++++ b/mt7996/mcu.h +@@ -220,6 +220,31 @@ struct mt7996_mcu_all_sta_info_event { + __le32 tx_msdu_cnt; + __le32 rx_msdu_cnt; + } __packed, msdu_cnt); ++ ++ DECLARE_FLEX_ARRAY(struct { ++ __le16 wlan_idx; ++ u8 rsv[2]; ++ __le32 tx[IEEE80211_NUM_ACS]; ++ __le32 rx[IEEE80211_NUM_ACS]; ++ } __packed, airtime); ++ } __packed; ++} __packed; ++ ++struct mt7996_mcu_per_sta_info_event { ++ u8 rsv[4]; ++ __le16 tag; ++ __le16 len; ++ u8 more; ++ u8 rsv2; ++ __le16 sta_num; ++ u8 rsv3[4]; ++ ++ union { ++ DECLARE_FLEX_ARRAY(struct { ++ __le16 wlan_idx; ++ u8 rsv[2]; ++ u8 rcpi[4]; ++ } __packed, rssi); + } __packed; + } __packed; + +diff --git a/mt7996/mt7996.h b/mt7996/mt7996.h +index 7a884311800e..b523e971f78c 100644 +--- a/mt7996/mt7996.h ++++ b/mt7996/mt7996.h +@@ -222,7 +222,6 @@ struct mt7996_sta_link { + struct mt7996_sta *sta; + + struct list_head rc_list; +- u32 airtime_ac[8]; + + int ack_signal; + struct ewma_avg_signal avg_ack_signal; +@@ -741,6 +740,7 @@ int mt7996_mcu_trigger_assert(struct mt7996_dev *dev); + void mt7996_mcu_rx_event(struct mt7996_dev *dev, struct sk_buff *skb); + void mt7996_mcu_exit(struct mt7996_dev *dev); + int mt7996_mcu_get_all_sta_info(struct mt7996_phy *phy, u16 tag); ++int mt7996_mcu_get_per_sta_info(struct mt7996_phy *phy, u16 tag); + int mt7996_mcu_wed_rro_reset_sessions(struct mt7996_dev *dev, u16 id); + int mt7996_mcu_set_sniffer_mode(struct mt7996_phy *phy, bool enabled); + diff --git a/package/kernel/mt76/patches/0005-wifi-mt76-mt7996-fix-capability-of-EHT-MCS-15-in-MRU.patch b/package/kernel/mt76/patches/0005-wifi-mt76-mt7996-fix-capability-of-EHT-MCS-15-in-MRU.patch new file mode 100644 index 00000000000000..b9f4b01ee7b14e --- /dev/null +++ b/package/kernel/mt76/patches/0005-wifi-mt76-mt7996-fix-capability-of-EHT-MCS-15-in-MRU.patch @@ -0,0 +1,49 @@ +From: Shayne Chen +Date: Fri, 13 Mar 2026 00:00:00 +0000 +Subject: [PATCH] wifi: mt76: mt7996: fix capability of EHT-MCS 15 in MRU + +According to IEEE Std 802.11be-2024 Table 9-417r: +- If 80 MHz is not supported, bits 1-3 are cleared +- If 160 MHz is not supported, bits 2-3 are cleared +- If 320 MHz is not supported, bit 3 is cleared + +Fixes: 348533eb968d ("wifi: mt76: mt7996: add EHT capability init") +Signed-off-by: Shayne Chen +--- + mt7996/init.c | 16 ++++++++++------ + 1 file changed, 10 insertions(+), 6 deletions(-) + +diff --git a/mt7996/init.c b/mt7996/init.c +index 00a8286bd136..a08bc07a7f93 100644 +--- a/mt7996/init.c ++++ b/mt7996/init.c +@@ -1562,7 +1562,6 @@ mt7996_init_eht_caps(struct mt7996_phy *phy, enum nl80211_band band, + struct ieee80211_sta_eht_cap *eht_cap = &data->eht_cap; + struct ieee80211_eht_cap_elem_fixed *eht_cap_elem = &eht_cap->eht_cap_elem; + struct ieee80211_eht_mcs_nss_supp *eht_nss = &eht_cap->eht_mcs_nss_supp; +- enum nl80211_chan_width width = phy->mt76->chandef.width; + int nss = hweight8(phy->mt76->antenna_mask); + int sts = hweight16(phy->mt76->chainmask); + u8 val; +@@ -1638,11 +1637,16 @@ mt7996_init_eht_caps(struct mt7996_phy *phy, enum nl80211_band band, + u8_encode_bits(u8_get_bits(1, GENMASK(1, 0)), + IEEE80211_EHT_PHY_CAP5_MAX_NUM_SUPP_EHT_LTF_MASK); + +- val = width == NL80211_CHAN_WIDTH_320 ? 0xf : +- width == NL80211_CHAN_WIDTH_160 ? 0x7 : +- width == NL80211_CHAN_WIDTH_80 ? 0x3 : 0x1; +- eht_cap_elem->phy_cap_info[6] = +- u8_encode_bits(val, IEEE80211_EHT_PHY_CAP6_MCS15_SUPP_MASK); ++ eht_cap_elem->phy_cap_info[6] = IEEE80211_EHT_PHY_CAP6_MCS15_SUPP_MASK; ++ if (band != NL80211_BAND_6GHZ) { ++ eht_cap_elem->phy_cap_info[6] &= ++ ~IEEE80211_EHT_PHY_CAP6_MCS15_SUPP_320MHZ; ++ ++ if (band != NL80211_BAND_5GHZ) ++ eht_cap_elem->phy_cap_info[6] &= ++ ~(IEEE80211_EHT_PHY_CAP6_MCS15_SUPP_160MHZ | ++ IEEE80211_EHT_PHY_CAP6_MCS15_SUPP_80MHZ); ++ } + + val = u8_encode_bits(nss, IEEE80211_EHT_MCS_NSS_RX) | + u8_encode_bits(nss, IEEE80211_EHT_MCS_NSS_TX); diff --git a/package/kernel/mt76/patches/0006-wifi-mt76-mt7996-refresh-power-limits-on-txpower-changes.patch b/package/kernel/mt76/patches/0006-wifi-mt76-mt7996-refresh-power-limits-on-txpower-changes.patch new file mode 100644 index 00000000000000..dfb780e6110af2 --- /dev/null +++ b/package/kernel/mt76/patches/0006-wifi-mt76-mt7996-refresh-power-limits-on-txpower-changes.patch @@ -0,0 +1,100 @@ +From d07c2f82155d9c929703346006070fca42103563 Mon Sep 17 00:00:00 2001 +From: Yanghan Ye +Date: Wed, 24 Jun 2026 20:19:12 +0800 +Subject: [PATCH v1 1/2] wifi: mt76: mt7996: refresh power limits on txpower + changes + +mt7996_config() currently ignores mac80211 configuration changes. As a +result, user txpower updates may change the cfg80211 visible power level +without refreshing the firmware SKU table used by mt7996. + +Refresh the SKU table when mac80211 reports power or channel changes. Keep +using the cached per-PHY txpower value because hw->conf.power_level is only +a global compatibility value and may be shared across radios. + +Also refresh the SKU table and channel information for BSS txpower changes, +and report MCU failures instead of silently ignoring them. + +Signed-off-by: Yanghan Ye +--- + mt7996/main.c | 58 ++++++++++++++++++++++++++++++++++++++++++++++++-- + 1 file changed, 56 insertions(+), 2 deletions(-) + +diff --git a/mt7996/main.c b/mt7996/main.c +index 560fe30d..c32e7819 100644 +--- a/mt7996/main.c ++++ b/mt7996/main.c +@@ -736,7 +736,44 @@ static int mt7996_set_key(struct ieee80211_hw *hw, enum set_key_cmd cmd, + + static int mt7996_config(struct ieee80211_hw *hw, int radio_idx, u32 changed) + { +- return 0; ++ struct mt7996_dev *dev = mt7996_hw_dev(hw); ++ struct mt7996_phy *phy; ++ int ret = 0; ++ ++ if (!(changed & (IEEE80211_CONF_CHANGE_POWER | ++ IEEE80211_CONF_CHANGE_CHANNEL))) ++ return 0; ++ ++ mutex_lock(&dev->mt76.mutex); ++ ++ if (radio_idx >= 0) { ++ if (radio_idx >= ARRAY_SIZE(dev->radio_phy)) { ++ ret = -EINVAL; ++ goto out; ++ } ++ ++ phy = dev->radio_phy[radio_idx]; ++ if (phy) { ++ ret = mt7996_mcu_set_txpower_sku(phy); ++ if (!ret && (changed & IEEE80211_CONF_CHANGE_POWER)) ++ ret = mt7996_mcu_set_chan_info(phy, ++ UNI_CHANNEL_SWITCH); ++ } ++ } else { ++ mt7996_for_each_phy(dev, phy) { ++ ret = mt7996_mcu_set_txpower_sku(phy); ++ if (!ret && (changed & IEEE80211_CONF_CHANGE_POWER)) ++ ret = mt7996_mcu_set_chan_info(phy, ++ UNI_CHANNEL_SWITCH); ++ if (ret) ++ break; ++ } ++ } ++ ++out: ++ mutex_unlock(&dev->mt76.mutex); ++ ++ return ret; + } + + static int +@@ -1025,9 +1062,24 @@ mt7996_link_info_changed(struct ieee80211_hw *hw, struct ieee80211_vif *vif, + mt7996_update_mu_group(hw, link, info); + + if (changed & BSS_CHANGED_TXPOWER && ++ info->txpower != INT_MIN && + info->txpower != phy->txpower) { ++ int ret; ++ + phy->txpower = info->txpower; +- mt7996_mcu_set_txpower_sku(phy); ++ ret = mt7996_mcu_set_txpower_sku(phy); ++ if (ret) { ++ dev_err_ratelimited(dev->mt76.dev, ++ "failed to update txpower SKU: %d\n", ++ ret); ++ goto out; ++ } ++ ++ ret = mt7996_mcu_set_chan_info(phy, UNI_CHANNEL_SWITCH); ++ if (ret) ++ dev_err_ratelimited(dev->mt76.dev, ++ "failed to update txpower channel info: %d\n", ++ ret); + } + + out: +-- +2.43.0 diff --git a/package/kernel/mt76/patches/0007-wifi-mt76-mt7996-enable-firmware-txpower-limit-controls.patch b/package/kernel/mt76/patches/0007-wifi-mt76-mt7996-enable-firmware-txpower-limit-controls.patch new file mode 100644 index 00000000000000..a86ae170a90549 --- /dev/null +++ b/package/kernel/mt76/patches/0007-wifi-mt76-mt7996-enable-firmware-txpower-limit-controls.patch @@ -0,0 +1,601 @@ +From b7ba96ac12322a5c413311486902617d9433cf93 Mon Sep 17 00:00:00 2001 +From: Yanghan Ye +Date: Wed, 24 Jun 2026 20:19:12 +0800 +Subject: [PATCH v1 2/2] wifi: mt76: mt7996: enable firmware txpower limit + controls + +mt7996 firmware does not apply uploaded SKU and backoff power tables unless +the corresponding UNI_CMD(TXPOWER) controls are enabled. Enable the SKU and +backoff controls in the normal run path and upload the mt7996 backoff table +using the firmware table layout used by the MediaTek SDK. + +Keep the mt7996 backoff layout private instead of resizing the shared mt76 +power-limit structure used by older connac chipsets. Continue to respect +chan->max_reg_power when initializing channel power. + +Signed-off-by: Yanghan Ye +--- + eeprom.c | 9 ++ + mt7996/init.c | 16 ++- + mt7996/main.c | 10 ++ + mt7996/mcu.c | 365 +++++++++++++++++++++++++++++++++++++++++++++--- + mt7996/mcu.h | 25 ++++ + mt7996/mt7996.h | 6 + + 6 files changed, 411 insertions(+), 20 deletions(-) + +diff --git a/eeprom.c b/eeprom.c +index 28dbda6f..440a89dc 100644 +--- a/eeprom.c ++++ b/eeprom.c +@@ -511,6 +511,15 @@ s8 mt76_get_rate_power_limits(struct mt76_phy *phy, + ARRAY_SIZE(dest->ru), val, len, target_power, + txs_delta, &max_power, n_chains, MT76_SKU_RATE); + ++ val = mt76_get_of_array_s8(np, "rates-eht", &len, ++ ARRAY_SIZE(dest->eht[0]) + 1); ++ mt76_apply_multi_array_limit(dev, dest->eht[0], ARRAY_SIZE(dest->eht[0]), ++ ARRAY_SIZE(dest->eht), val, len, target_power, ++ txs_delta, &max_power, n_chains, MT76_SKU_RATE); ++ ++ if (is_mt799x(dev)) ++ return max_power == -127 ? target_power : max_power; ++ + val = mt76_get_of_array_s8(np, "paths-cck", &len, ARRAY_SIZE(dest->path.cck)); + mt76_apply_array_limit(dev, dest->path.cck, ARRAY_SIZE(dest->path.cck), val, + target_power, txs_delta, &max_power, n_chains, MT76_SKU_BACKOFF); +diff --git a/mt7996/init.c b/mt7996/init.c +index 2bf3ddd8..21c359f5 100644 +--- a/mt7996/init.c ++++ b/mt7996/init.c +@@ -362,15 +362,23 @@ static void __mt7996_init_txpower(struct mt7996_phy *phy, + int i, n_chains = hweight16(phy->mt76->chainmask); + int path_delta = mt76_tx_power_path_delta(n_chains); + int pwr_delta = mt7996_eeprom_get_power_delta(dev, sband->band); +- struct mt76_power_limits limits; ++ struct mt76_power_limits *limits; ++ ++ limits = kzalloc(sizeof(*limits), GFP_KERNEL); ++ if (!limits) ++ return; ++ ++ phy->sku_limit_en = true; + + for (i = 0; i < sband->n_channels; i++) { + struct ieee80211_channel *chan = &sband->channels[i]; + int target_power = mt7996_eeprom_get_target_power(dev, chan); + ++ phy->sku_path_en |= mt7996_has_power_path_limits(phy->mt76, chan); ++ + target_power += pwr_delta; + target_power = mt76_get_rate_power_limits(phy->mt76, chan, +- &limits, ++ limits, + target_power); + target_power += path_delta; + target_power = DIV_ROUND_UP(target_power, 2); +@@ -379,6 +387,8 @@ static void __mt7996_init_txpower(struct mt7996_phy *phy, + phy->txpower = max(phy->txpower, chan->max_power); + chan->orig_mpwr = target_power; + } ++ ++ kfree(limits); + } + + void mt7996_init_txpower(struct mt7996_phy *phy) +@@ -386,6 +396,8 @@ void mt7996_init_txpower(struct mt7996_phy *phy) + if (!phy) + return; + ++ phy->sku_path_en = false; ++ + if (phy->mt76->cap.has_2ghz) + __mt7996_init_txpower(phy, &phy->mt76->sband_2g.sband); + if (phy->mt76->cap.has_5ghz) +diff --git a/mt7996/main.c b/mt7996/main.c +index c32e7819..f3fec548 100644 +--- a/mt7996/main.c ++++ b/mt7996/main.c +@@ -34,6 +34,16 @@ int mt7996_run(struct mt7996_phy *phy) + if (ret) + return ret; + ++ ret = mt7996_mcu_set_tx_power_ctrl(phy, UNI_TXPOWER_SKU_POWER_LIMIT_CTRL, ++ phy->sku_limit_en); ++ if (ret) ++ return ret; ++ ++ ret = mt7996_mcu_set_tx_power_ctrl(phy, UNI_TXPOWER_BACKOFF_POWER_LIMIT_CTRL, ++ phy->sku_path_en); ++ if (ret) ++ return ret; ++ + set_bit(MT76_STATE_RUNNING, &phy->mt76->state); + + ieee80211_queue_delayed_work(dev->mphy.hw, &phy->mt76->mac_work, +diff --git a/mt7996/mcu.c b/mt7996/mcu.c +index c0b9b1bf..bf66f6d2 100644 +--- a/mt7996/mcu.c ++++ b/mt7996/mcu.c +@@ -5,6 +5,7 @@ + + #include + #include ++#include + #include "mt7996.h" + #include "mcu.h" + #include "mac.h" +@@ -108,6 +109,242 @@ static bool sr_scene_detect = true; + module_param(sr_scene_detect, bool, 0644); + MODULE_PARM_DESC(sr_scene_detect, "Enable firmware scene detection algorithm"); + ++#define MT7996_SKU_PATH_CCK_LEN 5 ++#define MT7996_SKU_PATH_OFDM_LEN 5 ++#define MT7996_SKU_PATH_OFDM_BF_LEN 4 ++#define MT7996_SKU_PATH_RU_NUM 16 ++#define MT7996_SKU_PATH_RU_LEN 15 ++#define MT7996_SKU_PATH_RU_GROUPS (MT7996_SKU_PATH_RU_NUM * 2) ++ ++#define MT7996_SKU_RATE_CCK_OFDM_LEN 12 ++#define MT7996_SKU_RATE_HT20_LEN 8 ++#define MT7996_SKU_RATE_HT40_LEN 9 ++#define MT7996_SKU_RATE_VHT_NUM 4 ++#define MT7996_SKU_RATE_VHT_LEN 10 ++#define MT7996_SKU_RATE_VHT_PAD_LEN 2 ++#define MT7996_SKU_RATE_HE_LEN 84 ++#define MT7996_SKU_RATE_EHT_LEN 256 ++ ++/* ++ * MTK SDK / UNI_CMD(TXPOWER) POWER_LIMIT_TABLE rate/backoff layout for mt7996. ++ * Keep the backoff table private because older mt76 chipsets use a smaller ++ * connac2 path ABI. ++ */ ++struct mt7996_power_path_limits { ++ s8 cck[MT7996_SKU_PATH_CCK_LEN]; ++ s8 ofdm[MT7996_SKU_PATH_OFDM_LEN]; ++ s8 ofdm_bf[MT7996_SKU_PATH_OFDM_BF_LEN]; ++ s8 ru[MT7996_SKU_PATH_RU_NUM][MT7996_SKU_PATH_RU_LEN]; ++ s8 ru_bf[MT7996_SKU_PATH_RU_NUM][MT7996_SKU_PATH_RU_LEN]; ++}; ++ ++static const s8 * ++mt7996_of_get_array_s8(struct device_node *np, const char *name, ++ size_t *len, int min) ++{ ++ struct property *prop = of_find_property(np, name, NULL); ++ ++ if (!prop || !prop->value || prop->length < min) ++ return NULL; ++ ++ *len = prop->length; ++ ++ return prop->value; ++} ++ ++static const __be32 * ++mt7996_of_get_array(struct device_node *np, const char *name, ++ size_t *len, int min) ++{ ++ struct property *prop = of_find_property(np, name, NULL); ++ ++ if (!prop || !prop->value || prop->length < min * 4) ++ return NULL; ++ ++ *len = prop->length; ++ ++ return prop->value; ++} ++ ++static s8 mt7996_get_txs_delta(struct device_node *np, u8 nss) ++{ ++ const __be32 *val; ++ size_t len; ++ ++ val = mt7996_of_get_array(np, "txs-delta", &len, nss); ++ if (!val) ++ return 0; ++ ++ return be32_to_cpu(val[nss - 1]); ++} ++ ++static u8 mt7996_backoff_n_chains(u8 idx) ++{ ++ /* 0:1T1ss, 1:2T1ss, ..., 14:5T5ss */ ++ static const u8 connac3_table[] = { ++ 1, 2, 3, 4, 5, 2, 3, 4, 5, 3, 4, 5, 4, 5, 5}; ++ ++ if (idx >= ARRAY_SIZE(connac3_table)) ++ return 0; ++ ++ return connac3_table[idx]; ++} ++ ++static void ++mt7996_apply_path_limit(s8 *pwr, size_t pwr_len, const s8 *data, ++ s8 target_power, s8 nss_delta, int n_chains, ++ bool bf) ++{ ++ int i; ++ ++ if (!data) ++ return; ++ ++ for (i = 0; i < pwr_len; i++) { ++ u8 backoff_chain_idx = i + bf; ++ int backoff_n_chains = mt7996_backoff_n_chains(backoff_chain_idx); ++ s8 backoff_delta = mt76_tx_power_path_delta(backoff_n_chains); ++ s8 delta = mt76_tx_power_path_delta(n_chains); ++ ++ pwr[i] = min_t(s8, target_power + delta - backoff_delta, ++ data[i] + nss_delta); ++ } ++} ++ ++static void ++mt7996_apply_multi_path_limit(s8 *pwr, size_t pwr_len, s8 pwr_num, ++ const s8 *data, size_t len, s8 target_power, ++ s8 nss_delta, int n_chains, bool bf) ++{ ++ int i, cur; ++ ++ if (!data) ++ return; ++ ++ cur = data[0]; ++ for (i = 0; i < pwr_num; i++) { ++ if (len < pwr_len + 1) ++ break; ++ ++ mt7996_apply_path_limit(pwr + pwr_len * i, pwr_len, data + 1, ++ target_power, nss_delta, n_chains, bf); ++ if (--cur > 0) ++ continue; ++ ++ data += pwr_len + 1; ++ len -= pwr_len + 1; ++ if (!len) ++ break; ++ ++ cur = data[0]; ++ } ++} ++ ++static struct device_node * ++mt7996_find_power_limit_channel(struct mt76_phy *mphy, ++ struct ieee80211_channel *chan) ++{ ++ struct device_node *np; ++ char name[16]; ++ char band; ++ ++ np = mt76_find_power_limits_node(mphy->dev); ++ if (!np) ++ return NULL; ++ ++ switch (chan->band) { ++ case NL80211_BAND_2GHZ: ++ band = '2'; ++ break; ++ case NL80211_BAND_5GHZ: ++ band = '5'; ++ break; ++ case NL80211_BAND_6GHZ: ++ band = '6'; ++ break; ++ default: ++ return NULL; ++ } ++ ++ snprintf(name, sizeof(name), "txpower-%cg", band); ++ np = of_get_child_by_name(np, name); ++ if (!np) ++ return NULL; ++ ++ return mt76_find_channel_node(np, chan); ++} ++ ++bool mt7996_has_power_path_limits(struct mt76_phy *mphy, ++ struct ieee80211_channel *chan) ++{ ++ struct device_node *np; ++ ++ if (!IS_ENABLED(CONFIG_OF)) ++ return false; ++ ++ np = mt7996_find_power_limit_channel(mphy, chan); ++ if (!np) ++ return false; ++ ++ return of_find_property(np, "paths-cck", NULL) || ++ of_find_property(np, "paths-ofdm", NULL) || ++ of_find_property(np, "paths-ofdm-bf", NULL) || ++ of_find_property(np, "paths-ru", NULL) || ++ of_find_property(np, "paths-ru-bf", NULL); ++} ++ ++static void ++mt7996_get_power_path_limits(struct mt76_phy *mphy, ++ struct ieee80211_channel *chan, ++ struct mt7996_power_path_limits *dest, ++ s8 target_power) ++{ ++ struct device_node *np; ++ const s8 *val; ++ size_t len; ++ s8 txs_delta; ++ int n_chains = hweight16(mphy->chainmask); ++ ++ memset(dest, 0, sizeof(*dest)); ++ ++ if (!IS_ENABLED(CONFIG_OF)) ++ return; ++ ++ np = mt7996_find_power_limit_channel(mphy, chan); ++ if (!np) ++ return; ++ ++ txs_delta = mt7996_get_txs_delta(np, n_chains); ++ ++ val = mt7996_of_get_array_s8(np, "paths-cck", &len, ++ ARRAY_SIZE(dest->cck)); ++ mt7996_apply_path_limit(dest->cck, ARRAY_SIZE(dest->cck), val, ++ target_power, txs_delta, n_chains, false); ++ ++ val = mt7996_of_get_array_s8(np, "paths-ofdm", &len, ++ ARRAY_SIZE(dest->ofdm)); ++ mt7996_apply_path_limit(dest->ofdm, ARRAY_SIZE(dest->ofdm), val, ++ target_power, txs_delta, n_chains, false); ++ ++ val = mt7996_of_get_array_s8(np, "paths-ofdm-bf", &len, ++ ARRAY_SIZE(dest->ofdm_bf)); ++ mt7996_apply_path_limit(dest->ofdm_bf, ARRAY_SIZE(dest->ofdm_bf), val, ++ target_power, txs_delta, n_chains, true); ++ ++ val = mt7996_of_get_array_s8(np, "paths-ru", &len, ++ ARRAY_SIZE(dest->ru[0]) + 1); ++ mt7996_apply_multi_path_limit(dest->ru[0], ARRAY_SIZE(dest->ru[0]), ++ ARRAY_SIZE(dest->ru), val, len, ++ target_power, txs_delta, n_chains, false); ++ ++ val = mt7996_of_get_array_s8(np, "paths-ru-bf", &len, ++ ARRAY_SIZE(dest->ru_bf[0]) + 1); ++ mt7996_apply_multi_path_limit(dest->ru_bf[0], ++ ARRAY_SIZE(dest->ru_bf[0]), ++ ARRAY_SIZE(dest->ru_bf), val, len, ++ target_power, txs_delta, n_chains, false); ++} ++ + static u8 + mt7996_mcu_get_sta_nss(u16 mcs_map) + { +@@ -5404,9 +5641,46 @@ int mt7996_mcu_set_sniffer_mode(struct mt7996_phy *phy, bool enabled) + sizeof(req), true); + } + ++int mt7996_mcu_set_tx_power_ctrl(struct mt7996_phy *phy, u8 power_ctrl_id, ++ u8 data) ++{ ++ struct mt7996_dev *dev = phy->dev; ++ struct tx_power_ctrl req = { ++ .tag = cpu_to_le16(power_ctrl_id), ++ .len = cpu_to_le16(sizeof(req) - 4), ++ .power_ctrl_id = power_ctrl_id, ++ .band_idx = phy->mt76->band_idx, ++ }; ++ ++ switch (power_ctrl_id) { ++ case UNI_TXPOWER_SKU_POWER_LIMIT_CTRL: ++ req.sku_enable = !!data; ++ break; ++ case UNI_TXPOWER_PERCENTAGE_CTRL: ++ req.percentage_ctrl_enable = !!data; ++ break; ++ case UNI_TXPOWER_PERCENTAGE_DROP_CTRL: ++ req.power_drop_level = data; ++ break; ++ case UNI_TXPOWER_BACKOFF_POWER_LIMIT_CTRL: ++ req.bf_backoff_enable = !!data; ++ break; ++ case UNI_TXPOWER_ATE_MODE_CTRL: ++ req.ate_mode_enable = !!data; ++ break; ++ default: ++ req.sku_enable = !!data; ++ break; ++ } ++ ++ return mt76_mcu_send_msg(&dev->mt76, MCU_WM_UNI_CMD(TXPOWER), ++ &req, sizeof(req), false); ++} ++ + int mt7996_mcu_set_txpower_sku(struct mt7996_phy *phy) + { + #define TX_POWER_LIMIT_TABLE_RATE 0 ++#define TX_POWER_LIMIT_TABLE_PATH 1 + struct mt7996_dev *dev = phy->dev; + struct mt76_phy *mphy = phy->mt76; + struct tx_power_limit_table_ctrl { +@@ -5424,45 +5698,100 @@ int mt7996_mcu_set_txpower_sku(struct mt7996_phy *phy) + .power_limit_type = TX_POWER_LIMIT_TABLE_RATE, + .band_idx = phy->mt76->band_idx, + }; +- struct mt76_power_limits la = {}; ++ struct mt7996_power_path_limits *path; ++ struct mt76_power_limits *la; + struct sk_buff *skb; +- int i, tx_power; ++ int i, ret, target_power, tx_power; + +- tx_power = mt76_get_power_bound(mphy, phy->txpower); +- tx_power = mt76_get_rate_power_limits(mphy, mphy->chandef.chan, +- &la, tx_power); +- mphy->txpower_cur = tx_power; ++ target_power = mt76_get_power_bound(mphy, phy->txpower); ++ tx_power = target_power; ++ if (phy->sku_limit_en) { ++ la = kzalloc(sizeof(*la), GFP_KERNEL); ++ if (!la) ++ return -ENOMEM; ++ ++ tx_power = mt76_get_rate_power_limits(mphy, mphy->chandef.chan, ++ la, tx_power); ++ mphy->txpower_cur = tx_power; ++ } else { ++ mphy->txpower_cur = tx_power; ++ return 0; ++ } + + skb = mt76_mcu_msg_alloc(&dev->mt76, NULL, + sizeof(req) + MT7996_SKU_PATH_NUM); + if (!skb) +- return -ENOMEM; ++ goto err_nomem; + + skb_put_data(skb, &req, sizeof(req)); + /* cck and ofdm */ +- skb_put_data(skb, &la.cck, sizeof(la.cck)); +- skb_put_data(skb, &la.ofdm, sizeof(la.ofdm)); ++ skb_put_data(skb, &la->cck, MT7996_SKU_RATE_CCK_OFDM_LEN); + /* ht20 */ +- skb_put_data(skb, &la.mcs[0], 8); ++ skb_put_data(skb, &la->mcs[0], MT7996_SKU_RATE_HT20_LEN); + /* ht40 */ +- skb_put_data(skb, &la.mcs[1], 9); ++ skb_put_data(skb, &la->mcs[1], MT7996_SKU_RATE_HT40_LEN); + + /* vht */ +- for (i = 0; i < 4; i++) { +- skb_put_data(skb, &la.mcs[i], sizeof(la.mcs[i])); +- skb_put_zero(skb, 2); /* padding */ ++ for (i = 0; i < MT7996_SKU_RATE_VHT_NUM; i++) { ++ skb_put_data(skb, &la->mcs[i], MT7996_SKU_RATE_VHT_LEN); ++ skb_put_zero(skb, MT7996_SKU_RATE_VHT_PAD_LEN); + } + + /* he */ +- skb_put_data(skb, &la.ru[0], sizeof(la.ru)); ++ skb_put_data(skb, &la->ru[0], MT7996_SKU_RATE_HE_LEN); + /* eht */ +- skb_put_data(skb, &la.eht[0], sizeof(la.eht)); ++ skb_put_data(skb, &la->eht[0], MT7996_SKU_RATE_EHT_LEN); + + /* padding */ + skb_put_zero(skb, MT7996_SKU_PATH_NUM - MT7996_SKU_RATE_NUM); + +- return mt76_mcu_skb_send_msg(&dev->mt76, skb, +- MCU_WM_UNI_CMD(TXPOWER), true); ++ ret = mt76_mcu_skb_send_msg(&dev->mt76, skb, ++ MCU_WM_UNI_CMD(TXPOWER), true); ++ if (ret) ++ goto out; ++ ++ if (!phy->sku_path_en) ++ goto out; ++ ++ path = kzalloc(sizeof(*path), GFP_KERNEL); ++ if (!path) ++ goto err_nomem; ++ ++ mt7996_get_power_path_limits(mphy, mphy->chandef.chan, path, ++ target_power); ++ ++ skb = mt76_mcu_msg_alloc(&dev->mt76, NULL, ++ sizeof(req) + MT7996_SKU_PATH_NUM); ++ if (!skb) ++ goto err_nomem_path; ++ ++ req.power_limit_type = TX_POWER_LIMIT_TABLE_PATH; ++ ++ skb_put_data(skb, &req, sizeof(req)); ++ skb_put_data(skb, &path->cck, MT7996_SKU_PATH_CCK_LEN); ++ skb_put_data(skb, &path->ofdm, MT7996_SKU_PATH_OFDM_LEN); ++ skb_put_data(skb, &path->ofdm_bf, MT7996_SKU_PATH_OFDM_BF_LEN); ++ ++ for (i = 0; i < MT7996_SKU_PATH_RU_GROUPS; i++) { ++ bool bf = i % 2; ++ u8 idx = i / 2; ++ s8 *buf = bf ? path->ru_bf[idx] : path->ru[idx]; ++ ++ skb_put_data(skb, buf, MT7996_SKU_PATH_RU_LEN); ++ } ++ ++ ret = mt76_mcu_skb_send_msg(&dev->mt76, skb, ++ MCU_WM_UNI_CMD(TXPOWER), true); ++ kfree(path); ++ goto out; ++ ++err_nomem_path: ++ kfree(path); ++err_nomem: ++ ret = -ENOMEM; ++out: ++ kfree(la); ++ return ret; + } + + int mt7996_mcu_cp_support(struct mt7996_dev *dev, u8 mode) +diff --git a/mt7996/mcu.h b/mt7996/mcu.h +index 55d42c9e..fe7b7d9d 100644 +--- a/mt7996/mcu.h ++++ b/mt7996/mcu.h +@@ -1030,9 +1030,34 @@ enum { + }; + + enum { ++ UNI_TXPOWER_SKU_POWER_LIMIT_CTRL = 0, ++ UNI_TXPOWER_PERCENTAGE_CTRL = 1, ++ UNI_TXPOWER_PERCENTAGE_DROP_CTRL = 2, ++ UNI_TXPOWER_BACKOFF_POWER_LIMIT_CTRL = 3, + UNI_TXPOWER_POWER_LIMIT_TABLE_CTRL = 4, ++ UNI_TXPOWER_ATE_MODE_CTRL = 6, ++ UNI_TXPOWER_SHOW_INFO = 7, + }; + ++struct tx_power_ctrl { ++ u8 _rsv[4]; ++ ++ __le16 tag; ++ __le16 len; ++ ++ u8 power_ctrl_id; ++ union { ++ bool sku_enable; ++ bool ate_mode_enable; ++ bool percentage_ctrl_enable; ++ bool bf_backoff_enable; ++ u8 show_info_category; ++ u8 power_drop_level; ++ }; ++ u8 band_idx; ++ u8 rsv[1]; ++} __packed; ++ + enum { + UNI_CMD_ACCESS_REG_BASIC = 0x0, + UNI_CMD_ACCESS_RF_REG_BASIC, +diff --git a/mt7996/mt7996.h b/mt7996/mt7996.h +index 0d648852..7e9e106f 100644 +--- a/mt7996/mt7996.h ++++ b/mt7996/mt7996.h +@@ -400,6 +400,8 @@ struct mt7996_phy { + bool has_aux_rx; + bool counter_reset; + bool rdd_tx_paused; ++ bool sku_limit_en; ++ bool sku_path_en; + }; + + struct mt7996_dev { +@@ -775,6 +777,10 @@ int mt7996_mcu_get_chan_mib_info(struct mt7996_phy *phy, bool chan_switch); + int mt7996_mcu_get_temperature(struct mt7996_phy *phy); + int mt7996_mcu_set_thermal_throttling(struct mt7996_phy *phy, u8 state); + int mt7996_mcu_set_thermal_protect(struct mt7996_phy *phy, bool enable); ++bool mt7996_has_power_path_limits(struct mt76_phy *mphy, ++ struct ieee80211_channel *chan); ++int mt7996_mcu_set_tx_power_ctrl(struct mt7996_phy *phy, u8 power_ctrl_id, ++ u8 data); + int mt7996_mcu_set_txpower_sku(struct mt7996_phy *phy); + int mt7996_mcu_rdd_resume_tx(struct mt7996_phy *phy); + int mt7996_mcu_rdd_cmd(struct mt7996_dev *dev, int cmd, u8 rdd_idx, u8 val); +-- +2.43.0 + diff --git a/package/kernel/mt76/patches/0010-wifi-mt76-mt7996-fix-DL-OFDMA-fail-after-sta-reconne.patch b/package/kernel/mt76/patches/0010-wifi-mt76-mt7996-fix-DL-OFDMA-fail-after-sta-reconne.patch new file mode 100644 index 00000000000000..48f7ca8f2a639b --- /dev/null +++ b/package/kernel/mt76/patches/0010-wifi-mt76-mt7996-fix-DL-OFDMA-fail-after-sta-reconne.patch @@ -0,0 +1,30 @@ +From 75b9b5aefe78f4e487190aad53e7cc097b20b3af Mon Sep 17 00:00:00 2001 +From: Howard Hsu +Date: Tue, 10 Feb 2026 13:56:32 +0800 +Subject: [PATCH 10/89] wifi: mt76: mt7996: fix DL OFDMA fail after sta + reconnect + +Without this commit, sta_rec_basic with discoonect state would be sent +to the underlying firmware, which causes the DL OFDMA algorithm to fail. + +Signed-off-by: Howard Hsu +--- + mt7996/main.c | 2 -- + 1 file changed, 2 deletions(-) + +diff --git a/mt7996/main.c b/mt7996/main.c +index fa875b295..b14636099 100644 +--- a/mt7996/main.c ++++ b/mt7996/main.c +@@ -1061,8 +1061,6 @@ mt7996_mac_sta_init_link(struct mt7996_dev *dev, + rcu_assign_pointer(msta->link[link_id], msta_link); + + mt7996_mac_wtbl_update(dev, idx, MT_WTBL_UPDATE_ADM_COUNT_CLEAR); +- mt7996_mcu_add_sta(dev, link_conf, link_sta, link, msta_link, +- CONN_STATE_DISCONNECT, true); + + rcu_assign_pointer(dev->mt76.wcid[idx], &msta_link->wcid); + mt76_wcid_init(&msta_link->wcid, phy->mt76->band_idx); +-- +2.45.2 + diff --git a/package/kernel/mt76/patches/0013-wifi-mt76-reset-channel-state-during-scanning.patch b/package/kernel/mt76/patches/0013-wifi-mt76-reset-channel-state-during-scanning.patch new file mode 100644 index 00000000000000..6ea0b5277bd6b7 --- /dev/null +++ b/package/kernel/mt76/patches/0013-wifi-mt76-reset-channel-state-during-scanning.patch @@ -0,0 +1,44 @@ +From 54b61cebb3e5d27aba7b293fe9d11d45df1acbd4 Mon Sep 17 00:00:00 2001 +From: Michael-CY Lee +Date: Thu, 16 Oct 2025 11:02:00 +0800 +Subject: [PATCH 13/89] wifi: mt76: reset channel state during scanning + +Reset the channel state of all channels during scanning to prevent the +ACS algorithm from selecting unexpected channel. + +Signed-off-by: Michael-CY Lee +Signed-off-by: Shayne Chen +--- + mac80211.c | 3 ++- + scan.c | 1 + + 2 files changed, 3 insertions(+), 1 deletion(-) + +diff --git a/mac80211.c b/mac80211.c +index 4f080f15e..0d5263140 100644 +--- a/mac80211.c ++++ b/mac80211.c +@@ -1042,7 +1042,8 @@ int __mt76_set_channel(struct mt76_phy *phy, struct cfg80211_chan_def *chandef, + if (!offchannel) + phy->main_chandef = *chandef; + +- if (chandef->chan != phy->main_chandef.chan) ++ if (chandef->chan != phy->main_chandef.chan || ++ test_bit(MT76_SCANNING, &phy->state)) + memset(phy->chan_state, 0, sizeof(*phy->chan_state)); + + ret = dev->drv->set_channel(phy); +diff --git a/scan.c b/scan.c +index 2b5f30cef..010e815ff 100644 +--- a/scan.c ++++ b/scan.c +@@ -208,6 +208,7 @@ int mt76_hw_scan(struct ieee80211_hw *hw, struct ieee80211_vif *vif, + dev->scan.vif = vif; + dev->scan.phy = phy; + dev->scan.mlink = mlink; ++ set_bit(MT76_SCANNING, &phy->state); + ieee80211_queue_delayed_work(dev->phy.hw, &dev->scan_work, 0); + + out: +-- +2.45.2 + diff --git a/package/kernel/mt76/patches/0020-mtk-fix-rmmod-issue-when-WED-is-off.patch b/package/kernel/mt76/patches/0020-mtk-fix-rmmod-issue-when-WED-is-off.patch new file mode 100644 index 00000000000000..575f3f5d336f78 --- /dev/null +++ b/package/kernel/mt76/patches/0020-mtk-fix-rmmod-issue-when-WED-is-off.patch @@ -0,0 +1,31 @@ +From 32626566cb095e818005c247947054a701488736 Mon Sep 17 00:00:00 2001 +From: Shayne Chen +Date: Thu, 26 Feb 2026 17:01:50 +0800 +Subject: [PATCH 20/89] mtk: fix rmmod issue when WED is off + +When WED is off, rmmod will crash. +This is upstream issue caused by: +(wifi: mt76: Fix memory leak destroying device) + +Signed-off-by: Shayne Chen +--- + dma.c | 3 +-- + 1 file changed, 1 insertion(+), 2 deletions(-) + +diff --git a/dma.c b/dma.c +index 2d133ace7..3b655fb16 100644 +--- a/dma.c ++++ b/dma.c +@@ -885,8 +885,7 @@ mt76_dma_rx_cleanup(struct mt76_dev *dev, struct mt76_queue *q) + mt76_queue_is_wed_rro(q)) + continue; + +- if (!mt76_queue_is_wed_rro_rxdmad_c(q) && +- !mt76_queue_is_wed_rro_ind(q)) ++ if (!mt76_queue_is_wed_rro(q)) + mt76_put_page_pool_buf(buf, false); + } while (1); + +-- +2.45.2 + diff --git a/package/kernel/mt76/patches/0021-mtk-mt76-mt7996-only-disallow-getting-global-wcid-in.patch b/package/kernel/mt76/patches/0021-mtk-mt76-mt7996-only-disallow-getting-global-wcid-in.patch new file mode 100644 index 00000000000000..a4d25cd4c621fd --- /dev/null +++ b/package/kernel/mt76/patches/0021-mtk-mt76-mt7996-only-disallow-getting-global-wcid-in.patch @@ -0,0 +1,40 @@ +From 95d9b94545e8f45766e1d9479321d55dc560fa07 Mon Sep 17 00:00:00 2001 +From: Michael-CY Lee +Date: Wed, 26 Nov 2025 17:25:30 +0800 +Subject: [PATCH 21/89] mtk: mt76: mt7996: only disallow getting global wcid in + mt7996_rx_get_wcid + +This commit refactors Felix'c change +(fb7e0501b8f920b22811f1900255a5646abf18b8) + +The original commit causes the wcid index to be retrieved incorrectly +when using vif wcid for RX. + +For example, after detecting beacon loss from the AP, STA will sends +a null func to AP and check if AP is still alive based on the TxS. +However, because the STA uses the vif's wcid to send the frame, the +wcid of the TxS cannot be correctly retrieved. + +Now we only prevent global wcid to be retrieved in the RX path. + +Signed-off-by: Michael-CY Lee +--- + mt7996/mac.c | 2 +- + 1 file changed, 1 insertion(+), 1 deletion(-) + +diff --git a/mt7996/mac.c b/mt7996/mac.c +index 17742209c..13d4bd0c6 100644 +--- a/mt7996/mac.c ++++ b/mt7996/mac.c +@@ -23,7 +23,7 @@ static struct mt76_wcid *mt7996_rx_get_wcid(struct mt7996_dev *dev, + int i; + + wcid = mt76_wcid_ptr(dev, idx); +- if (!wcid || !wcid->sta) ++ if (!wcid || wcid == &dev->mt76.global_wcid) + return NULL; + + if (!mt7996_band_valid(dev, band_idx)) +-- +2.45.2 + diff --git a/package/kernel/mt76/patches/0022-mtk-mt76-mt7996-fix-reg-addr-remap-when-addr-is-0.patch b/package/kernel/mt76/patches/0022-mtk-mt76-mt7996-fix-reg-addr-remap-when-addr-is-0.patch new file mode 100644 index 00000000000000..548a2aaa30332f --- /dev/null +++ b/package/kernel/mt76/patches/0022-mtk-mt76-mt7996-fix-reg-addr-remap-when-addr-is-0.patch @@ -0,0 +1,80 @@ +From b3ef09f83bf6966d847926135ac4cf51ba5d49f4 Mon Sep 17 00:00:00 2001 +From: StanleyYP Wang +Date: Mon, 24 Nov 2025 13:43:30 +0800 +Subject: [PATCH 22/89] mtk: mt76: mt7996: fix reg addr remap when addr is 0 + +When addr is less than the hardcoded threshold in __mt7996_reg_addr, +it indicates that remapping is unnecessary. +Currently, the flow remaps address 0x0 to MT_HIF_REMAP_BASE_L2, +which is incorrect. +To address this, modify __mt7996_reg_addr to return INVALID_REG_ADDR +if the address is not below the hardcoded value or is not present in +the mt7996_reg_map array. +Additionally, update the remap condition to check if addr == INVALID_REG_ADDR. + +Signed-off-by: StanleyYP Wang +--- + mt7996/mmio.c | 12 +++++++----- + 1 file changed, 7 insertions(+), 5 deletions(-) + +diff --git a/mt7996/mmio.c b/mt7996/mmio.c +index d9780bb42..4430f37cb 100644 +--- a/mt7996/mmio.c ++++ b/mt7996/mmio.c +@@ -17,6 +17,8 @@ + static bool wed_enable; + module_param(wed_enable, bool, 0644); + ++#define INVALID_REG_ADDR 0xffffffff ++ + static const struct __base mt7996_reg_base[] = { + [WF_AGG_BASE] = { { 0x820e2000, 0x820f2000, 0x830e2000 } }, + [WF_ARB_BASE] = { { 0x820e3000, 0x820f3000, 0x830e3000 } }, +@@ -334,7 +336,7 @@ static u32 __mt7996_reg_addr(struct mt7996_dev *dev, u32 addr) + return dev->reg.map[i].mapped + ofs; + } + +- return 0; ++ return INVALID_REG_ADDR; + } + + static u32 __mt7996_reg_remap_addr(struct mt7996_dev *dev, u32 addr) +@@ -366,7 +368,7 @@ void mt7996_memcpy_fromio(struct mt7996_dev *dev, void *buf, u32 offset, + { + u32 addr = __mt7996_reg_addr(dev, offset); + +- if (addr) { ++ if (addr != INVALID_REG_ADDR) { + memcpy_fromio(buf, dev->mt76.mmio.regs + addr, len); + return; + } +@@ -382,7 +384,7 @@ static u32 mt7996_rr(struct mt76_dev *mdev, u32 offset) + struct mt7996_dev *dev = container_of(mdev, struct mt7996_dev, mt76); + u32 addr = __mt7996_reg_addr(dev, offset), val; + +- if (addr) ++ if (addr != INVALID_REG_ADDR) + return dev->bus_ops->rr(mdev, addr); + + spin_lock_bh(&dev->reg_lock); +@@ -397,7 +399,7 @@ static void mt7996_wr(struct mt76_dev *mdev, u32 offset, u32 val) + struct mt7996_dev *dev = container_of(mdev, struct mt7996_dev, mt76); + u32 addr = __mt7996_reg_addr(dev, offset); + +- if (addr) { ++ if (addr != INVALID_REG_ADDR) { + dev->bus_ops->wr(mdev, addr, val); + return; + } +@@ -412,7 +414,7 @@ static u32 mt7996_rmw(struct mt76_dev *mdev, u32 offset, u32 mask, u32 val) + struct mt7996_dev *dev = container_of(mdev, struct mt7996_dev, mt76); + u32 addr = __mt7996_reg_addr(dev, offset); + +- if (addr) ++ if (addr != INVALID_REG_ADDR) + return dev->bus_ops->rmw(mdev, addr, mask, val); + + spin_lock_bh(&dev->reg_lock); +-- +2.45.2 + diff --git a/package/kernel/mt76/patches/0038-mtk-mt76-use-wcid_idx-instead-of-wcid-in-mt76_rx_sta.patch b/package/kernel/mt76/patches/0038-mtk-mt76-use-wcid_idx-instead-of-wcid-in-mt76_rx_sta.patch new file mode 100644 index 00000000000000..2bceb307817191 --- /dev/null +++ b/package/kernel/mt76/patches/0038-mtk-mt76-use-wcid_idx-instead-of-wcid-in-mt76_rx_sta.patch @@ -0,0 +1,213 @@ +From e5d47072bc6e00aeda8c3824abcc9628dd2eadf2 Mon Sep 17 00:00:00 2001 +From: Shayne Chen +Date: Mon, 23 Feb 2026 18:31:00 +0800 +Subject: [PATCH 38/89] mtk: mt76: use wcid_idx instead of wcid in + mt76_rx_status + +Use wcid_idx in mt76_rx_status and do dereference when it needs wcid. +In some cases, e.g., rx reorder, it would leave rcu critical +section so we need to dereference again. + +Signed-off-by: Peter Chiu +Signed-off-by: Shayne Chen +--- + agg-rx.c | 11 ++++++----- + mac80211.c | 36 +++++++++++++++++++++++------------- + mt76.h | 3 ++- + mt7996/mac.c | 3 ++- + 4 files changed, 33 insertions(+), 20 deletions(-) + +diff --git a/agg-rx.c b/agg-rx.c +index 3d34caf7e..02f7fc999 100644 +--- a/agg-rx.c ++++ b/agg-rx.c +@@ -116,11 +116,11 @@ mt76_rx_aggr_reorder_work(struct work_struct *work) + } + + static void +-mt76_rx_aggr_check_ctl(struct sk_buff *skb, struct sk_buff_head *frames) ++mt76_rx_aggr_check_ctl(struct sk_buff *skb, struct sk_buff_head *frames, ++ struct mt76_wcid *wcid) + { + struct mt76_rx_status *status = (struct mt76_rx_status *)skb->cb; + struct ieee80211_bar *bar = mt76_skb_get_hdr(skb); +- struct mt76_wcid *wcid = status->wcid; + struct mt76_rx_tid *tid; + u8 tidno; + u16 seqno; +@@ -145,10 +145,11 @@ mt76_rx_aggr_check_ctl(struct sk_buff *skb, struct sk_buff_head *frames) + spin_unlock_bh(&tid->lock); + } + +-void mt76_rx_aggr_reorder(struct sk_buff *skb, struct sk_buff_head *frames) ++void mt76_rx_aggr_reorder(struct mt76_dev *dev, struct sk_buff *skb, ++ struct sk_buff_head *frames) + { + struct mt76_rx_status *status = (struct mt76_rx_status *)skb->cb; +- struct mt76_wcid *wcid = status->wcid; ++ struct mt76_wcid *wcid = __mt76_wcid_ptr(dev, status->wcid_idx); + struct ieee80211_sta *sta; + struct mt76_rx_tid *tid; + bool sn_less; +@@ -164,7 +165,7 @@ void mt76_rx_aggr_reorder(struct sk_buff *skb, struct sk_buff_head *frames) + + if (!status->aggr) { + if (!(status->flag & RX_FLAG_8023)) +- mt76_rx_aggr_check_ctl(skb, frames); ++ mt76_rx_aggr_check_ctl(skb, frames, wcid); + return; + } + +diff --git a/mac80211.c b/mac80211.c +index 70aea0ee0..2d65ae289 100644 +--- a/mac80211.c ++++ b/mac80211.c +@@ -1234,6 +1234,7 @@ mt76_rx_convert(struct mt76_dev *dev, struct sk_buff *skb, + struct ieee80211_rx_status *status = IEEE80211_SKB_RXCB(skb); + struct ieee80211_hdr *hdr = mt76_skb_get_hdr(skb); + struct mt76_rx_status mstat; ++ struct mt76_wcid *wcid; + + mstat = *((struct mt76_rx_status *)skb->cb); + memset(status, 0, sizeof(*status)); +@@ -1275,20 +1276,21 @@ mt76_rx_convert(struct mt76_dev *dev, struct sk_buff *skb, + memcpy(status->chain_signal, mstat.chain_signal, + sizeof(mstat.chain_signal)); + +- if (mstat.wcid) { +- status->link_valid = mstat.wcid->link_valid; +- status->link_id = mstat.wcid->link_id; ++ wcid = __mt76_wcid_ptr(dev, mstat.wcid_idx); ++ if (wcid) { ++ status->link_valid = wcid->link_valid; ++ status->link_id = wcid->link_id; + } + +- *sta = wcid_to_sta(mstat.wcid); ++ *sta = wcid_to_sta(wcid); + *hw = mt76_phy_hw(dev, mstat.phy_idx); + } + + static void +-mt76_check_ccmp_pn(struct sk_buff *skb) ++mt76_check_ccmp_pn(struct mt76_dev *dev, struct sk_buff *skb) + { + struct mt76_rx_status *status = (struct mt76_rx_status *)skb->cb; +- struct mt76_wcid *wcid = status->wcid; ++ struct mt76_wcid *wcid; + struct ieee80211_hdr *hdr; + int security_idx; + int ret; +@@ -1299,6 +1301,7 @@ mt76_check_ccmp_pn(struct sk_buff *skb) + if (status->flag & RX_FLAG_ONLY_MONITOR) + return; + ++ wcid = __mt76_wcid_ptr(dev, status->wcid_idx); + if (!wcid || !wcid->rx_check_pn) + return; + +@@ -1346,7 +1349,7 @@ static void + mt76_airtime_report(struct mt76_dev *dev, struct mt76_rx_status *status, + int len) + { +- struct mt76_wcid *wcid = status->wcid; ++ struct mt76_wcid *wcid; + struct ieee80211_rx_status info = { + .enc_flags = status->enc_flags, + .rate_idx = status->rate_idx, +@@ -1364,6 +1367,7 @@ mt76_airtime_report(struct mt76_dev *dev, struct mt76_rx_status *status, + dev->cur_cc_bss_rx += airtime; + spin_unlock(&dev->cc_lock); + ++ wcid = __mt76_wcid_ptr(dev, status->wcid_idx); + if (!wcid || !wcid->sta) + return; + +@@ -1397,11 +1401,12 @@ static void + mt76_airtime_check(struct mt76_dev *dev, struct sk_buff *skb) + { + struct mt76_rx_status *status = (struct mt76_rx_status *)skb->cb; +- struct mt76_wcid *wcid = status->wcid; ++ struct mt76_wcid *wcid; + + if (!(dev->drv->drv_flags & MT_DRV_SW_RX_AIRTIME)) + return; + ++ wcid = __mt76_wcid_ptr(dev, status->wcid_idx); + if (!wcid || !wcid->sta) { + struct ieee80211_hdr *hdr = mt76_skb_get_hdr(skb); + +@@ -1440,16 +1445,21 @@ mt76_check_sta(struct mt76_dev *dev, struct sk_buff *skb) + struct ieee80211_hdr *hdr = mt76_skb_get_hdr(skb); + struct ieee80211_sta *sta; + struct ieee80211_hw *hw; +- struct mt76_wcid *wcid = status->wcid; ++ struct mt76_wcid *wcid = __mt76_wcid_ptr(dev, status->wcid_idx); + u8 tidno = status->qos_ctl & IEEE80211_QOS_CTL_TID_MASK; + bool ps; + + hw = mt76_phy_hw(dev, status->phy_idx); + if (ieee80211_is_pspoll(hdr->frame_control) && !wcid && + !(status->flag & RX_FLAG_8023)) { ++ if (hw->wiphy->flags & WIPHY_FLAG_SUPPORTS_MLO) ++ return; ++ + sta = ieee80211_find_sta_by_ifaddr(hw, hdr->addr2, NULL); +- if (sta) +- wcid = status->wcid = (struct mt76_wcid *)sta->drv_priv; ++ if (sta) { ++ wcid = (struct mt76_wcid *)sta->drv_priv; ++ status->wcid_idx = wcid->idx; ++ } + } + + mt76_airtime_check(dev, skb); +@@ -1513,7 +1523,7 @@ void mt76_rx_complete(struct mt76_dev *dev, struct sk_buff_head *frames, + while ((skb = __skb_dequeue(frames)) != NULL) { + struct sk_buff *nskb = skb_shinfo(skb)->frag_list; + +- mt76_check_ccmp_pn(skb); ++ mt76_check_ccmp_pn(dev, skb); + skb_shinfo(skb)->frag_list = NULL; + mt76_rx_convert(dev, skb, &hw, &sta); + ieee80211_rx_list(hw, sta, skb, &list); +@@ -1555,7 +1565,7 @@ void mt76_rx_poll_complete(struct mt76_dev *dev, enum mt76_rxq_id q, + mt76_npu_device_active(dev)) + __skb_queue_tail(&frames, skb); + else +- mt76_rx_aggr_reorder(skb, &frames); ++ mt76_rx_aggr_reorder(dev, skb, &frames); + } + + mt76_rx_complete(dev, &frames, napi); +diff --git a/mt76.h b/mt76.h +index 88de215fc..8528f8aea 100644 +--- a/mt76.h ++++ b/mt76.h +@@ -1797,7 +1797,8 @@ void mt76_rx_complete(struct mt76_dev *dev, struct sk_buff_head *frames, + struct napi_struct *napi); + void mt76_rx_poll_complete(struct mt76_dev *dev, enum mt76_rxq_id q, + struct napi_struct *napi); +-void mt76_rx_aggr_reorder(struct sk_buff *skb, struct sk_buff_head *frames); ++void mt76_rx_aggr_reorder(struct mt76_dev *dev, struct sk_buff *skb, ++ struct sk_buff_head *frames); + void mt76_testmode_tx_pending(struct mt76_phy *phy); + void mt76_queue_tx_complete(struct mt76_dev *dev, struct mt76_queue *q, + struct mt76_queue_entry *e); +diff --git a/mt7996/mac.c b/mt7996/mac.c +index 67532b04b..41dd250fb 100644 +--- a/mt7996/mac.c ++++ b/mt7996/mac.c +@@ -711,7 +711,8 @@ mt7996_mac_fill_rx(struct mt7996_dev *dev, enum mt76_rxq_id q, + } + } + +- if (!status->wcid || !ieee80211_is_data_qos(fc) || hw_aggr) ++ status->wcid_idx = status->wcid ? status->wcid->idx : 0; ++ if (!status->wcid_idx || !ieee80211_is_data_qos(fc) || hw_aggr) + return 0; + + status->aggr = unicast && +-- +2.45.2 + diff --git a/package/kernel/mt76/patches/0040-mtk-mt76-mt7996-Fix-call-trace-happened-when-wed-att.patch b/package/kernel/mt76/patches/0040-mtk-mt76-mt7996-Fix-call-trace-happened-when-wed-att.patch new file mode 100644 index 00000000000000..55120d420dbd55 --- /dev/null +++ b/package/kernel/mt76/patches/0040-mtk-mt76-mt7996-Fix-call-trace-happened-when-wed-att.patch @@ -0,0 +1,28 @@ +From 3365d5a99e648a51a638d68c384005765cd7a8c7 Mon Sep 17 00:00:00 2001 +From: Rex Lu +Date: Fri, 6 Sep 2024 15:32:05 +0800 +Subject: [PATCH 40/89] mtk: mt76: mt7996: Fix call trace happened when wed + attach fail + +1. when we attech wed0 fail, we will not going to attach wed1 + +Signed-off-by: Rex Lu +--- + mt7996/mmio.c | 1 + + 1 file changed, 1 insertion(+) + +diff --git a/mt7996/mmio.c b/mt7996/mmio.c +index 9d0d3f163..277978add 100644 +--- a/mt7996/mmio.c ++++ b/mt7996/mmio.c +@@ -614,6 +614,7 @@ int mt7996_mmio_wed_init(struct mt7996_dev *dev, void *pdev_ptr, + + if (mtk_wed_device_attach(wed)) { + dev->mt76.hwrro_mode = MT76_HWRRO_OFF; ++ wed_enable = false; + return 0; + } + +-- +2.45.2 + diff --git a/package/kernel/mt76/patches/0047-mtk-mt76-fix-RX-data-queuing-of-RRO-3.0.patch b/package/kernel/mt76/patches/0047-mtk-mt76-fix-RX-data-queuing-of-RRO-3.0.patch new file mode 100644 index 00000000000000..9f265145e11913 --- /dev/null +++ b/package/kernel/mt76/patches/0047-mtk-mt76-fix-RX-data-queuing-of-RRO-3.0.patch @@ -0,0 +1,43 @@ +From de15604b9b081c24f7f140dca517cf507c386cd2 Mon Sep 17 00:00:00 2001 +From: Rex Lu +Date: Fri, 19 Sep 2025 17:52:26 +0800 +Subject: [PATCH 47/89] mtk: mt76: fix RX data queuing of RRO 3.0 + +For RRO 3.0, RX data should be put to the indicator queue. +Otherwise, "WED OFF + HWRRO ON" won't work. + +This patch fixes upstream HWRRO bug, will be upstremed. + +Signed-off-by: Rex Lu +Signed-off-by: Shayne Chen +--- + mac80211.c | 6 ++++++ + 1 file changed, 6 insertions(+) + +diff --git a/mac80211.c b/mac80211.c +index 2d65ae289..63e1c88b9 100644 +--- a/mac80211.c ++++ b/mac80211.c +@@ -878,6 +878,7 @@ static void mt76_rx_release_amsdu(struct mt76_phy *phy, enum mt76_rxq_id q) + struct sk_buff *skb = phy->rx_amsdu[q].head; + struct mt76_rx_status *status = (struct mt76_rx_status *)skb->cb; + struct mt76_dev *dev = phy->dev; ++ struct mt76_queue *rxq = &dev->q_rx[q]; + + phy->rx_amsdu[q].head = NULL; + phy->rx_amsdu[q].tail = NULL; +@@ -906,6 +907,11 @@ static void mt76_rx_release_amsdu(struct mt76_phy *phy, enum mt76_rxq_id q) + return; + } + } ++ ++ /* for RRO 3.0, RX data should be put to the indicator queue */ ++ if (mt76_queue_is_wed_rro_data(rxq) && dev->hwrro_mode == MT76_HWRRO_V3) ++ q = MT_RXQ_RRO_IND; ++ + __skb_queue_tail(&dev->rx_skb[q], skb); + } + +-- +2.45.2 + diff --git a/package/kernel/mt76/patches/0080-wifi-mt76-mt7996-check-ddone-for-tx-queues.patch b/package/kernel/mt76/patches/0080-wifi-mt76-mt7996-check-ddone-for-tx-queues.patch new file mode 100644 index 00000000000000..f7b26cb53ef22a --- /dev/null +++ b/package/kernel/mt76/patches/0080-wifi-mt76-mt7996-check-ddone-for-tx-queues.patch @@ -0,0 +1,67 @@ +From dd1008398dcae8dceac44eee7cdf01c9e0d75edd Mon Sep 17 00:00:00 2001 +From: Peter Chiu +Date: Tue, 9 Dec 2025 20:28:52 +0800 +Subject: [PATCH 80/89] wifi: mt76: mt7996: check ddone for tx queues + +Check ddone for tx queues. +No issue encountered, just align logan configuraion to prevent +potential issue. + +Signed-off-by: Peter Chiu +--- + dma.c | 4 ++++ + mt76.h | 1 + + mt7996/dma.c | 6 +++--- + 3 files changed, 8 insertions(+), 3 deletions(-) + +diff --git a/dma.c b/dma.c +index dd93d5544..df9c392cd 100644 +--- a/dma.c ++++ b/dma.c +@@ -425,6 +425,10 @@ mt76_dma_tx_cleanup(struct mt76_dev *dev, struct mt76_queue *q, bool flush) + last = Q_READ(q, dma_idx, MT_QUEUE_DMA_IDX); + + while (q->queued > 0 && q->tail != last) { ++ if ((q->flags & MT_QFLAG_CHECK_DDONE) && ++ !(q->desc[q->tail].ctrl & cpu_to_le32(MT_DMA_CTL_DMA_DONE))) ++ break; ++ + mt76_dma_tx_cleanup_idx(dev, q, q->tail, &entry); + mt76_npu_txdesc_cleanup(q, q->tail); + mt76_queue_tx_complete(dev, q, &entry); +diff --git a/mt76.h b/mt76.h +index 1d66b65fd..6cd4e88f4 100644 +--- a/mt76.h ++++ b/mt76.h +@@ -70,6 +70,7 @@ + #define MT_QFLAG_WED_RRO_EN BIT(7) + #define MT_QFLAG_EMI_EN BIT(8) + #define MT_QFLAG_NPU BIT(9) ++#define MT_QFLAG_CHECK_DDONE BIT(10) + + #define __MT_WED_Q(_type, _n) (MT_QFLAG_WED | \ + FIELD_PREP(MT_QFLAG_WED_TYPE, _type) | \ +diff --git a/mt7996/dma.c b/mt7996/dma.c +index 8dc4b3244..6e595ef8c 100644 +--- a/mt7996/dma.c ++++ b/mt7996/dma.c +@@ -18,13 +18,13 @@ int mt7996_init_tx_queues(struct mt7996_phy *phy, int idx, int n_desc, + idx -= MT_TXQ_ID(0); + + if (wed == &dev->mt76.mmio.wed_hif2) +- flags = MT_WED_Q_TX(0); ++ flags |= MT_WED_Q_TX(0); + else +- flags = MT_WED_Q_TX(idx); ++ flags |= MT_WED_Q_TX(idx); + } + + if (mt76_npu_device_active(&dev->mt76)) +- flags = MT_NPU_Q_TX(phy->mt76->band_idx); ++ flags |= MT_NPU_Q_TX(phy->mt76->band_idx); + + return mt76_connac_init_tx_queues(phy->mt76, idx, n_desc, + ring_base, wed, flags); +-- +2.45.2 + diff --git a/package/kernel/mt76/patches/0083-mtk-mt76-remove-unnecessary-wcid-asignment.patch b/package/kernel/mt76/patches/0083-mtk-mt76-remove-unnecessary-wcid-asignment.patch new file mode 100644 index 00000000000000..a633d1c1b6f50c --- /dev/null +++ b/package/kernel/mt76/patches/0083-mtk-mt76-remove-unnecessary-wcid-asignment.patch @@ -0,0 +1,35 @@ +From 55700d80028fffb4d76ce9a04e0566aa1e4ef0e4 Mon Sep 17 00:00:00 2001 +From: Michael-CY Lee +Date: Wed, 31 Dec 2025 16:43:50 +0800 +Subject: [PATCH 83/89] mtk: mt76: remove unnecessary wcid asignment + +Without this patch, the wcid might incorrectly be changed after TX, +causing wcid->non_aql_packets being counted on wrong wcid. + +For example, on AP side, if the STA's setup link is 5G link, and the STA +uses 2G to TX frame. The value of non_aql_packets is increased on 5G +wcid, but decreased on 2G wcid. + +Signed-off-by: Michael-CY Lee +--- + tx.c | 4 ---- + 1 file changed, 4 deletions(-) + +diff --git a/tx.c b/tx.c +index 117e51aac..7796060cc 100644 +--- a/tx.c ++++ b/tx.c +@@ -324,10 +324,6 @@ __mt76_tx_queue_skb(struct mt76_phy *phy, int qid, struct sk_buff *skb, + if (idx < 0 || !sta) + return idx; + +- wcid = (struct mt76_wcid *)sta->drv_priv; +- if (!wcid->sta) +- return idx; +- + q->entry[idx].wcid = wcid->idx; + + if (!non_aql) +-- +2.45.2 + diff --git a/package/kernel/mt76/patches/9990-wifi-mt76-mt7996-advertise-eht-to-firmware-rate-control.patch b/package/kernel/mt76/patches/9990-wifi-mt76-mt7996-advertise-eht-to-firmware-rate-control.patch new file mode 100644 index 00000000000000..1f362c3ca04b65 --- /dev/null +++ b/package/kernel/mt76/patches/9990-wifi-mt76-mt7996-advertise-eht-to-firmware-rate-control.patch @@ -0,0 +1,37 @@ +From: Jihong Min +Subject: [PATCH] wifi: mt76: mt7996: advertise EHT to firmware rate control + +mt7996_mcu_sta_rate_ctrl_tlv() already marks HE-capable stations with +MODE_HE and fills the rest of the rate-control TLV from link capabilities. +Extend the same TLV construction for EHT stations by setting MODE_EHT when +the link station advertises EHT capability. + +With EHT explicitly advertised in the firmware rate-control input, mt7996 +firmware does not have to infer a 320 MHz Wi-Fi 7 peer from other station +records alone. On W1700K2 diagnostics this keeps the RA supp_mode in the +expected OFDM | HE | EHT shape instead of OFDM | HE for QCNCM865 EHT320 +clients. + +Assisted-by: Codex:gpt-5.5 +Signed-off-by: Jihong Min +--- + mt7996/mcu.c | 3 +++ + 1 file changed, 3 insertions(+) + +diff --git a/mt7996/mcu.c b/mt7996/mcu.c +index f3426115bba1..25c3d23538a8 100644 +--- a/mt7996/mcu.c ++++ b/mt7996/mcu.c +@@ -2549,7 +2549,10 @@ mt7996_mcu_sta_rate_ctrl_tlv(struct sk_buff *skb, struct mt7996_dev *dev, + if (link_sta->he_6ghz_capa.capa) + ra->af = le16_get_bits(link_sta->he_6ghz_capa.capa, + IEEE80211_HE_6GHZ_CAP_MAX_AMPDU_LEN_EXP); + } ++ if (link_sta->eht_cap.has_eht) ++ ra->supp_mode |= MODE_EHT; ++ + ra->sta_cap = cpu_to_le32(cap); + + memset(ra->rx_rcpi, INIT_RCPI, sizeof(ra->rx_rcpi)); +-- +2.43.0 diff --git a/package/kernel/mt76/patches/9991-wifi-mt76-mt7996-fill-160mhz-bf-fallback-for-eht-320.patch b/package/kernel/mt76/patches/9991-wifi-mt76-mt7996-fill-160mhz-bf-fallback-for-eht-320.patch new file mode 100644 index 00000000000000..49386398244de5 --- /dev/null +++ b/package/kernel/mt76/patches/9991-wifi-mt76-mt7996-fill-160mhz-bf-fallback-for-eht-320.patch @@ -0,0 +1,52 @@ +From: Jihong Min +Subject: [PATCH] wifi: mt76: mt7996: fill 160 MHz BF fallback for EHT 320 + +mt7996_mcu_sta_bfer_eht() packs EHT >80 MHz beamforming row/column +information into separate 160 MHz and 320 MHz nibbles. For 320 MHz +stations, populate the 160 MHz nibble from the peer 160 MHz EHT +capabilities before OR-ing in the existing 320 MHz values. + +This gives firmware a valid beamforming matrix fallback when feedback or +sounding bandwidth contracts from 320 MHz to 160 MHz during traffic. On +W1700K2 EHT320 diagnostics, both nibbles stay populated instead of leaving +the lower fallback nibble zero for QCNCM865 clients. + +Assisted-by: Codex:gpt-5.5 +Signed-off-by: Jihong Min +--- + mt7996/mcu.c | 13 +++++++++++-- + 1 file changed, 11 insertions(+), 2 deletions(-) + +diff --git a/mt7996/mcu.c b/mt7996/mcu.c +index f3426115bba1..cdddfd83708e 100644 +--- a/mt7996/mcu.c ++++ b/mt7996/mcu.c +@@ -1979,15 +1979,24 @@ mt7996_mcu_sta_bfer_eht(struct ieee80211_link_sta *link_sta, + bf->ncol_gt_bw80 = nss_mcs; + break; + case IEEE80211_STA_RX_BW_320: ++ bf->nrow_gt_bw80 = ++ min_t(u8, EHT_PHY(CAP2_SOUNDING_DIM_160MHZ_MASK, ++ ve->phy_cap_info[2]), ++ EHT_PHY(CAP1_BEAMFORMEE_SS_160MHZ_MASK, ++ pe->phy_cap_info[1])); ++ bf->ncol_gt_bw80 = ++ u8_get_bits(eht_nss->bw._160.rx_tx_mcs9_max_nss, ++ IEEE80211_EHT_MCS_NSS_RX) - 1; ++ + snd_dim = EHT_PHY(CAP2_SOUNDING_DIM_320MHZ_MASK, ve->phy_cap_info[2]) + + (EHT_PHY(CAP3_SOUNDING_DIM_320MHZ_MASK, + ve->phy_cap_info[3]) << 1); + sts = EHT_PHY(CAP1_BEAMFORMEE_SS_320MHZ_MASK, pe->phy_cap_info[1]); + nss_mcs = u8_get_bits(eht_nss->bw._320.rx_tx_mcs9_max_nss, + IEEE80211_EHT_MCS_NSS_RX) - 1; + +- bf->nrow_gt_bw80 = min_t(u8, snd_dim, sts) << 4; +- bf->ncol_gt_bw80 = nss_mcs << 4; ++ bf->nrow_gt_bw80 |= min_t(u8, snd_dim, sts) << 4; ++ bf->ncol_gt_bw80 |= nss_mcs << 4; + break; + default: + break; +-- +2.43.0 diff --git a/package/kernel/mt76/patches/9992-wifi-mt76-mt7996-validate-PS-sync-event-TLVs.patch b/package/kernel/mt76/patches/9992-wifi-mt76-mt7996-validate-PS-sync-event-TLVs.patch new file mode 100644 index 00000000000000..73c7a8ce2ffb19 --- /dev/null +++ b/package/kernel/mt76/patches/9992-wifi-mt76-mt7996-validate-PS-sync-event-TLVs.patch @@ -0,0 +1,77 @@ +From: Jihong Min +Subject: [PATCH] wifi: mt76: mt7996: validate PS sync event TLVs + +The mt7996 PS sync event parser walks a firmware-provided TLV stream by +pulling each TLV length from the skb. Reject malformed TLVs whose length is +smaller than the TLV header or larger than the remaining skb data, otherwise a +zero-length TLV can keep the parser in the same loop iteration indefinitely. + +Also validate the fixed-size TLV payloads before reading them and clamp the +multi-client flexible array count to the payload length. This keeps malformed +or truncated MCU events from stalling the threaded NAPI context. + +Assisted-by: Codex:gpt-5.5 +Signed-off-by: Jihong Min +--- + mt7996/mcu.c | 25 ++++++++++++++++++++++--- + 1 file changed, 22 insertions(+), 3 deletions(-) + +diff --git a/mt7996/mcu.c b/mt7996/mcu.c +--- a/mt7996/mcu.c ++++ b/mt7996/mcu.c +@@ -868,11 +868,13 @@ + static void + mt7996_mcu_rx_ps_sync(struct mt7996_dev *dev, struct sk_buff *skb) + { +- struct mt7996_mcu_ps_sync_event *event = (void *)skb->data; + struct tlv *tlv; + int len; + +- skb_pull(skb, sizeof(*event)); ++ if (unlikely(skb->len < sizeof(struct mt7996_mcu_ps_sync_event))) ++ return; ++ ++ skb_pull(skb, sizeof(struct mt7996_mcu_ps_sync_event)); + + len = skb->len; + while (len > sizeof(*tlv)) { +@@ -881,13 +883,16 @@ + tlv = (struct tlv *)skb->data; + tag = le16_to_cpu(tlv->tag); + tag_len = le16_to_cpu(tlv->len); +- if (tag_len > len) ++ if (tag_len < sizeof(*tlv) || tag_len > len) + break; + + switch (tag) { + case UNI_PS_CLIENT_INFO: { + struct mt7996_mcu_ps_client_info *info = (void *)tlv; + ++ if (tag_len < sizeof(*info)) ++ break; ++ + mt7996_mcu_ps_transition(dev, + le16_to_cpu(info->wlan_idx), + info->ps_bit); +@@ -895,9 +900,18 @@ + } + case UNI_PS_MULTI_CLIENT_INFO: { + struct mt7996_mcu_ps_multi_client_info *info = (void *)tlv; +- u16 cnt = le16_to_cpu(info->sta_cnt); ++ u16 cnt, max_cnt; + int i; + ++ if (tag_len < sizeof(*info)) ++ break; ++ ++ max_cnt = (tag_len - sizeof(*info)) / ++ sizeof(info->sta_ps_info[0]); ++ cnt = le16_to_cpu(info->sta_cnt); ++ if (cnt > max_cnt) ++ cnt = max_cnt; ++ + for (i = 0; i < cnt; i++) { + u16 entry = le16_to_cpu(info->sta_ps_info[i]); + +-- +2.43.0 diff --git a/package/luci-app-airoha-flowsense/Makefile b/package/luci-app-airoha-flowsense/Makefile new file mode 100644 index 00000000000000..af0ca5a452a286 --- /dev/null +++ b/package/luci-app-airoha-flowsense/Makefile @@ -0,0 +1,29 @@ +# This is free software, licensed under the Apache License, Version 2.0 . + +include $(TOPDIR)/rules.mk + +PKG_VERSION:=1.1.3 +PKG_RELEASE:=2 +PKG_LICENSE:=Apache-2.0 + +LUCI_TITLE:=LuCI Airoha FlowSense — Offload & PPE Monitor +LUCI_DEPENDS:=+luci-base @TARGET_airoha + +include ../../luci.mk + +define Package/luci-app-airoha-flowsense/conffiles +/etc/config/npu-monitor +endef + +define Package/luci-app-airoha-flowsense/postinst +#!/bin/sh +[ -x /etc/init.d/npu-jitter ] && /etc/init.d/npu-jitter restart +[ -n "$${IPKG_INSTROOT}" ] || { + rm -f /tmp/luci-indexcache.* + rm -rf /tmp/luci-modulecache/ + /etc/init.d/rpcd reload 2>/dev/null + exit 0 +} +endef + +# call BuildPackage - OpenWrt buildroot signature diff --git a/package/luci-app-airoha-flowsense/htdocs/luci-static/resources/view/airoha_flowsense/status.js b/package/luci-app-airoha-flowsense/htdocs/luci-static/resources/view/airoha_flowsense/status.js new file mode 100644 index 00000000000000..35f112b311ac07 --- /dev/null +++ b/package/luci-app-airoha-flowsense/htdocs/luci-static/resources/view/airoha_flowsense/status.js @@ -0,0 +1,1548 @@ +'use strict'; +'require view'; +'require poll'; +'require rpc'; +'require ui'; + +/* ── Drop-delta tracking (all counters are cumulative since interface up) ── */ +var _prevPseDrops = null; +var _prevCdmHwfDrops = null; +var _prevBridgeDrops = null; +var _prevPpeBnd = null; // for tachometer heartbeat +var _maxUnbSeen = 8; // UNB scale denominator — only grows, never shrinks +var _prevWifiRetry = {}; // keyed by band_idx: {tx_packets, tx_retries} +var _maxWifiThroughput = 1000; // legacy; superseded by per-band maxMbps in bandInfo +var _prevEthBytes = {}; // iface -> {tx, rx, time} +var _maxEthMbps = {}; // iface -> peak Mbps seen; grows, never shrinks + +/* ── RPC Declarations ── */ +var callNpuStatus = rpc.declare({ object: 'luci.airoha_flowsense', method: 'getStatus' }); +var callPpeEntries = rpc.declare({ object: 'luci.airoha_flowsense', method: 'getPpeEntries' }); +var callTokenInfo = rpc.declare({ object: 'luci.airoha_flowsense', method: 'getTokenInfo' }); +var callFrameEngine = rpc.declare({ object: 'luci.airoha_flowsense', method: 'getFrameEngine' }); +var callTxStats = rpc.declare({ object: 'luci.airoha_flowsense', method: 'getTxStats' }); +var callGetVlanOffload = rpc.declare({ object: 'luci.airoha_flowsense', method: 'getVlanOffload' }); +var callSetVlanOffload = rpc.declare({ object: 'luci.airoha_flowsense', method: 'setVlanOffload', params: ['enabled'] }); +var callGetFlowOffload = rpc.declare({ object: 'luci.airoha_flowsense', method: 'getFlowOffload' }); +var callSetFlowOffload = rpc.declare({ object: 'luci.airoha_flowsense', method: 'setFlowOffload', params: ['enabled'] }); +var callGetPppoeOffload = rpc.declare({ object: 'luci.airoha_flowsense', method: 'getPppoeOffload' }); +var callSetPppoeOffload = rpc.declare({ object: 'luci.airoha_flowsense', method: 'setPppoeOffload', params: ['enabled'] }); +var callGetDeviceMode = rpc.declare({ object: 'luci.airoha_flowsense', method: 'getDeviceMode' }); +var callGetNpuBypass = rpc.declare({ object: 'luci.airoha_flowsense', method: 'getNpuBypass' }); +var callGetWanHealth = rpc.declare({ object: 'luci.airoha_flowsense', method: 'getWanHealth' }); +var callGetJitterResult = rpc.declare({ object: 'luci.airoha_flowsense', method: 'getJitterResult' }); +var callGetConflictAlerts= rpc.declare({ object: 'luci.airoha_flowsense', method: 'getConflictAlerts' }); +var callGetWifiStats = rpc.declare({ object: 'luci.airoha_flowsense', method: 'getWifiStats' }); +var callGetBridgeStats = rpc.declare({ object: 'luci.airoha_flowsense', method: 'getBridgeStats' }); +var callGetEthStats = rpc.declare({ object: 'luci.airoha_flowsense', method: 'getEthStats' }); + +/* ── Theme-adaptive CSS ── */ +var themeCSS = '\ +.soc-card{background:var(--soc-card-bg);border:1px solid var(--soc-border);border-radius:8px;padding:14px;transition:border-color .3s}\ +.soc-card-accent{border-left-width:3px;border-left-style:solid}\ +.soc-muted{color:var(--soc-muted)}\ +.soc-text{color:var(--soc-text)}\ +.soc-label{font-size:11px;color:var(--soc-muted)}\ +.soc-bar-track{background:var(--soc-bar-track);border-radius:4px;overflow:hidden}\ +.soc-pse-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(130px,1fr));gap:6px}\ +.soc-pse-cell{background:var(--soc-card-bg);border:1px solid var(--soc-border);border-radius:5px;padding:6px 8px;font-size:12px}\ +.soc-band-grid{display:grid;grid-template-columns:repeat(3,1fr);gap:10px;margin-top:10px}\ +.soc-gdm-grid{display:grid;grid-template-columns:repeat(3,1fr);gap:10px;margin-bottom:10px}\ +.soc-cdm-grid{display:grid;grid-template-columns:repeat(2,1fr);gap:10px;margin-bottom:10px}\ +.compass-wrap{display:flex;flex-direction:row;align-items:flex-end;gap:2px;padding:4px 0;flex-wrap:wrap}\ +.eth-gauge-wrap{display:flex;flex-direction:row;gap:8px;flex-wrap:wrap;margin-top:8px}\ +.compass-svg-wrap{flex-shrink:0;max-width:326px;width:100%}\ +.compass-cards{display:flex;flex-direction:row;gap:8px;flex-wrap:wrap;margin-top:12px;margin-bottom:4px}\ +.compass-card{background:var(--soc-card-bg);border:1px solid var(--soc-border);border-radius:8px;padding:10px 14px;flex:1;min-width:140px}\ +.compass-card-title{font-size:10px;text-transform:uppercase;letter-spacing:1px;color:var(--soc-muted);margin-bottom:4px;font-family:monospace}\ +.compass-card-value{font-size:20px;font-weight:700;line-height:1.1;font-family:monospace}\ +.compass-card-sub{font-size:11px;color:var(--soc-muted);margin-top:3px}\ +.mode-banner{display:flex;align-items:center;gap:12px;padding:8px 14px;border-radius:6px;margin-bottom:8px;border:1px solid var(--soc-border);background:var(--soc-card-bg)}\ +.mode-badge{font-size:11px;font-weight:700;letter-spacing:1px;padding:3px 10px;border-radius:3px;font-family:monospace}\ +.mode-router{background:rgba(0,200,255,0.15);color:#00c8ff;border:1px solid rgba(0,200,255,0.35)}\ +.mode-ap{background:rgba(255,160,0,0.15);color:#ffa000;border:1px solid rgba(255,160,0,0.35)}\ +.offload-badge{font-size:13px;font-weight:700;letter-spacing:1px;padding:0 10px;border-radius:3px;font-family:monospace;display:inline-flex;align-items:center;align-self:stretch}\ +.offload-on{background:rgba(0,255,0,0.12);color:#00ff00;border:1px solid rgba(0,255,0,0.35)}\ +.offload-off{background:rgba(255,160,0,0.15);color:#ffa000;border:1px solid rgba(255,160,0,0.35)}\ +.alert-wrap{margin-bottom:8px}\ +.alert-item{display:flex;align-items:flex-start;gap:10px;padding:8px 12px;border-radius:5px;margin-bottom:5px;font-size:13px}\ +.alert-warning{border-left:3px solid #f5a623;background:rgba(245,166,35,0.1)}\ +.alert-error{border-left:3px solid #d0021b;background:rgba(208,2,27,0.1)}\ +.alert-icon{font-size:16px;line-height:1;flex-shrink:0;margin-top:1px}\ +.alert-title{font-weight:600;margin-bottom:2px}\ +.alert-msg{font-size:12px;color:var(--soc-muted)}\ +@keyframes sqm-pulse{0%{opacity:0.2}50%{opacity:1}100%{opacity:0.2}}\ +@keyframes ppe-blink{0%,100%{opacity:1}50%{opacity:0}}\ +.ppe-terminal{background:#0c0c0c;border:1px solid #2a2a2a;border-radius:6px;overflow:hidden;display:flex;flex-direction:column;flex:1;min-width:220px}\ +.ppe-terminal-bar{background:#1a1a1a;padding:5px 10px;display:flex;align-items:center;gap:5px;border-bottom:1px solid #2a2a2a;flex-shrink:0}\ +.ppe-terminal-dot{width:10px;height:10px;border-radius:50%;display:inline-block;flex-shrink:0}\ +.ppe-terminal-title{color:#555;font-size:10px;margin-left:6px;font-family:monospace;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}\ +.ppe-terminal-body{padding:8px 10px;overflow-y:auto;flex:1;font-size:10px;line-height:1.55;color:#ccc;font-family:"Courier New",Courier,monospace;white-space:pre-wrap;word-break:break-all;min-height:200px}\ +.ppe-cursor{animation:ppe-blink 1s step-end infinite}\ +'; + +function isDarkMode() { + var els = [document.body, document.querySelector('.main-content'), document.querySelector('#maincontent'), document.querySelector('.cbi-map')]; + for (var i = 0; i < els.length; i++) { + if (!els[i]) continue; + var bg = window.getComputedStyle(els[i]).backgroundColor; + var m = bg.match(/\d+/g); + if (m && m.length >= 3) { + var a = m.length >= 4 ? parseFloat(m[3]) : 1; + if (a < 0.1) continue; + var lum = (parseInt(m[0]) * 299 + parseInt(m[1]) * 587 + parseInt(m[2]) * 114) / 1000; + return lum < 128; + } + } + var sheets = document.querySelectorAll('link[href*="dark"], link[href*="glass"]'); + return sheets.length > 0; +} + +var _lastDarkMode = null; + +function injectCSS() { + var el = document.getElementById('soc-theme-css'); + if (!el) { el = document.createElement('style'); el.id = 'soc-theme-css'; document.head.appendChild(el); } + var dark = isDarkMode(); + if (dark === _lastDarkMode) return; + _lastDarkMode = dark; + var vars = dark + ? ':root{--soc-card-bg:#1e1e1e;--soc-border:#333;--soc-muted:#999;--soc-text:#e0e0e0;--soc-bar-track:#333}' + : ':root{--soc-card-bg:#fff;--soc-border:#d0d0d0;--soc-muted:#666;--soc-text:#222;--soc-bar-track:#e0e0e0}'; + el.textContent = themeCSS + vars; +} + +/* ── Existing Helpers ── */ +var bandInfo = [ + { name: '2.4 GHz', accent: '#FFFFFF', rtyCol: '#EFBF04', maxMbps: 688 }, + { name: '5 GHz', accent: '#FFFFFF', rtyCol: '#305CDE', maxMbps: 5765 }, + { name: '6 GHz', accent: '#FFFFFF', rtyCol: '#2CFF05', maxMbps: 11529 } +]; + + +function fmtFreq(khz) { return (!khz || khz === 0) ? 'N/A' : (khz / 1000).toFixed(0) + ' MHz'; } +function fmtK(n) { + if (!n || n === 0) return '0'; + if (n >= 1e6) return (n / 1e6).toFixed(1) + 'M'; + if (n >= 1e3) return (n / 1e3).toFixed(1) + 'K'; + return n.toString(); +} + +function calcTotalMem(regions) { + var t = 0; + (regions || []).forEach(function(r) { + var m = (r.size || '').match(/(\d+)\s*(KiB|MiB|GiB)/i); + if (m) { var s = parseInt(m[1]); var u = m[2][0].toUpperCase(); t += u === 'G' ? s*1048576 : u === 'M' ? s*1024 : s; } + }); + return t >= 1024 ? (t/1024).toFixed(0)+' MiB' : t+' KiB'; +} + +function pleHealth(free) { + if (typeof free !== 'number' || free < 0) return { text: 'N/A', color: '#888' }; + if (free >= 1000000) return { text: 'OK', color: '#4caf50' }; + if (free >= 100000) return { text: 'WARN', color: '#ff9800' }; + return { text: 'CRIT', color: '#f44336' }; +} + +function formatPleCount(free) { + if (typeof free !== 'number' || free < 0) return '—'; + if (free >= 1000000) return (free/1000000).toFixed(1) + 'M'; + if (free >= 1000) return Math.round(free/1000) + 'K'; + return free + ''; +} + +function getBandStats(ti, b) { + var c = Array.isArray(ti.station_counts) ? ti.station_counts : []; + for (var i=0;i0.5 ? {text:'Poor',color:'#f44336'} : r>0.2 ? {text:'Fair',color:'#ff9800'} : {text:'Good',color:'#4caf50'}; +} + +function retryPct(s) { + if (!s || !s.tx_packets) return '-'; + return (s.tx_retries/(s.tx_packets+s.tx_retries)*100).toFixed(1)+'%'; +} + +function getTxStatsBand(txs, band) { + var bands = txs && Array.isArray(txs.bands) ? txs.bands : []; + for (var i = 0; i < bands.length; i++) if (bands[i].band === band) return bands[i]; + return null; +} + +function perColor(per) { return per > 15 ? '#f44336' : per > 5 ? '#ff9800' : '#4caf50'; } + +function renderBandChip(band, txQ, stats, txs) { + var info = bandInfo[band] || { name: 'Band '+band, accent: '#888' }; + var id = 'band-'+band; + var h = bandHealth(stats); + var type = txQ ? txQ.type : '?'; + var rp = retryPct(stats); + var rows = [ + E('div', { 'style': 'display:flex;justify-content:space-between;align-items:center;margin-bottom:6px' }, [ + E('span', { 'class': 'soc-text', 'style': 'font-size:13px;font-weight:bold' }, info.name), + E('span', { 'style': 'background:'+(type==='npu'?'#1565c0':'#666')+';color:#fff;padding:1px 6px;border-radius:3px;font-size:9px;font-weight:600' }, type.toUpperCase()) + ]), + E('div', { 'style': 'display:flex;justify-content:space-between;align-items:center;font-size:12px' }, [ + E('div', { 'id': id+'-health', 'style': 'display:flex;align-items:center;gap:4px' }, [ + E('span', { 'style': 'width:7px;height:7px;border-radius:50%;background:'+h.color+';display:inline-block' }), + E('span', { 'style': 'color:'+h.color+';font-weight:500' }, h.text) + ]), + E('span', { 'id': id+'-clients', 'class': 'soc-muted' }, stats.count + ' sta'), + (stats.tx_packets > 0) ? E('span', { 'id': id+'-retries', 'class': 'soc-muted' }, rp) : E('span') + ]) + ]; + rows.push(E('div', { 'id': id+'-txstats', 'style': 'display:flex;gap:10px;font-size:11px;margin-top:5px;padding-top:4px;border-top:1px solid var(--soc-border)' }, + (txs && txs.attempts > 0) ? [ + E('span', { 'class': 'soc-muted' }, 'Drop:'), + E('span', { 'style': 'color:'+(txs.drops > 0 ? '#f44336' : '#4caf50') }, fmtK(txs.drops)), + E('span', { 'style': 'color:'+perColor(txs.per) }, txs.per+'%') + ] : [ E('span', { 'class': 'soc-muted' }, 'Drop: -') ] + )); + return E('div', { 'id': id, 'style': 'background:var(--soc-card-bg);border:1px solid var(--soc-border);border-left:2px solid '+info.accent+';border-radius:6px;padding:10px 12px' }, rows); +} + +function updateBandChip(band, stats) { + var id = 'band-'+band, h = bandHealth(stats); + var el = document.getElementById(id+'-health'); + if (el) { el.innerHTML = ''; el.appendChild(E('span',{'style':'width:6px;height:6px;border-radius:50%;background:'+h.color+';display:inline-block'})); el.appendChild(E('span',{'style':'color:'+h.color+';font-weight:500;font-size:11px'},h.text)); } + var cl = document.getElementById(id+'-clients'); if (cl) cl.textContent = stats.count+'sta'; + var re = document.getElementById(id+'-retries'); if (re) re.textContent = retryPct(stats); +} + +/* ── CPU Frequency State (used by CPU/NPU tachometer) ── */ +function freqBarState(hw, min, max, pll, gov) { + var pll_khz = (pll || 0) * 1000; + // cpufreq sysfs missing (e.g. AN7581 broken DVFS) — fall back to PLL hardware read + if (!hw && pll_khz > 0) + return { freq: pll_khz, max: pll_khz, oc: false }; + var oc = gov==='performance' && pll>0 && pll_khz>max; + return { freq: oc ? pll_khz : Math.min(hw,max), max: oc ? pll_khz : max, oc: oc }; +} + + + + + + +function renderVlanOffloadSelect(enabled) { + var cur = enabled ? '1' : '0'; + return E('select', { 'id': 'vlan-offload-select', 'class': 'cbi-input-select', 'style': 'min-width:140px', 'change': function(ev) { + var v = parseInt(ev.target.value); + ev.target.disabled = true; + callSetVlanOffload(v).then(function(r) { + ev.target.disabled = false; + if (r && r.error) ui.addNotification(null, E('p', {}, _('Error: ') + r.error), 'error'); + }).catch(function() { ev.target.disabled = false; }); + }}, [ + E('option', { 'value': '0', 'selected': cur === '0' ? '' : null }, _('Disabled')), + E('option', { 'value': '1', 'selected': cur === '1' ? '' : null }, _('Enabled')) + ]); +} + +function renderFlowOffloadSelect(enabled) { + var cur = enabled ? '1' : '0'; + return E('select', { 'id': 'flow-offload-select', 'class': 'cbi-input-select', 'style': 'min-width:140px', 'change': function(ev) { + var v = parseInt(ev.target.value); + ev.target.disabled = true; + callSetFlowOffload(v).then(function(r) { + ev.target.disabled = false; + if (r && r.error) ui.addNotification(null, E('p', {}, _('Error: ') + r.error), 'error'); + }).catch(function() { ev.target.disabled = false; }); + }}, [ + E('option', { 'value': '0', 'selected': cur === '0' ? '' : null }, _('Disabled')), + E('option', { 'value': '1', 'selected': cur === '1' ? '' : null }, _('Enabled')) + ]); +} + +function renderPppoeOffloadSelect(enabled) { + var cur = enabled ? '1' : '0'; + return E('select', { 'id': 'pppoe-offload-select', 'class': 'cbi-input-select', 'style': 'min-width:140px', 'change': function(ev) { + var v = parseInt(ev.target.value); + ev.target.disabled = true; + callSetPppoeOffload(v).then(function(r) { + ev.target.disabled = false; + if (r && r.error) ui.addNotification(null, E('p', {}, _('Error: ') + r.error), 'error'); + }).catch(function() { ev.target.disabled = false; }); + }}, [ + E('option', { 'value': '0', 'selected': cur === '0' ? '' : null }, _('Disabled')), + E('option', { 'value': '1', 'selected': cur === '1' ? '' : null }, _('Enabled')) + ]); +} + +/* ── PPE Panels ── */ +function renderPpePanel(label, labelColor, stateLabel, stateClass, entries, total, showNew) { + var rows = [ + E('tr', { 'class': 'tr cbi-section-table-titles' }, [ + E('th', { 'class': 'th', 'style': 'width:55px' }, _('Index')), + E('th', { 'class': 'th', 'style': 'width:45px' }, _('State')), + E('th', { 'class': 'th', 'style': 'width:65px' }, _('Type')), + E('th', { 'class': 'th' }, _('Original')) + ].concat(showNew ? [ E('th', { 'class': 'th' }, _('Translated')) ] : [])) + ]; + + (entries || []).forEach(function(e) { + rows.push(E('tr', { 'class': 'tr' }, [ + E('td', { 'class': 'td', 'style': 'font-size:11px;font-family:monospace' }, e.index||'-'), + E('td', { 'class': 'td' }, E('span', { 'class': stateClass, 'style': 'font-size:10px' }, stateLabel)), + E('td', { 'class': 'td', 'style': 'font-size:11px' }, (e.type||'').trim()), + E('td', { 'class': 'td', 'style': 'font-size:11px;font-family:monospace;word-break:break-all' }, e.orig||'-') + ].concat(showNew ? [ + E('td', { 'class': 'td', 'style': 'font-size:11px;font-family:monospace;word-break:break-all' }, e.new_flow||'-') + ] : []))); + }); + + if (!entries || entries.length === 0) { + rows.push(E('tr', { 'class': 'tr' }, [ + E('td', { 'class': 'td soc-muted', 'colspan': showNew ? '5' : '4', 'style': 'text-align:center;padding:12px' }, _('No entries')) + ])); + } + + var countText = total + ' entries' + (total > 25 ? ' (showing 25)' : ''); + return E('div', { 'style': 'flex:1;min-width:0' }, [ + E('div', { 'style': 'display:flex;align-items:baseline;gap:8px;margin-bottom:6px' }, [ + E('span', { 'style': 'font-weight:700;font-size:13px;color:'+labelColor }, label), + E('span', { 'class': 'soc-muted', 'style': 'font-size:11px', 'id': 'ppe-count-'+(showNew?'bnd':'unb') }, countText) + ]), + E('div', { 'style': 'overflow-x:auto' }, [ + E('table', { 'class': 'table', 'id': 'ppe-table-'+(showNew?'bnd':'unb'), 'style': 'font-size:11px' }, rows) + ]) + ]); +} + +function renderPpePanels(ppe) { + var bnd = ppe.bnd || { total: 0, entries: [] }; + var unb = ppe.unb || { total: 0, entries: [] }; + return E('div', { 'style': 'display:flex;gap:16px;align-items:flex-start' }, [ + renderPpePanel('BND — Hardware Offloaded', '#00c8ff', 'BND', 'label-success', bnd.entries, bnd.total, true), + renderPpePanel('UNB — Pending / Learning', '#4caf50', 'UNB', '', unb.entries, unb.total, false) + ]); +} + +function updatePpePanels(ppe) { + var bnd = ppe.bnd || { total: 0, entries: [] }; + var unb = ppe.unb || { total: 0, entries: [] }; + + function refreshPanel(tableId, countId, entries, total, showNew, stateLabel, stateClass) { + var el = document.getElementById(countId); + if (el) el.textContent = total + ' entries' + (total > 25 ? ' (showing 25)' : ''); + var tb = document.getElementById(tableId); + if (!tb) return; + while (tb.rows.length > 1) tb.deleteRow(1); + if (!entries || entries.length === 0) { + var row = tb.insertRow(-1); row.className = 'tr'; + var cell = row.insertCell(-1); cell.className = 'td soc-muted'; + cell.colSpan = showNew ? 5 : 4; + cell.style.textAlign = 'center'; cell.style.padding = '12px'; + cell.textContent = 'No entries'; + return; + } + entries.forEach(function(e) { + var row = tb.insertRow(-1); row.className = 'tr'; + var c1 = row.insertCell(-1); c1.className='td'; c1.style='font-size:11px;font-family:monospace'; c1.textContent=e.index||'-'; + var c2 = row.insertCell(-1); c2.className='td'; + var badge = document.createElement('span'); + if (stateClass) badge.className = stateClass; + badge.style.fontSize = '10px'; + badge.textContent = stateLabel; + c2.appendChild(badge); + var c3 = row.insertCell(-1); c3.className='td'; c3.style='font-size:11px'; c3.textContent=(e.type||'').trim(); + var c4 = row.insertCell(-1); c4.className='td'; c4.style='font-size:11px;font-family:monospace;word-break:break-all'; c4.textContent=e.orig||'-'; + if (showNew) { var c5=row.insertCell(-1); c5.className='td'; c5.style='font-size:11px;font-family:monospace;word-break:break-all'; c5.textContent=e.new_flow||'-'; } + }); + } + + refreshPanel('ppe-table-bnd', 'ppe-count-bnd', bnd.entries, bnd.total, true, 'BND', 'label-success'); + refreshPanel('ppe-table-unb', 'ppe-count-unb', unb.entries, unb.total, false, 'UNB', ''); +} + +/* ── PPE Tachometer (embedded inside compass inner fill) ── */ +function buildTachoInner(ppe, cs, mode) { + var bnd = ppe.bnd || {}; + var unb = ppe.unb || {}; + var bndTot = bnd.total || 0; + var unbTot = unb.total || 0; + var n4 = bnd.ipv4 || 0; + var n6 = bnd.ipv6 || 0; + + // Heartbeat: fires once when new BND flows arrive this poll + var pulsing = (_prevPpeBnd !== null && bndTot > _prevPpeBnd); + _prevPpeBnd = bndTot; + + var TICKS = 90; // 4° per tick + var cx = 150, cy = 150; + // BND inner (anti-CW): 1 flow = 1 tick, max 90 + var bndLit = Math.min(TICKS, bndTot); + // UNB outer (CW): sticky power-of-2 scale — grows when new peak is seen, never shrinks + // This prevents the ring going backwards when UNB count drops across a scale boundary + if (unbTot > _maxUnbSeen) _maxUnbSeen = unbTot; + var UNB_SCALE = Math.pow(2, Math.ceil(Math.log2(_maxUnbSeen + 1))); + UNB_SCALE = Math.max(UNB_SCALE, 8); + var unbLit = Math.min(TICKS, Math.round((unbTot / UNB_SCALE) * TICKS)); + + var modeText = mode === 'ap' ? 'AP MODE' : 'ROUTER'; + var statusText = cs.npuActive ? 'HW ACCELERATED' : (cs.hwEnabled ? 'NPU IDLE' : 'CPU PATH'); + var statusCol = cs.npuActive ? '#00c8ff' : (cs.hwEnabled ? '#888' : '#ff6b35'); + var bndColor = bndTot > 0 ? '#00c8ff' : 'var(--soc-muted)'; + var unbColor = unbTot > 0 ? '#ff9800' : 'var(--soc-muted)'; + + var p = []; + + // Inner fill + dashed boundary ring (replaces old compass inner circles) + p.push(''); + p.push(''); + // Guide rings at scaled tick boundaries + p.push(''); + p.push(''); + p.push(''); + // Ring labels — at 9 and 3 o'clock inside the BND inner ring (r<56 from centre) + p.push('◄BND'); + p.push('UNB►'); + + // Heartbeat pulse on BND ring when new flows arrive + if (pulsing) { + p.push(''); + } + + // 90 tick marks scaled to fit inside compass inner fill (r<98): + // BND inner anti-clockwise: r=57–67 + // UNB outer clockwise: r=71–81 + for (var i = 0; i < TICKS; i++) { + // UNB outer: clockwise (r=71–81) + var degU = (i / TICKS) * 360 - 90; + var radU = degU * Math.PI / 180; + var cU = Math.cos(radU), sU = Math.sin(radU); + var litU = i < unbLit; + var isEdgeU = litU && i === unbLit - 1; + p.push(''); + + // BND inner: anti-clockwise (r=57–67) + var degB = -90 - (i / TICKS) * 360; + var radB = degB * Math.PI / 180; + var cB = Math.cos(radB), sB = Math.sin(radB); + var litB = i < bndLit; + var isEdgeB = litB && i === bndLit - 1; + p.push(''); + } + + // Centre readout — mode + status at top, BND count large, IPv4/IPv6 split, UNB below + p.push(''+modeText+''); + p.push(''+statusText+''); + p.push(''+bndTot+''); + p.push('BND FLOWS'); + p.push('v4: '+n4+''); + p.push('v6: '+n6+''); + p.push(''+unbTot+''); + p.push('UNB FLOWS'); + + return p.join(''); +} + +/* ── PPE Terminal Panel ── */ +function buildPpeTerminalBody(ppe) { + var bnd = ppe.bnd || {}, unb = ppe.unb || {}; + var bndTot = bnd.total || 0, unbTot = unb.total || 0; + var bndE = bnd.entries || [], unbE = unb.entries || []; + + var grn = 'color:#55ff55'; + var wht = 'color:#e0e0e0'; + var dim = 'color:#555'; + var sep = 'color:#333'; + var mute = 'color:#666'; + var cyn = 'color:#00c8ff'; + var org = 'color:#ff9800'; + var pur = 'color:#9c27b0'; + var grey = 'color:#999'; + + function sp(style, text) { return ''+text+''; } + function esc(s) { return (s||'').replace(/&/g,'&').replace(//g,'>'); } + // pad: pads or truncates plain string to exactly n chars, then HTML-escapes + function pad(s, n) { + s = s || ''; + if (s.length > n) return esc(s.substring(0, n-1)) + '\u2026'; + var out = esc(s); + for (var i = s.length; i < n; i++) out += ' '; + return out; + } + + // Column widths (chars) + var W = { idx: 6, state: 6, type: 9, orig: 45, flow: 45, eth: 37 }; + var lineLen = W.idx+2+W.state+2+W.type+2+W.orig+2+W.flow+2+W.eth; + var divLine = '\u2500'.repeat(lineLen); + + function hdrRow() { + return sp(mute, + pad('Index',W.idx)+' '+pad('State',W.state)+' '+pad('Type',W.type)+' '+ + pad('Original Flow',W.orig)+' '+pad('New Flow',W.flow)+' '+'Ethernet' + ) + '\n'; + } + + function entryRow(e, origCol) { + var typeStr = (e.type||'') + (e.proto ? ' '+e.proto : ''); + var isV6 = (e.type||'').indexOf('IPv6') >= 0; + var ipCol = isV6 ? pur : grey; + var stCol = (e.state === 'BND' || e.state === 'BIND') ? cyn : org; + return sp(dim, pad(e.index||'????', W.idx)) + ' ' + + sp(stCol, pad(e.state||'', W.state)) + ' ' + + sp(ipCol, pad(typeStr, W.type)) + ' ' + + sp(origCol, pad(e.orig||'', W.orig)) + ' ' + + sp(mute, pad(e.new_flow||'-', W.flow)) + ' ' + + sp(grey, esc(e.eth||'-')) + '\n'; + } + + var s = ''; + + // Prompt + command + s += sp(grn,'root@OpenWrt') + sp(mute,':~# ') + sp(wht,'ppe status --watch') + '\n\n'; + + // BND section + s += sp(cyn,'■ BND') + ' ' + sp(wht, bndTot+' flows'); + s += ' ' + sp(mute,'(v4:'+(bnd.ipv4||0)+' v6:'+(bnd.ipv6||0)+')') + '\n'; + s += sp(sep, divLine) + '\n'; + + if (bndTot === 0) { + s += sp(mute,' no entries') + '\n'; + } else { + s += hdrRow(); + s += sp(sep, divLine) + '\n'; + bndE.forEach(function(e) { s += entryRow(e, cyn); }); + if (bndTot > bndE.length) s += sp(mute,' +' + (bndTot - bndE.length) + ' more\n'); + } + + s += '\n'; + + // UNB section + s += sp(org,'■ UNB') + ' ' + sp(wht, unbTot+' flows') + '\n'; + s += sp(sep, divLine) + '\n'; + + if (unbTot === 0) { + s += sp(mute,' no entries') + '\n'; + } else { + s += hdrRow(); + s += sp(sep, divLine) + '\n'; + unbE.forEach(function(e) { s += entryRow(e, org); }); + if (unbTot > unbE.length) s += sp(mute,' +' + (unbTot - unbE.length) + ' more\n'); + } + + s += '\n'; + s += sp(grn,'root@OpenWrt') + sp(mute,':~# ') + ''; + return s; +} + +function renderPpeTerminal(ppe) { + var bar = E('div', { 'class': 'ppe-terminal-bar' }, [ + E('span', { 'class': 'ppe-terminal-title' }, 'ppe_monitor — root@OpenWrt:~') + ]); + var body = E('div', { 'class': 'ppe-terminal-body', 'id': 'ppe-terminal-body' }); + body.innerHTML = buildPpeTerminalBody(ppe); + return E('div', { 'class': 'ppe-terminal' }, [ bar, body ]); +} + +/* ── Compass Math ── */ +function arcPt(cx, cy, r, deg) { + var rad = deg * Math.PI / 180; + return [cx + r * Math.cos(rad), cy + r * Math.sin(rad)]; +} + +function arcPath(cx, cy, r, startDeg, endDeg) { + var s = arcPt(cx, cy, r, startDeg); + var e = arcPt(cx, cy, r, endDeg); + var span = endDeg - startDeg; + if (span < 0) span += 360; + var large = span > 180 ? 1 : 0; + return 'M '+s[0].toFixed(1)+' '+s[1].toFixed(1)+ + ' A '+r+' '+r+' 0 '+large+' 1 '+ + e[0].toFixed(1)+' '+e[1].toFixed(1); +} + +function needleTip(latencyMs) { + // Full west arc 120°→240° (120° sweep). Log scale so low-latency range is sensitive. + // 0ms = 120° (lower-left, good), 100ms = 240° (upper-left, bad/near north) + var clamped = Math.min(Math.max(latencyMs||0, 0), 100); + var logPct = Math.log(clamped + 1) / Math.log(101); // 0→0, 100ms→1 + var deg = 120 + logPct * 120; + var rad = deg * Math.PI / 180; + return [150 + 75 * Math.cos(rad), 150 + 75 * Math.sin(rad)]; +} + +function latencyColor(ms) { + if (ms <= 20) return '#00cc44'; + if (ms <= 60) return '#f5a623'; + return '#d0021b'; +} + +/* ── Mode Banner ── */ +function renderModeBanner(dm) { + var mode = dm.mode || 'router'; + var reason = dm.reason || ''; + var reasonMap = { dhcp_disabled: 'DHCP disabled in UCI', no_wan: 'No WAN IP detected', local_gateway: 'Local gateway detected' }; + var reasonText = reasonMap[reason] || ''; + return E('div', { 'class': 'mode-banner' }, [ + E('span', { 'class': 'mode-badge ' + (mode==='ap' ? 'mode-ap' : 'mode-router') }, + mode === 'ap' ? 'AP MODE' : 'ROUTER MODE'), + E('span', { 'class': 'soc-muted', 'style': 'font-size:12px' }, 'Auto-detected' + (reasonText ? ' \u2014 '+reasonText : '')), + E('span', { 'id': 'mode-banner-status', 'style': 'margin-left:auto;font-size:12px;color:var(--soc-muted)' }, '') + ]); +} + +/* ── Conflict Alerts ── */ +function renderConflictAlerts(alertData) { + var alerts = (alertData && Array.isArray(alertData.alerts)) ? alertData.alerts : []; + if (!alerts.length) return E('div', { 'id': 'conflict-alerts' }); + var items = alerts.map(function(a) { + var isErr = a.severity === 'error'; + return E('div', { 'class': 'alert-item ' + (isErr ? 'alert-error' : 'alert-warning') }, [ + E('span', { 'class': 'alert-icon' }, isErr ? '\u26A0' : '\u26A1'), + E('div', {}, [ + E('div', { 'class': 'alert-title' }, a.title || ''), + E('div', { 'class': 'alert-msg' }, a.message || '') + ]) + ]); + }); + return E('div', { 'id': 'conflict-alerts', 'class': 'alert-wrap' }, items); +} + +/* ── HW Buffer Health (replaces SQM — NPU traffic bypasses qdisc entirely) ── */ +function hwBufferState(fe, ppe, mode) { + fe = fe || {}; ppe = ppe || {}; mode = mode || 'router'; + + // PSE port drops: cumulative across all internal ports (0-9). + // These include CDM/PPE internal paths that drop normally — not a reliable + // congestion signal on their own. Track for display only. + var ports = Array.isArray(fe.pse_ports) ? fe.pse_ports : []; + var pseDrops = 0; + ports.forEach(function(p) { pseDrops += (p.drops || 0); }); + + // CDM HW-forwarding drops — frames the NPU forwarded that CDM couldn't accept. + // More sensitive than GDM TX drops (which only fire at wire-level jam) and + // directly reflects NPU path congestion. + var cdmHwfDrops = ((fe.cdm1||{}).rx_hwf_drop||0) + ((fe.cdm2||{}).rx_hwf_drop||0); + + // Delta since last poll — null on first call (baseline only, no alarm) + var pseDelta = (_prevPseDrops !== null && pseDrops >= _prevPseDrops) ? (pseDrops - _prevPseDrops) : 0; + var cdmHwfDelta = (_prevCdmHwfDrops !== null && cdmHwfDrops >= _prevCdmHwfDrops) ? (cdmHwfDrops - _prevCdmHwfDrops) : 0; + _prevPseDrops = pseDrops; + _prevCdmHwfDrops = cdmHwfDrops; + + // DROPPING on CDM HW-forwarding drops or very high PSE bursts (>200/poll). + var activeDrop = cdmHwfDelta > 0 || pseDelta > 200; + + // PPE offload efficiency — BND/(BND+UNB). Shown in subtitle for info only. + // LOW OFFLOAD state removed: low BND% when idle is expected, not a problem. + var ppeBound = (ppe.bnd || {}).total || 0; + var ppeUnb = (ppe.unb || {}).total || 0; + var ppeTotal = ppeBound + ppeUnb; + var ppePct = ppeTotal > 0 ? Math.round(ppeBound / ppeTotal * 100) : 0; + + var color = activeDrop ? '#f5a623' : '#00cc44'; + return { + pseDrops: pseDrops, cdmHwfDrops: cdmHwfDrops, pseDelta: pseDelta, cdmHwfDelta: cdmHwfDelta, + activeDrop: activeDrop, + ppeBound: ppeBound, ppeTotal: ppeTotal, ppePct: ppePct, + color: color, pulsing: activeDrop + }; +} + +/* ── Compass SVG ── */ +function compassState(bypass, hwBuf, jitter, wan, wifi, bridge, mode) { + bypass = bypass || {}; hwBuf = hwBuf || {}; jitter = jitter || {}; + wan = wan || {}; wifi = wifi || {}; bridge = bridge || {}; + + var npuActive = bypass.npu_active === true; + var hwEnabled = bypass.hw_offload_enabled === true; + var cpuPct = bypass.cpu_pct || 0; + var wanMbps = bypass.wan_mbps || 0; + + // Latency — jitter daemon pings upstream and works in both router and AP mode + var latMs = jitter.last_ping || 0; + + // Integrity / errors + var errCount = 0; + var eastAlarm = false; + var worstSignal = 0; // dBm — 0 means no data; always negative when valid + // wbDelta holds per-band signal data; stored in cs so both render paths share it. + var wbDelta = []; + if (mode === 'router') { + errCount = (wan.rx_errors||0) + (wan.tx_errors||0); + eastAlarm = errCount > 0; + } else { + // AP mode: use per-station RSSI from iw station dump (signal avg field). + // min_signal = worst (lowest dBm) station on that band — most meaningful + // for link integrity since one weak client degrades the whole band's airtime. + (wifi.bands||[]).filter(function(b){ return (b.stations||0) > 0; }).forEach(function(b) { + var sig = b.min_signal || 0; + wbDelta.push({ band: b.band, stations: b.stations, signal: sig, avg_signal: b.avg_signal || 0 }); + if (sig !== 0 && (worstSignal === 0 || sig < worstSignal)) worstSignal = sig; + }); + // Alarm thresholds: < -75 dBm = weak link, < -82 dBm = poor link + eastAlarm = worstSignal !== 0 && worstSignal < -75; + } + + var eastColor; + if (mode === 'router') { + eastColor = eastAlarm ? '#d0021b' : '#00cc44'; + } else { + eastColor = worstSignal === 0 ? '#888' + : worstSignal < -82 ? '#d0021b' + : worstSignal < -75 ? '#f5a623' + : '#00cc44'; + } + return { + npuActive:npuActive, hwEnabled:hwEnabled, cpuPct:cpuPct, wanMbps:wanMbps, + hwBuf:hwBuf, mode:mode, + latMs:latMs, errCount:errCount, eastAlarm:eastAlarm, + wbDelta:wbDelta, worstSignal:worstSignal, + latColor:latencyColor(latMs), + eastColor: eastColor + }; +} + +function buildCompassSVG(cs, mode, ppe) { + var cx=150, cy=150; + var npuOpacity = cs.npuActive ? '1' : cs.hwEnabled ? '0.45' : '0.2'; + var cpuOpacity = !cs.hwEnabled ? '1' : cs.npuActive ? '0.2' : '0.45'; + var npuGlow = cs.npuActive ? ' filter="url(#f-cyan)"' : ''; + var cpuGlow = !cs.hwEnabled ? ' filter="url(#f-orange)"' : ''; + var eastOpacity = cs.eastAlarm ? '1' : '0.45'; + var eastGlow = cs.eastAlarm ? ' filter="url(#f-red)"' : ''; + var southOpacity= cs.hwBuf.pulsing ? '1' : '0.45'; + var southAnim = cs.hwBuf.pulsing ? ' style="animation:sqm-pulse 1.5s ease-in-out infinite"' : ''; + var tip = needleTip(cs.latMs); + var ppeRing = _cnPpeRingStyle(ppe); + + // Arc paths + var pNpuOuter = arcPath(cx,cy,132, 210,330); + var pNpuInner = arcPath(cx,cy,118, 210,330); + var pEast = arcPath(cx,cy,126, 300, 60); + var pSouth = arcPath(cx,cy,126, 30,150); + var pWest = arcPath(cx,cy,126, 120,240); + + // Text label paths (pre-computed at r=140 for north, r=138 for others) + // North CW 215→325: text curves along top, reads L→R + var tpN = 'M 35.3 69.7 A 140 140 0 0 1 264.7 69.7'; + // South CCW 150→30: text curves along bottom, reads L→R + var tpS = 'M 30.5 219.0 A 138 138 0 0 0 269.5 219.0'; + // East CW 300→60: text curves along right side, reads top→bottom + var tpE = 'M 219.0 30.5 A 138 138 0 0 1 219.0 269.5'; + // West CCW 240→120: text curves along left side, reads top→bottom + var tpW = 'M 81.0 30.5 A 138 138 0 0 0 81.0 269.5'; + + return '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + // Outer background + '' + + // Tachometer embedded inside compass inner fill + ''+buildTachoInner(ppe, cs, mode)+'' + + // North: NPU arc (outer, cyan) + '' + + // North: CPU arc (inner, orange) + '' + + // East: integrity arc + '' + + // South: HW buffer arc + '' + + // West: latency arc + '' + + // Quadrant labels — curved textPath following each arc + 'NPU PATH' + + 'HW BUFFER' + + 'INTEGRITY' + + 'LATENCY' + + // Latency needle + '' + + '' + + // PPE state ring outside compass disc — cyan=BND present, invisible otherwise + '' + + // Solid silver outer ring — outermost dashboard border + '' + + ''; +} + +function updateCompassSVG(cs, mode, ppe) { + function sa(id, attr, val) { var el=document.getElementById(id); if(el) el.setAttribute(attr, val); } + + var npuOpacity = cs.npuActive ? '1' : cs.hwEnabled ? '0.45' : '0.2'; + var cpuOpacity = !cs.hwEnabled ? '1' : cs.npuActive ? '0.2' : '0.45'; + var eastOpacity = cs.eastAlarm ? '1' : '0.45'; + var southOpacity= cs.hwBuf.pulsing ? '1' : '0.45'; + var tip = needleTip(cs.latMs); + + sa('cp-arc-npu', 'opacity', npuOpacity); + sa('cp-arc-cpu', 'opacity', cpuOpacity); + sa('cp-arc-east', 'stroke', cs.eastColor); + sa('cp-arc-east', 'opacity', eastOpacity); + sa('cp-arc-south', 'stroke', cs.hwBuf.color); + sa('cp-arc-south', 'opacity', southOpacity); + sa('cp-arc-west', 'stroke', cs.latColor); + + // SQM pulse animation + var south = document.getElementById('cp-arc-south'); + if (south) south.style.animation = cs.hwBuf.pulsing ? 'sqm-pulse 1.5s ease-in-out infinite' : ''; + + // NPU glow + var arcNpu = document.getElementById('cp-arc-npu'); + if (arcNpu) { if(cs.npuActive) arcNpu.setAttribute('filter','url(#f-cyan)'); else arcNpu.removeAttribute('filter'); } + var arcCpu = document.getElementById('cp-arc-cpu'); + if (arcCpu) { if(!cs.hwEnabled) arcCpu.setAttribute('filter','url(#f-orange)'); else arcCpu.removeAttribute('filter'); } + var arcEast = document.getElementById('cp-arc-east'); + if (arcEast) { if(cs.eastAlarm) arcEast.setAttribute('filter','url(#f-red)'); else arcEast.removeAttribute('filter'); } + + sa('cp-needle', 'x2', tip[0].toFixed(1)); + sa('cp-needle', 'y2', tip[1].toFixed(1)); + sa('cp-needle', 'stroke',cs.latColor); + sa('cp-needle-pivot', 'fill', cs.latColor); + sa('cp-lbl-south', 'fill', cs.hwBuf.color); + sa('cp-lbl-west', 'fill', cs.latColor); + sa('cp-lbl-east', 'fill', cs.eastColor); + + // Rebuild tachometer group (also updates mode/status text inside) + var tg = document.getElementById('cp-tacho'); + if (tg) tg.innerHTML = buildTachoInner(ppe, cs, mode); + + // PPE state ring on compass outer edge + var ppeGlow = document.getElementById('cp-ppe-glow'); + if (ppeGlow) { + var ppeRing = _cnPpeRingStyle(ppe); + ppeGlow.setAttribute('stroke', ppeRing.color); + ppeGlow.setAttribute('style', ppeRing.style); + } +} + +/* ── CPU/NPU Load Tachometer ── */ +function buildCpuNpuTacho(cs, ppe, st, ti) { + st = st || {}; + var cpuPct = cs.cpuPct || 0; + var ppeBound = (ppe.bnd || {}).total || 0; + var ppeUnb = (ppe.unb || {}).total || 0; + var ppeTotal = ppeBound + ppeUnb; + var offloadPct = ppeTotal > 0 ? Math.round(ppeBound / ppeTotal * 100) + : (cs.npuActive ? 100 : 0); + + // CPU frequency — same source as the existing freq bar + var fs = freqBarState(st.cpu_hw_freq, st.cpu_min_freq, st.cpu_max_freq, st.pll_freq_mhz, st.cpu_governor); + var freqMhz = Math.round(fs.freq / 1000); + var governor = (st.cpu_governor && st.cpu_governor !== 'unknown') ? st.cpu_governor.toUpperCase() : ''; + + // Colours + var npuStatusCol = cs.npuActive ? '#00c8ff' : (cs.hwEnabled ? '#888' : '#ff6b35'); + var npuStatus = cs.npuActive ? 'HW ACCELERATED' : (cs.hwEnabled ? 'NPU IDLE' : 'CPU PATH'); + var npuColor = offloadPct > 60 ? '#00c8ff' : offloadPct > 30 ? '#f5a623' : '#888'; + + var TICKS = 90; + // Outer ring (CW, r=71-81): CPU frequency, 500–1400 MHz + var FREQ_MIN = 500, FREQ_MAX = 1400; + var freqLit = Math.round(Math.max(0, Math.min(TICKS, (freqMhz - FREQ_MIN) / (FREQ_MAX - FREQ_MIN) * TICKS))); + // Inner ring (anti-CW, r=57-67): CPU load, 0–100% = full scale + var cpuLit = Math.round(Math.min(cpuPct, 100) / 100 * TICKS); + + var cx = 150, cy = 150; + var p = []; + + // Inner fill + boundary rings + p.push(''); + p.push(''); + p.push(''); + p.push(''); + p.push(''); + + // Ring labels at 9 and 3 o'clock + p.push('◄LOAD'); + p.push('FREQ►'); + + // Tachometer ticks + for (var i = 0; i < TICKS; i++) { + // Outer ring (CW, r=71-81): CPU frequency + var degU = (i / TICKS) * 360 - 90; + var radU = degU * Math.PI / 180; + var cU = Math.cos(radU), sU = Math.sin(radU); + var litU = i < freqLit, isEdgeU = litU && i === freqLit - 1; + p.push(''); + + // Inner ring (anti-CW, r=57-67): CPU load + var degB = -90 - (i / TICKS) * 360; + var radB = degB * Math.PI / 180; + var cB = Math.cos(radB), sB = Math.sin(radB); + var litB = i < cpuLit, isEdgeB = litB && i === cpuLit - 1; + p.push(''); + } + + // Centre readout + if (governor) p.push(''+governor+''); + if (freqMhz) p.push(''+freqMhz+' MHz'); + p.push(''+cpuPct+'%'); + p.push('CPU LOAD'); + p.push(''+npuStatus+''); + p.push(''+offloadPct+'%'); + p.push('OFFLOADED'); + + // PLE pool — curved textPath at r=91, south arc (CCW 160°→20°), outside the freq ring. + // Matches the "UNB/BND FLOWS" curved-text style used on the WiFi band gauges. + var pleFree = (ti && typeof ti.ple_free === 'number') ? ti.ple_free : -1; + var pleH = pleHealth(pleFree); + var pleStr = formatPleCount(pleFree); + var plR = 91; + var plSX = (150 + plR * Math.cos(160 * Math.PI / 180)).toFixed(1); + var plSY = (150 + plR * Math.sin(160 * Math.PI / 180)).toFixed(1); + var plEX = (150 + plR * Math.cos( 20 * Math.PI / 180)).toFixed(1); + var plEY = (150 + plR * Math.sin( 20 * Math.PI / 180)).toFixed(1); + p.push(''); + p.push('PLE '+pleStr+' ● '+pleH.text+''); + + return p.join(''); +} + +function _cnPpeRingStyle(ppe, ti) { + // PLE pool health overrides the BND-based cyan default when the WiFi TX buffer pool + // drops toward zero — that's the precursor signal for mt7996 SER / TX wedge. + var ple = (ti && typeof ti.ple_free === 'number') ? ti.ple_free : -1; + if (ple >= 0 && ple < 100000) { + return { style: 'filter:blur(6px);opacity:0.85', color: '#f44336' }; + } + if (ple >= 0 && ple < 1000000) { + return { style: 'filter:blur(5px);opacity:0.7', color: '#ff9800' }; + } + + // Default: cyan-on-BND, invisible when no BND + var bnd = (ppe && ppe.bnd) ? (ppe.bnd.total || 0) : 0; + if (bnd === 0) return { style: 'opacity:0', color: '#00c8ff' }; + var intensity = Math.min(1, bnd / 100); + var blur = (3 + intensity * 6).toFixed(1); + var op = (0.5 + intensity * 0.45).toFixed(2); + return { style: 'filter:blur('+blur+'px);opacity:'+op, color: '#00c8ff' }; +} + +function buildCpuNpuCompassSVG(cs, ppe, st, ti) { + // viewBox tightly wraps the tachometer (r=102 outer circle + glow headroom + silver ring) + var ring = _cnPpeRingStyle(ppe, ti); + return '' + + '' + + '' + + '' + + '' + + // PPE/PLE-state ring rendered BEFORE outer circle so the circle fill covers the inward bleed + '' + + // Outer border — tight around the tachometer content + '' + + ''+buildCpuNpuTacho(cs, ppe, st, ti)+'' + + // Solid silver outer ring — sits outside the PPE/PLE glow ring + '' + + ''; +} + +function updateCpuNpuCompassSVG(cs, ppe, st, ti) { + // Update PPE/PLE-state ring on the outer border + var glow = document.getElementById('cn-glow'); + if (glow) { + var ring = _cnPpeRingStyle(ppe, ti); + glow.setAttribute('stroke', ring.color); + glow.setAttribute('style', ring.style); + } + + var tg = document.getElementById('cn-tacho'); + if (tg) tg.innerHTML = buildCpuNpuTacho(cs, ppe, st, ti); +} + +/* ── WiFi Band Tachometers ── */ +function _wifiBandHealth(ws) { + var stations = ws ? (ws.stations || 0) : 0; + if (stations === 0) return { text: 'NO CLIENTS', color: '#888' }; + var rty = ws.retry_pct || 0; + if (rty > 50) return { text: 'WEAK', color: '#f44336' }; + if (rty > 20) return { text: 'FAIR', color: '#ff9800' }; + return { text: 'GOOD', color: '#4caf50' }; +} + +function _wifiPpeRingStyle(ws, ppe, bandIdx) { + ws = ws || {}; + // No ring if this band has no connected clients + if ((ws.stations || 0) === 0) return { style: 'opacity:0', color: '#888' }; + // Per-band BND count drives cyan; invisible when no BND flows for this band + var bandBnd = (ppe && ppe.bnd && Array.isArray(ppe.bnd.band_bnd)) ? (ppe.bnd.band_bnd[bandIdx] || 0) : 0; + if (bandBnd === 0) return { style: 'opacity:0', color: '#888' }; + // BND >= 1 for this band: cyan, brighter as count grows (saturates ~100 flows) + var intensity = Math.min(1, bandBnd / 100); + var blur = (3 + intensity * 6).toFixed(1); + var op = (0.5 + intensity * 0.45).toFixed(2); + return { style: 'filter:blur('+blur+'px);opacity:'+op, color: '#00c8ff' }; +} + +function buildWifiBandTacho(bandIdx, ws, qType, bndCount, unbCount) { + ws = ws || {}; + var info = bandInfo[bandIdx] || { name: 'Band '+bandIdx, accent: '#888' }; + var accent = info.accent; + var retryCol = info.rtyCol || '#888'; + // Effective throughput: link capacity × success rate (1 − retry fraction) + // Uses avg_exp_throughput since MT7996 HW TX path bypasses all kernel byte counters + var mbps = (ws.avg_exp_throughput || 0) * (100 - (ws.retry_pct || 0)) / 100; + var retryVal = ws.retry_pct || 0; + var stations = ws.stations || 0; + var signal = ws.avg_signal || 0; + var isNpu = qType === 'npu'; + + var maxScale = info.maxMbps || 1000; + + var TICKS = 90; + var txLit = Math.round(Math.min(TICKS, (mbps / maxScale) * TICKS)); + var retryLit = Math.round(retryVal / 100 * TICKS); // 0–100% maps to 0–90 ticks + + var cx = 150, cy = 150; + var p = []; + + // Background boundary rings + p.push(''); + p.push(''); + p.push(''); + p.push(''); + p.push(''); + + // Ring labels at 9 / 3 o'clock + p.push('◄RTY'); + p.push('TX►'); + + // Tachometer ticks + for (var i = 0; i < TICKS; i++) { + // Outer ring CW (r=71-81): TX throughput + var degU = (i / TICKS) * 360 - 90; + var radU = degU * Math.PI / 180; + var cU = Math.cos(radU), sU = Math.sin(radU); + var litU = i < txLit, edgeU = litU && i === txLit - 1; + p.push(''); + + // Inner ring anti-CW (r=57-67): retry health + var degB = -90 - (i / TICKS) * 360; + var radB = degB * Math.PI / 180; + var cB = Math.cos(radB), sB = Math.sin(radB); + var litB = i < retryLit, edgeB = litB && i === retryLit - 1; + p.push(''); + } + + // UNB count arc at 12 o'clock (curved textPath, r=91, CW 200°→340°) + var unbCnt = unbCount || 0; + var uR = 91; + var uSX = (150 + uR * Math.cos(200 * Math.PI / 180)).toFixed(1); + var uSY = (150 + uR * Math.sin(200 * Math.PI / 180)).toFixed(1); + var uEX = (150 + uR * Math.cos(340 * Math.PI / 180)).toFixed(1); + var uEY = (150 + uR * Math.sin(340 * Math.PI / 180)).toFixed(1); + var uPid = 'wifi-u-arc-'+bandIdx; + var unbCol = unbCnt > 0 ? '#ff6b35' : '#555'; + p.push(''); + p.push(''+unbCnt+' UNB'); + + // BND count arc at 6 o'clock (curved textPath, r=91, CCW 160°→20°) — opposite health arc + var bndCnt = bndCount || 0; + var bR = 91; + var bSX = (150 + bR * Math.cos(160 * Math.PI / 180)).toFixed(1); + var bSY = (150 + bR * Math.sin(160 * Math.PI / 180)).toFixed(1); + var bEX = (150 + bR * Math.cos(20 * Math.PI / 180)).toFixed(1); + var bEY = (150 + bR * Math.sin(20 * Math.PI / 180)).toFixed(1); + var bPid = 'wifi-b-arc-'+bandIdx; + var bndCol = bndCnt > 0 ? '#00c8ff' : '#555'; + p.push(''); + p.push(''+bndCnt+' BND'); + + // Centre readout — retry % above band name + if (retryVal > 0) + p.push(''+retryVal+'% RTY'); + p.push(''+info.name.toUpperCase()+''); + + // NPU/DMA badge (rect + text) + var badgeCol = isNpu ? '#1565c0' : '#444'; + p.push(''); + p.push(''+(isNpu?'NPU':'DMA')+''); + + // Main throughput value + var mbpsLabel = mbps > 0 ? Math.round(mbps).toString() : (stations > 0 ? '0' : '\u2014'); + p.push(''+mbpsLabel+''); + p.push('MBPS'); + + // Max scale hint + p.push('max '+Math.round(maxScale)+''); + + // Station count + p.push(''+stations+(stations===1?' STA':' STA')+''); + + // Signal + retry (only when relevant) + if (stations > 0 && signal !== 0) + p.push(''+signal+' dBm'); + + return p.join(''); +} + +function buildWifiBandSVG(bandIdx, ws, qType, ppe) { + var idx = bandIdx; + var ring = _wifiPpeRingStyle(ws, ppe, bandIdx); + var bndCount = (ppe && ppe.bnd && Array.isArray(ppe.bnd.band_bnd)) ? (ppe.bnd.band_bnd[bandIdx] || 0) : 0; + var unbCount = (ppe && ppe.unb && Array.isArray(ppe.unb.band_unb)) ? (ppe.unb.band_unb[bandIdx] || 0) : 0; + return '' + + '' + + '' + + '' + + '' + + '' + + '' + + ''+buildWifiBandTacho(bandIdx, ws, qType, bndCount, unbCount)+'' + + '' + + ''; +} + +function updateWifiBandSVG(bandIdx, ws, qType, ppe) { + var glowEl = document.getElementById('wifi-glow-'+bandIdx); + if (glowEl) { + var ring = _wifiPpeRingStyle(ws, ppe, bandIdx); + glowEl.setAttribute('stroke', ring.color); + glowEl.setAttribute('style', ring.style); + } + var tg = document.getElementById('wifi-tacho-'+bandIdx); + var bndCount = (ppe && ppe.bnd && Array.isArray(ppe.bnd.band_bnd)) ? (ppe.bnd.band_bnd[bandIdx] || 0) : 0; + var unbCount = (ppe && ppe.unb && Array.isArray(ppe.unb.band_unb)) ? (ppe.unb.band_unb[bandIdx] || 0) : 0; + if (tg) tg.innerHTML = buildWifiBandTacho(bandIdx, ws, qType, bndCount, unbCount); +} + +// Returns array of 3 elements (6 GHz, 5 GHz, 2.4 GHz) for direct inclusion in compass-wrap +function buildWifiTachoElements(wifi, ti, st, ppe) { + var bands = (wifi && Array.isArray(wifi.bands)) ? wifi.bands : []; + var fallbackType = (st && st.npu_loaded) ? 'npu' : 'dma'; + var elems = []; + // 6 GHz → 5 GHz → 2.4 GHz (band index 2 → 1 → 0) + for (var b = 2; b >= 0; b--) { + var ws = null; + for (var j = 0; j < bands.length; j++) if (bands[j].band === b) { ws = bands[j]; break; } + var txQ = getTxQueue(ti, b) || { type: fallbackType }; + var svgWrap = E('div', { 'id': 'wifi-svg-wrap-'+b }); + svgWrap.innerHTML = buildWifiBandSVG(b, ws, txQ.type, ppe); + elems.push(svgWrap); + } + return elems; +} + +/* ── Ethernet Port Horizontal Bar Gauges ── */ +function _ethLabel(iface) { + var m = { wan:'WAN', lan1:'LAN 1', lan2:'LAN 2', lan3:'LAN 3', lan4:'LAN 4' }; + return m[iface] || iface.toUpperCase(); +} +function _ethSpeed(speed) { + if (!speed || speed <= 0) return 'NO LINK'; + if (speed >= 10000) return '10G'; + if (speed >= 5000) return '5G'; + if (speed >= 2500) return '2.5G'; + if (speed >= 1000) return '1G'; + if (speed >= 100) return '100M'; + return speed + 'M'; +} +function _ethFmt(mbps) { + if (mbps >= 100) return mbps.toFixed(0); + if (mbps >= 10) return mbps.toFixed(1); + return mbps.toFixed(2); +} + +function buildEthPortSVG(port, txMbps, rxMbps, ppe) { + var iface = port.iface || ''; + var isWan = (iface === 'wan'); + var up = !!port.up; + var maxSc = _maxEthMbps[iface] || 100; + var barW = 140; + var txPct = up ? Math.min(1, txMbps / maxSc) : 0; + var rxPct = up ? Math.min(1, rxMbps / maxSc) : 0; + var txW = (txPct * barW).toFixed(1); + var rxW = (rxPct * barW).toFixed(1); + var txVal = up ? _ethFmt(txMbps) : '--'; + var rxVal = up ? _ethFmt(rxMbps) : '--'; + var label = _ethLabel(iface); + var spLbl = _ethSpeed(up ? (port.speed || 0) : 0); + var portClr = up ? (isWan ? '#00ffff' : '#00ff00') : '#555'; + var dimOp = up ? '1' : '0.4'; + // Footer: WAN shows wired BND (total minus per-band WiFi BND); LAN ports show per-port BND from bridge FDB match + var footerTxt, footerClr; + if (isWan) { + if (up) { + var bndTotal = (ppe && ppe.bnd) ? (ppe.bnd.total || 0) : 0; + var bandBnd = (ppe && ppe.bnd && ppe.bnd.band_bnd) ? ppe.bnd.band_bnd : [0,0,0]; + var wifiBnd = (bandBnd[0]||0) + (bandBnd[1]||0) + (bandBnd[2]||0); + var wiredBnd = Math.max(0, bndTotal - wifiBnd); + var unbTotal = (ppe && ppe.unb) ? (ppe.unb.total || 0) : 0; + var bandUnb = (ppe && ppe.unb && ppe.unb.band_unb) ? ppe.unb.band_unb : [0,0,0]; + var wifiUnb = (bandUnb[0]||0) + (bandUnb[1]||0) + (bandUnb[2]||0); + var wiredUnb = Math.max(0, unbTotal - wifiUnb); + footerTxt = 'BND: ' + wiredBnd + ' UNB: ' + wiredUnb; + footerClr = '#00c8ff'; + } else { + footerTxt = ''; + footerClr = '#555'; + } + } else { + var portIdx = {lan1:0, lan2:1, lan3:2, lan4:3}[iface]; + var bndPort = (ppe && ppe.bnd && ppe.bnd.port_bnd) ? (ppe.bnd.port_bnd[portIdx] || 0) : 0; + footerTxt = 'BND: ' + bndPort; + footerClr = bndPort > 0 ? '#00c8ff' : '#555'; + } + + return '' + + '' + + '' + + ''+label+'' + + ''+spLbl+'' + + '' + + 'TX' + + '' + + (txPct > 0 ? '' : '') + + ''+txVal+'' + + 'Mb' + + 'RX' + + '' + + (rxPct > 0 ? '' : '') + + ''+rxVal+'' + + 'Mb' + + ''+footerTxt+'' + + ''; +} + +function updateEthPortSVG(port, txMbps, rxMbps, ppe) { + var wrap = document.getElementById('eth-port-svg-' + port.iface); + if (wrap) wrap.innerHTML = buildEthPortSVG(port, txMbps, rxMbps, ppe); +} + +function buildEthGaugeRow(ethPorts, ppe) { + var wrap = E('div', { 'class': 'eth-gauge-wrap', 'id': 'eth-gauge-wrap' }); + ethPorts.forEach(function(p) { + var div = E('div', { 'id': 'eth-port-svg-' + p.iface, 'style': 'flex:1;min-width:140px' }); + div.innerHTML = buildEthPortSVG(p, 0, 0, ppe); + wrap.appendChild(div); + }); + return wrap; +} + +/* ── Compass Data Cards ── */ +function renderCompassCards(cs, bypass, jitter, wan, wifi, bridge, mode) { + bypass=bypass||{}; jitter=jitter||{}; wan=wan||{}; wifi=wifi||{}; + + // North card: NPU Path + bridge = bridge || {}; + var rawBridgeDrops = bridge.tx_dropped || 0; + var bridgeDelta = (_prevBridgeDrops !== null && rawBridgeDrops >= _prevBridgeDrops) ? (rawBridgeDrops - _prevBridgeDrops) : 0; + _prevBridgeDrops = rawBridgeDrops; + var northVal = cs.npuActive ? 'ACTIVE' : (cs.hwEnabled ? 'IDLE' : 'CPU PATH'); + var northColor = cs.npuActive ? '#00c8ff' : (cs.hwEnabled ? '#888' : '#ff6b35'); + var northSub = mode === 'ap' + ? 'CPU: '+cs.cpuPct+'% | Bridge drops Δ: '+bridgeDelta + : 'CPU: '+cs.cpuPct+'% | WAN: '+cs.wanMbps+' Mbps'; + + // East card: Integrity + var eastVal, eastSub; + if (mode === 'router') { + eastVal = cs.eastAlarm ? cs.errCount+' ERROR'+(cs.errCount>1?'S':'') : 'CLEAN'; + eastSub = 'RX errors: '+(wan.rx_errors||0)+' TX errors: '+(wan.tx_errors||0); + } else { + var ws = cs.worstSignal; + eastVal = cs.wbDelta.length === 0 ? 'NO CLIENTS' + : ws === 0 ? 'NO DATA' + : ws < -82 ? 'POOR' + : ws < -75 ? 'WEAK' + : 'CLEAN'; + var bnames = ['2.4G','5G','6G']; + eastSub = cs.wbDelta.length > 0 + ? 'Signal: '+cs.wbDelta.map(function(b){ return (bnames[b.band]||('B'+b.band))+': '+b.signal+' dBm'; }).join(' | ') + : 'No connected clients'; + } + var eastColor = cs.eastColor; + + // South card: HW Buffer Health + var hb = cs.hwBuf || {}; + var southVal = hb.activeDrop ? 'DROPPING' : 'HEALTHY'; + var southColor = hb.color || '#00cc44'; + var southSub = 'PSE Δ: '+hb.pseDelta+' CDM Δ: '+hb.cdmHwfDelta+' | PPE: '+hb.ppePct+'% BND ('+hb.ppeBound+'/'+hb.ppeTotal+')'; + + // West card: Latency + var latVal = cs.latMs > 0 ? cs.latMs.toFixed(1)+'ms' : (jitter.available===false ? 'N/A' : '---'); + var latColor = cs.latColor; + var latSub = 'Jitter: '+(jitter.jitter||0).toFixed(1)+'ms | '+(jitter.samples||0)+' samples | '+(jitter.target||'1.1.1.1'); + + function card(title, val, color, sub) { + return E('div', { 'class': 'compass-card' }, [ + E('div', { 'class': 'compass-card-title' }, title), + E('div', { 'class': 'compass-card-value', 'style': 'color:'+color }, val), + E('div', { 'class': 'compass-card-sub' }, sub) + ]); + } + + return E('div', { 'class': 'compass-cards', 'id': 'compass-cards' }, [ + card('NPU Path', northVal, northColor, northSub), + card('Integrity', eastVal, eastColor, eastSub), + card('Latency', latVal, latColor, latSub), + card('HW Buffer', southVal, southColor, southSub) + ]); +} + +function updateCompassCards(cs, bypass, jitter, wan, wifi, bridge, mode) { + var cards = document.getElementById('compass-cards'); + if (!cards) return; + var divs = cards.querySelectorAll('.compass-card'); + if (divs.length < 4) return; + + bypass=bypass||{}; jitter=jitter||{}; wan=wan||{}; wifi=wifi||{}; bridge=bridge||{}; + + function setCard(div, val, color, sub) { + var v = div.querySelector('.compass-card-value'); + var s = div.querySelector('.compass-card-sub'); + if (v) { v.textContent=val; v.style.color=color; } + if (s) s.textContent=sub; + } + + var rawBridgeDrops2 = bridge.tx_dropped || 0; + var bridgeDelta2 = (_prevBridgeDrops !== null && rawBridgeDrops2 >= _prevBridgeDrops) ? (rawBridgeDrops2 - _prevBridgeDrops) : 0; + _prevBridgeDrops = rawBridgeDrops2; + + setCard(divs[0], cs.npuActive?'ACTIVE':(cs.hwEnabled?'IDLE':'CPU PATH'), + cs.npuActive?'#00c8ff':(cs.hwEnabled?'#888':'#ff6b35'), + mode==='ap' + ?'CPU: '+cs.cpuPct+'% | Bridge drops Δ: '+bridgeDelta2 + :'CPU: '+cs.cpuPct+'% | WAN: '+cs.wanMbps+' Mbps'); + + if (mode === 'router') { + setCard(divs[1], cs.eastAlarm?cs.errCount+' ERROR'+(cs.errCount>1?'S':''):'CLEAN', + cs.eastColor, + 'RX errors: '+(wan.rx_errors||0)+' TX errors: '+(wan.tx_errors||0)); + } else { + var ws2 = cs.worstSignal; + var bnames2 = ['2.4G','5G','6G']; + setCard(divs[1], + cs.wbDelta.length === 0 ? 'NO CLIENTS' + : ws2 === 0 ? 'NO DATA' + : ws2 < -82 ? 'POOR' + : ws2 < -75 ? 'WEAK' + : 'CLEAN', + cs.eastColor, + cs.wbDelta.length > 0 + ? 'Signal: '+cs.wbDelta.map(function(b){ return (bnames2[b.band]||('B'+b.band))+': '+b.signal+' dBm'; }).join(' | ') + : 'No connected clients'); + } + + var latVal = cs.latMs > 0 ? cs.latMs.toFixed(1)+'ms' : (jitter.available===false?'N/A':'---'); + setCard(divs[2], latVal, cs.latColor, + 'Jitter: '+(jitter.jitter||0).toFixed(1)+'ms | '+(jitter.samples||0)+' samples | '+(jitter.target||'1.1.1.1')); + + var hb = cs.hwBuf || {}; + setCard(divs[3], + hb.activeDrop?'DROPPING':'HEALTHY', + hb.color||'#00cc44', + 'PSE Δ: '+hb.pseDelta+' CDM Δ: '+hb.cdmHwfDelta+' | PPE: '+hb.ppePct+'% BND ('+hb.ppeBound+'/'+hb.ppeTotal+')'); +} + +/* ── Main View ── */ +return view.extend({ + load: function() { + return Promise.all([ + callNpuStatus(), // d[0] + callPpeEntries(), // d[1] + callTokenInfo(), // d[2] + callFrameEngine(), // d[3] + callGetVlanOffload(), // d[4] + callTxStats(), // d[5] + callGetDeviceMode(), // d[6] + callGetNpuBypass(), // d[7] + callGetWanHealth(), // d[8] + callGetJitterResult(), // d[9] + callGetConflictAlerts(),// d[10] + callGetWifiStats(), // d[11] + callGetBridgeStats(), // d[12] + callGetFlowOffload(), // d[13] + callGetPppoeOffload(), // d[14] + callGetEthStats() // d[15] + ]); + }, + + render: function(data) { + injectCSS(); + var st=data[0]||{}, ppe=data[1]||{}, ti=data[2]||{}, fe=data[3]||{}; + var vo=data[4]||{}, txs=data[5]||{}, dm=data[6]||{}; + var bypass=data[7]||{}, wan=data[8]||{}; + var jitter=data[9]||{}, alertData=data[10]||{}; + var wifi=data[11]||{}, bridge=data[12]||{}; + var flo=data[13]||{}, ppo=data[14]||{}; + var eth=data[15]||{}; + var memR = Array.isArray(st.memory_regions) ? st.memory_regions : []; + var mode = dm.mode || 'router'; + + var hwBuf = hwBufferState(fe, ppe, mode); + var cs = compassState(bypass, hwBuf, jitter, wan, wifi, bridge, mode); + + // Compass SVG container — tachometer is embedded inside (innerHTML so we can update by element ID) + var compassSvgWrap = E('div', { 'class': 'compass-svg-wrap', 'id': 'compass-svg-wrap' }); + compassSvgWrap.innerHTML = buildCompassSVG(cs, mode, ppe); + + var cnWrap = E('div', { 'id': 'cpu-npu-svg-wrap', 'style': 'flex-shrink:0' }); + cnWrap.innerHTML = buildCpuNpuCompassSVG(cs, ppe, st, ti); + + var view = E('div',{'class':'cbi-map'},[ + E('h2',{},_('Airoha FlowSense')), + + // Conflict alerts + renderConflictAlerts(alertData), + + // Offload Monitor + E('div',{'class':'cbi-section'},[ + // Gauges: CPU/NPU tachometer, compass, WiFi tachometers, compass cards, mode banner + E('div', { 'class': 'compass-wrap' }, [ + cnWrap, + compassSvgWrap + ].concat(buildWifiTachoElements(wifi, ti, st, ppe))), + renderCompassCards(cs, bypass, jitter, wan, wifi, bridge, mode), + // Ethernet port gauges row + buildEthGaugeRow((eth && Array.isArray(eth.ports)) ? eth.ports : [], ppe), + renderModeBanner(dm), + E('div',{'style':'display:flex;align-items:center;justify-content:space-evenly;margin-top:10px;flex-wrap:wrap;width:100%'},[ + E('label',{'style':'display:flex;align-items:center;gap:6px;font-size:13px'},[ + E('span',{'id':'flow-offload-badge','class':'offload-badge '+(flo.enabled?'offload-on':'offload-off')},_('HW Flow Offload')), + renderFlowOffloadSelect(flo.enabled) + ]), + E('label',{'style':'display:flex;align-items:center;gap:6px;font-size:13px'},[ + E('span',{'id':'vlan-offload-badge','class':'offload-badge '+(vo.enabled?'offload-on':'offload-off')},_('VLAN Offload')), + renderVlanOffloadSelect(vo.enabled) + ]), + E('label',{'style':'display:flex;align-items:center;gap:6px;font-size:13px'},[ + E('span',{'id':'pppoe-offload-badge','class':'offload-badge '+(ppo.enabled?'offload-on':'offload-off')},_('PPPoE Offload')), + renderPppoeOffloadSelect(ppo.enabled) + ]) + ]), + E('div',{'style':'margin-top:12px'}, renderPpeTerminal(ppe)) + ]), + ]); + + poll.add(L.bind(function() { + return Promise.all([ + callNpuStatus(), callPpeEntries(), callTokenInfo(), callFrameEngine(), + callGetVlanOffload(), callTxStats(), + callGetDeviceMode(), callGetNpuBypass(), + callGetWanHealth(), callGetJitterResult(), callGetConflictAlerts(), + callGetWifiStats(), callGetBridgeStats(), + callGetFlowOffload(), callGetPppoeOffload(), + callGetEthStats() + ]).then(L.bind(function(d) { + injectCSS(); + var st=d[0]||{}, ppe=d[1]||{}, ti=d[2]||{}, fe=d[3]||{}; + var vo=d[4]||{}, txs=d[5]||{}, dm=d[6]||{}; + var bypass=d[7]||{}, wan=d[8]||{}; + var jitter=d[9]||{}, alertData=d[10]||{}; + var wifi=d[11]||{}, bridge=d[12]||{}; + var flo=d[13]||{}, ppo=d[14]||{}; + var eth=d[15]||{}; + var mode = dm.mode || 'router'; + + // Compass update (tachometer embedded inside compass) + var hwBuf = hwBufferState(fe, ppe, mode); + var cs = compassState(bypass, hwBuf, jitter, wan, wifi, bridge, mode); + updateCompassSVG(cs, mode, ppe); + updateCompassCards(cs, bypass, jitter, wan, wifi, bridge, mode); + + // CPU/NPU Load compass update + updateCpuNpuCompassSVG(cs, ppe, st, ti); + + // WiFi band tachometers + var wbands = (wifi && Array.isArray(wifi.bands)) ? wifi.bands : []; + var wFallback = st.npu_loaded ? 'npu' : 'dma'; + for (var wb = 0; wb < 3; wb++) { + var wws = null; + for (var wj = 0; wj < wbands.length; wj++) if (wbands[wj].band === wb) { wws = wbands[wj]; break; } + updateWifiBandSVG(wb, wws, (getTxQueue(ti, wb) || { type: wFallback }).type, ppe); + } + + // Conflict alerts + var alertWrap = document.getElementById('conflict-alerts'); + if (alertWrap) { + var fresh = renderConflictAlerts(alertData); + alertWrap.innerHTML = fresh.innerHTML; + } + + // Offload selects + badges + function _setOffloadBadge(id, on) { var b=document.getElementById(id); if(b) b.className='offload-badge '+(on?'offload-on':'offload-off'); } + var vs=document.getElementById('vlan-offload-select'); if(vs&&!vs.matches(':focus')) vs.value=(vo.enabled?'1':'0'); _setOffloadBadge('vlan-offload-badge',vo.enabled); + var fls=document.getElementById('flow-offload-select'); if(fls&&!fls.matches(':focus')) fls.value=(flo.enabled?'1':'0'); _setOffloadBadge('flow-offload-badge',flo.enabled); + var pps=document.getElementById('pppoe-offload-select'); if(pps&&!pps.matches(':focus')) pps.value=(ppo.enabled?'1':'0'); _setOffloadBadge('pppoe-offload-badge',ppo.enabled); + + // Ethernet port gauges — compute per-port Mbps deltas from cumulative byte counters + var ethPorts = (eth && Array.isArray(eth.ports)) ? eth.ports : []; + var now = Date.now() / 1000; + ethPorts.forEach(function(p) { + var prev = _prevEthBytes[p.iface]; + var txMbps = 0, rxMbps = 0; + if (prev && prev.time) { + var dt = now - prev.time; + if (dt > 0) { + txMbps = Math.max(0, (p.tx_bytes - prev.tx) * 8 / dt / 1e6); + rxMbps = Math.max(0, (p.rx_bytes - prev.rx) * 8 / dt / 1e6); + } + } + _prevEthBytes[p.iface] = { tx: p.tx_bytes, rx: p.rx_bytes, time: now }; + if (!_maxEthMbps[p.iface] || txMbps > _maxEthMbps[p.iface]) _maxEthMbps[p.iface] = Math.max(txMbps, 100); + if (rxMbps > _maxEthMbps[p.iface]) _maxEthMbps[p.iface] = rxMbps; + updateEthPortSVG(p, txMbps, rxMbps, ppe); + }); + + // PPE terminal + var tb = document.getElementById('ppe-terminal-body'); + if (tb) tb.innerHTML = buildPpeTerminalBody(ppe); + },this)); + },this), 5); + + return view; + }, + + handleSaveApply: null, handleSave: null, handleReset: null +}); diff --git a/package/luci-app-airoha-flowsense/po/templates/luci-app-airoha-flowsense.pot b/package/luci-app-airoha-flowsense/po/templates/luci-app-airoha-flowsense.pot new file mode 100644 index 00000000000000..8baa1fc290464c --- /dev/null +++ b/package/luci-app-airoha-flowsense/po/templates/luci-app-airoha-flowsense.pot @@ -0,0 +1,68 @@ +msgid "" +msgstr "Content-Type: text/plain; charset=UTF-8" + +#: htdocs/luci-static/resources/view/airoha_flowsense/status.js:1434 +msgid "Airoha FlowSense" +msgstr "" + +#: htdocs/luci-static/resources/view/airoha_flowsense/status.js:301 +msgid "Index" +msgstr "" + +#: htdocs/luci-static/resources/view/airoha_flowsense/status.js:302 +msgid "State" +msgstr "" + +#: htdocs/luci-static/resources/view/airoha_flowsense/status.js:303 +msgid "Type" +msgstr "" + +#: htdocs/luci-static/resources/view/airoha_flowsense/status.js:304 +msgid "Original" +msgstr "" + +#: htdocs/luci-static/resources/view/airoha_flowsense/status.js:305 +msgid "Translated" +msgstr "" + +#: htdocs/luci-static/resources/view/airoha_flowsense/status.js:321 +msgid "No entries" +msgstr "" + +#: htdocs/luci-static/resources/view/airoha_flowsense/status.js:262 +#: htdocs/luci-static/resources/view/airoha_flowsense/status.js:277 +#: htdocs/luci-static/resources/view/airoha_flowsense/status.js:292 +msgid "Disabled" +msgstr "" + +#: htdocs/luci-static/resources/view/airoha_flowsense/status.js:263 +#: htdocs/luci-static/resources/view/airoha_flowsense/status.js:278 +#: htdocs/luci-static/resources/view/airoha_flowsense/status.js:293 +msgid "Enabled" +msgstr "" + +#: htdocs/luci-static/resources/view/airoha_flowsense/status.js:259 +#: htdocs/luci-static/resources/view/airoha_flowsense/status.js:274 +#: htdocs/luci-static/resources/view/airoha_flowsense/status.js:289 +msgid "Error: " +msgstr "" + +#: htdocs/luci-static/resources/view/airoha_flowsense/status.js:1452 +msgid "HW Flow Offload" +msgstr "" + +#: htdocs/luci-static/resources/view/airoha_flowsense/status.js:1456 +msgid "VLAN Offload" +msgstr "" + +#: htdocs/luci-static/resources/view/airoha_flowsense/status.js:1460 +msgid "PPPoE Offload" +msgstr "" + +#: root/usr/share/luci/menu.d/luci-app-airoha-flowsense.json:3 +msgid "FlowSense" +msgstr "" + +#: root/usr/share/rpcd/acl.d/luci-app-airoha-flowsense.json:3 +msgid "Grant access to Airoha FlowSense offload & PPE monitor" +msgstr "" diff --git a/package/luci-app-airoha-flowsense/po/zh_Hans/luci-app-airoha-flowsense.po b/package/luci-app-airoha-flowsense/po/zh_Hans/luci-app-airoha-flowsense.po new file mode 100644 index 00000000000000..a8f82f4854d140 --- /dev/null +++ b/package/luci-app-airoha-flowsense/po/zh_Hans/luci-app-airoha-flowsense.po @@ -0,0 +1,79 @@ +msgid "" +msgstr "" +"Project-Id-Version: \n" +"POT-Creation-Date: \n" +"PO-Revision-Date: 2026-07-08 23:00+0800\n" +"Last-Translator: \n" +"Language-Team: \n" +"Language: zh_Hans\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=1; plural=0;\n" +"X-Generator: manual\n" + +#: htdocs/luci-static/resources/view/airoha_flowsense/status.js:1434 +msgid "Airoha FlowSense" +msgstr "Airoha FlowSense" + +#: htdocs/luci-static/resources/view/airoha_flowsense/status.js:301 +msgid "Index" +msgstr "序号" + +#: htdocs/luci-static/resources/view/airoha_flowsense/status.js:302 +msgid "State" +msgstr "状态" + +#: htdocs/luci-static/resources/view/airoha_flowsense/status.js:303 +msgid "Type" +msgstr "类型" + +#: htdocs/luci-static/resources/view/airoha_flowsense/status.js:304 +msgid "Original" +msgstr "原始流" + +#: htdocs/luci-static/resources/view/airoha_flowsense/status.js:305 +msgid "Translated" +msgstr "转换后" + +#: htdocs/luci-static/resources/view/airoha_flowsense/status.js:321 +msgid "No entries" +msgstr "无条目" + +#: htdocs/luci-static/resources/view/airoha_flowsense/status.js:262 +#: htdocs/luci-static/resources/view/airoha_flowsense/status.js:277 +#: htdocs/luci-static/resources/view/airoha_flowsense/status.js:292 +msgid "Disabled" +msgstr "禁用" + +#: htdocs/luci-static/resources/view/airoha_flowsense/status.js:263 +#: htdocs/luci-static/resources/view/airoha_flowsense/status.js:278 +#: htdocs/luci-static/resources/view/airoha_flowsense/status.js:293 +msgid "Enabled" +msgstr "启用" + +#: htdocs/luci-static/resources/view/airoha_flowsense/status.js:259 +#: htdocs/luci-static/resources/view/airoha_flowsense/status.js:274 +#: htdocs/luci-static/resources/view/airoha_flowsense/status.js:289 +msgid "Error: " +msgstr "错误:" + +#: htdocs/luci-static/resources/view/airoha_flowsense/status.js:1452 +msgid "HW Flow Offload" +msgstr "硬件流加速" + +#: htdocs/luci-static/resources/view/airoha_flowsense/status.js:1456 +msgid "VLAN Offload" +msgstr "VLAN 加速" + +#: htdocs/luci-static/resources/view/airoha_flowsense/status.js:1460 +msgid "PPPoE Offload" +msgstr "PPPoE 加速" + +#: root/usr/share/luci/menu.d/luci-app-airoha-flowsense.json:3 +msgid "FlowSense" +msgstr "FlowSense" + +#: root/usr/share/rpcd/acl.d/luci-app-airoha-flowsense.json:3 +msgid "Grant access to Airoha FlowSense offload & PPE monitor" +msgstr "授予 Airoha FlowSense 硬件加速与 PPE 监控访问权限" diff --git a/package/luci-app-airoha-flowsense/root/etc/config/npu-monitor b/package/luci-app-airoha-flowsense/root/etc/config/npu-monitor new file mode 100644 index 00000000000000..f287e2bf60247d --- /dev/null +++ b/package/luci-app-airoha-flowsense/root/etc/config/npu-monitor @@ -0,0 +1,2 @@ +config jitter + option target '1.1.1.1' diff --git a/package/luci-app-airoha-flowsense/root/etc/init.d/npu-jitter b/package/luci-app-airoha-flowsense/root/etc/init.d/npu-jitter new file mode 100755 index 00000000000000..2eb37cbf7b9565 --- /dev/null +++ b/package/luci-app-airoha-flowsense/root/etc/init.d/npu-jitter @@ -0,0 +1,30 @@ +#!/bin/sh /etc/rc.common + +START=99 +STOP=10 +USE_PROCD=1 + +DAEMON=/usr/libexec/npu-jitter-daemon +PIDFILE=/var/run/npu-jitter.pid + +start_service() { + local target + target=$(uci get npu-monitor.jitter.target 2>/dev/null || echo "1.1.1.1") + + procd_open_instance + procd_set_param command "$DAEMON" "$target" + procd_set_param pidfile "$PIDFILE" + procd_set_param respawn 30 5 0 + procd_set_param stdout 0 + procd_set_param stderr 0 + procd_close_instance +} + +stop_service() { + rm -f /tmp/npu-jitter.json +} + +reload_service() { + stop + start +} diff --git a/package/luci-app-airoha-flowsense/root/usr/libexec/npu-jitter-daemon b/package/luci-app-airoha-flowsense/root/usr/libexec/npu-jitter-daemon new file mode 100755 index 00000000000000..08ae320e7f9132 --- /dev/null +++ b/package/luci-app-airoha-flowsense/root/usr/libexec/npu-jitter-daemon @@ -0,0 +1,53 @@ +#!/bin/sh + +# NPU Jitter Daemon +# Continuously pings a target and writes jitter stats to /tmp/npu-jitter.json +# Started by /etc/init.d/npu-jitter at boot + +TARGET="${1:-1.1.1.1}" +RESULT_FILE="/tmp/npu-jitter.json" +WINDOW=10 +INTERVAL=2 # seconds between pings + +ping_values="" +count=0 + +while true; do + # Single ping, 1 second timeout + rtt=$(ping -c 1 -W 1 "$TARGET" 2>/dev/null | awk -F'/' '/min.*avg/{printf "%.2f", $5}') + + if [ -n "$rtt" ]; then + ping_values="$ping_values $rtt" + count=$((count + 1)) + + # Trim to window size + if [ "$count" -gt "$WINDOW" ]; then + ping_values=$(echo "$ping_values" | awk '{ + for (i = NF - '"$WINDOW"' + 1; i <= NF; i++) printf $i " " + }') + count=$WINDOW + fi + + # Calculate mean absolute deviation (jitter) and last ping + result=$(echo "$ping_values" | awk -v c="$count" '{ + n = NF + sum = 0 + for (i = 1; i <= n; i++) sum += $i + avg = sum / n + dev = 0 + for (i = 1; i <= n; i++) dev += ($i > avg ? $i - avg : avg - $i) + jitter = dev / n + last = $n + printf "{\"jitter\":%.2f,\"last_ping\":%.2f,\"samples\":%d,\"target\":\"%s\",\"reachable\":true,\"available\":true}", \ + jitter, last, c, "'"$TARGET"'" + }') + + echo "$result" > "$RESULT_FILE" + else + # Unreachable — preserve sample count but mark unreachable + printf '{"jitter":0,"last_ping":0,"samples":%d,"target":"%s","reachable":false,"available":true}\n' \ + "$count" "$TARGET" > "$RESULT_FILE" + fi + + sleep "$INTERVAL" +done diff --git a/package/luci-app-airoha-flowsense/root/usr/libexec/rpcd/luci.airoha_flowsense b/package/luci-app-airoha-flowsense/root/usr/libexec/rpcd/luci.airoha_flowsense new file mode 100755 index 00000000000000..a1a2e86c5a3ec5 --- /dev/null +++ b/package/luci-app-airoha-flowsense/root/usr/libexec/rpcd/luci.airoha_flowsense @@ -0,0 +1,1046 @@ +#!/bin/sh + +# Airoha FlowSense RPC backend for LuCI (Offload & PPE Monitor) + +. /lib/functions.sh + +get_status() { + # === NPU Status === + local npu_fw="/lib/firmware/airoha/en7581_MT7996_npu_rv32.bin" + local npu_ver="Unknown" + + if [ -f "$npu_fw" ]; then + npu_ver=$(strings "$npu_fw" 2>/dev/null | grep -oE '([0-9]+\.[0-9]+\.[0-9]+-)?TLB[0-9.]+[-_v0-9]*' | head -1) + [ -z "$npu_ver" ] && npu_ver="Unknown" + fi + + local npu_loaded="false" + local npu_device="" + local npu_driver_path="/sys/bus/platform/drivers/airoha-npu" + + if [ -d "$npu_driver_path" ]; then + npu_device=$(ls -1 "$npu_driver_path" 2>/dev/null | grep '\.npu$' | head -1) + if [ -n "$npu_device" ]; then + npu_loaded="true" + fi + fi + + local npu_clock=0 + if [ -f "/sys/kernel/debug/clk/npu/clk_rate" ]; then + npu_clock=$(cat /sys/kernel/debug/clk/npu/clk_rate 2>/dev/null || echo 0) + fi + + local npu_cores=$(grep -c "airoha-npu-wdt" /proc/interrupts 2>/dev/null || echo 0) + + local offload_bound=$(cat /sys/kernel/debug/ppe/bind 2>/dev/null | grep -c "^[0-9a-fA-F]") + local offload_total=$(cat /sys/kernel/debug/ppe/entries 2>/dev/null | grep -c "^[0-9a-fA-F]") + + local mem_info=$(dmesg | grep "reserved mem.*npu" | head -4) + local regions="[]" + + if [ -n "$mem_info" ]; then + regions=$(echo "$mem_info" | awk ' + BEGIN { printf "[" } + { + if (NR > 1) printf "," + match($0, /0x[0-9a-fA-F]+\.\.0x[0-9a-fA-F]+/) + range = substr($0, RSTART, RLENGTH) + split(range, a, "\\.\\.") + match($0, /\([0-9]+ [KMG]i?B\)/) + size = substr($0, RSTART+1, RLENGTH-2) + match($0, /non-reusable [^ ]+/) + name = substr($0, RSTART+13) + gsub(/@.*/, "", name) + printf "{\"name\":\"%s\",\"start\":\"%s\",\"end\":\"%s\",\"size\":\"%s\"}", name, a[1], a[2], size + } + END { printf "]" } + ') + fi + + # === CPU Frequency Status === + local cpufreq_path="/sys/devices/system/cpu/cpufreq/policy0" + local cpu_cur_freq=0 + local cpu_hw_freq=0 + local cpu_governor="unknown" + local cpu_avail_governors="" + local cpu_min_freq=0 + local cpu_max_freq=0 + local cpu_avail_freqs="" + + if [ -d "$cpufreq_path" ]; then + cpu_cur_freq=$(cat "$cpufreq_path/scaling_cur_freq" 2>/dev/null || echo 0) + cpu_hw_freq=$(cat "$cpufreq_path/cpuinfo_cur_freq" 2>/dev/null || echo 0) + cpu_governor=$(cat "$cpufreq_path/scaling_governor" 2>/dev/null || echo "unknown") + cpu_avail_governors=$(cat "$cpufreq_path/scaling_available_governors" 2>/dev/null || echo "") + cpu_min_freq=$(cat "$cpufreq_path/scaling_min_freq" 2>/dev/null || echo 0) + cpu_max_freq=$(cat "$cpufreq_path/scaling_max_freq" 2>/dev/null || echo 0) + cpu_avail_freqs=$(cat "$cpufreq_path/scaling_available_frequencies" 2>/dev/null || echo "") + fi + + # Read actual PLL frequency from hardware (detects overclock) + local pll_freq=0 + if command -v devmem > /dev/null 2>&1; then + local pcw_raw=$(devmem 0x1fa202b4 2>/dev/null) + local chg_raw=$(devmem 0x1fa202b8 2>/dev/null) + if [ -n "$pcw_raw" ] && [ -n "$chg_raw" ]; then + pcw_raw=$((pcw_raw)) + chg_raw=$((chg_raw)) + local pcw_int=$(( (pcw_raw >> 24) & 0x7F )) + local pll_posdiv=$(( (chg_raw >> 4) & 7 )) + if [ "$pll_posdiv" -eq 0 ]; then + pll_freq=$((pcw_int * 50)) + else + pll_freq=$((pcw_int * 50 / (1 << pll_posdiv))) + fi + fi + fi + + # Count online CPUs + local cpu_count=0 + if [ -d "/sys/devices/system/cpu" ]; then + cpu_count=$(ls -d /sys/devices/system/cpu/cpu[0-9]* 2>/dev/null | wc -l) + fi + + printf '{"npu_version":"%s","npu_loaded":%s,"npu_device":"%s","npu_clock":%d,"npu_cores":%d,"offload_bound":%d,"offload_total":%d,"memory_regions":%s,"cpu_cur_freq":%d,"cpu_hw_freq":%d,"cpu_governor":"%s","cpu_avail_governors":"%s","cpu_min_freq":%d,"cpu_max_freq":%d,"cpu_avail_freqs":"%s","cpu_count":%d,"pll_freq_mhz":%d}' \ + "${npu_ver:-Unknown}" "$npu_loaded" "${npu_device:-}" "${npu_clock:-0}" "${npu_cores:-0}" "${offload_bound:-0}" "${offload_total:-0}" "$regions" \ + "${cpu_cur_freq:-0}" "${cpu_hw_freq:-0}" "${cpu_governor:-unknown}" "${cpu_avail_governors:-}" "${cpu_min_freq:-0}" "${cpu_max_freq:-0}" "${cpu_avail_freqs:-}" "${cpu_count:-0}" "${pll_freq:-0}" +} + +get_ppe_entries() { + local bind_file="/sys/kernel/debug/ppe/bind" + local entries_file="/sys/kernel/debug/ppe/entries" + + # Parse a PPE file into JSON entries array (awk function reused for both files) + parse_ppe() { + local file="$1" state_filter="$2" limit="$3" + awk -v state_filter="$state_filter" -v limit="$limit" ' + BEGIN { total=0; shown=0; n4=0; n6=0 } + /^[0-9a-fA-F]+ / { + state = $2 + if (state_filter != "" && state != state_filter) next + total++ + if ($3 == "IPv4") n4++ + if ($3 == "IPv6") n6++ + if (shown >= limit) next + shown++ + if ($3 == "IPv4" || $3 == "IPv6") { type=$3; subtype=$4 } + else if ($3 == "L2B" || $4 == "L2B") { type="L2B"; subtype="" } + else { type=$3; subtype=$4 } + orig=""; match($0,/orig=[^ ]+/); if(RSTART>0) orig=substr($0,RSTART+5,RLENGTH-5) + new_flow=""; match($0,/new=[^ ]+/); if(RSTART>0) new_flow=substr($0,RSTART+4,RLENGTH-4) + eth=""; match($0,/eth=[^ ]+/); if(RSTART>0) eth=substr($0,RSTART+4,RLENGTH-4) + entry[shown]=sprintf("{\"index\":\"%s\",\"state\":\"%s\",\"type\":\"%s\",\"proto\":\"%s\",\"orig\":\"%s\",\"new_flow\":\"%s\",\"eth\":\"%s\"}",$1,state,type,subtype,orig,new_flow,eth) + } + END { + printf "{\"total\":%d,\"ipv4\":%d,\"ipv6\":%d,\"entries\":[",total,n4,n6 + for(i=1;i<=shown;i++){if(i>1)printf ","; printf "%s",entry[i]} + printf "]}" + } + ' "$file" 2>/dev/null || echo '{"total":0,"ipv4":0,"ipv6":0,"entries":[]}' + } + + local bnd_json unb_json + + if [ -f "$bind_file" ]; then + local _bnd_parsed=$(parse_ppe "$bind_file" "" 25) + + # The bind file may be a plain-text count ("946\n" or "946" no newline) + # OR a line-per-entry table. Try both and take the larger value. + # Only override parse_ppe's total when we have something bigger. + local _cnt_direct=$(tr -d '[:space:]' < "$bind_file" 2>/dev/null) + case "$_cnt_direct" in ''|*[!0-9]*) _cnt_direct=0 ;; esac + local _cnt_wc=$(wc -l < "$bind_file" 2>/dev/null | tr -d '[:space:]') + case "$_cnt_wc" in ''|*[!0-9]*) _cnt_wc=0 ;; esac + local _bnd_total=$(( _cnt_direct > _cnt_wc ? _cnt_direct : _cnt_wc )) + + if [ "$_bnd_total" -gt 0 ] 2>/dev/null; then + bnd_json=$(printf '%s' "$_bnd_parsed" | sed 's/^{"total":[0-9]*/{"total":'"$_bnd_total"'/') + else + # Both external methods gave 0 — trust parse_ppe's own count + bnd_json="$_bnd_parsed" + fi + else + bnd_json='{"total":0,"ipv4":0,"ipv6":0,"entries":[]}' + fi + + if [ -f "$entries_file" ]; then + unb_json=$(parse_ppe "$entries_file" "UNB" 25) + else + unb_json='{"total":0,"ipv4":0,"ipv6":0,"entries":[]}' + fi + + # Per-band BND counts: collect client MACs per band, match against eth= in bind file + local macs0 macs1 macs2 + macs0=$(iw dev 2>/dev/null | awk '/Interface phy[0-9]*\.0-/{print $2}' | while read -r iface; do + iw dev "$iface" station dump 2>/dev/null | awk '/^Station/{print $2}' + done | tr '\n' '|' | sed 's/|$//') + macs1=$(iw dev 2>/dev/null | awk '/Interface phy[0-9]*\.1-/{print $2}' | while read -r iface; do + iw dev "$iface" station dump 2>/dev/null | awk '/^Station/{print $2}' + done | tr '\n' '|' | sed 's/|$//') + macs2=$(iw dev 2>/dev/null | awk '/Interface phy[0-9]*\.2-/{print $2}' | while read -r iface; do + iw dev "$iface" station dump 2>/dev/null | awk '/^Station/{print $2}' + done | tr '\n' '|' | sed 's/|$//') + + local band_bnd="[0,0,0]" + if [ -f "$bind_file" ]; then + band_bnd=$(awk -v m0="$macs0" -v m1="$macs1" -v m2="$macs2" ' + function has(macs, eth, n,arr,i,low) { + if (macs == "") return 0 + low = tolower(eth) + n = split(macs, arr, "|") + for (i=1; i<=n; i++) if (arr[i] != "" && index(low, tolower(arr[i])) > 0) return 1 + return 0 + } + /^[0-9a-fA-F]+ BND / { + eth=""; match($0,/eth=[^ \t]+/); if (RSTART>0) eth=substr($0,RSTART+4,RLENGTH-4) + if (has(m0,eth)) c0++ + else if (has(m1,eth)) c1++ + else if (has(m2,eth)) c2++ + } + END { printf "[%d,%d,%d]", c0+0, c1+0, c2+0 } + ' "$bind_file" 2>/dev/null || printf "[0,0,0]") + fi + bnd_json=$(printf '%s' "$bnd_json" | sed 's/}$/,"band_bnd":'"$band_bnd"'}/') + + # Per-band UNB counts: IP-based matching (UNB flows lack eth= MAC info) + # Cross-reference WiFi client MACs with ARP neighbour table to get per-band IPs, + # then match UNB orig= source IP against those sets. + local neigh + neigh=$(ip neigh show 2>/dev/null | grep ' lladdr ') + local ips0 ips1 ips2 + ips0=$(printf '%s\n' "$neigh" | awk -v macs="$macs0" ' + BEGIN{n=split(macs,a,"|");for(i=1;i<=n;i++)if(a[i]!="")m[tolower(a[i])]=1} + m[tolower($5)]{printf "%s|",$1}' | sed 's/|$//') + ips1=$(printf '%s\n' "$neigh" | awk -v macs="$macs1" ' + BEGIN{n=split(macs,a,"|");for(i=1;i<=n;i++)if(a[i]!="")m[tolower(a[i])]=1} + m[tolower($5)]{printf "%s|",$1}' | sed 's/|$//') + ips2=$(printf '%s\n' "$neigh" | awk -v macs="$macs2" ' + BEGIN{n=split(macs,a,"|");for(i=1;i<=n;i++)if(a[i]!="")m[tolower(a[i])]=1} + m[tolower($5)]{printf "%s|",$1}' | sed 's/|$//') + + local band_unb="[0,0,0]" + if [ -f "$entries_file" ]; then + band_unb=$(awk -v i0="$ips0" -v i1="$ips1" -v i2="$ips2" ' + function has(ips, ip, n,arr,i) { + if (ips == "") return 0 + n = split(ips, arr, "|") + for (i=1; i<=n; i++) if (arr[i] == ip) return 1 + return 0 + } + /^[0-9a-fA-F]+ UNB / { + src="" + match($0,/orig=[^ \t]+/) + if (RSTART>0) { + orig=substr($0,RSTART+5,RLENGTH-5) + if (substr(orig,1,1) != "[") { n=split(orig,p,":"); src=p[1] } + } + if (src != "") { + if (has(i0,src)) c0++ + else if (has(i1,src)) c1++ + else if (has(i2,src)) c2++ + } + } + END { printf "[%d,%d,%d]", c0+0, c1+0, c2+0 } + ' "$entries_file" 2>/dev/null || printf "[0,0,0]") + fi + unb_json=$(printf '%s' "$unb_json" | sed 's/}$/,"band_unb":'"$band_unb"'}/') + + # Per-port BND counts for LAN ports using bridge FDB learned MACs + local macs_p1 macs_p2 macs_p3 macs_p4 + macs_p1=$(bridge fdb show dev lan1 2>/dev/null | grep -v " self\| permanent" | awk '{print $1}' | tr '\n' '|' | sed 's/|$//') + macs_p2=$(bridge fdb show dev lan2 2>/dev/null | grep -v " self\| permanent" | awk '{print $1}' | tr '\n' '|' | sed 's/|$//') + macs_p3=$(bridge fdb show dev lan3 2>/dev/null | grep -v " self\| permanent" | awk '{print $1}' | tr '\n' '|' | sed 's/|$//') + macs_p4=$(bridge fdb show dev lan4 2>/dev/null | grep -v " self\| permanent" | awk '{print $1}' | tr '\n' '|' | sed 's/|$//') + + local port_bnd="[0,0,0,0]" + if [ -f "$bind_file" ]; then + port_bnd=$(awk -v m1="$macs_p1" -v m2="$macs_p2" -v m3="$macs_p3" -v m4="$macs_p4" ' + function has(macs, eth, n,arr,i,low) { + if (macs == "") return 0 + low = tolower(eth) + n = split(macs, arr, "|") + for (i=1; i<=n; i++) if (arr[i] != "" && index(low, tolower(arr[i])) > 0) return 1 + return 0 + } + /^[0-9a-fA-F]+ BND / { + eth=""; match($0,/eth=[^ \t]+/); if (RSTART>0) eth=substr($0,RSTART+4,RLENGTH-4) + if (has(m1,eth)) c1++ + else if (has(m2,eth)) c2++ + else if (has(m3,eth)) c3++ + else if (has(m4,eth)) c4++ + } + END { printf "[%d,%d,%d,%d]", c1+0, c2+0, c3+0, c4+0 } + ' "$bind_file" 2>/dev/null || printf "[0,0,0,0]") + fi + bnd_json=$(printf '%s' "$bnd_json" | sed 's/}$/,"port_bnd":'"$port_bnd"'}/') + + printf '{"bnd":%s,"unb":%s}' "$bnd_json" "$unb_json" +} + +get_token_info() { + # Per-band WiFi station stats (count, retries) from iw — works regardless of debugfs. + local sta_json="" band + for band in 0 1 2; do + local stats=$( + for iface in $(iw dev 2>/dev/null | awk '/Interface phy0\.'$band'-/{print $2}'); do + iw dev "$iface" station dump 2>/dev/null + done | awk ' + BEGIN { n=0; tp=0; tr=0 } + /^Station/ { n++ } + /tx packets:/ { tp += $3 } + /tx retries:/ { tr += $3 } + END { printf "%d %d %d", n, tp, tr }' + ) + set -- $stats + [ -n "$sta_json" ] && sta_json="${sta_json}," + sta_json="${sta_json}{\"band\":${band},\"count\":${1:-0},\"tx_packets\":${2:-0},\"tx_retries\":${3:-0}}" + done + + # PLE/HIF pool from hw-queues — drops toward zero before mt7996 SER. + local hw_queues="/sys/kernel/debug/ieee80211/phy0/mt76/hw-queues" + local ple_free=0 hif_used=0 hif_reserved=0 + if [ -f "$hw_queues" ]; then + local hex + hex=$(awk '/Total free page:/ {print $4; exit}' "$hw_queues" 2>/dev/null) + [ -n "$hex" ] && ple_free=$(printf "%d" "$hex" 2>/dev/null || echo 0) + hex=$(awk '/HIF free page:/ {print $6; exit}' "$hw_queues" 2>/dev/null) + [ -n "$hex" ] && hif_reserved=$(printf "%d" "$hex" 2>/dev/null || echo 0) + hex=$(awk '/HIF free page:/ {print $8; exit}' "$hw_queues" 2>/dev/null) + [ -n "$hex" ] && hif_used=$(printf "%d" "$hex" 2>/dev/null || echo 0) + fi + + printf '{"ple_free":%d,"hif_used":%d,"hif_reserved":%d,"tx_queues":[],"station_counts":[%s]}' \ + "$ple_free" "$hif_used" "$hif_reserved" "$sta_json" +} + + + + +get_frame_engine() { + if ! command -v devmem > /dev/null 2>&1; then + echo '{"error":"devmem not available"}' + return + fi + + # PSE queue status (IQ=hi16, OQ=lo16) and drop counts for each port + local i reg_q reg_d pse_ports="" + for i in 0 1 2 3 4 5 6 7 8 9; do + reg_q=$(printf "0x%x" $((0x1fb50150 + i * 4))) + reg_d=$(printf "0x%x" $((0x1fb50120 + i * 4))) + local qval=$(($(devmem $reg_q))) + local dval=$(($(devmem $reg_d))) + local iq=$(( (qval >> 16) & 0xFFFF )) + local oq=$(( qval & 0xFFFF )) + [ -n "$pse_ports" ] && pse_ports="${pse_ports}," + pse_ports="${pse_ports}{\"port\":${i},\"iq\":${iq},\"oq\":${oq},\"drops\":${dval}}" + done + + # PSE shared buffer + local psb=$(($(devmem 0x1fb50104))) + local pse_used=$(( (psb >> 16) & 0xFFFF )) + local pse_free=$(( psb & 0x7FFF )) + + # GDM counters (TX_OK, TX_DROP, RX_OK, RX_FC_DROP, RX_OVER_DROP) + # Read as hex to handle large 32-bit counters + local gdm1_tx=$(($(devmem 0x1fb50604))) + local gdm1_txd=$(($(devmem 0x1fb50608))) + local gdm1_rx=$(($(devmem 0x1fb50648))) + local gdm1_rxd=$(($(devmem 0x1fb5064c))) + + local gdm2_tx=$(($(devmem 0x1fb51604))) + local gdm2_txd=$(($(devmem 0x1fb51608))) + local gdm2_rx=$(($(devmem 0x1fb51648))) + local gdm2_rxd=$(($(devmem 0x1fb5164c))) + + local gdm4_tx=$(($(devmem 0x1fb52604))) + local gdm4_txd=$(($(devmem 0x1fb52608))) + local gdm4_rx=$(($(devmem 0x1fb52648))) + local gdm4_rxd=$(($(devmem 0x1fb5264c))) + + # CDM counters (TX_OK, RXCPU_OK, RXHWF_OK, RXCPU_DROP, RXHWF_DROP) + local cdm1_tx=$(($(devmem 0x1fb50580))) + local cdm1_rxcpu=$(($(devmem 0x1fb50590))) + local cdm1_rxhwf=$(($(devmem 0x1fb50594))) + local cdm1_rxcpud=$(($(devmem 0x1fb505a0))) + local cdm1_rxhwfd=$(($(devmem 0x1fb505a4))) + + local cdm2_tx=$(($(devmem 0x1fb51580))) + local cdm2_rxcpu=$(($(devmem 0x1fb51590))) + local cdm2_rxhwf=$(($(devmem 0x1fb51594))) + local cdm2_rxcpud=$(($(devmem 0x1fb515a0))) + local cdm2_rxhwfd=$(($(devmem 0x1fb515a4))) + + printf '{"pse_ports":[%s],"pse_used":%d,"pse_free":%d,' "$pse_ports" "$pse_used" "$pse_free" + printf '"gdm1":{"tx":%d,"tx_drop":%d,"rx":%d,"rx_drop":%d},' "$gdm1_tx" "$gdm1_txd" "$gdm1_rx" "$gdm1_rxd" + printf '"gdm2":{"tx":%d,"tx_drop":%d,"rx":%d,"rx_drop":%d},' "$gdm2_tx" "$gdm2_txd" "$gdm2_rx" "$gdm2_rxd" + printf '"gdm4":{"tx":%d,"tx_drop":%d,"rx":%d,"rx_drop":%d},' "$gdm4_tx" "$gdm4_txd" "$gdm4_rx" "$gdm4_rxd" + printf '"cdm1":{"tx":%d,"rx_cpu":%d,"rx_hwf":%d,"rx_cpu_drop":%d,"rx_hwf_drop":%d},' "$cdm1_tx" "$cdm1_rxcpu" "$cdm1_rxhwf" "$cdm1_rxcpud" "$cdm1_rxhwfd" + printf '"cdm2":{"tx":%d,"rx_cpu":%d,"rx_hwf":%d,"rx_cpu_drop":%d,"rx_hwf_drop":%d}}' "$cdm2_tx" "$cdm2_rxcpu" "$cdm2_rxhwf" "$cdm2_rxcpud" "$cdm2_rxhwfd" +} + +get_tx_stats() { + local stats_file="/sys/kernel/debug/ieee80211/phy0/mt76/tx_stats" + + if [ ! -f "$stats_file" ]; then + echo '{"bands":[]}' + return + fi + + awk ' + BEGIN { + band = -1 + printf "{\"bands\":[" + first = 1 + } + /Phy band [0-9]/ { + if (band >= 0) { + if (!first) printf "," + first = 0 + drops = attempts - success + if (drops < 0) drops = 0 + printf "{\"band\":%d,\"attempts\":%d,\"success\":%d,\"drops\":%d,\"per\":%d,\"ba_miss\":%d}", \ + band, attempts, success, drops, per, ba_miss + } + gsub(/.*band /, ""); band = $1 + 0 + attempts = 0; success = 0; per = 0; ba_miss = 0 + } + /BA miss count:/ { ba_miss = $NF + 0 } + /Tx attempts:/ { attempts = $3 + 0 } + /Tx success:/ { success = $3 + 0 } + /Tx PER:/ { p = $NF; gsub(/%/, "", p); per = p + 0 } + END { + if (band >= 0) { + if (!first) printf "," + drops = attempts - success + if (drops < 0) drops = 0 + printf "{\"band\":%d,\"attempts\":%d,\"success\":%d,\"drops\":%d,\"per\":%d,\"ba_miss\":%d}", \ + band, attempts, success, drops, per, ba_miss + } + printf "]}" + } + ' "$stats_file" 2>/dev/null || echo '{"bands":[]}' +} + +get_vlan_offload() { + local val + val=$(cat /proc/sys/net/bridge/bridge-nf-filter-vlan-tagged 2>/dev/null) + printf '{"enabled":%d}' "${val:-0}" +} + +set_vlan_offload() { + local enabled + enabled=$(echo "$1" | jsonfilter -e "@.enabled" 2>/dev/null) + + # jsonfilter returns empty string for integer 0; treat empty as 0 + [ -z "$enabled" ] && enabled="0" + + if [ "$enabled" != "0" ] && [ "$enabled" != "1" ]; then + echo '{"error":"enabled must be 0 or 1"}' + return + fi + + if ! echo "$enabled" > /proc/sys/net/bridge/bridge-nf-filter-vlan-tagged 2>/dev/null; then + echo '{"error":"failed - bridge module may not be loaded"}' + return + fi + + # Persist across reboots + echo "net.bridge.bridge-nf-filter-vlan-tagged=$enabled" > /etc/sysctl.d/14-vlan-offload.conf + chmod 644 /etc/sysctl.d/14-vlan-offload.conf + printf '{"result":"ok","enabled":%d}' "$enabled" +} + +get_flow_offload() { + local fo foh + fo=$(uci get firewall.@defaults[0].flow_offloading 2>/dev/null) + foh=$(uci get firewall.@defaults[0].flow_offloading_hw 2>/dev/null) + if [ "$fo" = "1" ] && [ "$foh" = "1" ]; then + printf '{"enabled":1}' + else + printf '{"enabled":0}' + fi +} + +set_flow_offload() { + local enabled + enabled=$(echo "$1" | jsonfilter -e "@.enabled" 2>/dev/null) + + [ -z "$enabled" ] && enabled="0" + + if [ "$enabled" != "0" ] && [ "$enabled" != "1" ]; then + echo '{"error":"enabled must be 0 or 1"}' + return + fi + + if [ "$enabled" = "1" ]; then + uci set firewall.@defaults[0].flow_offloading='1' + uci set firewall.@defaults[0].flow_offloading_hw='1' + else + uci delete firewall.@defaults[0].flow_offloading 2>/dev/null + uci delete firewall.@defaults[0].flow_offloading_hw 2>/dev/null + fi + uci commit firewall + /etc/init.d/firewall reload >/dev/null 2>&1 & + printf '{"result":"ok","enabled":%d}' "$enabled" +} + +get_pppoe_offload() { + local val + val=$(cat /proc/sys/net/bridge/bridge-nf-filter-pppoe-tagged 2>/dev/null) + printf '{"enabled":%d}' "${val:-0}" +} + +set_pppoe_offload() { + local enabled + enabled=$(echo "$1" | jsonfilter -e "@.enabled" 2>/dev/null) + + [ -z "$enabled" ] && enabled="0" + + if [ "$enabled" != "0" ] && [ "$enabled" != "1" ]; then + echo '{"error":"enabled must be 0 or 1"}' + return + fi + + if ! echo "$enabled" > /proc/sys/net/bridge/bridge-nf-filter-pppoe-tagged 2>/dev/null; then + echo '{"error":"failed - bridge module may not be loaded"}' + return + fi + + # Persist across reboots + echo "net.bridge.bridge-nf-filter-pppoe-tagged=$enabled" > /etc/sysctl.d/15-pppoe-offload.conf + chmod 644 /etc/sysctl.d/15-pppoe-offload.conf + printf '{"result":"ok","enabled":%d}' "$enabled" +} + +get_device_mode() { + local mode="router" + local reason="" + + # Primary check: has the user explicitly configured this as a dumb AP? + local dhcp_ignore + dhcp_ignore=$(uci get dhcp.lan.ignore 2>/dev/null) + if [ "$dhcp_ignore" = "1" ]; then + mode="ap" + reason="dhcp_disabled" + fi + + # Secondary check: is the WAN interface up with a real IP? + if [ "$mode" = "router" ]; then + local wan_up wan_ip + wan_up=$(ubus call network.interface.wan status 2>/dev/null | jsonfilter -e "@.up" 2>/dev/null) + wan_ip=$(ubus call network.interface.wan status 2>/dev/null | jsonfilter -e "@['ipv4-address'][0].address" 2>/dev/null) + if [ "$wan_up" != "true" ] || [ -z "$wan_ip" ]; then + mode="ap" + reason="no_wan" + fi + fi + + # Fallback: check if default route points to a local gateway (upstream router) + if [ "$mode" = "router" ]; then + local gw + gw=$(ip route show default 2>/dev/null | awk '/default via/{print $3; exit}') + if [ -n "$gw" ]; then + # If gateway is in 192.168.x.x or 10.x.x.x range, we're behind another router + case "$gw" in + 192.168.*.*|10.*.*.*) + # Only treat as AP if we have no public WAN IP + local wan_ip2 + wan_ip2=$(ubus call network.interface.wan status 2>/dev/null | jsonfilter -e "@['ipv4-address'][0].address" 2>/dev/null) + if [ -z "$wan_ip2" ]; then + mode="ap" + reason="local_gateway" + fi + ;; + esac + fi + fi + + printf '{"mode":"%s","reason":"%s"}' "$mode" "$reason" +} + + +get_wan_health() { + # Get WAN interface logical status + local wan_status + wan_status=$(ubus call network.interface.wan status 2>/dev/null) + + if [ -z "$wan_status" ]; then + echo '{"available":false}' + return + fi + + local up l3_device uptime + up=$(echo "$wan_status" | jsonfilter -e "@.up" 2>/dev/null) + l3_device=$(echo "$wan_status" | jsonfilter -e "@.l3_device" 2>/dev/null) + uptime=$(echo "$wan_status" | jsonfilter -e "@.uptime" 2>/dev/null) + + # Get device-level statistics via ip -s link + local rx_bytes=0 tx_bytes=0 rx_errors=0 tx_errors=0 rx_dropped=0 tx_dropped=0 collisions=0 + if [ -n "$l3_device" ]; then + local stats + stats=$(ip -s link show "$l3_device" 2>/dev/null) + if [ -n "$stats" ]; then + # ip -s link output: RX line followed by bytes/packets/errors/dropped/... + # TX line followed by same + rx_bytes=$(echo "$stats" | awk '/^ [0-9]/{if(rx==1){print $1+0; rx=0}} /RX:/{rx=1}') + tx_bytes=$(echo "$stats" | awk '/^ [0-9]/{if(tx==1){print $1+0; tx=0}} /TX:/{tx=1}') + rx_errors=$(echo "$stats" | awk '/RX:/{getline; print $3+0}') + tx_errors=$(echo "$stats" | awk '/TX:/{getline; print $3+0}') + rx_dropped=$(echo "$stats" | awk '/RX:/{getline; print $4+0}') + tx_dropped=$(echo "$stats" | awk '/TX:/{getline; print $4+0}') + fi + fi + + printf '{"available":true,"up":%s,"device":"%s","uptime":%d,"rx_bytes":%d,"tx_bytes":%d,"rx_errors":%d,"tx_errors":%d,"rx_dropped":%d,"tx_dropped":%d}' \ + "${up:-false}" "${l3_device:-}" "${uptime:-0}" \ + "${rx_bytes:-0}" "${tx_bytes:-0}" \ + "${rx_errors:-0}" "${tx_errors:-0}" \ + "${rx_dropped:-0}" "${tx_dropped:-0}" +} + +get_jitter_result() { + local result_file="/tmp/npu-jitter.json" + if [ -f "$result_file" ]; then + cat "$result_file" + else + echo '{"jitter":0,"last_ping":0,"samples":0,"target":"1.1.1.1","reachable":false,"available":false}' + fi +} + +get_wifi_stats() { + # Collect per-band station stats from all AP interfaces using iw. + # MT7996 appears as a single phy0 with sub-interfaces (phy0.0-ap0, + # phy0.1-ap0, phy0.2-ap0), so we loop over interfaces and detect + # each one's actual operating frequency to classify its band. + + if ! command -v iw > /dev/null 2>&1; then + echo '{"available":false,"bands":[]}' + return + fi + + # Find all AP interfaces + local all_ifaces + all_ifaces=$(iw dev 2>/dev/null | awk '/Interface/{iface=$2} /type AP/{print iface}') + + [ -z "$all_ifaces" ] && { printf '{"available":true,"bands":[]}'; return; } + + # Accumulate station dumps and interface names per band index (0=2.4G, 1=5G, 2=6G) + local dump0="" dump1="" dump2="" + local ifaces0="" ifaces1="" ifaces2="" + + for iface in $all_ifaces; do + # Determine band index. + # Primary: parse interface name (phyX.N-apY → N is band index: + # 0=2.4 GHz, 1=5 GHz, 2=6 GHz on MT7996). + # Fallback: read operating frequency from iw dev info. + local band_idx + band_idx=$(printf '%s' "$iface" | sed -n 's/^[^.]*\.\([0-9][0-9]*\)-.*/\1/p') + + if [ -z "$band_idx" ]; then + local freq + freq=$(iw dev "$iface" info 2>/dev/null | grep 'channel' | \ + grep -oE '[0-9]+' | awk '$0+0>1000{print $0+0; exit}') + if [ -z "$freq" ] || [ "$freq" -le 0 ] 2>/dev/null; then + continue + elif [ "$freq" -ge 5925 ]; then + band_idx=2 + elif [ "$freq" -ge 5000 ]; then + band_idx=1 + else + band_idx=0 + fi + fi + + # Always register interface for tx_bytes tracking (independent of client state) + case "$band_idx" in + 0) ifaces0="${ifaces0} ${iface}" ;; + 1) ifaces1="${ifaces1} ${iface}" ;; + 2) ifaces2="${ifaces2} ${iface}" ;; + esac + + # Station dump for client stats — skip band if no clients + local d + d=$(iw dev "$iface" station dump 2>/dev/null) + [ -z "$d" ] && continue + + case "$band_idx" in + 0) dump0="${dump0} +${d}" ;; + 1) dump1="${dump1} +${d}" ;; + 2) dump2="${dump2} +${d}" ;; + esac + done + + # Compute per-band TX throughput from mac80211 per-station tx bytes. + # The sysfs netdev tx_bytes counter is bypassed by MT7996 HW DMA. + # iw station dump 'tx bytes:' comes from mac80211 per-sta counters + # which are updated before frames enter the hardware TX queue. + local now; now=$(date +%s 2>/dev/null || awk '{printf "%d",$1}' /proc/uptime) + + local sta_tx0 sta_tx1 sta_tx2 + sta_tx0=$(printf '%s\n' "$dump0" | awk '/tx bytes:/{t+=$NF} END{printf "%d",t+0}') + sta_tx1=$(printf '%s\n' "$dump1" | awk '/tx bytes:/{t+=$NF} END{printf "%d",t+0}') + sta_tx2=$(printf '%s\n' "$dump2" | awk '/tx bytes:/{t+=$NF} END{printf "%d",t+0}') + + # awk reads OLD prev_file and computes Mbps deltas before we overwrite it + local _txv + _txv=$(awk -v now="$now" -v c0="$sta_tx0" -v c1="$sta_tx1" -v c2="$sta_tx2" \ + 'BEGIN { + pf = "/tmp/npu-wifi-bytes.prev" + tx0="0.0"; tx1="0.0"; tx2="0.0"; dbg="nofile" + if ((getline line < pf) > 0) { + split(line, a) + pt = a[1]+0 + dt = now - pt + dbg = sprintf("pt=%d dt=%d c2=%d a4=%d", pt, dt, c2, a[4]+0) + if (pt > 0 && now > pt) { + d0 = c0-a[2]; if (d0<0) d0=0 + d1 = c1-a[3]; if (d1<0) d1=0 + d2 = c2-a[4]; if (d2<0) d2=0 + tx0 = sprintf("%.1f", d0/dt/125000) + tx1 = sprintf("%.1f", d1/dt/125000) + tx2 = sprintf("%.1f", d2/dt/125000) + dbg = sprintf("pt=%d dt=%d d2=%d tx2=%s", pt, dt, d2, tx2) + } + } + printf "%s %s %s %s\n", tx0, tx1, tx2, dbg + }' /dev/null) + echo "$now $sta_tx0 $sta_tx1 $sta_tx2" > /tmp/npu-wifi-bytes.prev + local tx0_mbps tx1_mbps tx2_mbps + tx0_mbps=$(printf '%s\n' "$_txv" | awk '{print $1}') + tx1_mbps=$(printf '%s\n' "$_txv" | awk '{print $2}') + tx2_mbps=$(printf '%s\n' "$_txv" | awk '{print $3}') + + local bands_json="" + for band_idx in 0 1 2; do + local dump tx_mbps + case "$band_idx" in + 0) dump="$dump0"; tx_mbps="$tx0_mbps" ;; + 1) dump="$dump1"; tx_mbps="$tx1_mbps" ;; + 2) dump="$dump2"; tx_mbps="$tx2_mbps" ;; + esac + [ -z "$dump" ] && continue + + local stats + stats=$(echo "$dump" | awk ' + BEGIN { + stations=0; tx_pkts=0; tx_retries=0; tx_failed=0 + sum_phy=0; sum_exp=0; phy_count=0; exp_count=0 + sum_sig=0; sig_count=0; min_sig=0 + } + /^Station / { stations++ } + /tx packets:/ { tx_pkts += $3+0 } + /tx retries:/ { tx_retries += $3+0 } + /tx failed:/ { tx_failed += $3+0 } + /tx bitrate:/ { sum_phy += $3+0; phy_count++ } + /expected throughput:/ { + v = $3+0 + gsub(/[^0-9.]/, "", v) + if (v+0 > 0) { sum_exp += v+0; exp_count++ } + } + /signal avg:/ { + for (i = 1; i <= NF; i++) { + if ($i ~ /^-[0-9]+$/) { + sig = $i+0 + sum_sig += sig; sig_count++ + if (sig_count == 1 || sig < min_sig) min_sig = sig + break + } + } + } + END { + avg_phy = (phy_count > 0) ? sum_phy/phy_count : 0 + avg_exp = (exp_count > 0) ? sum_exp/exp_count : 0 + efficiency = (avg_phy > 0) ? int(avg_exp / avg_phy * 100) : 0 + retry_pct = ((tx_pkts + tx_retries) > 0) ? int(tx_retries / (tx_pkts + tx_retries) * 100) : 0 + avg_sig = (sig_count > 0) ? int(sum_sig / sig_count) : 0 + printf "%d %d %d %d %.1f %.1f %d %d %d %d", \ + stations, tx_pkts, tx_retries, tx_failed, avg_phy, avg_exp, efficiency, retry_pct, avg_sig, min_sig + }') + + set -- $stats + [ -n "$bands_json" ] && bands_json="${bands_json}," + local sta_tx_raw + case "$band_idx" in + 0) sta_tx_raw="$sta_tx0" ;; + 1) sta_tx_raw="$sta_tx1" ;; + 2) sta_tx_raw="$sta_tx2" ;; + esac + bands_json="${bands_json}{\"band\":${band_idx},\"stations\":${1:-0},\"tx_packets\":${2:-0},\"tx_retries\":${3:-0},\"tx_failed\":${4:-0},\"avg_phy_rate\":${5:-0},\"avg_exp_throughput\":${6:-0},\"airtime_efficiency\":${7:-0},\"retry_pct\":${8:-0},\"avg_signal\":${9:-0},\"min_signal\":${10:-0},\"tx_mbps\":${tx_mbps},\"tx_bytes_raw\":${sta_tx_raw:-0}}" + done + + printf '{"available":true,"bands":[%s]}' "$bands_json" +} + +get_bridge_stats() { + # Check br-lan for tx_dropped (CPU can't feed WiFi chip fast enough) + # and overall packet rates for bridge activity indication + + local stats + stats=$(ip -s link show br-lan 2>/dev/null) + + if [ -z "$stats" ]; then + echo '{"available":false}' + return + fi + + local rx_bytes rx_packets rx_dropped tx_bytes tx_packets tx_dropped + rx_bytes=$(echo "$stats" | awk '/RX:/{getline; print $1+0}') + rx_packets=$(echo "$stats" | awk '/RX:/{getline; print $2+0}') + rx_dropped=$(echo "$stats" | awk '/RX:/{getline; print $4+0}') + tx_bytes=$(echo "$stats" | awk '/TX:/{getline; print $1+0}') + tx_packets=$(echo "$stats" | awk '/TX:/{getline; print $2+0}') + tx_dropped=$(echo "$stats" | awk '/TX:/{getline; print $4+0}') + + # Also check for any bridge forwarding errors via brctl if available + local fwd_errors=0 + if command -v brctl > /dev/null 2>&1; then + fwd_errors=$(brctl showstp br-lan 2>/dev/null | awk '/forward transitions/{sum+=$NF} END{print sum+0}') + fi + + printf '{"available":true,"rx_bytes":%d,"rx_packets":%d,"rx_dropped":%d,"tx_bytes":%d,"tx_packets":%d,"tx_dropped":%d,"fwd_errors":%d}' \ + "${rx_bytes:-0}" "${rx_packets:-0}" "${rx_dropped:-0}" \ + "${tx_bytes:-0}" "${tx_packets:-0}" "${tx_dropped:-0}" \ + "${fwd_errors:-0}" +} + +get_npu_bypass() { + # Determines if the NPU is actively offloading traffic. + # Primary indicator: offload_bound > 0 (live PPE entries in hardware). + # This works in both Router and AP mode — no WAN assumption needed. + + # Check UCI for hw offload setting + local hw_offload + hw_offload=$(uci get firewall.@defaults[0].flow_offloading_hw 2>/dev/null) + + # Read PPE bound entry count directly from debugfs + local offload_bound=0 + local ppe_bind="/sys/kernel/debug/ppe/bind" + if [ -r "$ppe_bind" ]; then + offload_bound=$(wc -l < "$ppe_bind" 2>/dev/null || echo 0) + # Subtract 1 if file has a header line (starts with non-numeric) + local first + first=$(head -1 "$ppe_bind" 2>/dev/null) + case "$first" in [0-9]*) ;; *) offload_bound=$(( offload_bound - 1 )) ;; esac + [ "$offload_bound" -lt 0 ] && offload_bound=0 + fi + + # Calculate CPU usage % using /proc/stat delta + local cpu_pct=0 + local stat_file="/tmp/npu-cpu-stat.prev" + local cur_stat + cur_stat=$(awk '/^cpu /{print $2,$3,$4,$5,$6,$7,$8; exit}' /proc/stat 2>/dev/null) + + if [ -f "$stat_file" ] && [ -n "$cur_stat" ]; then + cpu_pct=$(echo "$cur_stat" | awk -v prev="$(cat $stat_file)" ' + BEGIN { split(prev, p, " ") } + { + user=$1; nice=$2; sys=$3; idle=$4; iowait=$5; irq=$6; softirq=$7 + p_user=p[1]; p_nice=p[2]; p_sys=p[3]; p_idle=p[4] + p_iowait=p[5]; p_irq=p[6]; p_softirq=p[7] + + total = user+nice+sys+idle+iowait+irq+softirq + p_total= p_user+p_nice+p_sys+p_idle+p_iowait+p_irq+p_softirq + dtotal = total - p_total + didle = idle - p_idle + if (dtotal > 0) + printf "%d", int((dtotal - didle) * 100 / dtotal) + else + print 0 + }') + fi + echo "$cur_stat" > "$stat_file" + + # Get WAN TX throughput delta (bytes/sec → Mbps) — router mode only + local wan_mbps=0 + local tx_file="/tmp/npu-wan-tx.prev" + local wan_dev + wan_dev=$(ubus call network.interface.wan status 2>/dev/null | jsonfilter -e "@.l3_device" 2>/dev/null) + + if [ -n "$wan_dev" ]; then + local cur_tx + cur_tx=$(cat "/sys/class/net/${wan_dev}/statistics/tx_bytes" 2>/dev/null || echo 0) + if [ -f "$tx_file" ]; then + local prev_tx prev_time cur_time elapsed delta + prev_tx=$(awk '{print $1}' "$tx_file" 2>/dev/null || echo 0) + prev_time=$(awk '{print $2}' "$tx_file" 2>/dev/null || echo 0) + cur_time=$(date +%s) + elapsed=$(( cur_time - prev_time )) + if [ "$elapsed" -gt 0 ] && [ "$cur_tx" -ge "$prev_tx" ]; then + delta=$(( cur_tx - prev_tx )) + wan_mbps=$(( delta * 8 / elapsed / 1000000 )) + fi + fi + printf "%s %s" "$cur_tx" "$(date +%s)" > "$tx_file" + fi + + # Determine NPU path status using PPE bound entries as ground truth + local npu_active="false" + local path="cpu" + if [ "$hw_offload" = "1" ] && [ "$offload_bound" -gt 0 ]; then + npu_active="true" + path="npu" + elif [ "$hw_offload" = "1" ]; then + path="npu_idle" + fi + + printf '{"hw_offload_enabled":%s,"cpu_pct":%d,"wan_mbps":%d,"offload_bound":%d,"npu_active":%s,"path":"%s"}' \ + "$([ "$hw_offload" = "1" ] && echo true || echo false)" \ + "${cpu_pct:-0}" "${wan_mbps:-0}" "${offload_bound:-0}" "$npu_active" "$path" +} + +get_conflict_alerts() { + local alerts="" + local alert_count=0 + + # ── Alert 1: Ghost Shaper ────────────────────────────────────────────── + # NPU HW offload is ON + CAKE is configured on WAN = NPU bypasses CAKE. + # Bufferbloat management is inactive for offloaded flows. + local hw_offload + hw_offload=$(uci get firewall.@defaults[0].flow_offloading_hw 2>/dev/null) + + local cake_on_wan="false" + local wan_dev + wan_dev=$(ubus call network.interface.wan status 2>/dev/null | jsonfilter -e "@.l3_device" 2>/dev/null) + if [ -n "$wan_dev" ] && command -v tc > /dev/null 2>&1; then + if tc qdisc show dev "$wan_dev" 2>/dev/null | grep -q "^qdisc cake"; then + cake_on_wan="true" + fi + fi + + if [ "$hw_offload" = "1" ] && [ "$cake_on_wan" = "true" ]; then + [ -n "$alerts" ] && alerts="${alerts}," + alerts="${alerts}{\"id\":\"ghost_shaper\",\"severity\":\"warning\",\"title\":\"Ghost Shaper Active\",\"message\":\"NPU HW offload is bypassing CAKE SQM. Offloaded flows skip bufferbloat management — latency protection is inactive under load.\"}" + alert_count=$((alert_count + 1)) + fi + + # ── Alert 2: Physical Bottleneck ────────────────────────────────────── + # WAN interface has hardware errors — cable/SFP issue, not software bloat. + local rx_errors=0 + if [ -n "$wan_dev" ]; then + rx_errors=$(cat "/sys/class/net/${wan_dev}/statistics/rx_errors" 2>/dev/null || echo 0) + fi + + if [ "${rx_errors:-0}" -gt 0 ]; then + [ -n "$alerts" ] && alerts="${alerts}," + alerts="${alerts}{\"id\":\"physical_bottleneck\",\"severity\":\"error\",\"title\":\"Physical Bottleneck\",\"message\":\"Hardware errors detected on WAN (${rx_errors} rx_errors). Check WAN cable or SFP. Latency stats may be misleading.\"}" + alert_count=$((alert_count + 1)) + fi + + # ── Alert 3: NPU Bypass + High Latency ──────────────────────────────── + # NPU is on but jitter result shows high latency — NPU not helping or + # the path has a real latency problem. + local jitter_file="/tmp/npu-jitter.json" + if [ "$hw_offload" = "1" ] && [ -f "$jitter_file" ]; then + local last_ping + last_ping=$(jsonfilter -i "$jitter_file" -e "@.last_ping" 2>/dev/null) + # Warn if ping > 60ms with hw offload on (something is wrong) + if [ -n "$last_ping" ] && awk "BEGIN{exit !($last_ping > 60)}"; then + [ -n "$alerts" ] && alerts="${alerts}," + alerts="${alerts}{\"id\":\"npu_bypass_latency\",\"severity\":\"warning\",\"title\":\"NPU Bypass Detected\",\"message\":\"HW offload is enabled but ISP latency is high (${last_ping}ms). Latency management may be skipped by hardware acceleration.\"}" + alert_count=$((alert_count + 1)) + fi + fi + + printf '{"alert_count":%d,"alerts":[%s]}' "$alert_count" "$alerts" +} + +get_eth_stats() { + printf '{"ports":[' + local first=1 + for iface in wan lan1 lan2 lan3 lan4; do + local sys="/sys/class/net/$iface" + [ -d "$sys" ] || continue + local operstate up=false + operstate=$(cat "$sys/operstate" 2>/dev/null || echo "unknown") + [ "$operstate" = "up" ] && up=true + local speed + speed=$(cat "$sys/speed" 2>/dev/null || echo "") + case "$speed" in ''|*[!0-9]*) speed=0 ;; esac + local tx_bytes rx_bytes tx_errors rx_errors + tx_bytes=$(cat "$sys/statistics/tx_bytes" 2>/dev/null || echo 0) + rx_bytes=$(cat "$sys/statistics/rx_bytes" 2>/dev/null || echo 0) + tx_errors=$(cat "$sys/statistics/tx_errors" 2>/dev/null || echo 0) + rx_errors=$(cat "$sys/statistics/rx_errors" 2>/dev/null || echo 0) + [ "$first" = "1" ] || printf "," + printf '{"iface":"%s","up":%s,"speed":%d,"tx_bytes":%d,"rx_bytes":%d,"tx_errors":%d,"rx_errors":%d}' \ + "$iface" "$up" "$speed" "$tx_bytes" "$rx_bytes" "$tx_errors" "$rx_errors" + first=0 + done + printf ']}' +} + +case "$1" in + list) + echo '{"getStatus":{},"getPpeEntries":{},"getTokenInfo":{},"getFrameEngine":{},"getTxStats":{},"getVlanOffload":{},"setVlanOffload":{"enabled":"int"},"getFlowOffload":{},"setFlowOffload":{"enabled":"int"},"getPppoeOffload":{},"setPppoeOffload":{"enabled":"int"},"getDeviceMode":{},"getWanHealth":{},"getJitterResult":{},"getWifiStats":{},"getBridgeStats":{},"getNpuBypass":{},"getConflictAlerts":{},"getEthStats":{}}' + ;; + call) + case "$2" in + getStatus) + get_status + ;; + getPpeEntries) + get_ppe_entries + ;; + getTokenInfo) + get_token_info + ;; + getFrameEngine) + get_frame_engine + ;; + getTxStats) + get_tx_stats + ;; + getVlanOffload) + get_vlan_offload + ;; + setVlanOffload) + read input + set_vlan_offload "$input" + ;; + getFlowOffload) + get_flow_offload + ;; + setFlowOffload) + read input + set_flow_offload "$input" + ;; + getPppoeOffload) + get_pppoe_offload + ;; + setPppoeOffload) + read input + set_pppoe_offload "$input" + ;; + getDeviceMode) + get_device_mode + ;; + getWanHealth) + get_wan_health + ;; + getJitterResult) + get_jitter_result + ;; + getWifiStats) + get_wifi_stats + ;; + getBridgeStats) + get_bridge_stats + ;; + getNpuBypass) + get_npu_bypass + ;; + getConflictAlerts) + get_conflict_alerts + ;; + getEthStats) + get_eth_stats + ;; + *) + echo '{"error":"Invalid method"}' + ;; + esac + ;; +esac diff --git a/package/luci-app-airoha-flowsense/root/usr/share/luci/menu.d/luci-app-airoha-flowsense.json b/package/luci-app-airoha-flowsense/root/usr/share/luci/menu.d/luci-app-airoha-flowsense.json new file mode 100644 index 00000000000000..8088db68585a12 --- /dev/null +++ b/package/luci-app-airoha-flowsense/root/usr/share/luci/menu.d/luci-app-airoha-flowsense.json @@ -0,0 +1,13 @@ +{ + "admin/status/flowsense": { + "title": "FlowSense", + "order": 16, + "action": { + "type": "view", + "path": "airoha_flowsense/status" + }, + "depends": { + "acl": [ "luci-app-airoha-flowsense" ] + } + } +} diff --git a/package/luci-app-airoha-flowsense/root/usr/share/rpcd/acl.d/luci-app-airoha-flowsense.json b/package/luci-app-airoha-flowsense/root/usr/share/rpcd/acl.d/luci-app-airoha-flowsense.json new file mode 100644 index 00000000000000..a6cf9738c83c27 --- /dev/null +++ b/package/luci-app-airoha-flowsense/root/usr/share/rpcd/acl.d/luci-app-airoha-flowsense.json @@ -0,0 +1,28 @@ +{ + "luci-app-airoha-flowsense": { + "description": "Grant access to Airoha FlowSense offload & PPE monitor", + "read": { + "ubus": { + "luci.airoha_flowsense": [ "getStatus", "getPpeEntries", "getTokenInfo", "getFrameEngine", "getVlanOffload", "getFlowOffload", "getPppoeOffload", "getTxStats", "getDeviceMode", "getWanHealth", "getJitterResult", "getWifiStats", "getBridgeStats", "getNpuBypass", "getConflictAlerts", "getEthStats" ] + }, + "file": { + "/sys/kernel/debug/ppe/entries": [ "read" ], + "/sys/kernel/debug/ppe/bind": [ "read" ], + "/sys/devices/system/cpu/cpufreq/policy0/*": [ "read" ], + "/sys/kernel/debug/ieee80211/*/mt76/token_info": [ "read" ], + "/sys/kernel/debug/ieee80211/*/mt76/tx_stats": [ "read" ] + } + }, + "write": { + "ubus": { + "luci.airoha_flowsense": [ "setVlanOffload", "setFlowOffload", "setPppoeOffload" ] + }, + "file": { + "/proc/sys/net/bridge/bridge-nf-filter-vlan-tagged": [ "read", "write" ], + "/proc/sys/net/bridge/bridge-nf-filter-pppoe-tagged": [ "read", "write" ], + "/etc/sysctl.d/14-vlan-offload.conf": [ "write" ], + "/etc/sysctl.d/15-pppoe-offload.conf": [ "write" ] + } + } + } +} diff --git a/package/luci-app-airoha-npu/LICENSE b/package/luci-app-airoha-npu/LICENSE new file mode 100644 index 00000000000000..ce0c9b7175a68a --- /dev/null +++ b/package/luci-app-airoha-npu/LICENSE @@ -0,0 +1,176 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to the Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS diff --git a/package/luci-app-airoha-npu/Makefile b/package/luci-app-airoha-npu/Makefile new file mode 100644 index 00000000000000..48b40e65cee885 --- /dev/null +++ b/package/luci-app-airoha-npu/Makefile @@ -0,0 +1,14 @@ +# This is free software, licensed under the Apache License, Version 2.0 . + +include $(TOPDIR)/rules.mk + +PKG_VERSION:=1.0.3 +PKG_RELEASE:=1 +PKG_LICENSE:=Apache-2.0 + +LUCI_TITLE:=LuCI Airoha SoC Status (NPU, CPU, Frame Engine) +LUCI_DEPENDS:=+luci-base @TARGET_airoha + +include $(TOPDIR)/feeds/luci/luci.mk + +# call BuildPackage - OpenWrt buildroot signature diff --git a/package/luci-app-airoha-npu/README.md b/package/luci-app-airoha-npu/README.md new file mode 100644 index 00000000000000..291a83609e34a8 --- /dev/null +++ b/package/luci-app-airoha-npu/README.md @@ -0,0 +1,165 @@ +# luci-app-airoha-npu + +Real-time monitoring and management dashboard for the Airoha AN7581 SoC on OpenWrt. Covers NPU offload, CPU frequency, WiFi band health, Frame Engine internals, and PPE flow tables. + +**[Download](https://github.com/rchen14b/luci-app-airoha-npu/releases/latest)** + +![License](https://img.shields.io/badge/license-Apache--2.0-blue.svg) +![OpenWrt](https://img.shields.io/badge/OpenWrt-24.10%2B-brightgreen.svg) +![Version](https://img.shields.io/badge/version-1.0.1-orange.svg) +[![Buy Me A Coffee](https://img.shields.io/badge/Buy%20Me%20A%20Coffee-support-yellow?style=flat&logo=buy-me-a-coffee)](https://buymeacoffee.com/rchen14b) + +## Screenshots + +### CPU Frequency & Overclock +![CPU Frequency](screenshots/cpu-frequency.png) + +### NPU & Offload Engine +![NPU & Offload Engine](screenshots/npu-offload-engine.png) + +### PPE Flow Offload Table +![PPE Flow Table](screenshots/ppe-flow-table.png) + +## Features + +### CPU Frequency Management +- Current frequency display with visual bar graph +- Governor selection (performance, ondemand, schedutil, etc.) +- Max frequency selection from available OPP entries +- Direct PLL overclock control (500-1600 MHz) with hardware register programming +- Overclock detection and warning for unstable frequencies + +### NPU & Offload Engine +- NPU firmware version (TLB format), clock frequency, core count +- NPU load status (active/inactive) and reserved memory regions +- WiFi token pool health indicator with in-flight count +- PPE flow offload summary (bound / total entries) +- Per-band WiFi status cards (2.4 GHz, 5 GHz, 6 GHz): + - Client count and link health indicator (Good / Fair / Poor) + - NPU vs DMA path badge + - TX retry rate percentage with color-coded thresholds + +### Frame Engine Visualization +- **PSE Shared Buffer** usage bar (congestion indicator) +- **GDM port cards** with live TX/RX packet counters and drop counts: + - GDM1: Internal Switch (1G LAN3/4) + - GDM2: WAN (USXGMII 10G) + - GDM4: LAN2 (USXGMII 10G) +- **CDM offload ratio** bars — HW-forwarded (PPE) vs CPU-path packets +- **PSE Port Queue Status** grid (P0-P9) with IQ/OQ queue depths and drop counts + +### PPE Flow Offload Table +- First 100 PPE entries with state (BND/UNB), type (IPv4/IPv6/L2B), 5-tuple, and MAC addresses +- Auto-refreshes every 5 seconds + +### Theme Support +- Auto-detects dark/light mode by sampling page background luminance at runtime +- Works with Glass, Bootstrap, Bootstrap-dark, and any LuCI theme +- No hardcoded colors — uses CSS custom properties throughout + +## Requirements + +- OpenWrt with LuCI (24.10+) +- Airoha AN7581 target (`@TARGET_airoha`) +- PPE debugfs (`/sys/kernel/debug/ppe/entries`) +- **`devmem`** busybox applet — required for Frame Engine register access and CPU overclock (`CONFIG_BUSYBOX_DEFAULT_DEVMEM=y`) +- WiFi token_info debugfs for per-band WiFi stats (`/sys/kernel/debug/ieee80211/phy0/mt76/token_info`) +- Optional: [air_tools](https://github.com/merbanan/air_tools) scripts for additional Frame Engine debugging + +## Installation + +### From OpenWrt build + +```sh +# Add to your build tree +git clone https://github.com/rchen14b/luci-app-airoha-npu.git package/luci-app-airoha-npu + +# Enable in menuconfig +make menuconfig +# Navigate to: LuCI -> Applications -> luci-app-airoha-npu + +# Build +make package/luci-app-airoha-npu/compile V=s +``` + +### Manual install (dev) + +```sh +# Copy files to router +scp root/usr/libexec/rpcd/luci.airoha_npu root@router:/usr/libexec/rpcd/ +scp root/usr/share/luci/menu.d/luci-app-airoha-npu.json root@router:/usr/share/luci/menu.d/ +scp root/usr/share/rpcd/acl.d/luci-app-airoha-npu.json root@router:/usr/share/rpcd/acl.d/ +scp htdocs/luci-static/resources/view/airoha_npu/status.js root@router:/www/luci-static/resources/view/airoha_npu/ + +# Set permissions and restart +ssh root@router 'chmod +x /usr/libexec/rpcd/luci.airoha_npu && /etc/init.d/rpcd restart' +``` + +## Data Sources + +| Data | Source | Required | +|------|--------|----------| +| NPU status | `/sys/bus/platform/drivers/airoha-npu/`, `dmesg` | Yes | +| CPU frequency | `/sys/devices/system/cpu/cpufreq/policy0/` | Yes | +| Overclock PLL | `devmem` registers (0x1fa202b4, 0x1fa202b8) | devmem | +| PPE entries | `/sys/kernel/debug/ppe/{entries,bind}` | Yes | +| WiFi token pool | `/sys/kernel/debug/ieee80211/phy0/mt76/token_info` | Optional | +| WiFi station stats | `iw dev station dump` | Optional | +| Frame Engine (GDM/CDM/PSE) | `devmem` registers (0x1fb50xxx-0x1fb53xxx) | devmem | + +## Project Structure + +``` +luci-app-airoha-npu/ +├── Makefile # OpenWrt package build +├── htdocs/ +│ └── luci-static/resources/view/airoha_npu/ +│ └── status.js # LuCI JavaScript view +├── root/ +│ └── usr/ +│ ├── libexec/rpcd/ +│ │ └── luci.airoha_npu # RPC backend (shell) +│ └── share/ +│ ├── luci/menu.d/ +│ │ └── luci-app-airoha-npu.json # Menu config +│ └── rpcd/acl.d/ +│ └── luci-app-airoha-npu.json # ACL permissions +└── screenshots/ +``` + +## RPC Methods + +| Method | Description | Parameters | +|--------|-------------|------------| +| `getStatus` | NPU, CPU, PPE summary | — | +| `getPpeEntries` | PPE flow table (first 100) | — | +| `getTokenInfo` | WiFi token pool & station stats | — | +| `getFrameEngine` | PSE/GDM/CDM register counters | — | +| `setGovernor` | Change CPU governor | `governor` | +| `setMaxFreq` | Set CPU max frequency | `freq` (kHz) | +| `setOverclock` | Direct PLL frequency set | `freq_mhz` | + +## Version History + +### v1.0.1 +- Unified Frame Engine diagram with architectural layout matching AN7581 data paths +- WiFi band cards moved into CDM4/WDMA section (reflects actual WiFi DMA path) +- Added NPU and PPE Engine cards with live status and flow counts +- Token pool and PPE flows removed from summary table (now in Frame Engine view) +- Fixed GDM4 register addresses (0x2500, not 0x3500) +- Improved compact WiFi band chip styling + +### v1.0.0 +- CPU frequency management with governor and overclock controls +- NPU & Offload Engine monitoring with WiFi band cards +- Frame Engine visualization (GDM ports, CDM offload ratio, PSE queues) +- PPE flow offload table with auto-refresh +- Theme-adaptive dark/light mode detection + +## License + +Apache-2.0 + +## Author + +Ryan Chen — Created for W1700K router (Airoha AN7581 + MT7996 BE19000) diff --git a/package/luci-app-airoha-npu/htdocs/luci-static/resources/view/airoha_npu/status.js b/package/luci-app-airoha-npu/htdocs/luci-static/resources/view/airoha_npu/status.js new file mode 100644 index 00000000000000..f781505bf0f78b --- /dev/null +++ b/package/luci-app-airoha-npu/htdocs/luci-static/resources/view/airoha_npu/status.js @@ -0,0 +1,523 @@ +'use strict'; +'require view'; +'require poll'; +'require rpc'; +'require ui'; + +var callNpuStatus = rpc.declare({ object: 'luci.airoha_npu', method: 'getStatus' }); +var callPpeEntries = rpc.declare({ object: 'luci.airoha_npu', method: 'getPpeEntries' }); +var callTokenInfo = rpc.declare({ object: 'luci.airoha_npu', method: 'getTokenInfo' }); +var callFrameEngine = rpc.declare({ object: 'luci.airoha_npu', method: 'getFrameEngine' }); +var callSetGovernor = rpc.declare({ object: 'luci.airoha_npu', method: 'setGovernor', params: ['governor'] }); +var callSetMaxFreq = rpc.declare({ object: 'luci.airoha_npu', method: 'setMaxFreq', params: ['freq'] }); +var callSetOverclock = rpc.declare({ object: 'luci.airoha_npu', method: 'setOverclock', params: ['freq_mhz'] }); +var callGetVlanOffload = rpc.declare({ object: 'luci.airoha_npu', method: 'getVlanOffload' }); +var callSetVlanOffload = rpc.declare({ object: 'luci.airoha_npu', method: 'setVlanOffload', params: ['enabled'] }); +var callGetPPPoEOffload = rpc.declare({ object: 'luci.airoha_npu', method: 'getPPPoEOffload' }); +var callSetPPPoEOffload = rpc.declare({ object: 'luci.airoha_npu', method: 'setPPPoEOffload', params: ['enabled'] }); + +/* ── Theme-adaptive CSS ── */ +var themeCSS = '\ +.soc-card{background:var(--soc-card-bg);border:1px solid var(--soc-border);border-radius:8px;padding:14px;transition:border-color .3s}\ +.soc-card-accent{border-left-width:3px;border-left-style:solid}\ +.soc-muted{color:var(--soc-muted)}\ +.soc-text{color:var(--soc-text)}\ +.soc-label{font-size:11px;color:var(--soc-muted)}\ +.soc-bar-track{background:var(--soc-bar-track);border-radius:4px;overflow:hidden}\ +.soc-pse-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(130px,1fr));gap:6px}\ +.soc-pse-cell{background:var(--soc-card-bg);border:1px solid var(--soc-border);border-radius:5px;padding:6px 8px;font-size:12px}\ +.soc-band-grid{display:grid;grid-template-columns:repeat(3,1fr);gap:10px;margin-top:10px}\ +.soc-gdm-grid{display:grid;grid-template-columns:repeat(3,1fr);gap:10px;margin-bottom:10px}\ +.soc-cdm-grid{display:grid;grid-template-columns:repeat(2,1fr);gap:10px;margin-bottom:10px}\ +'; + +function isDarkMode() { + // Sample multiple elements to get a reliable reading + var els = [document.body, document.querySelector('.main-content'), document.querySelector('#maincontent'), document.querySelector('.cbi-map')]; + for (var i = 0; i < els.length; i++) { + if (!els[i]) continue; + var bg = window.getComputedStyle(els[i]).backgroundColor; + var m = bg.match(/\d+/g); + if (m && m.length >= 3) { + var a = m.length >= 4 ? parseFloat(m[3]) : 1; + if (a < 0.1) continue; // transparent, skip + var lum = (parseInt(m[0]) * 299 + parseInt(m[1]) * 587 + parseInt(m[2]) * 114) / 1000; + return lum < 128; + } + } + // Fallback: check if any known dark theme stylesheet is loaded + var sheets = document.querySelectorAll('link[href*="dark"], link[href*="glass"]'); + return sheets.length > 0; +} + +var _lastDarkMode = null; + +function injectCSS() { + var el = document.getElementById('soc-theme-css'); + if (!el) { el = document.createElement('style'); el.id = 'soc-theme-css'; document.head.appendChild(el); } + + var dark = isDarkMode(); + if (dark === _lastDarkMode) return; + _lastDarkMode = dark; + + var vars = dark + ? ':root{--soc-card-bg:#1e1e1e;--soc-border:#333;--soc-muted:#999;--soc-text:#e0e0e0;--soc-bar-track:#333}' + : ':root{--soc-card-bg:#fff;--soc-border:#d0d0d0;--soc-muted:#666;--soc-text:#222;--soc-bar-track:#e0e0e0}'; + el.textContent = themeCSS + vars; +} + +/* ── Helpers ── */ +var bandInfo = [ + { name: '2.4 GHz', accent: '#ff9800' }, + { name: '5 GHz', accent: '#2196f3' }, + { name: '6 GHz', accent: '#9c27b0' } +]; + +var psePortMap = [ + { name: 'CDM1', label: 'CPU DMA 1', color: '#607d8b' }, + { name: 'GDM1', label: 'Switch 1G', color: '#ff9800' }, + { name: 'GDM2', label: 'WAN 10G', color: '#4caf50' }, + { name: 'GDM3', label: 'GDM3', color: '#607d8b' }, + { name: 'PPE1', label: 'PPE Eng 1', color: '#2196f3' }, + { name: 'CDM2', label: 'CPU DMA 2', color: '#607d8b' }, + { name: 'CDM3', label: 'CDM3', color: '#607d8b' }, + { name: 'CDM4', label: 'WDMA WiFi', color: '#9c27b0' }, + { name: 'PPE2', label: 'PPE Eng 2', color: '#2196f3' }, + { name: 'GDM4', label: 'LAN2 10G', color: '#4caf50' } +]; + +function fmtFreq(khz) { return (!khz || khz === 0) ? 'N/A' : (khz / 1000).toFixed(0) + ' MHz'; } +function fmtK(n) { + if (!n || n === 0) return '0'; + if (n >= 1e6) return (n / 1e6).toFixed(1) + 'M'; + if (n >= 1e3) return (n / 1e3).toFixed(1) + 'K'; + return n.toString(); +} + +function calcTotalMem(regions) { + var t = 0; + (regions || []).forEach(function(r) { + var m = (r.size || '').match(/(\d+)\s*(KiB|MiB|GiB)/i); + if (m) { var s = parseInt(m[1]); var u = m[2][0].toUpperCase(); t += u === 'G' ? s*1048576 : u === 'M' ? s*1024 : s; } + }); + return t >= 1024 ? (t/1024).toFixed(0)+' MiB' : t+' KiB'; +} + +function tokenHealth(c, s) { + if (!s) return { text: 'N/A', color: '#888' }; + var p = c/s*100; + return p < 50 ? { text:'Healthy', color:'#4caf50' } : p < 80 ? { text:'Warning', color:'#ff9800' } : { text:'Critical', color:'#f44336' }; +} + +function getBandStats(ti, b) { + var c = Array.isArray(ti.station_counts) ? ti.station_counts : []; + for (var i=0;i0.5 ? {text:'Poor',color:'#f44336'} : r>0.2 ? {text:'Fair',color:'#ff9800'} : {text:'Good',color:'#4caf50'}; +} + +function retryPct(s) { + if (!s || !s.tx_packets) return '-'; + return (s.tx_retries/(s.tx_packets+s.tx_retries)*100).toFixed(1)+'%'; +} + +/* ── Mini Band Chip (compact for FE diagram) ── */ +function renderBandChip(band, txQ, stats) { + var info = bandInfo[band] || { name: 'Band '+band, accent: '#888' }; + var id = 'band-'+band; + var h = bandHealth(stats); + var type = txQ ? txQ.type : '?'; + var rp = retryPct(stats); + + return E('div', { 'id': id, 'style': 'background:var(--soc-card-bg);border:1px solid var(--soc-border);border-left:2px solid '+info.accent+';border-radius:6px;padding:10px 12px' }, [ + E('div', { 'style': 'display:flex;justify-content:space-between;align-items:center;margin-bottom:6px' }, [ + E('span', { 'class': 'soc-text', 'style': 'font-size:13px;font-weight:bold' }, info.name), + E('span', { 'style': 'background:'+(type==='npu'?'#1565c0':'#666')+';color:#fff;padding:1px 6px;border-radius:3px;font-size:9px;font-weight:600' }, type.toUpperCase()) + ]), + E('div', { 'style': 'display:flex;justify-content:space-between;align-items:center;font-size:12px' }, [ + E('div', { 'id': id+'-health', 'style': 'display:flex;align-items:center;gap:4px' }, [ + E('span', { 'style': 'width:7px;height:7px;border-radius:50%;background:'+h.color+';display:inline-block' }), + E('span', { 'style': 'color:'+h.color+';font-weight:500' }, h.text) + ]), + E('span', { 'id': id+'-clients', 'class': 'soc-muted' }, stats.count + ' sta'), + (stats.tx_packets > 0) ? E('span', { 'id': id+'-retries', 'class': 'soc-muted' }, rp) : E('span') + ]) + ]); +} + +function updateBandChip(band, stats) { + var id = 'band-'+band, h = bandHealth(stats); + var el = document.getElementById(id+'-health'); + if (el) { el.innerHTML = ''; el.appendChild(E('span',{'style':'width:6px;height:6px;border-radius:50%;background:'+h.color+';display:inline-block'})); el.appendChild(E('span',{'style':'color:'+h.color+';font-weight:500;font-size:11px'},h.text)); } + var cl = document.getElementById(id+'-clients'); + if (cl) cl.textContent = stats.count+'sta'; + var re = document.getElementById(id+'-retries'); + if (re) { var rp2 = retryPct(stats); re.textContent = rp2; } +} + +/* ── Frame Engine Diagram (with WiFi bands, NPU, PPE flows) ── */ +function renderFeDiagram(fe, ti, st) { + if (!fe || fe.error) return E('div', { 'class': 'soc-muted' }, 'devmem not available on this build'); + ti = ti || {}; st = st || {}; + + var ports = Array.isArray(fe.pse_ports) ? fe.pse_ports : []; + + // Helper: GDM card + function gdmCard(key, name, label, color, pse) { + var d = fe[key] || {}; + var active = d.tx > 0 || d.rx > 0; + return E('div', { 'class': 'soc-card soc-card-accent', 'style': 'border-left-color:'+color + (active?';border-color:'+color:'') }, [ + E('div', { 'style': 'display:flex;justify-content:space-between;align-items:baseline;margin-bottom:2px' }, [ + E('span', { 'style': 'font-weight:bold;color:'+color+';font-size:14px' }, name), + E('span', { 'class': 'soc-label' }, pse) + ]), + E('div', { 'class': 'soc-label', 'style': 'margin-bottom:6px' }, label), + E('div', { 'style': 'display:grid;grid-template-columns:auto 1fr;gap:2px 10px;font-size:12px' }, [ + E('span', { 'class': 'soc-muted' }, 'TX'), E('span', { 'class': 'soc-text', 'style': 'text-align:right' }, fmtK(d.tx)), + E('span', { 'class': 'soc-muted' }, 'RX'), E('span', { 'class': 'soc-text', 'style': 'text-align:right' }, fmtK(d.rx)) + ].concat(d.tx_drop > 0 ? [ + E('span', { 'style': 'color:#f44336' }, 'TX Drop'), E('span', { 'style': 'color:#f44336;text-align:right' }, fmtK(d.tx_drop)) + ] : []).concat(d.rx_drop > 0 ? [ + E('span', { 'style': 'color:#f44336' }, 'RX Drop'), E('span', { 'style': 'color:#f44336;text-align:right' }, fmtK(d.rx_drop)) + ] : [])) + ]); + } + + // Helper: CDM offload bar + function cdmCard(key, name, label, pse) { + var d = fe[key] || {}; + var total = (d.rx_cpu||0) + (d.rx_hwf||0); + var pct = total > 0 ? ((d.rx_hwf/total)*100).toFixed(1) : '0.0'; + var barCol = total===0 ? 'var(--soc-border)' : parseFloat(pct)>80 ? '#4caf50' : parseFloat(pct)>50 ? '#ff9800' : '#f44336'; + return E('div', { 'class': 'soc-card' }, [ + E('div', { 'style': 'display:flex;justify-content:space-between;margin-bottom:4px' }, [ + E('span', { 'style': 'font-weight:bold;color:#607d8b;font-size:13px' }, name+' '+pse), + E('span', { 'class': 'soc-label' }, label) + ]), + E('div', { 'class': 'soc-text', 'style': 'font-size:12px;margin-bottom:4px' }, 'HW Offload: '+pct+'%'), + E('div', { 'class': 'soc-bar-track', 'style': 'height:6px' }, [ + E('div', { 'style': 'background:'+barCol+';height:100%;width:'+pct+'%;transition:width .5s;border-radius:4px' }) + ]), + E('div', { 'style': 'display:flex;justify-content:space-between;font-size:11px;margin-top:4px' }, [ + E('span', { 'class': 'soc-muted' }, 'CPU: '+fmtK(d.rx_cpu||0)), + E('span', { 'class': 'soc-muted' }, 'HWF: '+fmtK(d.rx_hwf||0)), + E('span', { 'class': 'soc-muted' }, 'TX: '+fmtK(d.tx||0)) + ]) + ]); + } + + // WiFi band chips for CDM4 + var bandChips = []; + for (var b = 0; b < 3; b++) bandChips.push(renderBandChip(b, getTxQueue(ti, b), getBandStats(ti, b))); + + // CDM4/WDMA + WiFi bands grouped + var p7 = ports[7] || { iq: 0, oq: 0, drops: 0 }; + var cdm4WiFi = E('div', { 'class': 'soc-card soc-card-accent', 'style': 'border-left-color:#9c27b0' }, [ + E('div', { 'style': 'display:flex;justify-content:space-between;align-items:baseline;margin-bottom:2px' }, [ + E('span', { 'style': 'font-weight:bold;color:#9c27b0;font-size:14px' }, 'CDM4'), + E('span', { 'class': 'soc-label' }, 'P7 WiFi DMA') + ]), + E('div', { 'style': 'display:flex;gap:12px;font-size:11px;margin-bottom:8px' }, [ + E('span', { 'class': 'soc-muted' }, 'IQ '+p7.iq), + E('span', { 'class': 'soc-muted' }, 'OQ '+p7.oq), + p7.drops > 0 ? E('span', { 'style': 'color:#f44336' }, 'Drop '+fmtK(p7.drops)) : null + ].filter(Boolean)), + // WiFi bands inside + E('div', { 'style': 'display:grid;grid-template-columns:repeat(3,1fr);gap:6px' }, bandChips) + ]); + + // NPU indicator + var npuActive = st.npu_loaded; + var npuCard = E('div', { 'class': 'soc-card', 'style': 'border-color:'+(npuActive?'#00bcd4':'var(--soc-border)') }, [ + E('div', { 'style': 'display:flex;justify-content:space-between;align-items:center;margin-bottom:4px' }, [ + E('span', { 'style': 'font-weight:bold;color:#00bcd4;font-size:14px' }, 'NPU'), + E('span', { 'style': 'background:'+(npuActive?'#00695c':'#666')+';color:#fff;padding:1px 7px;border-radius:3px;font-size:10px;font-weight:600' }, npuActive ? 'ACTIVE' : 'OFF') + ]), + E('div', { 'class': 'soc-label', 'style': 'margin-bottom:4px' }, '8x RISC-V via PCIe RAM'), + E('div', { 'style': 'font-size:11px' }, [ + E('span', { 'class': 'soc-muted' }, 'Manages: '), + E('span', { 'class': 'soc-text', 'style': 'font-size:11px' }, 'PPE init, WDMA rings, flow stats') + ]) + ]); + + // PPE engines with flow count + var ppeCard = E('div', { 'class': 'soc-card', 'style': 'border-color:#2196f3' }, [ + E('div', { 'style': 'display:flex;justify-content:space-between;align-items:center;margin-bottom:4px' }, [ + E('span', { 'style': 'font-weight:bold;color:#2196f3;font-size:14px' }, 'PPE Engines'), + E('span', { 'class': 'soc-label' }, 'P4 + P8') + ]), + E('div', { 'style': 'display:flex;gap:16px;font-size:12px' }, [ + E('span', {}, [ + E('span', { 'class': 'soc-muted' }, 'Bound '), + E('span', { 'class': 'soc-text', 'style': 'font-weight:bold', 'id': 'fe-ppe-bound' }, (st.offload_bound||0).toString()) + ]), + E('span', {}, [ + E('span', { 'class': 'soc-muted' }, 'Total '), + E('span', { 'class': 'soc-text', 'id': 'fe-ppe-total' }, (st.offload_total||0).toString()) + ]) + ]) + ]); + + // PSE buffer + var pseT = (fe.pse_used||0)+(fe.pse_free||0); + var pseP = pseT>0 ? ((fe.pse_used/pseT)*100).toFixed(1) : '0'; + var pseCol = parseFloat(pseP)>80?'#f44336':parseFloat(pseP)>50?'#ff9800':'#4caf50'; + + // PSE port cells (skip P7 since it's shown in CDM4/WiFi section) + var portCells = ports.filter(function(p){ return p.port !== 7; }).map(function(p) { + var info = psePortMap[p.port] || { name:'P'+p.port, label:'?', color:'#666' }; + var drop = p.drops > 0; + return E('div', { 'class': 'soc-pse-cell', 'style': drop ? 'border-color:#f44336' : '' }, [ + E('div', { 'style': 'font-weight:600;color:'+info.color+';font-size:11px' }, 'P'+p.port+' '+info.name), + E('div', { 'style': 'display:flex;gap:8px;font-size:11px;margin-top:2px' }, [ + E('span', { 'class': 'soc-muted' }, 'IQ '+p.iq), + E('span', { 'class': 'soc-muted' }, 'OQ '+p.oq), + drop ? E('span', { 'style': 'color:#f44336' }, fmtK(p.drops)) : null + ].filter(Boolean)) + ]); + }); + + return E('div', { 'id': 'fe-diagram' }, [ + // PSE buffer bar + E('div', { 'class': 'soc-card', 'style': 'margin-bottom:10px' }, [ + E('div', { 'style': 'display:flex;justify-content:space-between;margin-bottom:4px' }, [ + E('span', { 'class': 'soc-text', 'style': 'font-weight:bold;font-size:13px' }, 'PSE Shared Buffer'), + E('span', { 'class': 'soc-muted', 'style': 'font-size:12px' }, (fe.pse_used||0)+' used / '+(fe.pse_free||0)+' free ('+pseP+'%)') + ]), + E('div', { 'class': 'soc-bar-track', 'style': 'height:8px' }, [ + E('div', { 'style': 'background:'+pseCol+';height:100%;width:'+pseP+'%;border-radius:4px;transition:width .5s' }) + ]) + ]), + // Row 1: GDM ports + E('div', { 'class': 'soc-gdm-grid' }, [ + gdmCard('gdm1', 'GDM1', 'Internal Switch (1G LAN3/4)', '#ff9800', 'P1'), + gdmCard('gdm2', 'GDM2', 'WAN (USXGMII 10G)', '#4caf50', 'P2'), + gdmCard('gdm4', 'GDM4', 'LAN2 (USXGMII 10G)', '#4caf50', 'P9') + ]), + // Row 2: CDM1/CDM2 (CPU) + CDM4/WiFi + E('div', { 'style': 'display:grid;grid-template-columns:1fr 1fr 1fr;gap:10px;margin-bottom:10px' }, [ + cdmCard('cdm1', 'CDM1', 'CPU DMA 1', 'P0'), + cdmCard('cdm2', 'CDM2', 'CPU DMA 2', 'P5'), + cdm4WiFi + ]), + // Row 3: PPE + NPU + E('div', { 'style': 'display:grid;grid-template-columns:1fr 1fr;gap:10px;margin-bottom:10px' }, [ + ppeCard, + npuCard + ]), + // PSE port grid + E('div', { 'class': 'soc-text', 'style': 'font-size:12px;font-weight:600;margin-bottom:6px' }, 'PSE Port Queue Status'), + E('div', { 'class': 'soc-pse-grid' }, portCells) + ]); +} + +/* ── CPU Frequency ── */ +function freqBarState(hw, min, max, pll, gov) { + var oc = gov==='performance' && pll>0 && (pll*1000)>max; + return { freq: oc ? pll*1000 : Math.min(hw,max), max: oc ? pll*1000 : max, oc: oc }; +} + +function renderFreqBar(hw, min, max, pll, gov) { + if (!max) return E('span',{},'N/A'); + var s = freqBarState(hw,min,max,pll,gov); + var pct = Math.round(((s.freq-min)/(s.max-min))*100); + pct = Math.max(0,Math.min(100,pct)); + var bg = s.oc ? 'linear-gradient(90deg,#e65100,#ff9800)' : 'linear-gradient(90deg,#2e7d32,#66bb6a)'; + var label = s.oc ? (pll+' MHz (OC)') : fmtFreq(s.freq); + + return E('div', { 'id':'cpu-freq-bar-wrap', 'style':'display:flex;align-items:center;gap:10px' }, [ + E('span', { 'class':'soc-muted', 'style':'font-size:90%' }, fmtFreq(min)), + E('div', { 'style':'flex:1;border-radius:4px;height:22px;position:relative;min-width:180px;max-width:350px;overflow:hidden', 'class':'soc-bar-track' }, [ + E('div', { 'id':'cpu-freq-fill', 'style':'background:'+bg+';height:100%;border-radius:4px;width:'+pct+'%;transition:width .5s' }), + E('span', { 'id':'cpu-freq-text', 'style':'position:absolute;inset:0;display:flex;align-items:center;justify-content:center;font-weight:bold;font-size:13px;color:#fff;text-shadow:0 1px 2px rgba(0,0,0,.6)' }, label) + ]), + E('span', { 'id':'cpu-freq-max-label', 'class':'soc-muted', 'style':'font-size:90%' }, fmtFreq(s.max)) + ]); +} + +function updateFreqBar(hw, min, max, pll, gov) { + var s = freqBarState(hw,min,max,pll,gov); + var el = document.getElementById('cpu-freq-text'), fl = document.getElementById('cpu-freq-fill'), ml = document.getElementById('cpu-freq-max-label'); + if (el) el.textContent = s.oc ? (pll+' MHz (OC)') : fmtFreq(s.freq); + if (fl && s.max>0) { var pct=Math.max(0,Math.min(100,Math.round(((s.freq-min)/(s.max-min))*100))); fl.style.width=pct+'%'; fl.style.background=s.oc?'linear-gradient(90deg,#e65100,#ff9800)':'linear-gradient(90deg,#2e7d32,#66bb6a)'; } + if (ml) ml.textContent = fmtFreq(s.max); +} + +function renderGovSelect(avail, active) { + var gs = (avail||'').trim().split(/\s+/).filter(Boolean); + if (!gs.length) return E('span',{},'N/A'); + return E('select', { 'id':'cpu-governor-select','class':'cbi-input-select','style':'min-width:140px','change':function(ev){ + var g=ev.target.value; ev.target.disabled=true; + callSetGovernor(g).then(function(r){ev.target.disabled=false;if(r&&r.error) ui.addNotification(null,E('p',{},_('Error: ')+r.error),'error');}).catch(function(){ev.target.disabled=false;}); + }}, gs.map(function(g){return E('option',{'value':g,'selected':g===active?'':null},g);})); +} + +function renderMaxFreqSelect(avail, cur) { + var fs = (avail||'').trim().split(/\s+/).filter(Boolean); + if (!fs.length) return E('span',{},'N/A'); + return E('select', { 'id':'cpu-maxfreq-select','class':'cbi-input-select','style':'min-width:140px','change':function(ev){ + var f=ev.target.value; ev.target.disabled=true; + callSetMaxFreq(parseInt(f)).then(function(r){ev.target.disabled=false;if(r&&r.error) ui.addNotification(null,E('p',{},_('Error: ')+r.error),'error');}).catch(function(){ev.target.disabled=false;}); + }}, fs.map(function(f){return E('option',{'value':f,'selected':parseInt(f)===parseInt(cur)?'':null},(parseInt(f)/1000).toFixed(0)+' MHz');})); +} + +function renderOcControls() { + var inp = E('input',{'id':'oc-freq-input','type':'number','min':'500','max':'1600','step':'50','value':'1400','class':'cbi-input-text','style':'width:100px'}); + var btn = E('button',{'class':'cbi-button cbi-button-action','style':'margin-left:8px','click':function(){ + var f=parseInt(document.getElementById('oc-freq-input').value); + if(isNaN(f)||f<500||f>1600){ui.addNotification(null,E('p',{},_('Must be 500-1600 MHz')),'error');return;} + if(f>1400&&!confirm('Frequencies above 1400 MHz may be unstable. Continue?')) return; + btn.disabled=true;btn.textContent=_('Applying...'); + callSetOverclock(f).then(function(r){btn.disabled=false;btn.textContent=_('Apply'); + if(r&&r.error) ui.addNotification(null,E('p',{},_('Failed: ')+r.error),'error'); + else if(r&&r.result==='ok') ui.addNotification(null,E('p',{},_('CPU set to ')+r.actual_mhz+' MHz'),'info'); + }).catch(function(e){btn.disabled=false;btn.textContent=_('Apply');}); + }},_('Apply')); + return E('div',{'style':'display:flex;align-items:center;gap:8px;flex-wrap:wrap'},[ + inp, E('span',{'class':'soc-muted'},'MHz'), btn, + E('span',{'class':'soc-muted','style':'font-size:85%;margin-left:8px'},_('Direct PLL. Stock max 1200 MHz. Stable up to 1500 MHz.')) + ]); +} + +/* ── PPE Table ── */ +function renderPpeRows(entries) { + return entries.slice(0,100).map(function(e) { + var eth = e.eth||''; if(eth==='00:00:00:00:00:00->00:00:00:00:00:00') eth='-'; + return E('tr',{'class':'tr'},[ + E('td',{'class':'td'},e.index), E('td',{'class':'td'},E('span',{'class':e.state==='BND'?'label-success':''},e.state)), + E('td',{'class':'td'},e.type), E('td',{'class':'td'},e.orig||'-'), E('td',{'class':'td'},e.new_flow||'-'), E('td',{'class':'td'},eth) + ]); + }); +} + +function renderVlanOffloadSelect(enabled) { + var cur = enabled ? '1' : '0'; + return E('select', { 'id': 'vlan-offload-select', 'class': 'cbi-input-select', 'style': 'min-width:140px', 'change': function(ev) { + var v = parseInt(ev.target.value); + ev.target.disabled = true; + callSetVlanOffload(v).then(function(r) { + ev.target.disabled = false; + if (r && r.error) ui.addNotification(null, E('p', {}, _('Error: ') + r.error), 'error'); + }).catch(function() { ev.target.disabled = false; }); + }}, [ + E('option', { 'value': '0', 'selected': cur === '0' ? '' : null }, _('Disabled')), + E('option', { 'value': '1', 'selected': cur === '1' ? '' : null }, _('Enabled')) + ]); +} + +function renderPPPoEOffloadSelect(enabled) { + var cur = enabled ? '1' : '0'; + return E('select', { 'id': 'pppoe-offload-select', 'class': 'cbi-input-select', 'style': 'min-width:140px', 'change': function(ev) { + var v = parseInt(ev.target.value); + ev.target.disabled = true; + callSetPPPoEOffload(v).then(function(r) { + ev.target.disabled = false; + if (r && r.error) ui.addNotification(null, E('p', {}, _('Error: ') + r.error), 'error'); + }).catch(function() { ev.target.disabled = false; }); + }}, [ + E('option', { 'value': '0', 'selected': cur === '0' ? '' : null }, _('Disabled')), + E('option', { 'value': '1', 'selected': cur === '1' ? '' : null }, _('Enabled')) + ]); +} + +/* ── Main View ── */ +return view.extend({ + load: function() { + return Promise.all([ callNpuStatus(), callPpeEntries(), callTokenInfo(), callFrameEngine(), callGetVlanOffload(), callGetPPPoEOffload() ]); + }, + + render: function(data) { + injectCSS(); + var st = data[0]||{}, ppe = data[1]||{}, ti = data[2]||{}, fe = data[3]||{}, vo = data[4]||{}, po = data[5]||{}; + var entries = Array.isArray(ppe.entries) ? ppe.entries : []; + var memR = Array.isArray(st.memory_regions) ? st.memory_regions : []; + + var view = E('div',{'class':'cbi-map'},[ + E('h2',{},_('Airoha SoC Status')), + + // CPU Frequency + E('div',{'class':'cbi-section'},[ + E('h3',{},_('CPU Frequency')), + E('table',{'class':'table'},[ + E('tr',{'class':'tr'},[ E('td',{'class':'td','width':'33%'},E('strong',{},_('Current Frequency'))), E('td',{'class':'td'}, renderFreqBar(st.cpu_hw_freq,st.cpu_min_freq,st.cpu_max_freq,st.pll_freq_mhz,st.cpu_governor)) ]), + E('tr',{'class':'tr'},[ E('td',{'class':'td'},E('strong',{},_('Governor'))), E('td',{'class':'td'}, renderGovSelect(st.cpu_avail_governors,st.cpu_governor)) ]), + E('tr',{'class':'tr'},[ E('td',{'class':'td'},E('strong',{},_('Max Frequency'))), E('td',{'class':'td'}, renderMaxFreqSelect(st.cpu_avail_freqs,st.cpu_max_freq)) ]), + E('tr',{'class':'tr'},[ E('td',{'class':'td'},E('strong',{},_('Overclock'))), E('td',{'class':'td'}, renderOcControls()) ]), + E('tr',{'class':'tr'},[ E('td',{'class':'td'},E('strong',{},_('CPU Cores'))), E('td',{'class':'td'},(st.cpu_count||0).toString()) ]) + ]) + ]), + + // NPU & Frame Engine (unified) + E('div',{'class':'cbi-section'},[ + E('h3',{},_('NPU & Offload Engine')), + E('table',{'class':'table'},[ + E('tr',{'class':'tr'},[ E('td',{'class':'td','width':'33%'},E('strong',{},_('NPU Status'))), + E('td',{'class':'td','id':'npu-status'}, st.npu_loaded ? + E('span',{'class':'label-success'},_('Active')+(st.npu_device?' ('+st.npu_device+')':'')) : + E('span',{'class':'label-danger'},_('Not Active'))) ]), + E('tr',{'class':'tr'},[ E('td',{'class':'td'},E('strong',{},_('Firmware / Clock / Cores'))), + E('td',{'class':'td','id':'npu-info'}, (st.npu_version||'N/A')+' | '+(st.npu_clock?(st.npu_clock/1e6).toFixed(0)+' MHz':'N/A')+' | '+(st.npu_cores||0)+' cores') ]), + E('tr',{'class':'tr'},[ E('td',{'class':'td'},E('strong',{},_('Reserved Memory'))), + E('td',{'class':'td','id':'npu-memory'}, calcTotalMem(memR)+' ('+memR.length+' regions)') ]), + E('tr',{'class':'tr'},[ E('td',{'class':'td'},E('strong',{},_('VLAN Offload'))), + E('td',{'class':'td'}, renderVlanOffloadSelect(vo.enabled)) ]), + E('tr',{'class':'tr'},[ E('td',{'class':'td'},E('strong',{},_('PPPoE Offload'))), + E('td',{'class':'td'}, renderPPPoEOffloadSelect(po.enabled)) ]) + ]), + + // Frame Engine diagram (includes WiFi bands, PPE flows, NPU indicator) + E('div',{'style':'margin-top:12px'},[ E('h4',{'class':'soc-text','style':'font-size:14px;margin-bottom:8px'},_('Frame Engine'))]), + E('div',{'id':'fe-container'}, renderFeDiagram(fe, ti, st)) + ]), + + // PPE Flow Table + E('div',{'class':'cbi-section'},[ + E('h3',{},_('PPE Flow Offload Entries')), + E('table',{'class':'table','id':'ppe-entries-table'},[ + E('tr',{'class':'tr cbi-section-table-titles'},[ + E('th',{'class':'th'},_('Index')), E('th',{'class':'th'},_('State')), E('th',{'class':'th'},_('Type')), + E('th',{'class':'th'},_('Original Flow')), E('th',{'class':'th'},_('New Flow')), E('th',{'class':'th'},_('Ethernet')) + ]) + ].concat(renderPpeRows(entries))) + ]) + ]); + + poll.add(L.bind(function() { + return Promise.all([ callNpuStatus(), callPpeEntries(), callTokenInfo(), callFrameEngine(), callGetVlanOffload(), callGetPPPoEOffload() ]).then(L.bind(function(d) { + injectCSS(); + var st=d[0]||{}, ppe=d[1]||{}, ti=d[2]||{}, fe=d[3]||{}, vo=d[4]||{}, po=d[5]||{}; + var entries = Array.isArray(ppe.entries)?ppe.entries:[]; + + updateFreqBar(st.cpu_hw_freq,st.cpu_min_freq,st.cpu_max_freq,st.pll_freq_mhz,st.cpu_governor); + var gs=document.getElementById('cpu-governor-select'); if(gs&&!gs.matches(':focus')) gs.value=st.cpu_governor||''; + var fs=document.getElementById('cpu-maxfreq-select'); if(fs&&!fs.matches(':focus')) fs.value=(st.cpu_max_freq||0).toString(); + var vs=document.getElementById('vlan-offload-select'); if(vs&&!vs.matches(':focus')) vs.value=(vo.enabled?'1':'0'); + var ps=document.getElementById('pppoe-offload-select'); if(ps&&!ps.matches(':focus')) ps.value=(po.enabled?'1':'0'); + + var se=document.getElementById('npu-status'); + if(se){se.innerHTML='';var sp=document.createElement('span');sp.className=st.npu_loaded?'label-success':'label-danger';sp.textContent=st.npu_loaded?(_('Active')+(st.npu_device?' ('+st.npu_device+')':'')):_('Not Active');se.appendChild(sp);} + + var fc=document.getElementById('fe-container'); if(fc){fc.innerHTML='';fc.appendChild(renderFeDiagram(fe, ti, st));} + + var tb=document.getElementById('ppe-entries-table'); + if(tb){while(tb.rows.length>1)tb.deleteRow(1);renderPpeRows(entries).forEach(function(r){tb.appendChild(r);});} + },this)); + },this), 5); + + return view; + }, + + handleSaveApply: null, handleSave: null, handleReset: null +}); diff --git a/package/luci-app-airoha-npu/po/es/luci-app-airoha-npu.po b/package/luci-app-airoha-npu/po/es/luci-app-airoha-npu.po new file mode 100644 index 00000000000000..f7cc0b5ca41fb5 --- /dev/null +++ b/package/luci-app-airoha-npu/po/es/luci-app-airoha-npu.po @@ -0,0 +1,176 @@ +msgid "" +msgstr "" +"Project-Id-Version: \n" +"POT-Creation-Date: \n" +"PO-Revision-Date: \n" +"Last-Translator: \n" +"Language-Team: \n" +"Language: es\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"X-Generator: Poedit 3.8\n" + +#: feeds/luci/applications/luci-app-airoha-npu/htdocs/luci-static/resources/view/airoha_npu/status.js:327 +#: feeds/luci/applications/luci-app-airoha-npu/htdocs/luci-static/resources/view/airoha_npu/status.js:406 +msgid "Active" +msgstr "Activo" + +#: feeds/luci/applications/luci-app-airoha-npu/htdocs/luci-static/resources/view/airoha_npu/status.js:284 +msgid "Airoha SoC Status" +msgstr "Estado del SoC Airoha" + +#: feeds/luci/applications/luci-app-airoha-npu/htdocs/luci-static/resources/view/airoha_npu/status.js:224 +#: feeds/luci/applications/luci-app-airoha-npu/htdocs/luci-static/resources/view/airoha_npu/status.js:233 +#: feeds/luci/applications/luci-app-airoha-npu/htdocs/luci-static/resources/view/airoha_npu/status.js:237 +msgid "Apply" +msgstr "Aplicar" + +#: feeds/luci/applications/luci-app-airoha-npu/htdocs/luci-static/resources/view/airoha_npu/status.js:221 +msgid "Applying..." +msgstr "Aplicando..." + +#: feeds/luci/applications/luci-app-airoha-npu/htdocs/luci-static/resources/view/airoha_npu/status.js:350 +#: feeds/luci/applications/luci-app-airoha-npu/htdocs/luci-static/resources/view/airoha_npu/status.js:420 +msgid "Bound:" +msgstr "Atado:" + +#: feeds/luci/applications/luci-app-airoha-npu/htdocs/luci-static/resources/view/airoha_npu/status.js:362 +msgid "Bytes" +msgstr "Bytes" + +#: feeds/luci/applications/luci-app-airoha-npu/htdocs/luci-static/resources/view/airoha_npu/status.js:310 +msgid "CPU Cores" +msgstr "Núcleos de CPU" + +#: feeds/luci/applications/luci-app-airoha-npu/htdocs/luci-static/resources/view/airoha_npu/status.js:288 +msgid "CPU Frequency" +msgstr "Frecuencia de la CPU" + +#: feeds/luci/applications/luci-app-airoha-npu/htdocs/luci-static/resources/view/airoha_npu/status.js:229 +msgid "CPU set to" +msgstr "CPU configurada en" + +#: feeds/luci/applications/luci-app-airoha-npu/htdocs/luci-static/resources/view/airoha_npu/status.js:291 +msgid "Current Frequency" +msgstr "Frecuencia actual" + +#: feeds/luci/applications/luci-app-airoha-npu/htdocs/luci-static/resources/view/airoha_npu/status.js:244 +msgid "" +"Direct PLL programming. Governor locked to performance. Stock max: 1200 MHz. " +"Tested stable up to 1500 MHz." +msgstr "" +"Programación directa por PLL. Regulador ajustado al rendimiento. Máxima de " +"fábrica: 1200 MHz. Estabilidad comprobada hasta 1500 MHz." + +#: feeds/luci/applications/luci-app-airoha-npu/htdocs/luci-static/resources/view/airoha_npu/status.js:360 +msgid "Ethernet" +msgstr "Ethernet" + +#: feeds/luci/applications/luci-app-airoha-npu/htdocs/luci-static/resources/view/airoha_npu/status.js:85 +msgid "Failed to set governor:" +msgstr "No se pudo establecer el regulador:" + +#: feeds/luci/applications/luci-app-airoha-npu/htdocs/luci-static/resources/view/airoha_npu/status.js:112 +msgid "Failed to set max frequency:" +msgstr "No se pudo establecer la frecuencia máxima:" + +#: feeds/luci/applications/luci-app-airoha-npu/htdocs/luci-static/resources/view/airoha_npu/status.js:212 +msgid "Frequency must be 500-1600 MHz" +msgstr "La frecuencia debe ser de 500-1600 MHz" + +#: feeds/luci/applications/luci-app-airoha-npu/htdocs/luci-static/resources/view/airoha_npu/status.js:296 +msgid "Governor" +msgstr "Regulador" + +#: feeds/luci/applications/luci-app-airoha-npu/root/usr/share/rpcd/acl.d/luci-app-airoha-npu.json:3 +msgid "Grant access to Airoha SoC status (NPU + CPU + Overclock)" +msgstr "Otorgar acceso al estado del SoC Airoha (NPU + CPU + Overclock)" + +#: feeds/luci/applications/luci-app-airoha-npu/htdocs/luci-static/resources/view/airoha_npu/status.js:355 +msgid "Index" +msgstr "Índice" + +#: feeds/luci/applications/luci-app-airoha-npu/htdocs/luci-static/resources/view/airoha_npu/status.js:301 +msgid "Max Frequency" +msgstr "Máx. frecuencia" + +#: feeds/luci/applications/luci-app-airoha-npu/htdocs/luci-static/resources/view/airoha_npu/status.js:331 +msgid "NPU Clock / Cores" +msgstr "Reloj/núcleos de NPU" + +#: feeds/luci/applications/luci-app-airoha-npu/htdocs/luci-static/resources/view/airoha_npu/status.js:321 +msgid "NPU Firmware Version" +msgstr "Versión de firmware de la NPU" + +#: feeds/luci/applications/luci-app-airoha-npu/htdocs/luci-static/resources/view/airoha_npu/status.js:318 +msgid "NPU Information" +msgstr "Información de la NPU" + +#: feeds/luci/applications/luci-app-airoha-npu/htdocs/luci-static/resources/view/airoha_npu/status.js:325 +msgid "NPU Status" +msgstr "Estado de la NPU" + +#: feeds/luci/applications/luci-app-airoha-npu/htdocs/luci-static/resources/view/airoha_npu/status.js:359 +msgid "New Flow" +msgstr "Nuevo flujo" + +#: feeds/luci/applications/luci-app-airoha-npu/htdocs/luci-static/resources/view/airoha_npu/status.js:328 +#: feeds/luci/applications/luci-app-airoha-npu/htdocs/luci-static/resources/view/airoha_npu/status.js:411 +msgid "Not Active" +msgstr "No activo" + +#: feeds/luci/applications/luci-app-airoha-npu/htdocs/luci-static/resources/view/airoha_npu/status.js:322 +msgid "Not available" +msgstr "No disponible" + +#: feeds/luci/applications/luci-app-airoha-npu/htdocs/luci-static/resources/view/airoha_npu/status.js:339 +msgid "Offload Statistics" +msgstr "Estadísticas de descarga" + +#: feeds/luci/applications/luci-app-airoha-npu/htdocs/luci-static/resources/view/airoha_npu/status.js:358 +msgid "Original Flow" +msgstr "Flujo original" + +#: feeds/luci/applications/luci-app-airoha-npu/htdocs/luci-static/resources/view/airoha_npu/status.js:306 +msgid "Overclock" +msgstr "Overclock" + +#: feeds/luci/applications/luci-app-airoha-npu/htdocs/luci-static/resources/view/airoha_npu/status.js:226 +#: feeds/luci/applications/luci-app-airoha-npu/htdocs/luci-static/resources/view/airoha_npu/status.js:234 +msgid "Overclock failed:" +msgstr "Overclock fallido:" + +#: feeds/luci/applications/luci-app-airoha-npu/htdocs/luci-static/resources/view/airoha_npu/status.js:347 +msgid "PPE Flow Offload Entries" +msgstr "Entradas de descarga de flujo de PPE" + +#: feeds/luci/applications/luci-app-airoha-npu/htdocs/luci-static/resources/view/airoha_npu/status.js:361 +msgid "Packets" +msgstr "Packets" + +#: feeds/luci/applications/luci-app-airoha-npu/htdocs/luci-static/resources/view/airoha_npu/status.js:335 +msgid "Reserved Memory" +msgstr "Memoria reservada" + +#: feeds/luci/applications/luci-app-airoha-npu/root/usr/share/luci/menu.d/luci-app-airoha-npu.json:3 +msgid "SoC Status" +msgstr "Procesadores" + +#: feeds/luci/applications/luci-app-airoha-npu/htdocs/luci-static/resources/view/airoha_npu/status.js:356 +msgid "State" +msgstr "Estado" + +#: feeds/luci/applications/luci-app-airoha-npu/htdocs/luci-static/resources/view/airoha_npu/status.js:349 +#: feeds/luci/applications/luci-app-airoha-npu/htdocs/luci-static/resources/view/airoha_npu/status.js:419 +msgid "Total:" +msgstr "Total:" + +#: feeds/luci/applications/luci-app-airoha-npu/htdocs/luci-static/resources/view/airoha_npu/status.js:357 +msgid "Type" +msgstr "Tipo" + +#: feeds/luci/applications/luci-app-airoha-npu/htdocs/luci-static/resources/view/airoha_npu/status.js:351 +#: feeds/luci/applications/luci-app-airoha-npu/htdocs/luci-static/resources/view/airoha_npu/status.js:421 +msgid "Unbound:" +msgstr "Desatado:" diff --git a/package/luci-app-airoha-npu/po/templates/luci-app-airoha-npu.pot b/package/luci-app-airoha-npu/po/templates/luci-app-airoha-npu.pot new file mode 100644 index 00000000000000..6ac192e98bff55 --- /dev/null +++ b/package/luci-app-airoha-npu/po/templates/luci-app-airoha-npu.pot @@ -0,0 +1,164 @@ +msgid "" +msgstr "Content-Type: text/plain; charset=UTF-8" + +#: feeds/luci/applications/luci-app-airoha-npu/htdocs/luci-static/resources/view/airoha_npu/status.js:327 +#: feeds/luci/applications/luci-app-airoha-npu/htdocs/luci-static/resources/view/airoha_npu/status.js:406 +msgid "Active" +msgstr "" + +#: feeds/luci/applications/luci-app-airoha-npu/htdocs/luci-static/resources/view/airoha_npu/status.js:284 +msgid "Airoha SoC Status" +msgstr "" + +#: feeds/luci/applications/luci-app-airoha-npu/htdocs/luci-static/resources/view/airoha_npu/status.js:224 +#: feeds/luci/applications/luci-app-airoha-npu/htdocs/luci-static/resources/view/airoha_npu/status.js:233 +#: feeds/luci/applications/luci-app-airoha-npu/htdocs/luci-static/resources/view/airoha_npu/status.js:237 +msgid "Apply" +msgstr "" + +#: feeds/luci/applications/luci-app-airoha-npu/htdocs/luci-static/resources/view/airoha_npu/status.js:221 +msgid "Applying..." +msgstr "" + +#: feeds/luci/applications/luci-app-airoha-npu/htdocs/luci-static/resources/view/airoha_npu/status.js:350 +#: feeds/luci/applications/luci-app-airoha-npu/htdocs/luci-static/resources/view/airoha_npu/status.js:420 +msgid "Bound:" +msgstr "" + +#: feeds/luci/applications/luci-app-airoha-npu/htdocs/luci-static/resources/view/airoha_npu/status.js:362 +msgid "Bytes" +msgstr "" + +#: feeds/luci/applications/luci-app-airoha-npu/htdocs/luci-static/resources/view/airoha_npu/status.js:310 +msgid "CPU Cores" +msgstr "" + +#: feeds/luci/applications/luci-app-airoha-npu/htdocs/luci-static/resources/view/airoha_npu/status.js:288 +msgid "CPU Frequency" +msgstr "" + +#: feeds/luci/applications/luci-app-airoha-npu/htdocs/luci-static/resources/view/airoha_npu/status.js:229 +msgid "CPU set to" +msgstr "" + +#: feeds/luci/applications/luci-app-airoha-npu/htdocs/luci-static/resources/view/airoha_npu/status.js:291 +msgid "Current Frequency" +msgstr "" + +#: feeds/luci/applications/luci-app-airoha-npu/htdocs/luci-static/resources/view/airoha_npu/status.js:244 +msgid "" +"Direct PLL programming. Governor locked to performance. Stock max: 1200 MHz. " +"Tested stable up to 1500 MHz." +msgstr "" + +#: feeds/luci/applications/luci-app-airoha-npu/htdocs/luci-static/resources/view/airoha_npu/status.js:360 +msgid "Ethernet" +msgstr "" + +#: feeds/luci/applications/luci-app-airoha-npu/htdocs/luci-static/resources/view/airoha_npu/status.js:85 +msgid "Failed to set governor:" +msgstr "" + +#: feeds/luci/applications/luci-app-airoha-npu/htdocs/luci-static/resources/view/airoha_npu/status.js:112 +msgid "Failed to set max frequency:" +msgstr "" + +#: feeds/luci/applications/luci-app-airoha-npu/htdocs/luci-static/resources/view/airoha_npu/status.js:212 +msgid "Frequency must be 500-1600 MHz" +msgstr "" + +#: feeds/luci/applications/luci-app-airoha-npu/htdocs/luci-static/resources/view/airoha_npu/status.js:296 +msgid "Governor" +msgstr "" + +#: feeds/luci/applications/luci-app-airoha-npu/root/usr/share/rpcd/acl.d/luci-app-airoha-npu.json:3 +msgid "Grant access to Airoha SoC status (NPU + CPU + Overclock)" +msgstr "" + +#: feeds/luci/applications/luci-app-airoha-npu/htdocs/luci-static/resources/view/airoha_npu/status.js:355 +msgid "Index" +msgstr "" + +#: feeds/luci/applications/luci-app-airoha-npu/htdocs/luci-static/resources/view/airoha_npu/status.js:301 +msgid "Max Frequency" +msgstr "" + +#: feeds/luci/applications/luci-app-airoha-npu/htdocs/luci-static/resources/view/airoha_npu/status.js:331 +msgid "NPU Clock / Cores" +msgstr "" + +#: feeds/luci/applications/luci-app-airoha-npu/htdocs/luci-static/resources/view/airoha_npu/status.js:321 +msgid "NPU Firmware Version" +msgstr "" + +#: feeds/luci/applications/luci-app-airoha-npu/htdocs/luci-static/resources/view/airoha_npu/status.js:318 +msgid "NPU Information" +msgstr "" + +#: feeds/luci/applications/luci-app-airoha-npu/htdocs/luci-static/resources/view/airoha_npu/status.js:325 +msgid "NPU Status" +msgstr "" + +#: feeds/luci/applications/luci-app-airoha-npu/htdocs/luci-static/resources/view/airoha_npu/status.js:359 +msgid "New Flow" +msgstr "" + +#: feeds/luci/applications/luci-app-airoha-npu/htdocs/luci-static/resources/view/airoha_npu/status.js:328 +#: feeds/luci/applications/luci-app-airoha-npu/htdocs/luci-static/resources/view/airoha_npu/status.js:411 +msgid "Not Active" +msgstr "" + +#: feeds/luci/applications/luci-app-airoha-npu/htdocs/luci-static/resources/view/airoha_npu/status.js:322 +msgid "Not available" +msgstr "" + +#: feeds/luci/applications/luci-app-airoha-npu/htdocs/luci-static/resources/view/airoha_npu/status.js:339 +msgid "Offload Statistics" +msgstr "" + +#: feeds/luci/applications/luci-app-airoha-npu/htdocs/luci-static/resources/view/airoha_npu/status.js:358 +msgid "Original Flow" +msgstr "" + +#: feeds/luci/applications/luci-app-airoha-npu/htdocs/luci-static/resources/view/airoha_npu/status.js:306 +msgid "Overclock" +msgstr "" + +#: feeds/luci/applications/luci-app-airoha-npu/htdocs/luci-static/resources/view/airoha_npu/status.js:226 +#: feeds/luci/applications/luci-app-airoha-npu/htdocs/luci-static/resources/view/airoha_npu/status.js:234 +msgid "Overclock failed:" +msgstr "" + +#: feeds/luci/applications/luci-app-airoha-npu/htdocs/luci-static/resources/view/airoha_npu/status.js:347 +msgid "PPE Flow Offload Entries" +msgstr "" + +#: feeds/luci/applications/luci-app-airoha-npu/htdocs/luci-static/resources/view/airoha_npu/status.js:361 +msgid "Packets" +msgstr "" + +#: feeds/luci/applications/luci-app-airoha-npu/htdocs/luci-static/resources/view/airoha_npu/status.js:335 +msgid "Reserved Memory" +msgstr "" + +#: feeds/luci/applications/luci-app-airoha-npu/root/usr/share/luci/menu.d/luci-app-airoha-npu.json:3 +msgid "SoC Status" +msgstr "" + +#: feeds/luci/applications/luci-app-airoha-npu/htdocs/luci-static/resources/view/airoha_npu/status.js:356 +msgid "State" +msgstr "" + +#: feeds/luci/applications/luci-app-airoha-npu/htdocs/luci-static/resources/view/airoha_npu/status.js:349 +#: feeds/luci/applications/luci-app-airoha-npu/htdocs/luci-static/resources/view/airoha_npu/status.js:419 +msgid "Total:" +msgstr "" + +#: feeds/luci/applications/luci-app-airoha-npu/htdocs/luci-static/resources/view/airoha_npu/status.js:357 +msgid "Type" +msgstr "" + +#: feeds/luci/applications/luci-app-airoha-npu/htdocs/luci-static/resources/view/airoha_npu/status.js:351 +#: feeds/luci/applications/luci-app-airoha-npu/htdocs/luci-static/resources/view/airoha_npu/status.js:421 +msgid "Unbound:" +msgstr "" diff --git a/package/luci-app-airoha-npu/root/usr/libexec/rpcd/luci.airoha_npu b/package/luci-app-airoha-npu/root/usr/libexec/rpcd/luci.airoha_npu new file mode 100755 index 00000000000000..2ee955c6d67b21 --- /dev/null +++ b/package/luci-app-airoha-npu/root/usr/libexec/rpcd/luci.airoha_npu @@ -0,0 +1,607 @@ +#!/bin/sh + +# Airoha SoC Status RPC backend for LuCI (NPU + CPU + Overclock) + +# Timeout-protected hardware register read via devmem. +# Prevents rpcd/LuCI hang when a register bus stalls. +# Usage: _devmem_read
[timeout_sec] +# Returns register value (hex or decimal) on success, "0" on timeout/error. +_devmem_read() { + local addr="$1" + local to="${2:-2}" + local val="" tmp pid i + + # Preferred path: busybox/coreutils timeout + if command -v timeout >/dev/null 2>&1; then + val=$(timeout "$to" devmem "$addr" 2>/dev/null) || val="" + echo "${val:-0}" + return 0 + fi + + # Fallback: background process + manual kill-on-timeout + tmp="/tmp/_dm_${$}" + ( devmem "$addr" >"$tmp" 2>/dev/null ) & + pid=$! + i=0 + while [ "$i" -lt "$to" ]; do + kill -0 "$pid" 2>/dev/null || break + sleep 1 + i=$((i + 1)) + done + + if kill -0 "$pid" 2>/dev/null; then + kill -9 "$pid" 2>/dev/null + wait "$pid" 2>/dev/null + rm -f "$tmp" + echo "0" + return 0 + fi + + wait "$pid" 2>/dev/null + if [ -f "$tmp" ] && [ -s "$tmp" ]; then + read -r val <"$tmp" 2>/dev/null + fi + rm -f "$tmp" + echo "${val:-0}" + return 0 +} + +# Check if NPU bus driver is loaded and ready for register access. +# Returns 0 (true) if ready, 1 (false) otherwise. +_npu_bus_ready() { + [ -d "/sys/bus/platform/drivers/airoha-npu" ] && \ + ls -1 "/sys/bus/platform/drivers/airoha-npu" 2>/dev/null | grep -q '\.npu$' +} + +get_status() { + # === NPU Status === + # Read the actually-loaded NPU firmware path from DTS (firmware-name property). + local npu_fw="" + local npu_dt="/proc/device-tree/soc/npu@1e900000/firmware-name" + if [ -f "$npu_dt" ]; then + # firmware-name is a string list (null-separated). First entry is the rv32 code. + local fw_rel=$(tr '\0' '\n' < "$npu_dt" 2>/dev/null | grep -m1 "rv32") + [ -n "$fw_rel" ] && npu_fw="/lib/firmware/$fw_rel" + fi + + local npu_ver="Unknown" + if [ -n "$npu_fw" ] && [ -f "$npu_fw" ]; then + npu_ver=$(strings "$npu_fw" 2>/dev/null | grep -oE '([0-9]+\.[0-9]+\.[0-9]+-)?TLB[0-9.]+[-_v0-9]*' | head -1) + [ -z "$npu_ver" ] && npu_ver="Unknown" + fi + + local npu_loaded="false" + local npu_device="" + local npu_driver_path="/sys/bus/platform/drivers/airoha-npu" + + if [ -d "$npu_driver_path" ]; then + npu_device=$(ls -1 "$npu_driver_path" 2>/dev/null | grep '\.npu$' | head -1) + if [ -n "$npu_device" ]; then + npu_loaded="true" + fi + fi + + local npu_clock=0 + if [ -f "/sys/kernel/debug/clk/npu/clk_rate" ]; then + npu_clock=$(cat /sys/kernel/debug/clk/npu/clk_rate 2>/dev/null || echo 0) + fi + + local npu_cores=$(grep -c "airoha-npu-wdt" /proc/interrupts 2>/dev/null || echo 0) + + local offload_bound=$(cat /sys/kernel/debug/ppe/bind 2>/dev/null | grep -c "^[0-9a-fA-F]") + local offload_total=$(cat /sys/kernel/debug/ppe/entries 2>/dev/null | grep -c "^[0-9a-fA-F]") + + local mem_info=$(dmesg | grep "reserved mem.*npu" | head -4) + local regions="[]" + + if [ -n "$mem_info" ]; then + regions=$(echo "$mem_info" | awk ' + BEGIN { printf "[" } + { + if (NR > 1) printf "," + match($0, /0x[0-9a-fA-F]+\.\.0x[0-9a-fA-F]+/) + range = substr($0, RSTART, RLENGTH) + split(range, a, "\\.\\.") + match($0, /\([0-9]+ [KMG]i?B\)/) + size = substr($0, RSTART+1, RLENGTH-2) + match($0, /non-reusable [^ ]+/) + name = substr($0, RSTART+13) + gsub(/@.*/, "", name) + printf "{\"name\":\"%s\",\"start\":\"%s\",\"end\":\"%s\",\"size\":\"%s\"}", name, a[1], a[2], size + } + END { printf "]" } + ') + fi + + # === CPU Frequency Status === + local cpufreq_path="/sys/devices/system/cpu/cpufreq/policy0" + local cpu_cur_freq=0 + local cpu_hw_freq=0 + local cpu_governor="unknown" + local cpu_avail_governors="" + local cpu_min_freq=0 + local cpu_max_freq=0 + local cpu_avail_freqs="" + + if [ -d "$cpufreq_path" ]; then + cpu_cur_freq=$(cat "$cpufreq_path/scaling_cur_freq" 2>/dev/null || echo 0) + cpu_hw_freq=$(cat "$cpufreq_path/cpuinfo_cur_freq" 2>/dev/null || echo 0) + cpu_governor=$(cat "$cpufreq_path/scaling_governor" 2>/dev/null || echo "unknown") + cpu_avail_governors=$(cat "$cpufreq_path/scaling_available_governors" 2>/dev/null || echo "") + cpu_min_freq=$(cat "$cpufreq_path/scaling_min_freq" 2>/dev/null || echo 0) + cpu_max_freq=$(cat "$cpufreq_path/scaling_max_freq" 2>/dev/null || echo 0) + cpu_avail_freqs=$(cat "$cpufreq_path/scaling_available_frequencies" 2>/dev/null || echo "") + fi + + # Read actual PLL frequency from hardware (detects overclock) + local pll_freq=0 + if _npu_bus_ready && command -v devmem > /dev/null 2>&1; then + local pcw_raw=$(_devmem_read 0x1fa202b4) + local chg_raw=$(_devmem_read 0x1fa202b8) + if [ -n "$pcw_raw" ] && [ -n "$chg_raw" ]; then + pcw_raw=$((pcw_raw)) + chg_raw=$((chg_raw)) + local pcw_int=$(( (pcw_raw >> 24) & 0x7F )) + local pll_posdiv=$(( (chg_raw >> 4) & 7 )) + if [ "$pll_posdiv" -eq 0 ]; then + pll_freq=$((pcw_int * 50)) + else + pll_freq=$((pcw_int * 50 / (1 << pll_posdiv))) + fi + fi + fi + + # Count online CPUs + local cpu_count=0 + if [ -d "/sys/devices/system/cpu" ]; then + cpu_count=$(ls -d /sys/devices/system/cpu/cpu[0-9]* 2>/dev/null | wc -l) + fi + + printf '{"npu_version":"%s","npu_loaded":%s,"npu_device":"%s","npu_clock":%d,"npu_cores":%d,"offload_bound":%d,"offload_total":%d,"memory_regions":%s,"cpu_cur_freq":%d,"cpu_hw_freq":%d,"cpu_governor":"%s","cpu_avail_governors":"%s","cpu_min_freq":%d,"cpu_max_freq":%d,"cpu_avail_freqs":"%s","cpu_count":%d,"pll_freq_mhz":%d}' \ + "${npu_ver:-Unknown}" "$npu_loaded" "${npu_device:-}" "${npu_clock:-0}" "${npu_cores:-0}" "${offload_bound:-0}" "${offload_total:-0}" "$regions" \ + "${cpu_cur_freq:-0}" "${cpu_hw_freq:-0}" "${cpu_governor:-unknown}" "${cpu_avail_governors:-}" "${cpu_min_freq:-0}" "${cpu_max_freq:-0}" "${cpu_avail_freqs:-}" "${cpu_count:-0}" "${pll_freq:-0}" +} + +get_ppe_entries() { + local entries_file="/sys/kernel/debug/ppe/entries" + + if [ ! -f "$entries_file" ]; then + echo '{"entries":[]}' + return + fi + + awk ' + BEGIN { + printf "{\"entries\":[" + first = 1 + } + /^[0-9a-fA-F]+ / { + if (first == 0) printf "," + first = 0 + + index_val = $1 + state = $2 + + type = "" + subtype = "" + if ($3 == "IPv4" || $3 == "IPv6") { + type = $3 + subtype = $4 + } else if ($3 == "L2B" || $4 == "L2B") { + type = "L2B" + subtype = "" + } else { + type = $3 + subtype = $4 + } + + orig = "" + match($0, /orig=[^ ]+/) + if (RSTART > 0) { + orig = substr($0, RSTART+5, RLENGTH-5) + } + + new_flow = "" + match($0, /new=[^ ]+/) + if (RSTART > 0) { + new_flow = substr($0, RSTART+4, RLENGTH-4) + } + + eth = "" + match($0, /eth=[^ ]+/) + if (RSTART > 0) { + eth = substr($0, RSTART+4, RLENGTH-4) + } + + printf "{\"index\":\"%s\",\"state\":\"%s\",\"type\":\"%s %s\",\"orig\":\"%s\",\"new_flow\":\"%s\",\"eth\":\"%s\"}", \ + index_val, state, type, subtype, orig, new_flow, eth + } + END { + printf "]}" + } + ' "$entries_file" 2>/dev/null || echo '{"entries":[]}' +} + +get_token_info() { + local token_file="/sys/kernel/debug/ieee80211/phy0/mt76/token_info" + + # Gather per-band WiFi station stats (count, retries, failures) + local sta_json="" band + for band in 0 1 2; do + local stats=$( + for iface in $(iw dev 2>/dev/null | awk '/Interface phy0\.'$band'-/{print $2}'); do + iw dev "$iface" station dump 2>/dev/null + done | awk ' + BEGIN { n=0; tp=0; tr=0 } + /^Station/ { n++ } + /tx packets:/ { tp += $3 } + /tx retries:/ { tr += $3 } + END { printf "%d %d %d", n, tp, tr }' + ) + set -- $stats + [ -n "$sta_json" ] && sta_json="${sta_json}," + sta_json="${sta_json}{\"band\":${band},\"count\":${1:-0},\"tx_packets\":${2:-0},\"tx_retries\":${3:-0}}" + done + + if [ ! -f "$token_file" ]; then + printf '{"token_count":0,"token_size":0,"npu_active":false,"tx_queues":[],"station_counts":[%s]}' "$sta_json" + return + fi + + awk -v stations="$sta_json" ' + BEGIN { + tc=0; ts=0; npu=0 + tq_n=0 + } + /^ token_count:/ { tc=$2+0; ts=$4+0 } + /status:.*active/ { npu=1 } + /band [0-9]/ { + gsub(/\|/, " ") + for(i=1;i<=NF;i++) { + if($i=="band") tq_band[tq_n]=$(i+1)+0 + if($i=="npu" || $i=="dma") tq_type[tq_n]=$i + } + cnt=0 + for(i=1;i<=NF;i++) { + if($i ~ /^[0-9]+$/ && $(i-1)!="band") { + tq_v[tq_n,cnt++]=$i+0 + } + } + tq_n++ + } + END { + printf "{\"token_count\":%d,\"token_size\":%d,\"npu_active\":%s,\"tx_queues\":[", \ + tc, ts, (npu?"true":"false") + + for(i=0;i0) printf "," + printf "{\"band\":%d,\"type\":\"%s\",\"ndesc\":%d,\"queued\":%d,\"head\":%d,\"tail\":%d}", \ + tq_band[i], tq_type[i], tq_v[i,0], tq_v[i,1], tq_v[i,2], tq_v[i,3] + } + + printf "],\"station_counts\":[%s]}", stations + } + ' "$token_file" 2>/dev/null || printf '{"token_count":0,"token_size":0,"npu_active":false,"tx_queues":[],"station_counts":[%s]}' "$sta_json" +} + +set_governor() { + local governor + governor=$(echo "$1" | jsonfilter -e "@.governor" 2>/dev/null) + + if [ -z "$governor" ]; then + echo '{"error":"missing governor parameter"}' + return + fi + + # Validate governor is available + local avail=$(cat /sys/devices/system/cpu/cpufreq/policy0/scaling_available_governors 2>/dev/null) + if ! echo "$avail" | grep -qw "$governor"; then + printf '{"error":"invalid governor: %s"}' "$governor" + return + fi + + echo "$governor" > /sys/devices/system/cpu/cpufreq/policy0/scaling_governor 2>/dev/null + if [ $? -eq 0 ]; then + printf '{"result":"ok","governor":"%s"}' "$governor" + else + echo '{"error":"failed to set governor"}' + fi +} + +set_max_freq() { + local freq + freq=$(echo "$1" | jsonfilter -e "@.freq" 2>/dev/null) + + if [ -z "$freq" ]; then + echo '{"error":"missing freq parameter"}' + return + fi + + # Validate freq is in available list + local avail=$(cat /sys/devices/system/cpu/cpufreq/policy0/scaling_available_frequencies 2>/dev/null) + if ! echo "$avail" | grep -qw "$freq"; then + printf '{"error":"invalid frequency: %s"}' "$freq" + return + fi + + echo "$freq" > /sys/devices/system/cpu/cpufreq/policy0/scaling_max_freq 2>/dev/null + if [ $? -eq 0 ]; then + printf '{"result":"ok","freq":%d}' "$freq" + else + echo '{"error":"failed to set max frequency"}' + fi +} + +set_overclock() { + local target_mhz + target_mhz=$(echo "$1" | jsonfilter -e "@.freq_mhz" 2>/dev/null) + + if [ -z "$target_mhz" ]; then + echo '{"error":"missing freq_mhz parameter"}' + return + fi + + # Validate range (500-1600, crash likely above 1500 at stock voltage) + if [ "$target_mhz" -lt 500 ] || [ "$target_mhz" -gt 1600 ]; then + echo '{"error":"frequency must be 500-1600 MHz"}' + return + fi + + if ! command -v devmem > /dev/null 2>&1; then + echo '{"error":"devmem not available"}' + return + fi + + # Calculate PCW and posdiv + local posdiv pcw_int + if [ "$target_mhz" -lt 1000 ]; then + posdiv=1 + pcw_int=$((target_mhz / 25)) + else + posdiv=0 + pcw_int=$((target_mhz / 50)) + fi + + # Lock governor to performance to prevent cpufreq from fighting + echo performance > /sys/devices/system/cpu/cpufreq/policy0/scaling_governor 2>/dev/null + + # Read current register values + local cur_clkmux=$(($(_devmem_read 0x1fa201e0))) + local cur_pcw=$(($(_devmem_read 0x1fa202b4))) + local cur_chg=$(($(_devmem_read 0x1fa202b8))) + + # Step 1: Enable PLL2 path in CLK_MUX (set bit 2) + devmem 0x1fa201e0 32 $(printf "0x%08X" $((cur_clkmux | 0x4))) + + # Step 2: MCUCFG switch to PLL2 (sel=3 in bits[10:9]) + local cur_u=$(($(_devmem_read 0x1efbe640))) + devmem 0x1efbe640 32 $(printf "0x%08X" $(((cur_u & 0xFFFFFFE0) | 0x12))) + local cur_s=$(($(_devmem_read 0x1efbe7c0))) + devmem 0x1efbe7c0 32 $(printf "0x%08X" $(((cur_s & 0xFFFFF9FF) | (3 << 9)))) + + # Step 3: Unlock PLL registers + local cur_p=$(($(_devmem_read 0x1fa20268))) + devmem 0x1fa20268 32 $(printf "0x%08X" $(((cur_p & 0xFFFFFF00) | 0x12))) + + # Step 4: Write new PCW integer + devmem 0x1fa202b4 32 $(printf "0x%08X" $(((cur_pcw & 0x00FFFFFF) | (pcw_int << 24)))) + + # Step 5: Write posdiv and toggle CHG bit + local chg_bit=$((cur_chg & 1)) + local cleared=$((cur_chg & 0xFFFFFF8E)) + if [ "$chg_bit" -eq 1 ]; then + devmem 0x1fa202b8 32 $(printf "0x%08X" $((cleared | (posdiv << 4)))) + else + devmem 0x1fa202b8 32 $(printf "0x%08X" $((cleared | (posdiv << 4) | 1))) + fi + + # Step 6: Wait for PLL lock + sleep 0.02 + + # Step 7: Enable ARMPLL path in CLK_MUX (set bit 0) + local cur_clkmux2=$(($(_devmem_read 0x1fa201e0))) + devmem 0x1fa201e0 32 $(printf "0x%08X" $((cur_clkmux2 | 0x1))) + + # Step 8: MCUCFG switch back to ARMPLL + local cur_u2=$(($(_devmem_read 0x1efbe640))) + devmem 0x1efbe640 32 $(printf "0x%08X" $(((cur_u2 & 0xFFFFFFE0) | 0x12))) + local cur_s2=$(($(_devmem_read 0x1efbe7c0))) + devmem 0x1efbe7c0 32 $(printf "0x%08X" $(((cur_s2 & 0xFFFFF9FF) | (1 << 9)))) + + # Step 9: Clear PLL2 path in CLK_MUX + local cur_clkmux3=$(($(_devmem_read 0x1fa201e0))) + devmem 0x1fa201e0 32 $(printf "0x%08X" $((cur_clkmux3 & 0xFFFFFFFB))) + + # Step 10: Re-lock PLL registers + local cur_p2=$(($(_devmem_read 0x1fa20268))) + devmem 0x1fa20268 32 $(printf "0x%08X" $((cur_p2 & 0xFFFFFF00))) + + # Verify + local final_pcw=$(($(_devmem_read 0x1fa202b4))) + local final_chg=$(($(_devmem_read 0x1fa202b8))) + local read_pcw=$(( (final_pcw >> 24) & 0x7F )) + local read_posdiv=$(( (final_chg >> 4) & 7 )) + local read_freq + if [ "$read_posdiv" -eq 0 ]; then + read_freq=$((read_pcw * 50)) + else + read_freq=$((read_pcw * 50 / (1 << read_posdiv))) + fi + + printf '{"result":"ok","target_mhz":%d,"actual_mhz":%d,"pcw":%d,"posdiv":%d}' \ + "$target_mhz" "$read_freq" "$read_pcw" "$read_posdiv" +} + +get_frame_engine() { + if ! command -v devmem > /dev/null 2>&1; then + echo '{"error":"devmem not available"}' + return + fi + + if ! _npu_bus_ready; then + echo '{"error":"NPU bus not ready"}' + return + fi + + # PSE queue status (IQ=hi16, OQ=lo16) and drop counts for each port + local i reg_q reg_d pse_ports="" + for i in 0 1 2 3 4 5 6 7 8 9; do + reg_q=$(printf "0x%x" $((0x1fb50150 + i * 4))) + reg_d=$(printf "0x%x" $((0x1fb50120 + i * 4))) + local qval=$(($(_devmem_read $reg_q))) + local dval=$(($(_devmem_read $reg_d))) + local iq=$(( (qval >> 16) & 0xFFFF )) + local oq=$(( qval & 0xFFFF )) + [ -n "$pse_ports" ] && pse_ports="${pse_ports}," + pse_ports="${pse_ports}{\"port\":${i},\"iq\":${iq},\"oq\":${oq},\"drops\":${dval}}" + done + + # PSE shared buffer + local psb=$(($(_devmem_read 0x1fb50104))) + local pse_used=$(( (psb >> 16) & 0xFFFF )) + local pse_free=$(( psb & 0x7FFF )) + + # GDM counters (TX_OK, TX_DROP, RX_OK, RX_FC_DROP, RX_OVER_DROP) + # Read as hex to handle large 32-bit counters + local gdm1_tx=$(($(_devmem_read 0x1fb50604))) + local gdm1_txd=$(($(_devmem_read 0x1fb50608))) + local gdm1_rx=$(($(_devmem_read 0x1fb50648))) + local gdm1_rxd=$(($(_devmem_read 0x1fb5064c))) + + local gdm2_tx=$(($(_devmem_read 0x1fb51604))) + local gdm2_txd=$(($(_devmem_read 0x1fb51608))) + local gdm2_rx=$(($(_devmem_read 0x1fb51648))) + local gdm2_rxd=$(($(_devmem_read 0x1fb5164c))) + + local gdm4_tx=$(($(_devmem_read 0x1fb52604))) + local gdm4_txd=$(($(_devmem_read 0x1fb52608))) + local gdm4_rx=$(($(_devmem_read 0x1fb52648))) + local gdm4_rxd=$(($(_devmem_read 0x1fb5264c))) + + # CDM counters (TX_OK, RXCPU_OK, RXHWF_OK, RXCPU_DROP, RXHWF_DROP) + local cdm1_tx=$(($(_devmem_read 0x1fb50580))) + local cdm1_rxcpu=$(($(_devmem_read 0x1fb50590))) + local cdm1_rxhwf=$(($(_devmem_read 0x1fb50594))) + local cdm1_rxcpud=$(($(_devmem_read 0x1fb505a0))) + local cdm1_rxhwfd=$(($(_devmem_read 0x1fb505a4))) + + local cdm2_tx=$(($(_devmem_read 0x1fb51580))) + local cdm2_rxcpu=$(($(_devmem_read 0x1fb51590))) + local cdm2_rxhwf=$(($(_devmem_read 0x1fb51594))) + local cdm2_rxcpud=$(($(_devmem_read 0x1fb515a0))) + local cdm2_rxhwfd=$(($(_devmem_read 0x1fb515a4))) + + printf '{"pse_ports":[%s],"pse_used":%d,"pse_free":%d,' "$pse_ports" "$pse_used" "$pse_free" + printf '"gdm1":{"tx":%d,"tx_drop":%d,"rx":%d,"rx_drop":%d},' "$gdm1_tx" "$gdm1_txd" "$gdm1_rx" "$gdm1_rxd" + printf '"gdm2":{"tx":%d,"tx_drop":%d,"rx":%d,"rx_drop":%d},' "$gdm2_tx" "$gdm2_txd" "$gdm2_rx" "$gdm2_rxd" + printf '"gdm4":{"tx":%d,"tx_drop":%d,"rx":%d,"rx_drop":%d},' "$gdm4_tx" "$gdm4_txd" "$gdm4_rx" "$gdm4_rxd" + printf '"cdm1":{"tx":%d,"rx_cpu":%d,"rx_hwf":%d,"rx_cpu_drop":%d,"rx_hwf_drop":%d},' "$cdm1_tx" "$cdm1_rxcpu" "$cdm1_rxhwf" "$cdm1_rxcpud" "$cdm1_rxhwfd" + printf '"cdm2":{"tx":%d,"rx_cpu":%d,"rx_hwf":%d,"rx_cpu_drop":%d,"rx_hwf_drop":%d}}' "$cdm2_tx" "$cdm2_rxcpu" "$cdm2_rxhwf" "$cdm2_rxcpud" "$cdm2_rxhwfd" +} + +get_vlan_offload() { + local val + val=$(cat /proc/sys/net/bridge/bridge-nf-filter-vlan-tagged 2>/dev/null) + printf '{"enabled":%d}' "${val:-0}" +} + +set_vlan_offload() { + local enabled + enabled=$(echo "$1" | jsonfilter -e "@.enabled" 2>/dev/null) + + # jsonfilter returns empty string for integer 0; treat empty as 0 + [ -z "$enabled" ] && enabled="0" + + if [ "$enabled" != "0" ] && [ "$enabled" != "1" ]; then + echo '{"error":"enabled must be 0 or 1"}' + return + fi + + if ! echo "$enabled" > /proc/sys/net/bridge/bridge-nf-filter-vlan-tagged 2>/dev/null; then + echo '{"error":"failed - bridge module may not be loaded"}' + return + fi + + # Persist across reboots + echo "net.bridge.bridge-nf-filter-vlan-tagged=$enabled" > /etc/sysctl.d/14-vlan-offload.conf + printf '{"result":"ok","enabled":%d}' "$enabled" +} + +get_pppoe_offload() { + local val + val=$(cat /proc/sys/net/bridge/bridge-nf-filter-pppoe-tagged 2>/dev/null) + printf '{"enabled":%d}' "${val:-0}" +} + +set_pppoe_offload() { + local enabled + enabled=$(echo "$1" | jsonfilter -e "@.enabled" 2>/dev/null) + + # jsonfilter returns empty string for integer 0; treat empty as 0 + [ -z "$enabled" ] && enabled="0" + + if [ "$enabled" != "0" ] && [ "$enabled" != "1" ]; then + echo '{"error":"enabled must be 0 or 1"}' + return + fi + + if ! echo "$enabled" > /proc/sys/net/bridge/bridge-nf-filter-pppoe-tagged 2>/dev/null; then + echo '{"error":"failed - bridge module may not be loaded"}' + return + fi + + # Persist across reboots + echo "net.bridge.bridge-nf-filter-pppoe-tagged=$enabled" > /etc/sysctl.d/15-pppoe-offload.conf + printf '{"result":"ok","enabled":%d}' "$enabled" +} + +case "$1" in + list) + echo '{"getStatus":{},"getPpeEntries":{},"getTokenInfo":{},"getFrameEngine":{},"setGovernor":{"governor":"str"},"setMaxFreq":{"freq":"int"},"setOverclock":{"freq_mhz":"int"},"getVlanOffload":{},"setVlanOffload":{"enabled":"int"},"getPPPoEOffload":{},"setPPPoEOffload":{"enabled":"int"}}' + ;; + call) + case "$2" in + getStatus) + get_status + ;; + getPpeEntries) + get_ppe_entries + ;; + getTokenInfo) + get_token_info + ;; + setGovernor) + read input + set_governor "$input" + ;; + setMaxFreq) + read input + set_max_freq "$input" + ;; + setOverclock) + read input + set_overclock "$input" + ;; + getFrameEngine) + get_frame_engine + ;; + getVlanOffload) + get_vlan_offload + ;; + setVlanOffload) + read input + set_vlan_offload "$input" + ;; + getPPPoEOffload) + get_pppoe_offload + ;; + setPPPoEOffload) + read input + set_pppoe_offload "$input" + ;; + *) + echo '{"error":"Invalid method"}' + ;; + esac + ;; +esac diff --git a/package/luci-app-airoha-npu/root/usr/share/luci/menu.d/luci-app-airoha-npu.json b/package/luci-app-airoha-npu/root/usr/share/luci/menu.d/luci-app-airoha-npu.json new file mode 100644 index 00000000000000..338065189c6949 --- /dev/null +++ b/package/luci-app-airoha-npu/root/usr/share/luci/menu.d/luci-app-airoha-npu.json @@ -0,0 +1,13 @@ +{ + "admin/status/npu": { + "title": "SoC Status", + "order": 15, + "action": { + "type": "view", + "path": "airoha_npu/status" + }, + "depends": { + "acl": [ "luci-app-airoha-npu" ] + } + } +} diff --git a/package/luci-app-airoha-npu/root/usr/share/rpcd/acl.d/luci-app-airoha-npu.json b/package/luci-app-airoha-npu/root/usr/share/rpcd/acl.d/luci-app-airoha-npu.json new file mode 100644 index 00000000000000..1b48738eb9aece --- /dev/null +++ b/package/luci-app-airoha-npu/root/usr/share/rpcd/acl.d/luci-app-airoha-npu.json @@ -0,0 +1,26 @@ +{ + "luci-app-airoha-npu": { + "description": "Grant access to Airoha SoC status (NPU + CPU + Overclock)", + "read": { + "ubus": { + "luci.airoha_npu": [ "getStatus", "getPpeEntries", "getTokenInfo", "getFrameEngine", "getVlanOffload", "getPPPoEOffload" ] + }, + "file": { + "/sys/kernel/debug/ppe/entries": [ "read" ], + "/sys/kernel/debug/ppe/bind": [ "read" ], + "/sys/devices/system/cpu/cpufreq/policy0/*": [ "read" ], + "/sys/kernel/debug/ieee80211/*/mt76/token_info": [ "read" ] + } + }, + "write": { + "ubus": { + "luci.airoha_npu": [ "setGovernor", "setMaxFreq", "setOverclock", "setVlanOffload", "setPPPoEOffload" ] + }, + "file": { + "/sys/devices/system/cpu/cpufreq/policy0/scaling_governor": [ "write" ], + "/sys/devices/system/cpu/cpufreq/policy0/scaling_max_freq": [ "write" ], + "/dev/mem": [ "read", "write" ] + } + } + } +} diff --git a/package/luci-app-airoha-npu/screenshots/cpu-frequency.png b/package/luci-app-airoha-npu/screenshots/cpu-frequency.png new file mode 100644 index 00000000000000..ac48598d4a2404 Binary files /dev/null and b/package/luci-app-airoha-npu/screenshots/cpu-frequency.png differ diff --git a/package/luci-app-airoha-npu/screenshots/npu-offload-engine.png b/package/luci-app-airoha-npu/screenshots/npu-offload-engine.png new file mode 100644 index 00000000000000..4d398d36725ea6 Binary files /dev/null and b/package/luci-app-airoha-npu/screenshots/npu-offload-engine.png differ diff --git a/package/luci-app-airoha-npu/screenshots/ppe-flow-table.png b/package/luci-app-airoha-npu/screenshots/ppe-flow-table.png new file mode 100644 index 00000000000000..2562b0d79db422 Binary files /dev/null and b/package/luci-app-airoha-npu/screenshots/ppe-flow-table.png differ diff --git a/package/luci-app-mlo/Makefile b/package/luci-app-mlo/Makefile new file mode 100644 index 00000000000000..0536311214025c --- /dev/null +++ b/package/luci-app-mlo/Makefile @@ -0,0 +1,13 @@ +# This is free software, licensed under the Apache License, Version 2.0. + +include $(TOPDIR)/rules.mk + +LUCI_TITLE:=LuCI Support for Wi-Fi MLO +LUCI_DEPENDS:=+luci-base +luci-mod-network + +PKG_LICENSE:=Apache-2.0 +PKG_MAINTAINER:=YYH2913 + +include $(TOPDIR)/feeds/luci/luci.mk + +# call BuildPackage - OpenWrt buildroot signature diff --git a/package/luci-app-mlo/README.md b/package/luci-app-mlo/README.md new file mode 100644 index 00000000000000..096a137a1d5ff7 --- /dev/null +++ b/package/luci-app-mlo/README.md @@ -0,0 +1,23 @@ +# luci-app-mlo + +Standalone LuCI package for editing OpenWrt Wi-Fi MLO settings. + +What it does: +- Adds a new `Network -> MLO` page in LuCI +- Writes directly to `wireless` UCI sections +- Exposes `option mlo '1'` and multi-value `device` entries +- Avoids the stock wireless page limitation where `device` is still handled like a single value +- Shows runtime MLD detection and active ifnames from `luci-rpc getWirelessDevices` +- Provides quick-create buttons for AP and STA MLO profiles with safer defaults + +Suggested workflow: +1. Place or symlink this folder into your OpenWrt tree, for example: + `ln -s ../../luci-app-mlo openwrt/package/luci-app-mlo` +2. Enable `luci-app-mlo` in `make menuconfig` +3. Build and flash +4. Open LuCI at `Network -> MLO` + +Notes: +- This app edits `/etc/config/wireless` only +- Save & Apply will use normal LuCI/UCI apply flow +- Leave `ifname` empty to let netifd create names such as `ap-mld0` or `sta-mld0` diff --git a/package/luci-app-mlo/htdocs/luci-static/resources/view/mlo.js b/package/luci-app-mlo/htdocs/luci-static/resources/view/mlo.js new file mode 100644 index 00000000000000..168c9c815e9c5f --- /dev/null +++ b/package/luci-app-mlo/htdocs/luci-static/resources/view/mlo.js @@ -0,0 +1,693 @@ +'use strict'; +'require view'; +'require form'; +'require uci'; +'require ui'; +'require poll'; +'require rpc'; + +const callLuciWirelessDevices = rpc.declare({ + object: 'luci-rpc', + method: 'getWirelessDevices', + expect: { '': {} } +}); + +const callIwinfoAssoclist = rpc.declare({ + object: 'iwinfo', + method: 'assoclist', + params: [ 'device', 'mac' ], + expect: { results: [] } +}); + +function listValues(value) { + return L.toArray(value).map(v => String(v).trim()).filter(v => v.length > 0); +} + +function uniqueValues(value) { + let seen = {}; + let out = []; + + for (let item of listValues(value)) { + if (seen[item]) + continue; + + seen[item] = true; + out.push(item); + } + + return out; +} + +function optionValue(section, option) { + return uci.get('wireless', section, option); +} + +function nextSectionName(prefix) { + let idx = 0; + let sid = null; + + do { + sid = '%s%d'.format(prefix, idx++); + } while (uci.get('wireless', sid) != null); + + return sid; +} + +function radioLabel(radio) { + let bits = [radio['.name']]; + + if (radio.band) + bits.push(radio.band); + + if (radio.channel) + bits.push(_('channel %s').format(radio.channel)); + + return bits.join(' | '); +} + +function radioMap(radios) { + let out = {}; + + for (let radio of radios) + out[radio['.name']] = radio; + + return out; +} + +function renderBadge(label, background, foreground) { + return E('span', { + 'style': 'display:inline-block;margin:0 .35em .35em 0;padding:.15em .55em;border-radius:999px;background:%s;color:%s;font-size:12px;line-height:1.3;white-space:nowrap;'.format(background, foreground || '#fff') + }, label); +} + +function renderMetaLine(label, value) { + return E('div', { 'style': 'margin:.15em 0;' }, [ + E('strong', { 'style': 'margin-right:.35em;' }, label), + value + ]); +} + +function compactChildren(items) { + return items.filter(item => item !== null && item !== undefined && item !== false); +} + +function renderJoinedValues(values, mapper) { + let out = []; + + for (let i = 0; i < values.length; i++) { + if (i > 0) + out.push(', '); + + out.push(mapper(values[i], i)); + } + + return out; +} + +function replaceNode(oldNode, newNode) { + if (oldNode && oldNode.parentNode) + oldNode.parentNode.replaceChild(newNode, oldNode); +} + +function stationKey(station) { + if (typeof(station) == 'string') + return station.toLowerCase(); + + if (!station || typeof(station) != 'object') + return null; + + return (station.mac || station.addr || station.mld_addr || station.mldAddr || + station.address || JSON.stringify(station)).toLowerCase(); +} + +function flattenWirelessStatus(status) { + let runtime = { + radios: [], + sections: {}, + activeMldIfnames: [] + }; + + for (let radioName in status || {}) { + let radio = status[radioName] || {}; + let cfg = radio.config || {}; + + runtime.radios.push({ + name: radioName, + up: !!radio.up, + band: cfg.band, + channel: cfg.channel + }); + + for (let iface of L.toArray(radio.interfaces)) { + let sid = iface.section; + let ifaceCfg = iface.config || {}; + let info; + + if (!sid) + continue; + + info = runtime.sections[sid] || { + ifnames: [], + radios: [], + up: false, + stations: 0, + stationMap: {}, + mode: null, + ssid: null, + encryption: null, + mldDetected: false + }; + + if (iface.ifname && info.ifnames.indexOf(iface.ifname) < 0) + info.ifnames.push(iface.ifname); + + if (info.radios.indexOf(radioName) < 0) + info.radios.push(radioName); + + info.up = info.up || !!radio.up; + info.mode = info.mode || ifaceCfg.mode; + info.ssid = info.ssid || ifaceCfg.ssid; + info.encryption = info.encryption || ifaceCfg.encryption; + info.mldDetected = info.mldDetected || + (iface.ifname && iface.ifname.indexOf('-mld') > -1) || + uniqueValues(ifaceCfg.device).length > 1; + + for (let station of L.toArray(iface.stations)) { + let key = stationKey(station); + + if (key) + info.stationMap[key] = true; + } + + info.stations = Object.keys(info.stationMap).length; + + runtime.sections[sid] = info; + + if (iface.ifname && iface.ifname.indexOf('-mld') > -1 && runtime.activeMldIfnames.indexOf(iface.ifname) < 0) + runtime.activeMldIfnames.push(iface.ifname); + } + } + + runtime.radios.sort((a, b) => L.naturalCompare(a.name, b.name)); + runtime.activeMldIfnames.sort(L.naturalCompare); + + return runtime; +} + +function enrichRuntimeStations(runtime) { + let tasks = []; + + for (let sectionName in runtime.sections) { + let info = runtime.sections[sectionName]; + + info.stationMap = {}; + info.stations = 0; + + for (let ifname of info.ifnames) { + tasks.push( + L.resolveDefault(callIwinfoAssoclist(ifname, null), []).then(function(stations) { + for (let station of L.toArray(stations)) { + let key = stationKey(station); + + if (key) + info.stationMap[key] = true; + } + + info.stations = Object.keys(info.stationMap).length; + }) + ); + } + } + + return Promise.all(tasks).then(function() { + return runtime; + }); +} + +function collectSummary(runtime, radios) { + let sections = uci.sections('wireless', 'wifi-iface'); + let summary = { + totalIfaces: sections.length, + mloIfaces: 0, + enabledIfaces: 0, + invalidMlo: 0, + activeMlo: 0, + customIfname: 0, + warnings: [] + }; + + for (let section of sections) { + let devices = uniqueValues(section.device); + let isMlo = section.mlo == '1'; + let runtimeInfo = runtime.sections[section['.name']]; + + if (section.disabled != '1') + summary.enabledIfaces++; + + if (!isMlo) + continue; + + summary.mloIfaces++; + + if (devices.length < 2) + summary.invalidMlo++; + + if (section.ifname) + summary.customIfname++; + + if (runtimeInfo && (runtimeInfo.mldDetected || runtimeInfo.ifnames.length)) + summary.activeMlo++; + } + + if (radios.length < 2) + summary.warnings.push(_('Only one radio is configured. MLO needs at least two radios.')); + + if (!summary.totalIfaces) + summary.warnings.push(_('No wireless interfaces are configured yet.')); + else if (!summary.mloIfaces) + summary.warnings.push(_('No MLO-enabled wireless interface is configured yet.')); + + if (summary.invalidMlo) + summary.warnings.push(_('%d MLO interface(s) still have fewer than two radios selected.').format(summary.invalidMlo)); + + if (summary.mloIfaces && !runtime.activeMldIfnames.length) + summary.warnings.push(_('MLO is configured, but no active runtime MLD interface is currently reported.')); + + if (summary.customIfname) + summary.warnings.push(_('%d MLO interface(s) override the auto-generated MLD ifname.').format(summary.customIfname)); + + return summary; +} + +function renderMetric(label, value) { + return E('div', { + 'style': 'flex:1 1 10em;min-width:10em;padding:.85em 1em;border:1px solid #d7d7d7;border-radius:10px;background:#fafafa;' + }, [ + E('div', { 'style': 'font-size:12px;color:#666;margin-bottom:.25em;' }, label), + E('div', { 'style': 'font-size:22px;font-weight:600;line-height:1.2;' }, value) + ]); +} + +function renderSummaryBox(runtime, summary, addApMlo, addStaMlo, refreshStatus) { + let warningList = summary.warnings.length + ? E('ul', { 'style': 'margin:.75em 0 0 1.2em;' }, + summary.warnings.map(w => E('li', { 'style': 'margin:.25em 0;' }, w))) + : E('div', { 'style': 'margin-top:.75em;color:#2f6f3e;' }, + _('Runtime detection looks healthy. Auto-generated MLD names appear as ap-mldX or sta-mldX.')); + + return E('div', { + 'class': 'cbi-section', + 'data-mlo-summary-box': '1' + }, [ + E('h3', _('MLO Overview')), + E('div', { + 'style': 'display:flex;gap:.75em;flex-wrap:wrap;margin:.5em 0 1em 0;' + }, [ + renderMetric(_('Configured radios'), String(runtime.radios.length)), + renderMetric(_('Wireless interfaces'), String(summary.totalIfaces)), + renderMetric(_('MLO-enabled'), String(summary.mloIfaces)), + renderMetric(_('Active MLD ifnames'), runtime.activeMldIfnames.length ? runtime.activeMldIfnames.join(', ') : _('none')) + ]), + E('div', { + 'style': 'display:flex;gap:.5em;flex-wrap:wrap;margin-bottom:.5em;' + }, [ + E('button', { + 'class': 'cbi-button cbi-button-add', + 'click': function(ev) { + ev.preventDefault(); + return addApMlo(); + } + }, _('Quick Add AP MLO')), + E('button', { + 'class': 'cbi-button', + 'click': function(ev) { + ev.preventDefault(); + return addStaMlo(); + } + }, _('Quick Add STA MLO')), + E('button', { + 'class': 'cbi-button', + 'click': function(ev) { + ev.preventDefault(); + return refreshStatus(); + } + }, _('Refresh Runtime Status')) + ]), + warningList + ]); +} + +function renderSectionOverview(section_id, radiosByName) { + let cfg = uci.get('wireless', section_id) || {}; + let devices = uniqueValues(cfg.device); + let networks = uniqueValues(cfg.network); + let issues = []; + + if (cfg.mlo == '1' && devices.length < 2) + issues.push(E('div', { 'style': 'color:#b94a48;margin-top:.25em;' }, _('Needs at least two radios'))); + + if (cfg.mlo == '1' && cfg.mode == 'ap' && ![ 'sae', 'sae-mixed', 'owe' ].includes(cfg.encryption)) + issues.push(E('div', { 'style': 'color:#8a6d3b;margin-top:.25em;' }, _('AP MLO is usually paired with WPA3-SAE or OWE'))); + + if (cfg.mlo == '1' && cfg.ifname && cfg.ifname.indexOf('mld') < 0) + issues.push(E('div', { 'style': 'color:#8a6d3b;margin-top:.25em;' }, _('Custom ifname overrides the default ap-mldX / sta-mldX naming'))); + + return E('div', { + 'data-mlo-overview-section': section_id, + 'style': 'min-width:17em;' + }, [ + E('div', { 'style': 'margin-bottom:.2em;' }, [ + renderBadge(cfg.mlo == '1' ? _('MLO') : _('Single-link'), cfg.mlo == '1' ? '#2d6cdf' : '#6c757d'), + renderBadge((cfg.mode || 'ap').toUpperCase(), '#495057'), + renderBadge(cfg.disabled == '1' ? _('Disabled') : _('Enabled'), cfg.disabled == '1' ? '#b94a48' : '#2f6f3e') + ]), + renderMetaLine(_('SSID'), cfg.ssid || E('em', _('unset'))), + renderMetaLine(_('Radios'), devices.length + ? renderJoinedValues(devices, dev => radiosByName[dev] ? radioLabel(radiosByName[dev]) : dev) + : E('em', _('none'))), + renderMetaLine(_('Networks'), networks.length ? networks.join(', ') : E('em', _('none'))), + renderMetaLine(_('Security'), cfg.encryption || E('em', _('unset'))), + issues + ]); +} + +function renderRuntimeCell(section_id, runtime) { + let cfg = uci.get('wireless', section_id) || {}; + let state = runtime.sections[section_id]; + + if (cfg.disabled == '1') { + return E('div', { + 'data-mlo-runtime-section': section_id, + 'style': 'min-width:15em;' + }, [ + renderBadge(_('Disabled'), '#b94a48'), + E('div', { 'style': 'margin-top:.25em;color:#666;' }, _('Section is disabled in UCI')) + ]); + } + + if (!state) { + return E('div', { + 'data-mlo-runtime-section': section_id, + 'style': 'min-width:15em;' + }, [ + renderBadge(_('No runtime state'), '#6c757d'), + E('div', { 'style': 'margin-top:.25em;color:#666;' }, _('No active interface was reported yet')), + cfg.mlo == '1' + ? E('div', { 'style': 'margin-top:.25em;color:#8a6d3b;' }, _('Save & Apply, then verify driver support if this persists')) + : null + ]); + } + + return E('div', { + 'data-mlo-runtime-section': section_id, + 'style': 'min-width:15em;' + }, [ + E('div', { 'style': 'margin-bottom:.2em;' }, compactChildren([ + renderBadge(state.up ? _('Active') : _('Present'), state.up ? '#2f6f3e' : '#6c757d'), + state.mldDetected ? renderBadge(_('MLD runtime'), '#2d6cdf') : null + ])), + renderMetaLine(_('ifname'), state.ifnames.length ? state.ifnames.join(', ') : E('em', _('unknown'))), + renderMetaLine(_('Runtime radios'), state.radios.join(', ')), + renderMetaLine(_('Stations'), String(state.stations)) + ]); +} + +return view.extend({ + callLuciWirelessDevices: callLuciWirelessDevices, + + load: function() { + return Promise.all([ + uci.load('wireless'), + uci.load('network'), + L.resolveDefault(this.callLuciWirelessDevices(), {}) + ]); + }, + + render: function(data) { + let m, s, o; + let initialStatus = data[2] || {}; + let radios = uci.sections('wireless', 'wifi-device'); + let networks = uci.sections('network', 'interface'); + let radiosByName = radioMap(radios); + let runtime = flattenWirelessStatus(initialStatus); + let summary = collectSummary(runtime, radios); + let quickAdd; + let refreshRuntime; + + m = new form.Map('wireless', _('Wi-Fi MLO')); + m.chain('network'); + + s = m.section(form.GridSection, 'wifi-iface', _('Wireless Interfaces')); + s.anonymous = true; + s.addremove = true; + s.sortable = true; + s.nodescriptions = true; + s.addbtntitle = _('Add MLO-ready interface'); + s.modaltitle = _('Edit wireless interface'); + s.sectiontitle = function(section_id) { + return optionValue(section_id, 'ssid') || + optionValue(section_id, 'ifname') || + section_id; + }; + + quickAdd = function(mode) { + let selectedRadios = radios.slice(0, 2).map(r => r['.name']); + let sid; + let defaultNetworks = []; + + if (selectedRadios.length < 2) { + ui.addNotification(null, E('p', _('At least two configured radios are required before an MLO interface can be created.'))); + return Promise.resolve(); + } + + if (mode == 'ap') { + if (networks.some(n => n['.name'] == 'lan')) + defaultNetworks = [ 'lan' ]; + else if (networks.length) + defaultNetworks = [ networks[0]['.name'] ]; + } + else { + if (networks.some(n => n['.name'] == 'wwan')) + defaultNetworks = [ 'wwan' ]; + else if (networks.some(n => n['.name'] == 'wan')) + defaultNetworks = [ 'wan' ]; + } + + sid = nextSectionName('mlo'); + + uci.add('wireless', 'wifi-iface', sid); + uci.set('wireless', sid, 'mode', mode); + uci.set('wireless', sid, 'mlo', '1'); + uci.set('wireless', sid, 'disabled', '0'); + uci.set('wireless', sid, 'device', selectedRadios); + uci.set('wireless', sid, 'network', defaultNetworks); + uci.set('wireless', sid, 'encryption', mode == 'ap' ? 'sae' : 'sae'); + uci.set('wireless', sid, 'ieee80211w', '2'); + + if (mode == 'ap') + uci.set('wireless', sid, 'ssid', 'OpenWrt-MLO'); + + return s.renderMoreOptionsModal(sid); + }; + + s.handleAdd = function(ev) { + if (ev) + ev.preventDefault(); + + return quickAdd('ap'); + }; + + o = s.option(form.DummyValue, '_overview', _('Profile')); + o.modalonly = false; + o.textvalue = function(section_id) { + return renderSectionOverview(section_id, radiosByName); + }; + + o = s.option(form.DummyValue, '_runtime', _('Runtime')); + o.modalonly = false; + o.textvalue = function(section_id) { + return renderRuntimeCell(section_id, runtime); + }; + + s.tab('general', _('General')); + s.tab('security', _('Security')); + s.tab('advanced', _('Advanced')); + + o = s.taboption('general', form.Flag, 'disabled', _('Enabled')); + o.enabled = '0'; + o.disabled = '1'; + o.default = '0'; + o.rmempty = false; + + o = s.taboption('general', form.Flag, 'mlo', _('Enable MLO'), + _('When enabled, this wifi-iface spans multiple radios through a multi-value device list.')); + o.default = o.enabled; + o.rmempty = false; + + o = s.taboption('general', form.ListValue, 'mode', _('Mode')); + o.value('ap', _('Access Point')); + o.value('sta', _('Client')); + o.default = 'ap'; + o.rmempty = false; + + o = s.taboption('general', form.MultiValue, 'device', _('Radio devices'), + _('Select one radio for normal Wi-Fi, or two or more radios for MLO.')); + for (let radio of radios) + o.value(radio['.name'], radioLabel(radio)); + o.rmempty = true; + o.widget = 'select'; + o.validate = function(section_id, value) { + let values = uniqueValues(value); + let mloOption = this.section.children.find(opt => opt.option == 'mlo'); + let mloEnabled = mloOption ? mloOption.formvalue(section_id) : optionValue(section_id, 'mlo'); + + if (!values.length) + return _('Select at least one radio device'); + + if (mloEnabled == '1' && values.length < 2) + return _('MLO requires at least two radio devices'); + + return true; + }; + + o = s.taboption('general', form.MultiValue, 'network', _('Attached network(s)'), + _('Logical network names from /etc/config/network, for example lan.')); + for (let network of networks) + o.value(network['.name']); + o.rmempty = true; + o.widget = 'select'; + o.modalonly = true; + + o = s.taboption('general', form.Value, 'ssid', _('SSID')); + o.datatype = 'maxlength(32)'; + o.depends('mode', 'ap'); + o.depends('mode', 'sta'); + + o = s.taboption('general', form.Flag, 'hidden', _('Hide SSID')); + o.depends('mode', 'ap'); + o.modalonly = true; + + o = s.taboption('security', form.ListValue, 'encryption', _('Encryption'), + _('For AP MLO, WPA3-SAE with Management Frame Protection set to Required is the most compatible starting point in this tree.')); + o.value('sae', _('WPA3-SAE')); + o.value('sae-mixed', _('WPA2/WPA3 mixed')); + o.value('psk2', _('WPA2-PSK')); + o.value('psk-mixed', _('WPA/WPA2 mixed PSK')); + o.value('owe', _('OWE')); + o.value('none', _('No encryption')); + o.default = 'sae'; + o.rmempty = false; + + o = s.taboption('security', form.Value, 'key', _('Passphrase')); + o.password = true; + o.datatype = 'wpakey'; + o.depends('encryption', 'sae'); + o.depends('encryption', 'sae-mixed'); + o.depends('encryption', 'psk2'); + o.depends('encryption', 'psk-mixed'); + o.modalonly = true; + o.validate = function(section_id, value) { + let encryptionOption = this.section.children.find(opt => opt.option == 'encryption'); + let encryption = encryptionOption ? encryptionOption.formvalue(section_id) : optionValue(section_id, 'encryption'); + + if ([ 'sae', 'sae-mixed', 'psk2', 'psk-mixed' ].includes(encryption) && !value) + return _('Passphrase is required for the selected encryption'); + + return true; + }; + + o = s.taboption('security', form.ListValue, 'ieee80211w', _('802.11w Management Frame Protection')); + o.value('0', _('Disabled')); + o.value('1', _('Optional')); + o.value('2', _('Required')); + o.default = '2'; + o.rmempty = false; + o.depends('encryption', 'sae'); + o.depends('encryption', 'sae-mixed'); + o.depends('encryption', 'psk2'); + o.depends('encryption', 'psk-mixed'); + o.depends('encryption', 'owe'); + o.modalonly = true; + + o = s.taboption('advanced', form.Value, 'ifname', _('Interface name'), + _('Optional override. Leave empty to use auto-generated names such as ap-mld0 or sta-mld0.')); + o.datatype = 'netdevname'; + o.placeholder = 'ap-mld0'; + o.modalonly = true; + + o = s.taboption('advanced', form.Value, 'macaddr', _('MAC address'), + _('Optional override for the generated interface MAC address.')); + o.datatype = 'macaddr'; + o.modalonly = true; + + o = s.taboption('advanced', form.Value, 'bssid', _('BSSID')); + o.datatype = 'macaddr'; + o.depends('mode', 'sta'); + o.modalonly = true; + + o = s.taboption('advanced', form.Flag, 'wds', _('Enable WDS / 4-address mode')); + o.depends('mode', 'ap'); + o.depends('mode', 'sta'); + o.modalonly = true; + + o = s.taboption('advanced', form.Flag, 'uapsd', _('Enable U-APSD')); + o.depends('mode', 'ap'); + o.modalonly = true; + + o = s.taboption('advanced', form.Flag, 'ocv', _('Enable OCV')); + o.depends('encryption', 'sae'); + o.depends('encryption', 'sae-mixed'); + o.depends('encryption', 'owe'); + o.modalonly = true; + + o = s.taboption('advanced', form.Flag, 'disassoc_low_ack', _('Disassociate on low ACK')); + o.depends('mode', 'ap'); + o.modalonly = true; + + refreshRuntime = function(nodes) { + return L.resolveDefault(callLuciWirelessDevices(), {}).then(function(status) { + let nextRuntime = flattenWirelessStatus(status); + return enrichRuntimeStations(nextRuntime).then(function(nextRuntime) { + let nextSummary = collectSummary(nextRuntime, radios); + let summaryNode = nodes.querySelector('[data-mlo-summary-box="1"]'); + let nextSummaryNode = renderSummaryBox(nextRuntime, nextSummary, + function() { return quickAdd('ap'); }, + function() { return quickAdd('sta'); }, + function() { return refreshRuntime(nodes); }); + + runtime = nextRuntime; + summary = nextSummary; + + replaceNode(summaryNode, nextSummaryNode); + + uci.sections('wireless', 'wifi-iface').forEach(function(section) { + let overviewNode = nodes.querySelector('[data-mlo-overview-section="%s"]'.format(section['.name'])); + let rowNode = nodes.querySelector('[data-mlo-runtime-section="%s"]'.format(section['.name'])); + + if (overviewNode) + replaceNode(overviewNode, renderSectionOverview(section['.name'], radiosByName)); + + if (rowNode) + replaceNode(rowNode, renderRuntimeCell(section['.name'], runtime)); + }); + }); + }); + }; + + return m.render().then(function(nodes) { + nodes.insertBefore(renderSummaryBox(runtime, summary, + function() { return quickAdd('ap'); }, + function() { return quickAdd('sta'); }, + function() { return refreshRuntime(nodes); }), + nodes.firstChild); + + return refreshRuntime(nodes).then(function() { + poll.add(function() { + return refreshRuntime(nodes); + }, 5); + + return nodes; + }); + }); + } +}); diff --git a/package/luci-app-mlo/po/templates/mlo.pot b/package/luci-app-mlo/po/templates/mlo.pot new file mode 100644 index 00000000000000..a04ee021ca1536 --- /dev/null +++ b/package/luci-app-mlo/po/templates/mlo.pot @@ -0,0 +1,381 @@ +msgid "" +msgstr "Content-Type: text/plain; charset=UTF-8" + +#: luci-app-mlo/htdocs/luci-static/resources/view/mlo.js:203 +msgid "%d MLO interface(s) override the auto-generated MLD ifname." +msgstr "" + +#: luci-app-mlo/htdocs/luci-static/resources/view/mlo.js:197 +msgid "%d MLO interface(s) still have fewer than two radios selected." +msgstr "" + +#: luci-app-mlo/htdocs/luci-static/resources/view/mlo.js:529 +msgid "802.11w Management Frame Protection" +msgstr "" + +#: luci-app-mlo/htdocs/luci-static/resources/view/mlo.js:369 +msgid "" +"A dedicated editor for OpenWrt Multi-Link Operation. Compared with the stock " +"Wireless page, this view can manage multi-radio device lists, " +"show MLD runtime state, and prefill safer MLO defaults." +msgstr "" + +#: luci-app-mlo/htdocs/luci-static/resources/view/mlo.js:278 +msgid "AP MLO is usually paired with WPA3-SAE or OWE" +msgstr "" + +#: luci-app-mlo/htdocs/luci-static/resources/view/mlo.js:461 +msgid "Access Point" +msgstr "" + +#: luci-app-mlo/htdocs/luci-static/resources/view/mlo.js:337 +msgid "Active" +msgstr "" + +#: luci-app-mlo/htdocs/luci-static/resources/view/mlo.js:237 +msgid "Active MLD ifnames" +msgstr "" + +#: luci-app-mlo/htdocs/luci-static/resources/view/mlo.js:378 +msgid "Add MLO-ready interface" +msgstr "" + +#: luci-app-mlo/htdocs/luci-static/resources/view/mlo.js:447 +msgid "Advanced" +msgstr "" + +#: luci-app-mlo/htdocs/luci-static/resources/view/mlo.js:392 +msgid "" +"At least two configured radios are required before an MLO interface can be " +"created." +msgstr "" + +#: luci-app-mlo/htdocs/luci-static/resources/view/mlo.js:485 +msgid "Attached network(s)" +msgstr "" + +#: luci-app-mlo/htdocs/luci-static/resources/view/mlo.js:553 +msgid "BSSID" +msgstr "" + +#: luci-app-mlo/htdocs/luci-static/resources/view/mlo.js:462 +msgid "Client" +msgstr "" + +#: luci-app-mlo/htdocs/luci-static/resources/view/mlo.js:234 +msgid "Configured radios" +msgstr "" + +#: luci-app-mlo/htdocs/luci-static/resources/view/mlo.js:281 +msgid "Custom ifname overrides the default ap-mldX / sta-mldX naming" +msgstr "" + +#: luci-app-mlo/htdocs/luci-static/resources/view/mlo.js:290 +#: luci-app-mlo/htdocs/luci-static/resources/view/mlo.js:314 +#: luci-app-mlo/htdocs/luci-static/resources/view/mlo.js:530 +msgid "Disabled" +msgstr "" + +#: luci-app-mlo/htdocs/luci-static/resources/view/mlo.js:573 +msgid "Disassociate on low ACK" +msgstr "" + +#: luci-app-mlo/htdocs/luci-static/resources/view/mlo.js:373 +msgid "" +"Edit all wireless interfaces here. The dedicated overview columns help " +"identify which sections are already MLO-ready and which ones still need " +"runtime or security cleanup." +msgstr "" + +#: luci-app-mlo/htdocs/luci-static/resources/view/mlo.js:379 +msgid "Edit wireless interface" +msgstr "" + +#: luci-app-mlo/htdocs/luci-static/resources/view/mlo.js:455 +msgid "Enable MLO" +msgstr "" + +#: luci-app-mlo/htdocs/luci-static/resources/view/mlo.js:567 +msgid "Enable OCV" +msgstr "" + +#: luci-app-mlo/htdocs/luci-static/resources/view/mlo.js:563 +msgid "Enable U-APSD" +msgstr "" + +#: luci-app-mlo/htdocs/luci-static/resources/view/mlo.js:558 +msgid "Enable WDS / 4-address mode" +msgstr "" + +#: luci-app-mlo/htdocs/luci-static/resources/view/mlo.js:290 +#: luci-app-mlo/htdocs/luci-static/resources/view/mlo.js:449 +msgid "Enabled" +msgstr "" + +#: luci-app-mlo/htdocs/luci-static/resources/view/mlo.js:500 +msgid "Encryption" +msgstr "" + +#: luci-app-mlo/htdocs/luci-static/resources/view/mlo.js:501 +msgid "" +"For AP MLO, WPA3-SAE with Management Frame Protection set to Required is the " +"most compatible starting point in this tree." +msgstr "" + +#: luci-app-mlo/htdocs/luci-static/resources/view/mlo.js:445 +msgid "General" +msgstr "" + +#: luci-app-mlo/root/usr/share/rpcd/acl.d/luci-app-mlo.json:3 +msgid "Grant access to LuCI app MLO" +msgstr "" + +#: luci-app-mlo/htdocs/luci-static/resources/view/mlo.js:496 +msgid "Hide SSID" +msgstr "" + +#: luci-app-mlo/htdocs/luci-static/resources/view/mlo.js:542 +msgid "Interface name" +msgstr "" + +#: luci-app-mlo/htdocs/luci-static/resources/view/mlo.js:486 +msgid "" +"Logical network names from /etc/config/network, for example " +"lan." +msgstr "" + +#: luci-app-mlo/htdocs/luci-static/resources/view/mlo.js:548 +msgid "MAC address" +msgstr "" + +#: luci-app-mlo/htdocs/luci-static/resources/view/mlo.js:338 +msgid "MLD runtime" +msgstr "" + +#: luci-app-mlo/htdocs/luci-static/resources/view/mlo.js:288 +#: luci-app-mlo/root/usr/share/luci/menu.d/luci-app-mlo.json:3 +msgid "MLO" +msgstr "" + +#: luci-app-mlo/htdocs/luci-static/resources/view/mlo.js:228 +msgid "MLO Overview" +msgstr "" + +#: luci-app-mlo/htdocs/luci-static/resources/view/mlo.js:200 +msgid "" +"MLO is configured, but no active runtime MLD interface is currently reported." +msgstr "" + +#: luci-app-mlo/htdocs/luci-static/resources/view/mlo.js:480 +msgid "MLO requires at least two radio devices" +msgstr "" + +#: luci-app-mlo/htdocs/luci-static/resources/view/mlo.js:236 +msgid "MLO-enabled" +msgstr "" + +#: luci-app-mlo/htdocs/luci-static/resources/view/mlo.js:460 +msgid "Mode" +msgstr "" + +#: luci-app-mlo/htdocs/luci-static/resources/view/mlo.js:275 +msgid "Needs at least two radios" +msgstr "" + +#: luci-app-mlo/htdocs/luci-static/resources/view/mlo.js:299 +msgid "Networks" +msgstr "" + +#: luci-app-mlo/htdocs/luci-static/resources/view/mlo.js:194 +msgid "No MLO-enabled wireless interface is configured yet." +msgstr "" + +#: luci-app-mlo/htdocs/luci-static/resources/view/mlo.js:325 +msgid "No active interface was reported yet" +msgstr "" + +#: luci-app-mlo/htdocs/luci-static/resources/view/mlo.js:507 +msgid "No encryption" +msgstr "" + +#: luci-app-mlo/htdocs/luci-static/resources/view/mlo.js:324 +msgid "No runtime state" +msgstr "" + +#: luci-app-mlo/htdocs/luci-static/resources/view/mlo.js:192 +msgid "No wireless interfaces are configured yet." +msgstr "" + +#: luci-app-mlo/htdocs/luci-static/resources/view/mlo.js:506 +msgid "OWE" +msgstr "" + +#: luci-app-mlo/htdocs/luci-static/resources/view/mlo.js:189 +msgid "Only one radio is configured. MLO needs at least two radios." +msgstr "" + +#: luci-app-mlo/htdocs/luci-static/resources/view/mlo.js:531 +msgid "Optional" +msgstr "" + +#: luci-app-mlo/htdocs/luci-static/resources/view/mlo.js:549 +msgid "Optional override for the generated interface MAC address." +msgstr "" + +#: luci-app-mlo/htdocs/luci-static/resources/view/mlo.js:543 +msgid "" +"Optional override. Leave empty to use auto-generated names such as ap-" +"mld0 or sta-mld0." +msgstr "" + +#: luci-app-mlo/htdocs/luci-static/resources/view/mlo.js:511 +msgid "Passphrase" +msgstr "" + +#: luci-app-mlo/htdocs/luci-static/resources/view/mlo.js:524 +msgid "Passphrase is required for the selected encryption" +msgstr "" + +#: luci-app-mlo/htdocs/luci-static/resources/view/mlo.js:337 +msgid "Present" +msgstr "" + +#: luci-app-mlo/htdocs/luci-static/resources/view/mlo.js:433 +msgid "Profile" +msgstr "" + +#: luci-app-mlo/htdocs/luci-static/resources/view/mlo.js:248 +msgid "Quick Add AP MLO" +msgstr "" + +#: luci-app-mlo/htdocs/luci-static/resources/view/mlo.js:255 +msgid "Quick Add STA MLO" +msgstr "" + +#: luci-app-mlo/htdocs/luci-static/resources/view/mlo.js:466 +msgid "Radio devices" +msgstr "" + +#: luci-app-mlo/htdocs/luci-static/resources/view/mlo.js:293 +msgid "Radios" +msgstr "" + +#: luci-app-mlo/htdocs/luci-static/resources/view/mlo.js:262 +msgid "Refresh Runtime Status" +msgstr "" + +#: luci-app-mlo/htdocs/luci-static/resources/view/mlo.js:532 +msgid "Required" +msgstr "" + +#: luci-app-mlo/htdocs/luci-static/resources/view/mlo.js:439 +msgid "Runtime" +msgstr "" + +#: luci-app-mlo/htdocs/luci-static/resources/view/mlo.js:222 +msgid "" +"Runtime detection looks healthy. Auto-generated MLD names appear as ap-" +"mldX or sta-mldX." +msgstr "" + +#: luci-app-mlo/htdocs/luci-static/resources/view/mlo.js:341 +msgid "Runtime radios" +msgstr "" + +#: luci-app-mlo/htdocs/luci-static/resources/view/mlo.js:292 +#: luci-app-mlo/htdocs/luci-static/resources/view/mlo.js:491 +msgid "SSID" +msgstr "" + +#: luci-app-mlo/htdocs/luci-static/resources/view/mlo.js:327 +msgid "Save & Apply, then verify driver support if this persists" +msgstr "" + +#: luci-app-mlo/htdocs/luci-static/resources/view/mlo.js:315 +msgid "Section is disabled in UCI" +msgstr "" + +#: luci-app-mlo/htdocs/luci-static/resources/view/mlo.js:300 +#: luci-app-mlo/htdocs/luci-static/resources/view/mlo.js:446 +msgid "Security" +msgstr "" + +#: luci-app-mlo/htdocs/luci-static/resources/view/mlo.js:477 +msgid "Select at least one radio device" +msgstr "" + +#: luci-app-mlo/htdocs/luci-static/resources/view/mlo.js:467 +msgid "Select one radio for normal Wi-Fi, or two or more radios for MLO." +msgstr "" + +#: luci-app-mlo/htdocs/luci-static/resources/view/mlo.js:288 +msgid "Single-link" +msgstr "" + +#: luci-app-mlo/htdocs/luci-static/resources/view/mlo.js:342 +msgid "Stations" +msgstr "" + +#: luci-app-mlo/htdocs/luci-static/resources/view/mlo.js:230 +msgid "" +"This page is focused on backend MLO settings and runtime visibility. It " +"complements the stock Wireless page by handling multi-radio device lists directly." +msgstr "" + +#: luci-app-mlo/htdocs/luci-static/resources/view/mlo.js:505 +msgid "WPA/WPA2 mixed PSK" +msgstr "" + +#: luci-app-mlo/htdocs/luci-static/resources/view/mlo.js:504 +msgid "WPA2-PSK" +msgstr "" + +#: luci-app-mlo/htdocs/luci-static/resources/view/mlo.js:503 +msgid "WPA2/WPA3 mixed" +msgstr "" + +#: luci-app-mlo/htdocs/luci-static/resources/view/mlo.js:502 +msgid "WPA3-SAE" +msgstr "" + +#: luci-app-mlo/htdocs/luci-static/resources/view/mlo.js:456 +msgid "" +"When enabled, this wifi-iface spans multiple radios through a " +"multi-value device list." +msgstr "" + +#: luci-app-mlo/htdocs/luci-static/resources/view/mlo.js:368 +msgid "Wi-Fi MLO" +msgstr "" + +#: luci-app-mlo/htdocs/luci-static/resources/view/mlo.js:372 +msgid "Wireless Interfaces" +msgstr "" + +#: luci-app-mlo/htdocs/luci-static/resources/view/mlo.js:235 +msgid "Wireless interfaces" +msgstr "" + +#: luci-app-mlo/htdocs/luci-static/resources/view/mlo.js:56 +msgid "channel %s" +msgstr "" + +#: luci-app-mlo/htdocs/luci-static/resources/view/mlo.js:340 +msgid "ifname" +msgstr "" + +#: luci-app-mlo/htdocs/luci-static/resources/view/mlo.js:237 +#: luci-app-mlo/htdocs/luci-static/resources/view/mlo.js:298 +#: luci-app-mlo/htdocs/luci-static/resources/view/mlo.js:299 +msgid "none" +msgstr "" + +#: luci-app-mlo/htdocs/luci-static/resources/view/mlo.js:340 +msgid "unknown" +msgstr "" + +#: luci-app-mlo/htdocs/luci-static/resources/view/mlo.js:292 +#: luci-app-mlo/htdocs/luci-static/resources/view/mlo.js:300 +msgid "unset" +msgstr "" diff --git a/package/luci-app-mlo/po/zh_Hans/mlo.po b/package/luci-app-mlo/po/zh_Hans/mlo.po new file mode 100644 index 00000000000000..94eea7049dafe8 --- /dev/null +++ b/package/luci-app-mlo/po/zh_Hans/mlo.po @@ -0,0 +1,395 @@ +msgid "" +msgstr "" +"Project-Id-Version: luci-app-mlo\n" +"PO-Revision-Date: 2026-04-10 18:00+0800\n" +"Last-Translator: OpenAI Codex\n" +"Language-Team: Chinese (Simplified Han script)\n" +"Language: zh_Hans\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=1; plural=0;\n" + +#: luci-app-mlo/htdocs/luci-static/resources/view/mlo.js:203 +msgid "%d MLO interface(s) override the auto-generated MLD ifname." +msgstr "%d 个 MLO 接口覆盖了自动生成的 MLD ifname。" + +#: luci-app-mlo/htdocs/luci-static/resources/view/mlo.js:197 +msgid "%d MLO interface(s) still have fewer than two radios selected." +msgstr "%d 个 MLO 接口当前选择的 radio 少于 2 个。" + +#: luci-app-mlo/htdocs/luci-static/resources/view/mlo.js:529 +msgid "802.11w Management Frame Protection" +msgstr "802.11w 管理帧保护" + +#: luci-app-mlo/htdocs/luci-static/resources/view/mlo.js:369 +msgid "" +"A dedicated editor for OpenWrt Multi-Link Operation. Compared with the stock " +"Wireless page, this view can manage multi-radio device lists, " +"show MLD runtime state, and prefill safer MLO defaults." +msgstr "" +"这是一个专用于 OpenWrt 多链路操作(MLO)的编辑器。相比默认无线页面,它可以管" +"理多 radio 的 device 列表、显示 MLD 运行状态,并预填更安全的 " +"MLO 默认值。" + +#: luci-app-mlo/htdocs/luci-static/resources/view/mlo.js:278 +msgid "AP MLO is usually paired with WPA3-SAE or OWE" +msgstr "AP MLO 通常搭配 WPA3-SAE 或 OWE 使用" + +#: luci-app-mlo/htdocs/luci-static/resources/view/mlo.js:461 +msgid "Access Point" +msgstr "接入点" + +#: luci-app-mlo/htdocs/luci-static/resources/view/mlo.js:337 +msgid "Active" +msgstr "活动" + +#: luci-app-mlo/htdocs/luci-static/resources/view/mlo.js:237 +msgid "Active MLD ifnames" +msgstr "活动的 MLD ifname" + +#: luci-app-mlo/htdocs/luci-static/resources/view/mlo.js:378 +msgid "Add MLO-ready interface" +msgstr "添加支持 MLO 的接口" + +#: luci-app-mlo/htdocs/luci-static/resources/view/mlo.js:447 +msgid "Advanced" +msgstr "高级" + +#: luci-app-mlo/htdocs/luci-static/resources/view/mlo.js:392 +msgid "" +"At least two configured radios are required before an MLO interface can be " +"created." +msgstr "至少需要 2 个已配置 radio 才能创建 MLO 接口。" + +#: luci-app-mlo/htdocs/luci-static/resources/view/mlo.js:485 +msgid "Attached network(s)" +msgstr "绑定的网络" + +#: luci-app-mlo/htdocs/luci-static/resources/view/mlo.js:553 +msgid "BSSID" +msgstr "BSSID" + +#: luci-app-mlo/htdocs/luci-static/resources/view/mlo.js:462 +msgid "Client" +msgstr "客户端" + +#: luci-app-mlo/htdocs/luci-static/resources/view/mlo.js:234 +msgid "Configured radios" +msgstr "已配置的 radio" + +#: luci-app-mlo/htdocs/luci-static/resources/view/mlo.js:281 +msgid "Custom ifname overrides the default ap-mldX / sta-mldX naming" +msgstr "自定义 ifname 会覆盖默认的 ap-mldX / sta-mldX 命名" + +#: luci-app-mlo/htdocs/luci-static/resources/view/mlo.js:290 +#: luci-app-mlo/htdocs/luci-static/resources/view/mlo.js:314 +#: luci-app-mlo/htdocs/luci-static/resources/view/mlo.js:530 +msgid "Disabled" +msgstr "已禁用" + +#: luci-app-mlo/htdocs/luci-static/resources/view/mlo.js:573 +msgid "Disassociate on low ACK" +msgstr "低 ACK 时断开关联" + +#: luci-app-mlo/htdocs/luci-static/resources/view/mlo.js:373 +msgid "" +"Edit all wireless interfaces here. The dedicated overview columns help " +"identify which sections are already MLO-ready and which ones still need " +"runtime or security cleanup." +msgstr "" +"可在这里编辑所有无线接口。专用概览列可以帮助识别哪些 section 已具备 MLO 条" +"件,哪些仍需要补齐运行状态或安全配置。" + +#: luci-app-mlo/htdocs/luci-static/resources/view/mlo.js:379 +msgid "Edit wireless interface" +msgstr "编辑无线接口" + +#: luci-app-mlo/htdocs/luci-static/resources/view/mlo.js:455 +msgid "Enable MLO" +msgstr "启用 MLO" + +#: luci-app-mlo/htdocs/luci-static/resources/view/mlo.js:567 +msgid "Enable OCV" +msgstr "启用 OCV" + +#: luci-app-mlo/htdocs/luci-static/resources/view/mlo.js:563 +msgid "Enable U-APSD" +msgstr "启用 U-APSD" + +#: luci-app-mlo/htdocs/luci-static/resources/view/mlo.js:558 +msgid "Enable WDS / 4-address mode" +msgstr "启用 WDS / 4 地址模式" + +#: luci-app-mlo/htdocs/luci-static/resources/view/mlo.js:290 +#: luci-app-mlo/htdocs/luci-static/resources/view/mlo.js:449 +msgid "Enabled" +msgstr "已启用" + +#: luci-app-mlo/htdocs/luci-static/resources/view/mlo.js:500 +msgid "Encryption" +msgstr "加密" + +#: luci-app-mlo/htdocs/luci-static/resources/view/mlo.js:501 +msgid "" +"For AP MLO, WPA3-SAE with Management Frame Protection set to Required is the " +"most compatible starting point in this tree." +msgstr "对于 AP MLO,本代码树里最兼容的起始配置通常是 WPA3-SAE,并将管理帧保护设为“必需”。" + +#: luci-app-mlo/htdocs/luci-static/resources/view/mlo.js:445 +msgid "General" +msgstr "常规" + +#: luci-app-mlo/root/usr/share/rpcd/acl.d/luci-app-mlo.json:3 +msgid "Grant access to LuCI app MLO" +msgstr "授予访问 LuCI MLO 应用的权限" + +#: luci-app-mlo/htdocs/luci-static/resources/view/mlo.js:496 +msgid "Hide SSID" +msgstr "隐藏 SSID" + +#: luci-app-mlo/htdocs/luci-static/resources/view/mlo.js:542 +msgid "Interface name" +msgstr "接口名" + +#: luci-app-mlo/htdocs/luci-static/resources/view/mlo.js:486 +msgid "" +"Logical network names from /etc/config/network, for example " +"lan." +msgstr "来自 /etc/config/network 的逻辑网络名,例如 lan。" + +#: luci-app-mlo/htdocs/luci-static/resources/view/mlo.js:548 +msgid "MAC address" +msgstr "MAC 地址" + +#: luci-app-mlo/htdocs/luci-static/resources/view/mlo.js:338 +msgid "MLD runtime" +msgstr "MLD 运行态" + +#: luci-app-mlo/htdocs/luci-static/resources/view/mlo.js:288 +#: luci-app-mlo/root/usr/share/luci/menu.d/luci-app-mlo.json:3 +msgid "MLO" +msgstr "MLO" + +#: luci-app-mlo/htdocs/luci-static/resources/view/mlo.js:228 +msgid "MLO Overview" +msgstr "MLO 概览" + +#: luci-app-mlo/htdocs/luci-static/resources/view/mlo.js:200 +msgid "" +"MLO is configured, but no active runtime MLD interface is currently reported." +msgstr "已配置 MLO,但当前没有检测到活动的运行时 MLD 接口。" + +#: luci-app-mlo/htdocs/luci-static/resources/view/mlo.js:480 +msgid "MLO requires at least two radio devices" +msgstr "MLO 至少需要 2 个 radio 设备" + +#: luci-app-mlo/htdocs/luci-static/resources/view/mlo.js:236 +msgid "MLO-enabled" +msgstr "已启用 MLO" + +#: luci-app-mlo/htdocs/luci-static/resources/view/mlo.js:460 +msgid "Mode" +msgstr "模式" + +#: luci-app-mlo/htdocs/luci-static/resources/view/mlo.js:275 +msgid "Needs at least two radios" +msgstr "至少需要 2 个 radio" + +#: luci-app-mlo/htdocs/luci-static/resources/view/mlo.js:299 +msgid "Networks" +msgstr "网络" + +#: luci-app-mlo/htdocs/luci-static/resources/view/mlo.js:194 +msgid "No MLO-enabled wireless interface is configured yet." +msgstr "当前还没有配置启用 MLO 的无线接口。" + +#: luci-app-mlo/htdocs/luci-static/resources/view/mlo.js:325 +msgid "No active interface was reported yet" +msgstr "当前还没有上报活动接口" + +#: luci-app-mlo/htdocs/luci-static/resources/view/mlo.js:507 +msgid "No encryption" +msgstr "不加密" + +#: luci-app-mlo/htdocs/luci-static/resources/view/mlo.js:324 +msgid "No runtime state" +msgstr "无运行状态" + +#: luci-app-mlo/htdocs/luci-static/resources/view/mlo.js:192 +msgid "No wireless interfaces are configured yet." +msgstr "当前还没有配置任何无线接口。" + +#: luci-app-mlo/htdocs/luci-static/resources/view/mlo.js:506 +msgid "OWE" +msgstr "OWE" + +#: luci-app-mlo/htdocs/luci-static/resources/view/mlo.js:189 +msgid "Only one radio is configured. MLO needs at least two radios." +msgstr "当前只配置了 1 个 radio。MLO 至少需要 2 个 radio。" + +#: luci-app-mlo/htdocs/luci-static/resources/view/mlo.js:531 +msgid "Optional" +msgstr "可选" + +#: luci-app-mlo/htdocs/luci-static/resources/view/mlo.js:549 +msgid "Optional override for the generated interface MAC address." +msgstr "可选,用于覆盖自动生成的接口 MAC 地址。" + +#: luci-app-mlo/htdocs/luci-static/resources/view/mlo.js:543 +msgid "" +"Optional override. Leave empty to use auto-generated names such as ap-" +"mld0 or sta-mld0." +msgstr "可选覆盖项。留空时会使用自动生成的名称,例如 ap-mld0sta-mld0。" + +#: luci-app-mlo/htdocs/luci-static/resources/view/mlo.js:511 +msgid "Passphrase" +msgstr "密码" + +#: luci-app-mlo/htdocs/luci-static/resources/view/mlo.js:524 +msgid "Passphrase is required for the selected encryption" +msgstr "所选加密方式必须填写密码" + +#: luci-app-mlo/htdocs/luci-static/resources/view/mlo.js:337 +msgid "Present" +msgstr "已存在" + +#: luci-app-mlo/htdocs/luci-static/resources/view/mlo.js:433 +msgid "Profile" +msgstr "配置概览" + +#: luci-app-mlo/htdocs/luci-static/resources/view/mlo.js:248 +msgid "Quick Add AP MLO" +msgstr "快速添加 AP MLO" + +#: luci-app-mlo/htdocs/luci-static/resources/view/mlo.js:255 +msgid "Quick Add STA MLO" +msgstr "快速添加 STA MLO" + +#: luci-app-mlo/htdocs/luci-static/resources/view/mlo.js:466 +msgid "Radio devices" +msgstr "Radio 设备" + +#: luci-app-mlo/htdocs/luci-static/resources/view/mlo.js:293 +msgid "Radios" +msgstr "Radio 列表" + +#: luci-app-mlo/htdocs/luci-static/resources/view/mlo.js:262 +msgid "Refresh Runtime Status" +msgstr "刷新运行状态" + +#: luci-app-mlo/htdocs/luci-static/resources/view/mlo.js:532 +msgid "Required" +msgstr "必需" + +#: luci-app-mlo/htdocs/luci-static/resources/view/mlo.js:439 +msgid "Runtime" +msgstr "运行状态" + +#: luci-app-mlo/htdocs/luci-static/resources/view/mlo.js:222 +msgid "" +"Runtime detection looks healthy. Auto-generated MLD names appear as ap-" +"mldX or sta-mldX." +msgstr "运行状态检测正常。自动生成的 MLD 名称通常显示为 ap-mldXsta-mldX。" + +#: luci-app-mlo/htdocs/luci-static/resources/view/mlo.js:341 +msgid "Runtime radios" +msgstr "运行时 radios" + +#: luci-app-mlo/htdocs/luci-static/resources/view/mlo.js:292 +#: luci-app-mlo/htdocs/luci-static/resources/view/mlo.js:491 +msgid "SSID" +msgstr "SSID" + +#: luci-app-mlo/htdocs/luci-static/resources/view/mlo.js:327 +msgid "Save & Apply, then verify driver support if this persists" +msgstr "请先“保存并应用”;如果仍然如此,再检查驱动是否支持" + +#: luci-app-mlo/htdocs/luci-static/resources/view/mlo.js:315 +msgid "Section is disabled in UCI" +msgstr "该 section 在 UCI 中已禁用" + +#: luci-app-mlo/htdocs/luci-static/resources/view/mlo.js:300 +#: luci-app-mlo/htdocs/luci-static/resources/view/mlo.js:446 +msgid "Security" +msgstr "安全" + +#: luci-app-mlo/htdocs/luci-static/resources/view/mlo.js:477 +msgid "Select at least one radio device" +msgstr "至少选择 1 个 radio 设备" + +#: luci-app-mlo/htdocs/luci-static/resources/view/mlo.js:467 +msgid "Select one radio for normal Wi-Fi, or two or more radios for MLO." +msgstr "普通 Wi-Fi 请选择 1 个 radio;MLO 请至少选择 2 个 radio。" + +#: luci-app-mlo/htdocs/luci-static/resources/view/mlo.js:288 +msgid "Single-link" +msgstr "单链路" + +#: luci-app-mlo/htdocs/luci-static/resources/view/mlo.js:342 +msgid "Stations" +msgstr "终端数" + +#: luci-app-mlo/htdocs/luci-static/resources/view/mlo.js:230 +msgid "" +"This page is focused on backend MLO settings and runtime visibility. It " +"complements the stock Wireless page by handling multi-radio device lists directly." +msgstr "此页面聚焦于 MLO 后端配置和运行状态可见性。它通过直接处理多 radio 的 device 列表,补足了默认无线页面的不足。" + +#: luci-app-mlo/htdocs/luci-static/resources/view/mlo.js:505 +msgid "WPA/WPA2 mixed PSK" +msgstr "WPA/WPA2 混合 PSK" + +#: luci-app-mlo/htdocs/luci-static/resources/view/mlo.js:504 +msgid "WPA2-PSK" +msgstr "WPA2-PSK" + +#: luci-app-mlo/htdocs/luci-static/resources/view/mlo.js:503 +msgid "WPA2/WPA3 mixed" +msgstr "WPA2/WPA3 混合" + +#: luci-app-mlo/htdocs/luci-static/resources/view/mlo.js:502 +msgid "WPA3-SAE" +msgstr "WPA3-SAE" + +#: luci-app-mlo/htdocs/luci-static/resources/view/mlo.js:456 +msgid "" +"When enabled, this wifi-iface spans multiple radios through a " +"multi-value device list." +msgstr "启用后,此 wifi-iface 会通过多值 device 列表跨多个 radio 工作。" + +#: luci-app-mlo/htdocs/luci-static/resources/view/mlo.js:368 +msgid "Wi-Fi MLO" +msgstr "Wi-Fi MLO" + +#: luci-app-mlo/htdocs/luci-static/resources/view/mlo.js:372 +msgid "Wireless Interfaces" +msgstr "无线接口" + +#: luci-app-mlo/htdocs/luci-static/resources/view/mlo.js:235 +msgid "Wireless interfaces" +msgstr "无线接口" + +#: luci-app-mlo/htdocs/luci-static/resources/view/mlo.js:56 +msgid "channel %s" +msgstr "信道 %s" + +#: luci-app-mlo/htdocs/luci-static/resources/view/mlo.js:340 +msgid "ifname" +msgstr "ifname" + +#: luci-app-mlo/htdocs/luci-static/resources/view/mlo.js:237 +#: luci-app-mlo/htdocs/luci-static/resources/view/mlo.js:298 +#: luci-app-mlo/htdocs/luci-static/resources/view/mlo.js:299 +msgid "none" +msgstr "无" + +#: luci-app-mlo/htdocs/luci-static/resources/view/mlo.js:340 +msgid "unknown" +msgstr "未知" + +#: luci-app-mlo/htdocs/luci-static/resources/view/mlo.js:292 +#: luci-app-mlo/htdocs/luci-static/resources/view/mlo.js:300 +msgid "unset" +msgstr "未设置" diff --git a/package/luci-app-mlo/root/usr/share/luci/menu.d/luci-app-mlo.json b/package/luci-app-mlo/root/usr/share/luci/menu.d/luci-app-mlo.json new file mode 100644 index 00000000000000..0bbfb5a3793173 --- /dev/null +++ b/package/luci-app-mlo/root/usr/share/luci/menu.d/luci-app-mlo.json @@ -0,0 +1,18 @@ +{ + "admin/network/mlo": { + "title": "MLO", + "order": 16, + "action": { + "type": "view", + "path": "mlo" + }, + "depends": { + "acl": [ "luci-app-mlo" ], + "uci": { + "wireless": { + "@wifi-device": true + } + } + } + } +} diff --git a/package/luci-app-mlo/root/usr/share/rpcd/acl.d/luci-app-mlo.json b/package/luci-app-mlo/root/usr/share/rpcd/acl.d/luci-app-mlo.json new file mode 100644 index 00000000000000..f047a0a940b62d --- /dev/null +++ b/package/luci-app-mlo/root/usr/share/rpcd/acl.d/luci-app-mlo.json @@ -0,0 +1,15 @@ +{ + "luci-app-mlo": { + "description": "Grant access to LuCI app MLO", + "read": { + "ubus": { + "iwinfo": [ "assoclist" ], + "luci-rpc": [ "getWirelessDevices" ] + }, + "uci": [ "wireless", "network" ] + }, + "write": { + "uci": [ "wireless" ] + } + } +} diff --git a/package/luci-app-netspeedtest/.img/iperf3.png b/package/luci-app-netspeedtest/.img/iperf3.png new file mode 100644 index 00000000000000..374a681b5a5ac2 Binary files /dev/null and b/package/luci-app-netspeedtest/.img/iperf3.png differ diff --git a/package/luci-app-netspeedtest/.img/librespeed.png b/package/luci-app-netspeedtest/.img/librespeed.png new file mode 100644 index 00000000000000..6e0fadd8229643 Binary files /dev/null and b/package/luci-app-netspeedtest/.img/librespeed.png differ diff --git a/package/luci-app-netspeedtest/.img/librespeed_config.png b/package/luci-app-netspeedtest/.img/librespeed_config.png new file mode 100644 index 00000000000000..947df3eb77c93b Binary files /dev/null and b/package/luci-app-netspeedtest/.img/librespeed_config.png differ diff --git a/package/luci-app-netspeedtest/.img/speedtest.png b/package/luci-app-netspeedtest/.img/speedtest.png new file mode 100644 index 00000000000000..a198114d6fdd46 Binary files /dev/null and b/package/luci-app-netspeedtest/.img/speedtest.png differ diff --git a/package/luci-app-netspeedtest/.prepare.sh b/package/luci-app-netspeedtest/.prepare.sh new file mode 100755 index 00000000000000..8df9e161430c8e --- /dev/null +++ b/package/luci-app-netspeedtest/.prepare.sh @@ -0,0 +1,22 @@ +PKG_NAME="$1" +CURDIR="$2" +PKG_BUILD_DIR="$3" + +if [ -d "$CURDIR/.git" ]; then + config="$CURDIR/.git/config" +elif [ -f "$CURDIR/.git" ]; then + config="$(sed "s|^gitdir:\s*|$CURDIR/|;s|$|/config|" "$CURDIR/.git")" +else + exit 0 +fi +[ -r "$config" ] || exit 0 +[ -n "$(sed -En '/^\[remote /{h;:top;n;/^\[/b;s,(https?://gitcode\.(com|net)),\1,;T top;H;x;s|\n\s*|: |;p;}' "$config")" ] && { + for d in luasrc ucode htdocs root src; do + rm -rf "$PKG_BUILD_DIR"/$d + done + mkdir -p "$PKG_BUILD_DIR"/htdocs/luci-static/resources/view + touch "$PKG_BUILD_DIR"/htdocs/luci-static/resources/view/$PKG_NAME.js + mkdir -p "$PKG_BUILD_DIR"/root/usr/share/luci/menu.d + touch "$PKG_BUILD_DIR"/root/usr/share/luci/menu.d/$PKG_NAME.json +} +exit 0 diff --git a/package/luci-app-netspeedtest/LICENSE b/package/luci-app-netspeedtest/LICENSE new file mode 100644 index 00000000000000..5335f2c35aa160 --- /dev/null +++ b/package/luci-app-netspeedtest/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2023 muink + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/package/luci-app-netspeedtest/Makefile b/package/luci-app-netspeedtest/Makefile new file mode 100644 index 00000000000000..93400c5ac5dc84 --- /dev/null +++ b/package/luci-app-netspeedtest/Makefile @@ -0,0 +1,31 @@ +# SPDX-License-Identifier: MIT +# +# Copyright (C) 2023-2025 muink + +include $(TOPDIR)/rules.mk + +LUCI_NAME:=luci-app-netspeedtest + +LUCI_TITLE:=LuCI Net Speedtest +LUCI_DEPENDS:=+iperf3 +librespeed-go +ca-certificates +curl + +LUCI_DESCRIPTION:=Test Net speed + +PKG_MAINTAINER:=Anya Lin +PKG_LICENSE:=MIT + +PKG_UNPACK=$(CURDIR)/.prepare.sh $(PKG_NAME) $(CURDIR) $(PKG_BUILD_DIR) + +define Package/$(LUCI_NAME)/conffiles +/etc/config/netspeedtest +endef + +define Package/$(LUCI_NAME)/postinst +endef + +define Package/$(LUCI_NAME)/prerm +endef + +include $(TOPDIR)/feeds/luci/luci.mk + +# call BuildPackage - OpenWrt buildroot signature diff --git a/package/luci-app-netspeedtest/README.md b/package/luci-app-netspeedtest/README.md new file mode 100644 index 00000000000000..4445671c0ce764 --- /dev/null +++ b/package/luci-app-netspeedtest/README.md @@ -0,0 +1,52 @@ +# LuCI Network Speed Test + +### Screenshots + +![iperf3](.img/iperf3.png "iperf3")\ +![librespeed](.img/librespeed.png "librespeed")\ +![librespeed_config](.img/librespeed_config.png "librespeed_config")\ +![speedtest](.img/speedtest.png "speedtest") + +### How to install + +#### Prerequisites + +This package depends on python3-speedtest-cli which in turn depends on python3-base and various other python3 libraries. These packages combined can take up a lot of space on a router so before installing luci-app-netspeedtest, make sure you have enough space to install python3-speedtest-cli. You can check this by searching for python3-speedtest-cli under the software page in LuCI and try to install it, it will tell you how much space it and its dependencies require, just compare that to how much space you have left. + +#### Installation + +1. Goto ~~[releases](https://github.com/muink/luci-app-netspeedtest/tree/releases)~~ [here](https://fantastic-packages.github.io/releases/) +2. Download the latest version of ipk +3. Login router and goto **System --> Software** +4. Upload and install ipk +5. Reboot if the app is not automatically added in page +6. Goto **Network --> SpeedTest** + +### Build + +- Compile from OpenWrt/LEDE SDK + +``` +# Take the x86_64 platform as an example +tar xjf openwrt-sdk-21.02.3-x86-64_gcc-8.4.0_musl.Linux-x86_64.tar.xz +# Go to the SDK root dir +cd OpenWrt-sdk-*-x86_64_* +# First run to generate a .config file +make menuconfig +./scripts/feeds update -a +./scripts/feeds install -a +# Get Makefile +git clone --depth 1 --branch master --single-branch --no-checkout https://github.com/muink/luci-app-netspeedtest.git package/luci-app-netspeedtest +pushd package/luci-app-netspeedtest +umask 022 +git checkout +popd +# Select the package LuCI -> Applications -> luci-app-netspeedtest +make menuconfig +# Start compiling +make package/luci-app-netspeedtest/compile V=99 +``` + +### License + +- This project is licensed under the MIT License diff --git a/package/luci-app-netspeedtest/htdocs/luci-static/resources/view/netspeedtest/iperf3.js b/package/luci-app-netspeedtest/htdocs/luci-static/resources/view/netspeedtest/iperf3.js new file mode 100644 index 00000000000000..fbe3427b3fd3c6 --- /dev/null +++ b/package/luci-app-netspeedtest/htdocs/luci-static/resources/view/netspeedtest/iperf3.js @@ -0,0 +1,111 @@ +'use strict'; +'require view'; +'require poll'; +'require fs'; +'require rpc'; +'require uci'; +'require ui'; +'require form'; + +const conf = 'netspeedtest'; +const instance = 'iperf3'; + +const callServiceList = rpc.declare({ + object: 'service', + method: 'list', + params: ['name'], + expect: { '': {} } +}); + +function getServiceStatus() { + return L.resolveDefault(callServiceList(conf), {}) + .then((res) => { + let isrunning = false; + try { + isrunning = res[conf]['instances'][instance]['running']; + } catch (e) { } + return isrunning; + }); +} + +return view.extend({ +// handleSaveApply: null, +// handleSave: null, +// handleReset: null, + + load() { + return Promise.all([ + getServiceStatus(), + uci.load('netspeedtest') + ]); + }, + + poll_status(nodes, stat) { + const isRunning = stat[0]; + let view = nodes.querySelector('#service_status'); + + if (isRunning) { + view.innerHTML = "" + instance + " - " + _("SERVER RUNNING") + ""; + } else { + view.innerHTML = "" + instance + " - " + _("SERVER NOT RUNNING") + ""; + } + return; + }, + + render(res) { + const isRunning = res[0]; + + let m, s, o; + + m = new form.Map('netspeedtest', _('iperf3 Bandwidth Performance Test')); + + s = m.section(form.NamedSection, '_status'); + s.anonymous = true; + s.render = function (section_id) { + return E('div', { class: 'cbi-section' }, [ + E('div', { id: 'service_status' }, _('Collecting data ...')) + ]); + }; + + s = m.section(form.NamedSection, 'config', 'netspeedtest'); + s.anonymous = true; + + o = s.option(form.Flag, 'iperf3_enabled', _('Enable')); + o.default = o.disabled; + o.rmempty = false; + + o = s.option(form.Button, '_client_url', _('Download iperf3 client')); + o.inputtitle = _('Official website'); + o.inputstyle = 'apply'; + o.onclick = function () { + window.open("https://iperf.fr/iperf-download.php", '_blank'); + }; + + s = m.section(form.TypedSection, '_cmd_ref'); + s.anonymous = true; + s.render = function (section_id) { + return E('div', { 'class': 'cbi-section' }, [ + E('h3', {}, _('iperf3 Common commands reference')), + E('pre', {}, [ +" -c, --client \n\ + -u, --udp UDP mode\n\ + -b, --bandwidth [KMG] target bandwidth in bits/sec (0 for unlimited)\n\ + -t, --time time in seconds to transmit for (default 10 secs)\n\ + -i, --interval seconds between periodic bandwidth reports\n\ + -P, --parallel number of parallel client streams to run\n\ + -R, --reverse run in reverse mode (server sends, client receives)\n" + ]) + ]); + }; + + return m.render() + .then(L.bind(function(m, nodes) { + poll.add(L.bind(function() { + return Promise.all([ + getServiceStatus() + ]).then(L.bind(this.poll_status, this, nodes)); + }, this), 3); + return nodes; + }, this, m)); + } +}); diff --git a/package/luci-app-netspeedtest/htdocs/luci-static/resources/view/netspeedtest/librespeed-cfg.js b/package/luci-app-netspeedtest/htdocs/luci-static/resources/view/netspeedtest/librespeed-cfg.js new file mode 100644 index 00000000000000..1e3a1674ed1a57 --- /dev/null +++ b/package/luci-app-netspeedtest/htdocs/luci-static/resources/view/netspeedtest/librespeed-cfg.js @@ -0,0 +1,110 @@ +'use strict'; +'require view'; +'require poll'; +'require fs'; +'require rpc'; +'require uci'; +'require ui'; +'require form'; + +const conf = 'librespeed-go'; +const instance = 'librespeed-go'; + +const callServiceList = rpc.declare({ + object: 'service', + method: 'list', + params: ['name'], + expect: { '': {} } +}); + +function getServiceStatus() { + return L.resolveDefault(callServiceList(conf), {}) + .then((res) => { + let isrunning = false; + try { + isrunning = res[conf]['instances'][instance]['running']; + } catch (e) { } + return isrunning; + }); +} + +return view.extend({ +// handleSaveApply: null, +// handleSave: null, +// handleReset: null, + + load() { + return Promise.all([ + L.resolveDefault(getServiceStatus(), false), + uci.load('librespeed-go') + ]); + }, + + poll_status(nodes, stat) { + const isRunning = stat[0]; + let view = nodes.querySelector('#service_status'); + + if (isRunning) { + view.innerHTML = "" + instance + " - " + _("SERVER RUNNING") + ""; + } else { + view.innerHTML = "" + instance + " - " + _("SERVER NOT RUNNING") + ""; + } + return; + }, + + render(res) { + const isRunning = res[0]; + + let m, s, o; + + m = new form.Map('librespeed-go', _('librespeed Config')); + + s = m.section(form.NamedSection, '_status'); + s.anonymous = true; + s.render = function (section_id) { + return E('div', { class: 'cbi-section' }, [ + E('div', { id: 'service_status' }, _('Collecting data ...')) + ]); + }; + + s = m.section(form.NamedSection, 'config', 'librespeed-go'); + s.anonymous = true; + + o = s.option(form.Value, 'listen_port', _('Listen Port')); + o.datatype = 'port'; + o.default = 8989; + o.rmempty = false; + + o = s.option(form.Flag, 'enable_http2', _('Enable HTTP2')); + o.default = o.disabled; + o.rmempty = false; + + o = s.option(form.Flag, 'enable_tls', _('Enable TLS')); + o.default = o.disabled; + o.rmempty = false; + o.retain = true; + o.depends('enable_http2', '1'); + + o = s.option(form.Value, 'tls_cert_file', _('TLS Cert file')); + o.placeholder = '/etc/librespeed-go/cert.pem'; + o.rmempty = false; + o.retain = true; + o.depends('enable_tls', '1'); + + o = s.option(form.Value, 'tls_key_file', _('TLS Key file')); + o.placeholder = '/etc/librespeed-go/privkey.pem'; + o.rmempty = false; + o.retain = true; + o.depends('enable_tls', '1'); + + return m.render() + .then(L.bind(function(m, nodes) { + poll.add(L.bind(function() { + return Promise.all([ + L.resolveDefault(getServiceStatus(), false) + ]).then(L.bind(this.poll_status, this, nodes)); + }, this), 3); + return nodes; + }, this, m)); + } +}); diff --git a/package/luci-app-netspeedtest/htdocs/luci-static/resources/view/netspeedtest/librespeed.js b/package/luci-app-netspeedtest/htdocs/luci-static/resources/view/netspeedtest/librespeed.js new file mode 100644 index 00000000000000..a91e8ace2814e0 --- /dev/null +++ b/package/luci-app-netspeedtest/htdocs/luci-static/resources/view/netspeedtest/librespeed.js @@ -0,0 +1,51 @@ +'use strict'; +'require view'; +'require fs'; +'require rpc'; +'require uci'; +'require ui'; +'require form'; + +return view.extend({ +// handleSaveApply: null, +// handleSave: null, +// handleReset: null, + + load() { + return Promise.all([ + uci.load('netspeedtest'), + uci.load('librespeed-go') + ]); + }, + + render(res) { + const port = uci.get('librespeed-go', 'config', 'listen_port') || '8989'; + const ssl = uci.get('librespeed-go', 'config', 'enable_tls') || '0'; + + let m, s, o; + + m = new form.Map('netspeedtest', _('librespeed Site Speed Test')); + + s = m.section(form.NamedSection, 'config', 'netspeedtest'); + s.anonymous = true; + + o = s.option(form.Flag, 'librespeed_enabled', _('Enable')); + o.default = o.disabled; + o.rmempty = false; + + s = m.section(form.NamedSection, '_iframe'); + s.anonymous = true; + s.render = function (section_id) { + if (port === '0') { + return E('div', { class: 'alert-message warning' }, + _('Random port (port=0) is not supported.
Change to a fixed port and try again.')); + }; + return E('iframe', { + src: (ssl === '1' ? 'https' : 'http') + '://' + window.location.hostname + ':' + port, + style: 'width: 100%; min-height: 100vh; border: none; border-radius: 3px;' + }); + }; + + return m.render(); + } +}); diff --git a/package/luci-app-netspeedtest/htdocs/luci-static/resources/view/netspeedtest/speedtest.js b/package/luci-app-netspeedtest/htdocs/luci-static/resources/view/netspeedtest/speedtest.js new file mode 100644 index 00000000000000..95aa2ed1244b6e --- /dev/null +++ b/package/luci-app-netspeedtest/htdocs/luci-static/resources/view/netspeedtest/speedtest.js @@ -0,0 +1,192 @@ +'use strict'; +'require view'; +'require poll'; +'require dom'; +'require fs'; +'require rpc'; +'require uci'; +'require ui'; +'require form'; + +const TestTimeout = 240 * 1000; // 4 Minutes +const ResultFile = '/var/speedtest_result'; + +var callDownloadOokla = rpc.declare({ + object: 'luci.netspeedtest', + method: 'download_ookla', + params: ['arch'], + expect: { '': {} } +}); + +var callOoklaVerify = rpc.declare({ + object: 'luci.netspeedtest', + method: 'ookla_verify', + expect: { '': {} } +}); + +var callSpeedtest = rpc.declare({ + object: 'luci.netspeedtest', + method: 'speedtest', + expect: { '': {} } +}); + +return view.extend({ +// handleSaveApply: null, +// handleSave: null, +// handleReset: null, + + load() { + return Promise.all([ + callOoklaVerify(), + L.resolveDefault(fs.read(ResultFile), null), + L.resolveDefault(fs.stat(ResultFile), {}), + uci.load('netspeedtest') + ]); + }, + + render(data) { + const has_ookla = data[0].result; + const result_content = data[1] ? data[1].trim().split("\n") : []; + const result_mtime = data[2] ? data[2].mtime * 1000 : 0; + + let m, s, o; + + m = new form.Map('netspeedtest', _('Ookla SpeedTest')); + + s = m.section(form.TypedSection, '_result'); + s.anonymous = true; + s.render = function(section_id) { + const El = E('div', { 'id': 'speedtest_result' }, []); + const Testing = E('span', { 'class': 'spinning', 'style': 'color:yellow;font-weight:bold' }, [ + _('Testing in progress...') + ]); + const TestS = function(content) { + return E('div', { 'style': 'max-width:500px' }, [ + E('a', { 'href': content, 'target': '_blank' }, [ + E('img', { 'src': content + '.png', 'style': 'max-width:100%;max-height:100%; vertical-align:middle' }, []) + ]) + ]) + } + const NoSer = E('span', { 'style': 'color:red;font-weight:bold' }, [ _('No available servers.') ]); + const TestF = E('span', { 'style': 'color:red;font-weight:bold' }, [ _('Test failed.') ]); + const TestN = E('span', { 'style': 'color:red;font-weight:bold;display:none' }, [ _('No result.') ]); + + poll.add(function() { + return L.resolveDefault(fs.read(ResultFile), null).then((res) => { + const result_content = res ? res.trim().split("\n") : []; + const result_stat = document.querySelector('#speedtest_result'); + + if (result_content.length) { + if (result_content[0] == 'Testing') + dom.content(result_stat, [ Testing ]); + else if (result_content[0].match(/https?:\S+/)) + dom.content(result_stat, [ TestS(result_content[0]) ]); + else if (result_content[0] == 'No available servers') + dom.content(result_stat, [ NoSer ]); + else if (result_content[0] == 'Test failed') + dom.content(result_stat, [ TestF ]); + } else + dom.content(result_stat, [ TestN ]); + }); + }); + + if (result_content.length) { + if (result_content[0] == 'Testing') + El.appendChild(Testing); + else if (result_content[0].match(/https?:\S+/)) + El.appendChild(TestS(result_content[0])); + else if (result_content[0] == 'No available servers') + El.appendChild(NoSer); + else if (result_content[0] == 'Test failed') + El.appendChild(TestF); + } else + El.appendChild(TestN); + + return El; + } + + s = m.section(form.NamedSection, 'config', 'netspeedtest'); + s.anonymous = true; + + o = s.option(form.Button, '_start', _('Start Test')); + o.inputtitle = _('Start Test'); + o.inputstyle = 'apply'; + if (result_content.length && result_content[0] == 'Testing' && (Date.now() - result_mtime) < TestTimeout) + o.readonly = true; + o.onclick = function(ev, section_id) { + //L.env.rpctimeout = 180; // 3 minutes + window.setTimeout(function() { + window.location = window.location.href.split('#')[0]; + }, L.env.apply_display * 500); + + return callSpeedtest().then((res) => { + if (!res.result) + ui.addNotification(null, E('p', _('Test failed: %s').format(res.error)), 'error'); + }); + }; + + o = s.option(form.DummyValue, '_ookla_status', _('Ookla® SpeedTest-CLI Status')); + o.rawhtml = true; + o.cfgvalue = function() { + return E('span', { + id: 'ookla_status', + style: `color:${has_ookla ? 'green' : 'red'};font-weight:bold` + }, [ has_ookla ? _('Installed') : _('Not Installed') ]); + }; + poll.add(function() { + return callOoklaVerify().then((res) => { + const has_ookla = res.result; + const ookla_stat = document.querySelector('#ookla_status'); + + if (ookla_stat) { + ookla_stat.style.color = has_ookla ? 'green' : 'red'; + dom.content(ookla_stat, [ has_ookla ? _('Installed') : _('Not Installed') ]); + } + }); + }) + + o = s.option(form.Flag, 'proxy_enabled', _('Enable proxy for downloader and test')); + o.rmempty = false; + + o = s.option(form.ListValue, 'proxy_protocol', _('Proxy Protocol')); + o.value('http', 'HTTP'); + o.value('https', 'HTTPS'); + o.value('socks5', 'SOCKS5'); + o.value('socks5h', 'SOCKS5H'); + o.default = 'socks5'; + o.rmempty = false; + o.retain = true; + o.depends('proxy_enabled', '1'); + + o = s.option(form.Value, 'proxy_server', _('Proxy Server')); + o.placeholder = '[username[:password]@]address:port'; + o.rmempty = false; + o.retain = true; + o.depends('proxy_enabled', '1'); + + o = s.option(form.ListValue, '_arch', _('System Arch')); + o.value('i386'); + o.value('x86_64'); + o.value('armel'); + o.value('armhf'); + o.value('aarch64'); + o.default = 'x86_64'; + o.write = function() {}; + + o = s.option(form.Button, '_download', _('Download Ookla® SpeedTest-CLI')); + o.inputtitle = _('Download'); + o.inputstyle = 'apply'; + o.onclick = function(ev, section_id) { + const arch=this.section.getOption('_arch').formvalue(section_id); + //console.log(arch); + return callDownloadOokla(arch).then((res) => { + if (res.result === true) + ui.addNotification(null, E('p', _('Successfully download.'))); + else + ui.addNotification(null, E('p', _('Download failed: %s').format(res.error)), 'error'); + }); + } + + return m.render(); + } +}); diff --git a/package/luci-app-netspeedtest/htdocs/luci-static/resources/view/netspeedtest/webspeedtest.js b/package/luci-app-netspeedtest/htdocs/luci-static/resources/view/netspeedtest/webspeedtest.js new file mode 100644 index 00000000000000..4efa0aefd24304 --- /dev/null +++ b/package/luci-app-netspeedtest/htdocs/luci-static/resources/view/netspeedtest/webspeedtest.js @@ -0,0 +1,62 @@ +'use strict'; +'require view'; +'require uci'; +'require ui'; +'require form'; + +const testurls = [ + ['https://openspeedtest.com/speedtest', _('OpenSpeedTest')], + //['https://unidata.speedtestcustom.com', _('UNiDATA SpeedTest')], // not have SameSite=None + //['https://speed.cloudflare.com', _('Cloudflare SpeedTest')], //X-Frame-Options: DENY + ['https://static.hdslb.com', _('Bilibili hdslb')], + ['https://fast.com', _('Netflix Fast')], + ['https://test.ustc.edu.cn', _('USTC EDU SpeedTest')], + ['https://test.nju.edu.cn', _('Nanjing University Speed Test')], + ['http://speed5.ntu.edu.tw/speed5', _('NTU Speed Test')] +]; + +return view.extend({ +// handleSaveApply: null, +// handleSave: null, +// handleReset: null, + + load() { + return Promise.all([ + uci.load('netspeedtest') + ]); + }, + + render(res) { + let m, s, o; + + m = new form.Map('netspeedtest', _('Web SpeedTest')); + + s = m.section(form.NamedSection, 'config', 'netspeedtest'); + + /* Service switch */ + o = s.option(form.ListValue, '_service_list', _('Service list')); + o.default = testurls[0][0]; + testurls.forEach((res) => { + o.value.apply(o, res); + }) + o.onchange = function(ev, section_id, value) { + this.default = value; + + const iframe = document.getElementById('speedtest-iframe'); + if (iframe) + iframe.setAttribute('src', value); + } + + s = m.section(form.NamedSection, '_iframe'); + s.anonymous = true; + s.render = function (section_id) { + return E('iframe', { + id: 'speedtest-iframe', + src: this.map.findElement('id', 'widget.cbid.netspeedtest.config._service_list')?.value || testurls[0][0], + style: 'border:none;width:100%;height:100%;min-height:600px;border:none;overflow:hidden !important;' + }); + }; + + return m.render(); + } +}); diff --git a/package/luci-app-netspeedtest/po/templates/netspeedtest.pot b/package/luci-app-netspeedtest/po/templates/netspeedtest.pot new file mode 100644 index 00000000000000..2198f96edfea84 --- /dev/null +++ b/package/luci-app-netspeedtest/po/templates/netspeedtest.pot @@ -0,0 +1,196 @@ +msgid "" +msgstr "Content-Type: text/plain; charset=UTF-8" + +#: htdocs/luci-static/resources/view/netspeedtest/webspeedtest.js:11 +msgid "Bilibili hdslb" +msgstr "" + +#: root/usr/share/luci/menu.d/luci-app-netspeedtest.json:46 +msgid "Client SpeedTest" +msgstr "" + +#: htdocs/luci-static/resources/view/netspeedtest/iperf3.js:66 +#: htdocs/luci-static/resources/view/netspeedtest/librespeed-cfg.js:66 +msgid "Collecting data ..." +msgstr "" + +#: htdocs/luci-static/resources/view/netspeedtest/speedtest.js:177 +msgid "Download" +msgstr "" + +#: htdocs/luci-static/resources/view/netspeedtest/speedtest.js:176 +msgid "Download Ookla® SpeedTest-CLI" +msgstr "" + +#: htdocs/luci-static/resources/view/netspeedtest/speedtest.js:186 +msgid "Download failed: %s" +msgstr "" + +#: htdocs/luci-static/resources/view/netspeedtest/iperf3.js:77 +msgid "Download iperf3 client" +msgstr "" + +#: htdocs/luci-static/resources/view/netspeedtest/iperf3.js:73 +#: htdocs/luci-static/resources/view/netspeedtest/librespeed.js:32 +msgid "Enable" +msgstr "" + +#: htdocs/luci-static/resources/view/netspeedtest/librespeed-cfg.js:78 +msgid "Enable HTTP2" +msgstr "" + +#: htdocs/luci-static/resources/view/netspeedtest/librespeed-cfg.js:82 +msgid "Enable TLS" +msgstr "" + +#: htdocs/luci-static/resources/view/netspeedtest/speedtest.js:148 +msgid "Enable proxy for downloader and test" +msgstr "" + +#: root/usr/share/rpcd/acl.d/luci-app-netspeedtest.json:3 +msgid "Grant access to netspeedtest procedures" +msgstr "" + +#: htdocs/luci-static/resources/view/netspeedtest/speedtest.js:134 +#: htdocs/luci-static/resources/view/netspeedtest/speedtest.js:143 +msgid "Installed" +msgstr "" + +#: htdocs/luci-static/resources/view/netspeedtest/librespeed-cfg.js:73 +msgid "Listen Port" +msgstr "" + +#: htdocs/luci-static/resources/view/netspeedtest/webspeedtest.js:12 +msgid "Netflix Fast" +msgstr "" + +#: htdocs/luci-static/resources/view/netspeedtest/speedtest.js:70 +msgid "No available servers." +msgstr "" + +#: htdocs/luci-static/resources/view/netspeedtest/speedtest.js:72 +msgid "No result." +msgstr "" + +#: htdocs/luci-static/resources/view/netspeedtest/speedtest.js:134 +#: htdocs/luci-static/resources/view/netspeedtest/speedtest.js:143 +msgid "Not Installed" +msgstr "" + +#: htdocs/luci-static/resources/view/netspeedtest/iperf3.js:78 +msgid "Official website" +msgstr "" + +#: htdocs/luci-static/resources/view/netspeedtest/speedtest.js:54 +msgid "Ookla SpeedTest" +msgstr "" + +#: htdocs/luci-static/resources/view/netspeedtest/speedtest.js:128 +msgid "Ookla® SpeedTest-CLI Status" +msgstr "" + +#: htdocs/luci-static/resources/view/netspeedtest/webspeedtest.js:8 +msgid "OpenSpeedTest" +msgstr "" + +#: htdocs/luci-static/resources/view/netspeedtest/speedtest.js:151 +msgid "Proxy Protocol" +msgstr "" + +#: htdocs/luci-static/resources/view/netspeedtest/speedtest.js:161 +msgid "Proxy Server" +msgstr "" + +#: htdocs/luci-static/resources/view/netspeedtest/librespeed.js:41 +msgid "" +"Random port (port=0) is not supported.
Change to a fixed port and try " +"again." +msgstr "" + +#: root/usr/share/luci/menu.d/luci-app-netspeedtest.json:38 +msgid "Router SpeedTest" +msgstr "" + +#: htdocs/luci-static/resources/view/netspeedtest/iperf3.js:50 +#: htdocs/luci-static/resources/view/netspeedtest/librespeed-cfg.js:50 +msgid "SERVER NOT RUNNING" +msgstr "" + +#: htdocs/luci-static/resources/view/netspeedtest/iperf3.js:48 +#: htdocs/luci-static/resources/view/netspeedtest/librespeed-cfg.js:48 +msgid "SERVER RUNNING" +msgstr "" + +#: htdocs/luci-static/resources/view/netspeedtest/webspeedtest.js:35 +msgid "Service list" +msgstr "" + +#: root/usr/share/luci/menu.d/luci-app-netspeedtest.json:3 +msgid "SpeedTest" +msgstr "" + +#: htdocs/luci-static/resources/view/netspeedtest/speedtest.js:111 +#: htdocs/luci-static/resources/view/netspeedtest/speedtest.js:112 +msgid "Start Test" +msgstr "" + +#: htdocs/luci-static/resources/view/netspeedtest/speedtest.js:184 +msgid "Successfully download." +msgstr "" + +#: htdocs/luci-static/resources/view/netspeedtest/speedtest.js:167 +msgid "System Arch" +msgstr "" + +#: htdocs/luci-static/resources/view/netspeedtest/librespeed-cfg.js:88 +msgid "TLS Cert file" +msgstr "" + +#: htdocs/luci-static/resources/view/netspeedtest/librespeed-cfg.js:94 +msgid "TLS Key file" +msgstr "" + +#: htdocs/luci-static/resources/view/netspeedtest/speedtest.js:71 +msgid "Test failed." +msgstr "" + +#: htdocs/luci-static/resources/view/netspeedtest/speedtest.js:124 +msgid "Test failed: %s" +msgstr "" + +#: htdocs/luci-static/resources/view/netspeedtest/speedtest.js:61 +msgid "Testing in progress..." +msgstr "" + +#: htdocs/luci-static/resources/view/netspeedtest/webspeedtest.js:13 +msgid "USTC EDU SpeedTest" +msgstr "" + +#: htdocs/luci-static/resources/view/netspeedtest/webspeedtest.js:30 +msgid "Web SpeedTest" +msgstr "" + +#: htdocs/luci-static/resources/view/netspeedtest/iperf3.js:60 +msgid "iperf3 Bandwidth Performance Test" +msgstr "" + +#: htdocs/luci-static/resources/view/netspeedtest/iperf3.js:88 +msgid "iperf3 Common commands reference" +msgstr "" + +#: root/usr/share/luci/menu.d/luci-app-netspeedtest.json:14 +msgid "iperf3 Server" +msgstr "" + +#: htdocs/luci-static/resources/view/netspeedtest/librespeed-cfg.js:60 +#: root/usr/share/luci/menu.d/luci-app-netspeedtest.json:30 +msgid "librespeed Config" +msgstr "" + +#: root/usr/share/luci/menu.d/luci-app-netspeedtest.json:22 +msgid "librespeed Server" +msgstr "" + +#: htdocs/luci-static/resources/view/netspeedtest/librespeed.js:27 +msgid "librespeed Site Speed Test" +msgstr "" diff --git a/package/luci-app-netspeedtest/po/zh_Hans/netspeedtest.po b/package/luci-app-netspeedtest/po/zh_Hans/netspeedtest.po new file mode 100644 index 00000000000000..5d8183e290fcf4 --- /dev/null +++ b/package/luci-app-netspeedtest/po/zh_Hans/netspeedtest.po @@ -0,0 +1,210 @@ +msgid "" +msgstr "" +"Project-Id-Version: PACKAGE VERSION\n" +"PO-Revision-Date: 2023-01-09 10:03+0000\n" +"Last-Translator: muink \n" +"Language-Team: Chinese (Simplified) \n" +"Language: zh_Hans\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=1; plural=0;\n" +"X-Generator: Weblate 4.12.1-dev\n" + +#: htdocs/luci-static/resources/view/netspeedtest/webspeedtest.js:11 +msgid "Bilibili hdslb" +msgstr "" + +#: root/usr/share/luci/menu.d/luci-app-netspeedtest.json:46 +msgid "Client SpeedTest" +msgstr "客户端测速" + +#: htdocs/luci-static/resources/view/netspeedtest/iperf3.js:66 +#: htdocs/luci-static/resources/view/netspeedtest/librespeed-cfg.js:66 +msgid "Collecting data ..." +msgstr "正在收集数据..." + +#: htdocs/luci-static/resources/view/netspeedtest/speedtest.js:177 +msgid "Download" +msgstr "下载" + +#: htdocs/luci-static/resources/view/netspeedtest/speedtest.js:176 +msgid "Download Ookla® SpeedTest-CLI" +msgstr "下载 Ookla® SpeedTest-CLI" + +#: htdocs/luci-static/resources/view/netspeedtest/speedtest.js:186 +msgid "Download failed: %s" +msgstr "" + +#: htdocs/luci-static/resources/view/netspeedtest/iperf3.js:77 +msgid "Download iperf3 client" +msgstr "下载 iperf3 客户端" + +#: htdocs/luci-static/resources/view/netspeedtest/iperf3.js:73 +#: htdocs/luci-static/resources/view/netspeedtest/librespeed.js:32 +msgid "Enable" +msgstr "启用" + +#: htdocs/luci-static/resources/view/netspeedtest/librespeed-cfg.js:78 +msgid "Enable HTTP2" +msgstr "启用 HTTP2" + +#: htdocs/luci-static/resources/view/netspeedtest/librespeed-cfg.js:82 +msgid "Enable TLS" +msgstr "启用 TLS" + +#: htdocs/luci-static/resources/view/netspeedtest/speedtest.js:148 +msgid "Enable proxy for downloader and test" +msgstr "为下载器和测试启用代理" + +#: root/usr/share/rpcd/acl.d/luci-app-netspeedtest.json:3 +msgid "Grant access to netspeedtest procedures" +msgstr "授予访问 LuCI 应用 netspeedtest 的权限" + +#: htdocs/luci-static/resources/view/netspeedtest/speedtest.js:134 +#: htdocs/luci-static/resources/view/netspeedtest/speedtest.js:143 +msgid "Installed" +msgstr "已安装" + +#: htdocs/luci-static/resources/view/netspeedtest/librespeed-cfg.js:73 +msgid "Listen Port" +msgstr "监听端口" + +#: htdocs/luci-static/resources/view/netspeedtest/webspeedtest.js:12 +msgid "Netflix Fast" +msgstr "" + +#: htdocs/luci-static/resources/view/netspeedtest/speedtest.js:70 +msgid "No available servers." +msgstr "" + +#: htdocs/luci-static/resources/view/netspeedtest/speedtest.js:72 +msgid "No result." +msgstr "无结果." + +#: htdocs/luci-static/resources/view/netspeedtest/speedtest.js:134 +#: htdocs/luci-static/resources/view/netspeedtest/speedtest.js:143 +msgid "Not Installed" +msgstr "未安装" + +#: htdocs/luci-static/resources/view/netspeedtest/iperf3.js:78 +msgid "Official website" +msgstr "官方网站" + +#: htdocs/luci-static/resources/view/netspeedtest/speedtest.js:54 +msgid "Ookla SpeedTest" +msgstr "" + +#: htdocs/luci-static/resources/view/netspeedtest/speedtest.js:128 +msgid "Ookla® SpeedTest-CLI Status" +msgstr "Ookla® SpeedTest-CLI 状态" + +#: htdocs/luci-static/resources/view/netspeedtest/webspeedtest.js:8 +msgid "OpenSpeedTest" +msgstr "" + +#: htdocs/luci-static/resources/view/netspeedtest/speedtest.js:151 +msgid "Proxy Protocol" +msgstr "代理协议" + +#: htdocs/luci-static/resources/view/netspeedtest/speedtest.js:161 +msgid "Proxy Server" +msgstr "代理服务器" + +#: htdocs/luci-static/resources/view/netspeedtest/librespeed.js:41 +msgid "" +"Random port (port=0) is not supported.
Change to a fixed port and try " +"again." +msgstr "不支持随机端口 (port=0).
改为固定端口然后再试一次." + +#: root/usr/share/luci/menu.d/luci-app-netspeedtest.json:38 +msgid "Router SpeedTest" +msgstr "路由端测速" + +#: htdocs/luci-static/resources/view/netspeedtest/iperf3.js:50 +#: htdocs/luci-static/resources/view/netspeedtest/librespeed-cfg.js:50 +msgid "SERVER NOT RUNNING" +msgstr "服务器未运行" + +#: htdocs/luci-static/resources/view/netspeedtest/iperf3.js:48 +#: htdocs/luci-static/resources/view/netspeedtest/librespeed-cfg.js:48 +msgid "SERVER RUNNING" +msgstr "服务器运行中" + +#: htdocs/luci-static/resources/view/netspeedtest/webspeedtest.js:35 +msgid "Service list" +msgstr "服务列表" + +#: root/usr/share/luci/menu.d/luci-app-netspeedtest.json:3 +msgid "SpeedTest" +msgstr "网速测试" + +#: htdocs/luci-static/resources/view/netspeedtest/speedtest.js:111 +#: htdocs/luci-static/resources/view/netspeedtest/speedtest.js:112 +msgid "Start Test" +msgstr "启动测试" + +#: htdocs/luci-static/resources/view/netspeedtest/speedtest.js:184 +msgid "Successfully download." +msgstr "" + +#: htdocs/luci-static/resources/view/netspeedtest/speedtest.js:167 +msgid "System Arch" +msgstr "系统架构" + +#: htdocs/luci-static/resources/view/netspeedtest/librespeed-cfg.js:88 +msgid "TLS Cert file" +msgstr "TLS 证书文件" + +#: htdocs/luci-static/resources/view/netspeedtest/librespeed-cfg.js:94 +msgid "TLS Key file" +msgstr "TLS 密钥文件" + +#: htdocs/luci-static/resources/view/netspeedtest/speedtest.js:71 +msgid "Test failed." +msgstr "测试失败." + +#: htdocs/luci-static/resources/view/netspeedtest/speedtest.js:124 +msgid "Test failed: %s" +msgstr "" + +#: htdocs/luci-static/resources/view/netspeedtest/speedtest.js:61 +msgid "Testing in progress..." +msgstr "测试进行中..." + +#: htdocs/luci-static/resources/view/netspeedtest/webspeedtest.js:13 +msgid "USTC EDU SpeedTest" +msgstr "" + +#: htdocs/luci-static/resources/view/netspeedtest/webspeedtest.js:30 +msgid "Web SpeedTest" +msgstr "网页测速" + +#: htdocs/luci-static/resources/view/netspeedtest/iperf3.js:60 +msgid "iperf3 Bandwidth Performance Test" +msgstr "iperf3 带宽性能测试" + +#: htdocs/luci-static/resources/view/netspeedtest/iperf3.js:88 +msgid "iperf3 Common commands reference" +msgstr "iperf3 常用命令参考" + +#: root/usr/share/luci/menu.d/luci-app-netspeedtest.json:14 +msgid "iperf3 Server" +msgstr "iperf3 服务器" + +#: htdocs/luci-static/resources/view/netspeedtest/librespeed-cfg.js:60 +#: root/usr/share/luci/menu.d/luci-app-netspeedtest.json:30 +msgid "librespeed Config" +msgstr "librespeed 配置" + +#: root/usr/share/luci/menu.d/luci-app-netspeedtest.json:22 +msgid "librespeed Server" +msgstr "librespeed 服务器" + +#: htdocs/luci-static/resources/view/netspeedtest/librespeed.js:27 +msgid "librespeed Site Speed Test" +msgstr "librespeed 站点测速" + +#~ msgid "Use Ookla® SpeedTest-CLI" +#~ msgstr "使用 Ookla® SpeedTest-CLI" diff --git a/package/luci-app-netspeedtest/po/zh_Hant/netspeedtest.po b/package/luci-app-netspeedtest/po/zh_Hant/netspeedtest.po new file mode 100644 index 00000000000000..9fb3d2b8366e11 --- /dev/null +++ b/package/luci-app-netspeedtest/po/zh_Hant/netspeedtest.po @@ -0,0 +1,210 @@ +msgid "" +msgstr "" +"Project-Id-Version: PACKAGE VERSION\n" +"PO-Revision-Date: 2023-01-09 10:03+0000\n" +"Last-Translator: muink \n" +"Language-Team: Chinese (Traditional) \n" +"Language: zh_Hant\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=1; plural=0;\n" +"X-Generator: Weblate 4.12.1-dev\n" + +#: htdocs/luci-static/resources/view/netspeedtest/webspeedtest.js:11 +msgid "Bilibili hdslb" +msgstr "" + +#: root/usr/share/luci/menu.d/luci-app-netspeedtest.json:46 +msgid "Client SpeedTest" +msgstr "客戶端測速" + +#: htdocs/luci-static/resources/view/netspeedtest/iperf3.js:66 +#: htdocs/luci-static/resources/view/netspeedtest/librespeed-cfg.js:66 +msgid "Collecting data ..." +msgstr "正在收集數據..." + +#: htdocs/luci-static/resources/view/netspeedtest/speedtest.js:177 +msgid "Download" +msgstr "下載" + +#: htdocs/luci-static/resources/view/netspeedtest/speedtest.js:176 +msgid "Download Ookla® SpeedTest-CLI" +msgstr "下載 Ookla® SpeedTest-CLI" + +#: htdocs/luci-static/resources/view/netspeedtest/speedtest.js:186 +msgid "Download failed: %s" +msgstr "" + +#: htdocs/luci-static/resources/view/netspeedtest/iperf3.js:77 +msgid "Download iperf3 client" +msgstr "下載 iperf3 用戶端" + +#: htdocs/luci-static/resources/view/netspeedtest/iperf3.js:73 +#: htdocs/luci-static/resources/view/netspeedtest/librespeed.js:32 +msgid "Enable" +msgstr "啟用" + +#: htdocs/luci-static/resources/view/netspeedtest/librespeed-cfg.js:78 +msgid "Enable HTTP2" +msgstr "啟用 HTTP2" + +#: htdocs/luci-static/resources/view/netspeedtest/librespeed-cfg.js:82 +msgid "Enable TLS" +msgstr "HTTP2 TLS" + +#: htdocs/luci-static/resources/view/netspeedtest/speedtest.js:148 +msgid "Enable proxy for downloader and test" +msgstr "為下載器和測試啟用代理" + +#: root/usr/share/rpcd/acl.d/luci-app-netspeedtest.json:3 +msgid "Grant access to netspeedtest procedures" +msgstr "授予訪問 LuCI 應用 netspeedtest 的權限" + +#: htdocs/luci-static/resources/view/netspeedtest/speedtest.js:134 +#: htdocs/luci-static/resources/view/netspeedtest/speedtest.js:143 +msgid "Installed" +msgstr "已安裝" + +#: htdocs/luci-static/resources/view/netspeedtest/librespeed-cfg.js:73 +msgid "Listen Port" +msgstr "監聽埠" + +#: htdocs/luci-static/resources/view/netspeedtest/webspeedtest.js:12 +msgid "Netflix Fast" +msgstr "" + +#: htdocs/luci-static/resources/view/netspeedtest/speedtest.js:70 +msgid "No available servers." +msgstr "" + +#: htdocs/luci-static/resources/view/netspeedtest/speedtest.js:72 +msgid "No result." +msgstr "無結果." + +#: htdocs/luci-static/resources/view/netspeedtest/speedtest.js:134 +#: htdocs/luci-static/resources/view/netspeedtest/speedtest.js:143 +msgid "Not Installed" +msgstr "未安裝" + +#: htdocs/luci-static/resources/view/netspeedtest/iperf3.js:78 +msgid "Official website" +msgstr "官方網站" + +#: htdocs/luci-static/resources/view/netspeedtest/speedtest.js:54 +msgid "Ookla SpeedTest" +msgstr "" + +#: htdocs/luci-static/resources/view/netspeedtest/speedtest.js:128 +msgid "Ookla® SpeedTest-CLI Status" +msgstr "Ookla® SpeedTest-CLI 狀態" + +#: htdocs/luci-static/resources/view/netspeedtest/webspeedtest.js:8 +msgid "OpenSpeedTest" +msgstr "" + +#: htdocs/luci-static/resources/view/netspeedtest/speedtest.js:151 +msgid "Proxy Protocol" +msgstr "代理協議" + +#: htdocs/luci-static/resources/view/netspeedtest/speedtest.js:161 +msgid "Proxy Server" +msgstr "代理伺服器" + +#: htdocs/luci-static/resources/view/netspeedtest/librespeed.js:41 +msgid "" +"Random port (port=0) is not supported.
Change to a fixed port and try " +"again." +msgstr "不支持隨機連接埠 (port=0).
改為固定埠號然後再試一次." + +#: root/usr/share/luci/menu.d/luci-app-netspeedtest.json:38 +msgid "Router SpeedTest" +msgstr "路由端測速" + +#: htdocs/luci-static/resources/view/netspeedtest/iperf3.js:50 +#: htdocs/luci-static/resources/view/netspeedtest/librespeed-cfg.js:50 +msgid "SERVER NOT RUNNING" +msgstr "伺服器未運行" + +#: htdocs/luci-static/resources/view/netspeedtest/iperf3.js:48 +#: htdocs/luci-static/resources/view/netspeedtest/librespeed-cfg.js:48 +msgid "SERVER RUNNING" +msgstr "伺服器運行中" + +#: htdocs/luci-static/resources/view/netspeedtest/webspeedtest.js:35 +msgid "Service list" +msgstr "服務清單" + +#: root/usr/share/luci/menu.d/luci-app-netspeedtest.json:3 +msgid "SpeedTest" +msgstr "網速測試" + +#: htdocs/luci-static/resources/view/netspeedtest/speedtest.js:111 +#: htdocs/luci-static/resources/view/netspeedtest/speedtest.js:112 +msgid "Start Test" +msgstr "啟動測試" + +#: htdocs/luci-static/resources/view/netspeedtest/speedtest.js:184 +msgid "Successfully download." +msgstr "" + +#: htdocs/luci-static/resources/view/netspeedtest/speedtest.js:167 +msgid "System Arch" +msgstr "系統架構" + +#: htdocs/luci-static/resources/view/netspeedtest/librespeed-cfg.js:88 +msgid "TLS Cert file" +msgstr "TLS 憑證檔案" + +#: htdocs/luci-static/resources/view/netspeedtest/librespeed-cfg.js:94 +msgid "TLS Key file" +msgstr "TLS 密鑰檔案" + +#: htdocs/luci-static/resources/view/netspeedtest/speedtest.js:71 +msgid "Test failed." +msgstr "測試失敗." + +#: htdocs/luci-static/resources/view/netspeedtest/speedtest.js:124 +msgid "Test failed: %s" +msgstr "" + +#: htdocs/luci-static/resources/view/netspeedtest/speedtest.js:61 +msgid "Testing in progress..." +msgstr "測試進行中..." + +#: htdocs/luci-static/resources/view/netspeedtest/webspeedtest.js:13 +msgid "USTC EDU SpeedTest" +msgstr "" + +#: htdocs/luci-static/resources/view/netspeedtest/webspeedtest.js:30 +msgid "Web SpeedTest" +msgstr "網頁測速" + +#: htdocs/luci-static/resources/view/netspeedtest/iperf3.js:60 +msgid "iperf3 Bandwidth Performance Test" +msgstr "iperf3 帶寬性能測試" + +#: htdocs/luci-static/resources/view/netspeedtest/iperf3.js:88 +msgid "iperf3 Common commands reference" +msgstr "iperf3 常用命令參考" + +#: root/usr/share/luci/menu.d/luci-app-netspeedtest.json:14 +msgid "iperf3 Server" +msgstr "iperf3 伺服器" + +#: htdocs/luci-static/resources/view/netspeedtest/librespeed-cfg.js:60 +#: root/usr/share/luci/menu.d/luci-app-netspeedtest.json:30 +msgid "librespeed Config" +msgstr "librespeed 組態" + +#: root/usr/share/luci/menu.d/luci-app-netspeedtest.json:22 +msgid "librespeed Server" +msgstr "librespeed 伺服器" + +#: htdocs/luci-static/resources/view/netspeedtest/librespeed.js:27 +msgid "librespeed Site Speed Test" +msgstr "librespeed 站點測速" + +#~ msgid "Use Ookla® SpeedTest-CLI" +#~ msgstr "使用 Ookla® SpeedTest-CLI" diff --git a/package/luci-app-netspeedtest/root/etc/init.d/netspeedtest b/package/luci-app-netspeedtest/root/etc/init.d/netspeedtest new file mode 100755 index 00000000000000..42c4cf85900777 --- /dev/null +++ b/package/luci-app-netspeedtest/root/etc/init.d/netspeedtest @@ -0,0 +1,76 @@ +#!/bin/sh /etc/rc.common +# Copyright (C) 2023-2026 muink +# + +. "${IPKG_INSTROOT}/lib/functions.sh" + +START=99 +USE_PROCD=1 + +# uci +CONFIG_NAME='netspeedtest' +NAMEDDSECTION='config' + + + +iperf3_instance() { + procd_open_instance "iperf3" + procd_set_param command "iperf3" + procd_append_param command -s + procd_set_param respawn + procd_set_param user nobody + procd_set_param stdout 1 + procd_set_param stderr 1 + procd_close_instance +} + +librespeed_instance() { + local action="$1" + + case "$action" in + start) + uci set librespeed-go.config.enabled='1' + uci commit librespeed-go + /etc/init.d/librespeed-go reload + ;; + stop) + uci set librespeed-go.config.enabled='0' + uci commit librespeed-go + /etc/init.d/librespeed-go reload + ;; + esac +} + +kill_instance() { + local instance="$1" + pgrep -f "$instance" | xargs kill -9 >/dev/null 2>&1 + return 0 +} + +start_service() { + local eniperf3 enlibrespeed + + config_load "$CONFIG_NAME" + eval "eniperf3=\$CONFIG_${NAMEDDSECTION}_iperf3_enabled" + eval "enlibrespeed=\$CONFIG_${NAMEDDSECTION}_librespeed_enabled" + + [ "$eniperf3" == "1" ] && iperf3_instance + [ "${enlibrespeed:=0}" == "0" ] && librespeed_instance stop || librespeed_instance start +} + +service_triggers() { + procd_add_reload_trigger "$CONFIG_NAME" 'librespeed-go' 'network' +} + +stop_service() { + uci -q batch <<-EOF + set $CONFIG_NAME.$NAMEDDSECTION.iperf3_enabled='0' + set $CONFIG_NAME.$NAMEDDSECTION.librespeed_enabled='0' + commit $CONFIG_NAME + EOF + librespeed_instance stop +} + +restart() { + start +} diff --git a/package/luci-app-netspeedtest/root/etc/uci-defaults/99_netspeedtest b/package/luci-app-netspeedtest/root/etc/uci-defaults/99_netspeedtest new file mode 100755 index 00000000000000..680dc7961b365b --- /dev/null +++ b/package/luci-app-netspeedtest/root/etc/uci-defaults/99_netspeedtest @@ -0,0 +1,6 @@ +#!/bin/sh +touch /etc/config/netspeedtest +uci -q batch <<-EOF >/dev/null + set netspeedtest.config=netspeedtest + commit netspeedtest +EOF diff --git a/package/luci-app-netspeedtest/root/usr/libexec/rpcd/luci.netspeedtest b/package/luci-app-netspeedtest/root/usr/libexec/rpcd/luci.netspeedtest new file mode 100755 index 00000000000000..c51c7808e96e38 --- /dev/null +++ b/package/luci-app-netspeedtest/root/usr/libexec/rpcd/luci.netspeedtest @@ -0,0 +1,136 @@ +#!/bin/sh +# Copyright (C) 2023-2026 Anya Lin + +. /lib/functions.sh +. /usr/share/libubox/jshn.sh + +# uci +CONFIG_NAME='netspeedtest' +NAMEDDSECTION='config' + +# netspeedtest +OOKLA_SPEEDTEST='/usr/libexec/netspeedtest/speedtest' +SPEEDTEST_RESULT='/var/speedtest_result' + +# api +speedTestServersUrl="https://www.speedtest.net/api/js/servers" +speedTestServersAlternativeUrl="https://www.speedtest.net/speedtest-servers-static.php" +speedTestServersAndroid="https://www.speedtest.net/api/android/config.php" +speedTestServersIOS="https://www.speedtest.net/api/ios-config.php" + +ookla_verify() { + [ -x "$OOKLA_SPEEDTEST" ] + return $? +} + +# func [args...] +speedtest() { + echo "Testing" > "$SPEEDTEST_RESULT" + result=$($OOKLA_SPEEDTEST --accept-gdpr --accept-license --progress=no "$@" | grep 'Result URL' | awk '{print $NF}') + [ -n "$result" ] && echo "$result" > "$SPEEDTEST_RESULT" || echo "Test failed" > "$SPEEDTEST_RESULT" +} + +case "$1" in + list) + json_init + + json_add_object "download_ookla" + json_add_string "arch" + json_close_object + + json_add_object "ookla_verify" + json_close_object + + json_add_object "speedtest" + json_close_object + + json_dump + ;; + call) + case "$2" in + download_ookla) + read -t 1 -r input + ARCH=$(echo "$input" | jsonfilter -qe '@.arch') + + if [ -z "$ARCH" ]; then + json_init + json_add_boolean "result" 0 + json_add_string "error" 'illegal arch' + json_dump + exit + fi + + [ "$(uci -q get $CONFIG_NAME.$NAMEDDSECTION.proxy_enabled)" = "1" ] && \ + export ALL_PROXY=$(uci -q get $CONFIG_NAME.$NAMEDDSECTION.proxy_protocol)://$(uci -q get $CONFIG_NAME.$NAMEDDSECTION.proxy_server) + + UA='Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/143.0.0.0 Safari/537.36' + + url=$( \ + curl --connect-timeout 10 --retry 3 -sSL 'https://www.speedtest.net/apps/cli' \ + --user-agent "$UA" \ + | grep "Download for Linux" \ + | sed 's|<|\n<|g' \ + | sed -n '/Download for Linux/,/<\/div>/p' \ + | sed -En "s|.*${ARCH}|\1|p" \ + ) + if [ -z "$url" ]; then + json_init + json_add_boolean "result" 0 + json_add_string "error" 'invalid architecture' + json_dump + exit + fi + + [ -n "$url" ] && curl -sSL --url "$url" --user-agent "$UA" | tar -xvz -C /tmp >/dev/null + mkdir -p ${OOKLA_SPEEDTEST%/*} 2>/dev/null + cp -f /tmp/speedtest $OOKLA_SPEEDTEST + chmod 755 $OOKLA_SPEEDTEST + if ookla_verify; then + json_init + json_add_boolean "result" 1 + json_dump + exit + else + rm -f $OOKLA_SPEEDTEST + json_init + json_add_boolean "result" 0 + json_add_string "error" 'invalid executable file' + json_dump + exit + fi + ;; + ookla_verify) + json_init + json_add_boolean "result" $(ookla_verify && echo 1 || echo 0) + json_dump + ;; + speedtest) + if pgrep -f "$OOKLA_SPEEDTEST" >/dev/null; then + json_init + json_add_boolean "result" 0 + json_add_string "error" 'Speedtest is underway' + json_dump + exit + fi + + [ "$(uci -q get $CONFIG_NAME.$NAMEDDSECTION.proxy_enabled)" = "1" ] && \ + export ALL_PROXY=$(uci -q get $CONFIG_NAME.$NAMEDDSECTION.proxy_protocol)://$(uci -q get $CONFIG_NAME.$NAMEDDSECTION.proxy_server) + + serverList="$(curl --connect-timeout 10 --retry 3 -sSL "$speedTestServersUrl")" + if [ -z "$serverList" ]; then + echo "No available servers" > "$SPEEDTEST_RESULT" + json_init + json_add_boolean "result" 0 + json_add_string "error" 'No available servers' + json_dump + exit + fi + + speedtest --server-id=$(echo "$serverList" | jsonfilter -e '@[0].id') & + json_init + json_add_boolean "result" 1 + json_dump + ;; + esac + ;; +esac diff --git a/package/luci-app-netspeedtest/root/usr/share/luci/menu.d/luci-app-netspeedtest.json b/package/luci-app-netspeedtest/root/usr/share/luci/menu.d/luci-app-netspeedtest.json new file mode 100644 index 00000000000000..a48a96264cac5a --- /dev/null +++ b/package/luci-app-netspeedtest/root/usr/share/luci/menu.d/luci-app-netspeedtest.json @@ -0,0 +1,53 @@ +{ + "admin/network/netspeedtest": { + "title": "SpeedTest", + "order": 55, + "action": { + "type": "firstchild" + }, + "depends": { + "acl": [ "luci-app-netspeedtest" ], + "uci": { "netspeedtest": true } + } + }, + "admin/network/netspeedtest/iperf3": { + "title": "iperf3 Server", + "order": 1, + "action": { + "type": "view", + "path": "netspeedtest/iperf3" + } + }, + "admin/network/netspeedtest/librespeed": { + "title": "librespeed Server", + "order": 2, + "action": { + "type": "view", + "path": "netspeedtest/librespeed" + } + }, + "admin/network/netspeedtest/librespeed-cfg": { + "title": "librespeed Config", + "order": 3, + "action": { + "type": "view", + "path": "netspeedtest/librespeed-cfg" + } + }, + "admin/network/netspeedtest/speedtest": { + "title": "Router SpeedTest", + "order": 4, + "action": { + "type": "view", + "path": "netspeedtest/speedtest" + } + }, + "admin/network/netspeedtest/webspeedtest": { + "title": "Client SpeedTest", + "order": 5, + "action": { + "type": "view", + "path": "netspeedtest/webspeedtest" + } + } +} diff --git a/package/luci-app-netspeedtest/root/usr/share/rpcd/acl.d/luci-app-netspeedtest.json b/package/luci-app-netspeedtest/root/usr/share/rpcd/acl.d/luci-app-netspeedtest.json new file mode 100644 index 00000000000000..73d866a4e29ff7 --- /dev/null +++ b/package/luci-app-netspeedtest/root/usr/share/rpcd/acl.d/luci-app-netspeedtest.json @@ -0,0 +1,19 @@ +{ + "luci-app-netspeedtest": { + "description": "Grant access to netspeedtest procedures", + "read": { + "file": { + "/etc/init.d/netspeedtest": [ "exec" ], + "/var/speedtest_result": [ "read" ] + }, + "ubus": { + "service": [ "list" ], + "luci.netspeedtest": [ "*" ] + }, + "uci": [ "netspeedtest", "librespeed-go" ] + }, + "write": { + "uci": [ "netspeedtest", "librespeed-go" ] + } + } +} diff --git a/package/luci-app-w1700k-fancontrol/LICENSE b/package/luci-app-w1700k-fancontrol/LICENSE new file mode 100644 index 00000000000000..8cd03e62f3b994 --- /dev/null +++ b/package/luci-app-w1700k-fancontrol/LICENSE @@ -0,0 +1,176 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to the Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS diff --git a/package/luci-app-w1700k-fancontrol/Makefile b/package/luci-app-w1700k-fancontrol/Makefile new file mode 100644 index 00000000000000..b0d9f93e0bd56d --- /dev/null +++ b/package/luci-app-w1700k-fancontrol/Makefile @@ -0,0 +1,13 @@ +# This is free software, licensed under the Apache License, Version 2.0 . + +include $(TOPDIR)/rules.mk + +PKG_LICENSE:=Apache-2.0 + +LUCI_TITLE:=LuCI W1700K Fan Control +LUCI_PKGNAME:=luci-app-w1700k-fancontrol +LUCI_DEPENDS:=+luci-base @TARGET_airoha + +include ../../luci.mk + +# call BuildPackage - OpenWrt buildroot signature diff --git a/package/luci-app-w1700k-fancontrol/README.md b/package/luci-app-w1700k-fancontrol/README.md new file mode 100644 index 00000000000000..16303bb3b9656c --- /dev/null +++ b/package/luci-app-w1700k-fancontrol/README.md @@ -0,0 +1,222 @@ +# luci-app-w1700k-fancontrol + +LuCI application for fan control and thermal monitoring on W1700K router (Airoha AN7581 + MT7996 BE19000). + +## Features + +- **Real-time Temperature Monitoring** + - CPU temperature (AN7581 SoC die) + - Board temperature (NCT7802Y local sensor - used for fan curve) + - 10G PHY 1 temperature + - 10G PHY 2 temperature + - WiFi radio temperatures (2.4 GHz, 5 GHz, 6 GHz from MT7996) + +- **Fan Speed Control** + - Real-time RPM and PWM display + - Automatic mode with configurable fan curves + - Manual mode with fixed PWM control + - Visual temperature gauges with color-coded status + +- **Fan Curve Presets** + - **Quiet** - Lower fan speeds, allows higher temperatures + - **Balanced** - Good mix of noise and cooling (default) + - **Performance** - Aggressive cooling, higher fan speeds + - **Custom** - User-definable 5-point temperature curve + +- **Interactive Curve Editor** + - Visual graph showing all preset curves + - Real-time curve preview when switching presets + - 5-point temperature-to-PWM mapping + +## Hardware Architecture + +``` ++-------------------------------------------------------------------+ +| W1700K Router | +| | +| +----------------+ +------------------+ +--------------+ | +| | AN7581 SoC | | NCT7802Y Fan | | MT7996 | | +| | | | Controller | | WiFi 7 | | +| | CPU temp | | | | | | +| | (thermal | | Board temp | | 2.4G temp | | +| | zone0) | | (temp1_input) | | 5G temp | | +| | | | | | 6G temp | | +| +----------------+ | Fan control: | +--------------+ | +| | - PWM output | | +| +----------------+ | - RPM input | | +| | PHY Sensors | | - Auto curve | | +| | | +------------------+ | +| | 10G PHY 1 temp | | | +| | Switch temp | +------+------+ | +| +----------------+ | | | +| | FAN | | +| | (PWM) | | +| +-------------+ | ++-------------------------------------------------------------------+ +``` + +## Screenshots + +The app adds two pages under **System > Fan Control** in LuCI: + +### Status Page + +Real-time monitoring of fan speed and all temperature sensors with color-coded gauges. + +![Status Page](screenshots/fan_status.png) + +### Settings Page + +Configure fan control mode and customize fan curves with visual preview. + +![Settings Page](screenshots/fan_setting.png) + +## Installation + +### From OpenWrt Feeds + +Add this package to your OpenWrt build: + +```bash +# Copy to feeds/luci/applications/ +cp -r luci-app-w1700k-fancontrol feeds/luci/applications/ + +# Update and install +./scripts/feeds update luci +./scripts/feeds install luci-app-w1700k-fancontrol + +# Enable in menuconfig +make menuconfig +# Navigate to: LuCI -> Applications -> luci-app-w1700k-fancontrol + +# Build +make package/feeds/luci/luci-app-w1700k-fancontrol/compile +``` + +### Manual Installation + +Copy files directly to your router: + +```bash +# Create directories +mkdir -p /usr/share/luci/menu.d +mkdir -p /usr/share/rpcd/acl.d +mkdir -p /usr/libexec/rpcd +mkdir -p /www/luci-static/resources/view/fan +mkdir -p /etc/init.d +mkdir -p /etc/config + +# Copy files +cp root/usr/share/luci/menu.d/luci-app-w1700k-fancontrol.json /usr/share/luci/menu.d/ +cp root/usr/share/rpcd/acl.d/luci-app-w1700k-fancontrol.json /usr/share/rpcd/acl.d/ +cp root/usr/libexec/rpcd/luci.fan /usr/libexec/rpcd/ +cp htdocs/luci-static/resources/view/fan/status.js /www/luci-static/resources/view/fan/ +cp htdocs/luci-static/resources/view/fan/settings.js /www/luci-static/resources/view/fan/ +cp root/etc/init.d/fan /etc/init.d/ +cp root/etc/config/fan /etc/config/ + +# Set permissions +chmod +x /usr/libexec/rpcd/luci.fan +chmod +x /etc/init.d/fan + +# Enable and start service +/etc/init.d/fan enable +/etc/init.d/fan start +/etc/init.d/rpcd restart +``` + +## Requirements + +- OpenWrt with LuCI +- Airoha AN7581 target (`@TARGET_airoha`) +- NCT7802Y fan controller (hwmon5) +- MT7996 WiFi (for radio temperature monitoring) + +## Files + +``` +luci-app-w1700k-fancontrol/ +├── Makefile # OpenWrt package makefile +├── LICENSE # Apache 2.0 license +├── README.md # This file +├── screenshots/ +│ ├── fan_status.png # Status page screenshot +│ └── fan_setting.png # Settings page screenshot +├── htdocs/ +│ └── luci-static/resources/view/fan/ +│ ├── status.js # Status page with gauges +│ └── settings.js # Settings page with curve editor +└── root/ + ├── etc/ + │ ├── config/ + │ │ └── fan # UCI configuration with presets + │ └── init.d/ + │ └── fan # Init script for applying settings + └── usr/ + ├── libexec/rpcd/ + │ └── luci.fan # RPC backend script + └── share/ + ├── luci/menu.d/ + │ └── luci-app-w1700k-fancontrol.json # Menu configuration + └── rpcd/acl.d/ + └── luci-app-w1700k-fancontrol.json # ACL permissions +``` + +## Configuration + +The app uses UCI configuration at `/etc/config/fan`: + +``` +config fancontrol 'settings' + option mode 'auto' # 'auto' or 'manual' + option manual_pwm '127' # PWM value for manual mode (0-255) + option curve_preset 'balanced' # 'quiet', 'balanced', 'performance', 'custom' + +config curve 'quiet' + option point1_temp '45' + option point1_pwm '54' + # ... (5 points total) + +config curve 'balanced' + # ... default preset + +config curve 'performance' + # ... aggressive cooling + +config curve 'custom' + # ... user-editable curve +``` + +## Data Sources + +| Sensor | Source | Description | +|--------|--------|-------------| +| CPU | `/sys/class/thermal/thermal_zone0/temp` | AN7581 SoC die temperature | +| Board | `/sys/class/hwmon/hwmon5/temp1_input` | NCT7802Y local sensor (fan curve input) | +| 10G PHY 1 | `/sys/class/hwmon/hwmon0/temp1_input` | 10 Gigabit Ethernet PHY (lan2, RTL826x) | +| 10G PHY 2 | `/sys/class/hwmon/hwmon1/temp1_input` | 10 Gigabit Ethernet PHY (wan, RTL826x) | +| WiFi 2.4G | `/sys/class/hwmon/hwmon2/temp1_input` | MT7996 2.4 GHz radio | +| WiFi 5G | `/sys/class/hwmon/hwmon3/temp1_input` | MT7996 5 GHz radio | +| WiFi 6G | `/sys/class/hwmon/hwmon4/temp1_input` | MT7996 6 GHz radio | +| Fan RPM | `/sys/class/hwmon/hwmon5/fan1_input` | NCT7802Y tachometer input | +| Fan PWM | `/sys/class/hwmon/hwmon5/pwm1` | NCT7802Y PWM output (0-255) | + +## Fan Curve Details + +The NCT7802Y supports hardware-based automatic fan control with a 5-point temperature curve. PWM values were reverse-engineered from stock firmware: + +| RPM | PWM | +|-----|-----| +| 770 | 54 | +| 1000 | 69 | +| 1400 | 95 | +| 2700 | 199 | +| Max | 255 | + +## License + +Apache-2.0 + +## Author + +Created for W1700K router (Airoha AN7581 + MT7996 BE19000) diff --git a/package/luci-app-w1700k-fancontrol/htdocs/luci-static/resources/view/fan/settings.js b/package/luci-app-w1700k-fancontrol/htdocs/luci-static/resources/view/fan/settings.js new file mode 100644 index 00000000000000..cbea96e8d490a7 --- /dev/null +++ b/package/luci-app-w1700k-fancontrol/htdocs/luci-static/resources/view/fan/settings.js @@ -0,0 +1,227 @@ +'use strict'; +'require view'; +'require form'; +'require uci'; +'require rpc'; + +var callGetAllCurves = rpc.declare({ + object: 'luci.fan', + method: 'getAllCurves' +}); + +function drawCurveCanvas(canvasId, curves, activePreset) { + var canvas = document.getElementById(canvasId); + if (!canvas) return; + + var ctx = canvas.getContext('2d'); + var width = canvas.width; + var height = canvas.height; + var padding = 40; + + // Clear canvas + ctx.fillStyle = '#fff'; + ctx.fillRect(0, 0, width, height); + + // Draw grid + ctx.strokeStyle = '#e0e0e0'; + ctx.lineWidth = 1; + + for (var t = 0; t <= 100; t += 10) { + var x = padding + (t / 100) * (width - 2 * padding); + ctx.beginPath(); + ctx.moveTo(x, padding); + ctx.lineTo(x, height - padding); + ctx.stroke(); + } + + for (var p = 0; p <= 255; p += 51) { + var y = height - padding - (p / 255) * (height - 2 * padding); + ctx.beginPath(); + ctx.moveTo(padding, y); + ctx.lineTo(width - padding, y); + ctx.stroke(); + } + + // Draw axes + ctx.strokeStyle = '#333'; + ctx.lineWidth = 2; + ctx.beginPath(); + ctx.moveTo(padding, padding); + ctx.lineTo(padding, height - padding); + ctx.lineTo(width - padding, height - padding); + ctx.stroke(); + + // Axis labels + ctx.fillStyle = '#333'; + ctx.font = '12px sans-serif'; + ctx.textAlign = 'center'; + ctx.fillText('Temperature (\u00B0C)', width / 2, height - 5); + + ctx.save(); + ctx.translate(12, height / 2); + ctx.rotate(-Math.PI / 2); + ctx.fillText('PWM (0-255)', 0, 0); + ctx.restore(); + + ctx.textAlign = 'center'; + for (var t = 0; t <= 100; t += 20) { + var x = padding + (t / 100) * (width - 2 * padding); + ctx.fillText(t.toString(), x, height - padding + 15); + } + + ctx.textAlign = 'right'; + for (var p = 0; p <= 255; p += 51) { + var y = height - padding - (p / 255) * (height - 2 * padding); + ctx.fillText(p.toString(), padding - 5, y + 4); + } + + // Draw curves + var colors = { + 'quiet': '#28a745', + 'balanced': '#007bff', + 'performance': '#dc3545', + 'custom': '#6f42c1' + }; + + Object.keys(curves).forEach(function(preset) { + var points = curves[preset]; + var color = colors[preset] || '#999'; + var isActive = preset === activePreset; + + ctx.strokeStyle = color; + ctx.lineWidth = isActive ? 3 : 1.5; + ctx.globalAlpha = isActive ? 1 : 0.4; + + ctx.beginPath(); + points.forEach(function(point, idx) { + var x = padding + (point.temp / 100) * (width - 2 * padding); + var y = height - padding - (point.pwm / 255) * (height - 2 * padding); + if (idx === 0) ctx.moveTo(x, y); + else ctx.lineTo(x, y); + }); + ctx.stroke(); + + if (isActive) { + ctx.fillStyle = color; + points.forEach(function(point) { + var x = padding + (point.temp / 100) * (width - 2 * padding); + var y = height - padding - (point.pwm / 255) * (height - 2 * padding); + ctx.beginPath(); + ctx.arc(x, y, 5, 0, 2 * Math.PI); + ctx.fill(); + }); + } + ctx.globalAlpha = 1; + }); + + // Legend + var legendY = 15; + Object.keys(colors).forEach(function(preset) { + ctx.fillStyle = colors[preset]; + ctx.fillRect(width - 100, legendY, 15, 15); + ctx.fillStyle = '#333'; + ctx.textAlign = 'left'; + ctx.fillText(preset.charAt(0).toUpperCase() + preset.slice(1), width - 80, legendY + 12); + legendY += 20; + }); +} + +return view.extend({ + load: function() { + return Promise.all([ + uci.load('fan'), + callGetAllCurves() + ]); + }, + + render: function(data) { + var curves = data[1] || {}; + var m, s, o; + + m = new form.Map('fan', _('Fan Control - Settings'), + _('Configure fan control mode and speed curves.')); + + // Control Mode Section + s = m.section(form.NamedSection, 'settings', 'fancontrol', _('Control Mode')); + s.anonymous = true; + + o = s.option(form.ListValue, 'mode', _('Mode')); + o.value('auto', _('Automatic (Follow Curve)')); + o.value('manual', _('Manual (Fixed Speed)')); + o.default = 'auto'; + + // Manual PWM (only shown when mode is manual) + o = s.option(form.Value, 'manual_pwm', _('Manual Fan Speed (PWM)'), + _('Set a fixed PWM value (0-255). 0 = Off, 255 = Full Speed')); + o.datatype = 'range(0,255)'; + o.default = '127'; + o.depends('mode', 'manual'); + o.rmempty = false; + + // Curve Preset (only shown when mode is auto) + o = s.option(form.ListValue, 'curve_preset', _('Fan Curve Preset')); + o.value('quiet', _('Quiet - Lower speeds, higher temps')); + o.value('balanced', _('Balanced - Good mix of noise and cooling')); + o.value('performance', _('Performance - Higher speeds, lower temps')); + o.value('custom', _('Custom - Define your own curve')); + o.default = 'balanced'; + o.depends('mode', 'auto'); + + // Curve visualization + o = s.option(form.DummyValue, '_curve_graph', _('Curve Preview')); + o.depends('mode', 'auto'); + o.rawhtml = true; + o.cfgvalue = function() { + return ''; + }; + + // Custom Curve Section - only visible when mode=auto AND preset=custom + s = m.section(form.NamedSection, 'custom', 'curve', _('Custom Curve Editor'), + _('Define temperature thresholds and corresponding fan speeds.')); + s.anonymous = true; + s.addremove = false; + + // Add dependency - only show when curve_preset is 'custom' + // This section depends on settings in another section, so we use a custom render check + s.render = function() { + var mode = uci.get('fan', 'settings', 'mode'); + var preset = uci.get('fan', 'settings', 'curve_preset'); + if (mode !== 'auto' || preset !== 'custom') { + return E('div'); + } + return form.NamedSection.prototype.render.apply(this, arguments); + }; + + s.tab('points', _('Curve Points')); + + for (var i = 1; i <= 5; i++) { + o = s.taboption('points', form.Value, 'point' + i + '_temp', _('Point %d Temperature (\u00B0C)').format(i)); + o.datatype = 'range(0,100)'; + o.rmempty = false; + + o = s.taboption('points', form.Value, 'point' + i + '_pwm', _('Point %d PWM (0-255)').format(i)); + o.datatype = 'range(0,255)'; + o.rmempty = false; + if (i === 5) { + o.description = _('NCT7802 hardware fixes point 5 at full speed (255) — this value is stored but not written to the fan controller.'); + } + } + + // Draw curve after render + return m.render().then(function(node) { + requestAnimationFrame(function() { + var preset = uci.get('fan', 'settings', 'curve_preset') || 'balanced'; + drawCurveCanvas('curve-canvas', curves, preset); + + // Update curve when preset changes + var presetSelect = node.querySelector('[data-name="curve_preset"] select'); + if (presetSelect) { + presetSelect.addEventListener('change', function(ev) { + drawCurveCanvas('curve-canvas', curves, ev.target.value); + }); + } + }); + return node; + }); + } +}); diff --git a/package/luci-app-w1700k-fancontrol/htdocs/luci-static/resources/view/fan/status.js b/package/luci-app-w1700k-fancontrol/htdocs/luci-static/resources/view/fan/status.js new file mode 100644 index 00000000000000..a4441ebc4d5128 --- /dev/null +++ b/package/luci-app-w1700k-fancontrol/htdocs/luci-static/resources/view/fan/status.js @@ -0,0 +1,187 @@ +'use strict'; +'require view'; +'require poll'; +'require rpc'; +'require ui'; + +var callFanStatus = rpc.declare({ + object: 'luci.fan', + method: 'getStatus' +}); + +function tempColor(temp) { + if (temp <= 40) return '#28a745'; // Green - cool + if (temp <= 55) return '#ffc107'; // Yellow - warm + if (temp <= 70) return '#fd7e14'; // Orange - hot + return '#dc3545'; // Red - critical +} + +function createTempGauge(label, temp, id) { + var color = tempColor(temp); + var percentage = Math.min(100, Math.max(0, (temp / 100) * 100)); + + return E('div', { 'class': 'cbi-value', 'style': 'margin-bottom: 10px;' }, [ + E('label', { 'class': 'cbi-value-title', 'style': 'width: 150px;' }, label), + E('div', { 'class': 'cbi-value-field' }, [ + E('div', { 'style': 'display: flex; align-items: center; gap: 10px;' }, [ + E('div', { + 'style': 'width: 200px; height: 20px; background: #e9ecef; border-radius: 4px; overflow: hidden;' + }, [ + E('div', { + 'id': id + '-bar', + 'style': 'width: ' + percentage + '%; height: 100%; background: ' + color + '; transition: width 0.3s, background 0.3s;' + }) + ]), + E('span', { 'id': id + '-value', 'style': 'font-weight: bold; min-width: 50px;' }, temp + '\u00B0C') + ]) + ]) + ]); +} + +function createFanGauge(rpm, pwm, percentage) { + return E('div', { 'class': 'cbi-value', 'style': 'margin-bottom: 10px;' }, [ + E('label', { 'class': 'cbi-value-title', 'style': 'width: 150px;' }, _('Fan Speed')), + E('div', { 'class': 'cbi-value-field' }, [ + E('div', { 'style': 'display: flex; align-items: center; gap: 10px;' }, [ + E('div', { + 'style': 'width: 200px; height: 20px; background: #e9ecef; border-radius: 4px; overflow: hidden;' + }, [ + E('div', { + 'id': 'fan-bar', + 'style': 'width: ' + percentage + '%; height: 100%; background: #17a2b8; transition: width 0.3s;' + }) + ]), + E('span', { 'id': 'fan-value', 'style': 'font-weight: bold;' }, + rpm + ' RPM (' + percentage + '%) PWM: ' + pwm) + ]) + ]) + ]); +} + +function updateGauge(id, temp) { + var bar = document.getElementById(id + '-bar'); + var value = document.getElementById(id + '-value'); + if (bar && value) { + var color = tempColor(temp); + var percentage = Math.min(100, Math.max(0, (temp / 100) * 100)); + bar.style.width = percentage + '%'; + bar.style.background = color; + value.textContent = temp + '\u00B0C'; + } +} + +function updateFanGauge(rpm, pwm, percentage) { + var bar = document.getElementById('fan-bar'); + var value = document.getElementById('fan-value'); + if (bar && value) { + bar.style.width = percentage + '%'; + value.textContent = rpm + ' RPM (' + percentage + '%) PWM: ' + pwm; + } +} + +return view.extend({ + load: function() { + return callFanStatus(); + }, + + render: function(status) { + status = status || {}; + + var modeClass = status.fan_mode === 2 ? 'label-success' : 'label-warning'; + var modeText = status.fan_mode_desc || 'Unknown'; + + var viewEl = E('div', { 'class': 'cbi-map' }, [ + E('h2', {}, _('Fan Control - Status')), + + // Fan Status Section (on top) + E('div', { 'class': 'cbi-section' }, [ + E('h3', {}, _('Fan Status')), + E('div', { 'class': 'cbi-section-node' }, [ + createFanGauge(status.fan_rpm || 0, status.fan_pwm || 0, status.fan_percentage || 0), + E('div', { 'class': 'cbi-value' }, [ + E('label', { 'class': 'cbi-value-title', 'style': 'width: 150px;' }, _('Control Mode')), + E('div', { 'class': 'cbi-value-field' }, [ + E('span', { 'id': 'fan-mode', 'class': modeClass }, modeText) + ]) + ]), + E('div', { 'class': 'cbi-value' }, [ + E('label', { 'class': 'cbi-value-title', 'style': 'width: 150px;' }, _('Active Preset')), + E('div', { 'class': 'cbi-value-field' }, [ + E('span', { 'id': 'fan-preset' }, + status.uci_mode === 'manual' ? _('Manual Override') : + (status.uci_preset || 'balanced').charAt(0).toUpperCase() + + (status.uci_preset || 'balanced').slice(1)) + ]) + ]) + ]) + ]), + + // Temperatures Section (System and WiFi side by side) + E('div', { 'class': 'cbi-section' }, [ + E('h3', {}, _('Temperatures')), + E('div', { 'style': 'display: flex; flex-wrap: wrap; gap: 20px;' }, [ + // System Temperatures + E('div', { 'style': 'flex: 1; min-width: 300px;' }, [ + E('h4', { 'style': 'margin: 0 0 10px 0; color: #666;' }, _('System')), + E('div', { 'class': 'cbi-section-node' }, [ + createTempGauge(_('CPU'), status.temp_cpu || 0, 'temp-cpu'), + createTempGauge(_('Board (Fan Curve)'), status.temp_board || 0, 'temp-board'), + createTempGauge(_('10G PHY 1'), status.temp_phy1 || 0, 'temp-phy1'), + createTempGauge(_('10G PHY 2'), status.temp_phy2 || 0, 'temp-phy2') + ]) + ]), + // WiFi Temperatures + E('div', { 'style': 'flex: 1; min-width: 300px;', 'id': 'wifi-temps-section' }, [ + E('h4', { 'style': 'margin: 0 0 10px 0; color: #666;' }, _('WiFi')), + E('div', { 'class': 'cbi-section-node' }, [ + createTempGauge(_('2.4 GHz Radio'), status.wifi_24g || 0, 'temp-wifi24g'), + createTempGauge(_('5 GHz Radio'), status.wifi_5g || 0, 'temp-wifi5g'), + createTempGauge(_('6 GHz Radio'), status.wifi_6g || 0, 'temp-wifi6g') + ]) + ]) + ]) + ]) + ]); + + // Setup polling for live updates + poll.add(L.bind(function() { + return callFanStatus().then(L.bind(function(status) { + status = status || {}; + + // Update temperature gauges + updateGauge('temp-cpu', status.temp_cpu || 0); + updateGauge('temp-board', status.temp_board || 0); + updateGauge('temp-phy1', status.temp_phy1 || 0); + updateGauge('temp-phy2', status.temp_phy2 || 0); + + updateGauge('temp-wifi24g', status.wifi_24g || 0); + updateGauge('temp-wifi5g', status.wifi_5g || 0); + updateGauge('temp-wifi6g', status.wifi_6g || 0); + + // Update fan gauge + updateFanGauge(status.fan_rpm || 0, status.fan_pwm || 0, status.fan_percentage || 0); + + // Update mode display + var modeEl = document.getElementById('fan-mode'); + if (modeEl) { + modeEl.textContent = status.fan_mode_desc || 'Unknown'; + modeEl.className = status.fan_mode === 2 ? 'label-success' : 'label-warning'; + } + + // Update preset display + var presetEl = document.getElementById('fan-preset'); + if (presetEl) { + presetEl.textContent = status.uci_mode === 'manual' ? _('Manual Override') : + (status.uci_preset || 'balanced').charAt(0).toUpperCase() + + (status.uci_preset || 'balanced').slice(1); + } + }, this)); + }, this), 3); + + return viewEl; + }, + + handleSaveApply: null, + handleSave: null, + handleReset: null +}); diff --git a/package/luci-app-w1700k-fancontrol/po/es/luci-app-w1700k-fancontrol.po b/package/luci-app-w1700k-fancontrol/po/es/luci-app-w1700k-fancontrol.po new file mode 100644 index 00000000000000..d77a4459b05423 --- /dev/null +++ b/package/luci-app-w1700k-fancontrol/po/es/luci-app-w1700k-fancontrol.po @@ -0,0 +1,166 @@ +msgid "" +msgstr "" +"Project-Id-Version: \n" +"POT-Creation-Date: \n" +"PO-Revision-Date: \n" +"Last-Translator: \n" +"Language-Team: \n" +"Language: es\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"X-Generator: Poedit 3.8\n" + +#: feeds/luci/applications/luci-app-w1700k-fancontrol/htdocs/luci-static/resources/view/fan/status.js:129 +msgid "10G PHY 1" +msgstr "PHY de 10G 1" + +#: feeds/luci/applications/luci-app-w1700k-fancontrol/htdocs/luci-static/resources/view/fan/status.js:137 +msgid "2.4 GHz Radio" +msgstr "Radio de 2,4 GHz" + +#: feeds/luci/applications/luci-app-w1700k-fancontrol/htdocs/luci-static/resources/view/fan/status.js:138 +msgid "5 GHz Radio" +msgstr "Radio de 5 GHz" + +#: feeds/luci/applications/luci-app-w1700k-fancontrol/htdocs/luci-static/resources/view/fan/status.js:139 +msgid "6 GHz Radio" +msgstr "Radio de 6 GHz" + +#: feeds/luci/applications/luci-app-w1700k-fancontrol/htdocs/luci-static/resources/view/fan/status.js:108 +msgid "Active Preset" +msgstr "Preajuste activo" + +#: feeds/luci/applications/luci-app-w1700k-fancontrol/htdocs/luci-static/resources/view/fan/settings.js:149 +msgid "Automatic (Follow Curve)" +msgstr "Automático (seguir la curva)" + +#: feeds/luci/applications/luci-app-w1700k-fancontrol/htdocs/luci-static/resources/view/fan/settings.js:164 +msgid "Balanced - Good mix of noise and cooling" +msgstr "Equilibrado - Buena mezcla de ruido y enfriamiento" + +#: feeds/luci/applications/luci-app-w1700k-fancontrol/htdocs/luci-static/resources/view/fan/status.js:128 +msgid "Board (Fan Curve)" +msgstr "Placa (curva del ventilador)" + +#: feeds/luci/applications/luci-app-w1700k-fancontrol/htdocs/luci-static/resources/view/fan/status.js:127 +msgid "CPU" +msgstr "CPU" + +#: feeds/luci/applications/luci-app-w1700k-fancontrol/htdocs/luci-static/resources/view/fan/settings.js:142 +msgid "Configure fan control mode and speed curves." +msgstr "Configura el modo de control del ventilador y las curvas de velocidad." + +#: feeds/luci/applications/luci-app-w1700k-fancontrol/htdocs/luci-static/resources/view/fan/settings.js:145 +#: feeds/luci/applications/luci-app-w1700k-fancontrol/htdocs/luci-static/resources/view/fan/status.js:102 +msgid "Control Mode" +msgstr "Modo de control" + +#: feeds/luci/applications/luci-app-w1700k-fancontrol/htdocs/luci-static/resources/view/fan/settings.js:195 +msgid "Curve Points" +msgstr "Puntos de curva" + +#: feeds/luci/applications/luci-app-w1700k-fancontrol/htdocs/luci-static/resources/view/fan/settings.js:171 +msgid "Curve Preview" +msgstr "Vista previa de la curva" + +#: feeds/luci/applications/luci-app-w1700k-fancontrol/htdocs/luci-static/resources/view/fan/settings.js:166 +msgid "Custom - Define your own curve" +msgstr "Personalizado: defina su propia curva" + +#: feeds/luci/applications/luci-app-w1700k-fancontrol/htdocs/luci-static/resources/view/fan/settings.js:179 +msgid "Custom Curve Editor" +msgstr "Editor de curvas personalizado" + +#: feeds/luci/applications/luci-app-w1700k-fancontrol/htdocs/luci-static/resources/view/fan/settings.js:180 +msgid "Define temperature thresholds and corresponding fan speeds." +msgstr "Definir umbrales de temperatura y velocidades de ventilador correspondientes." + +#: feeds/luci/applications/luci-app-w1700k-fancontrol/root/usr/share/luci/menu.d/luci-app-w1700k-fancontrol.json:3 +msgid "Fan Control" +msgstr "Control del ventilador" + +#: feeds/luci/applications/luci-app-w1700k-fancontrol/htdocs/luci-static/resources/view/fan/settings.js:141 +msgid "Fan Control - Settings" +msgstr "Control del ventilador - Configuración" + +#: feeds/luci/applications/luci-app-w1700k-fancontrol/htdocs/luci-static/resources/view/fan/status.js:94 +msgid "Fan Control - Status" +msgstr "Control del ventilador - Estado" + +#: feeds/luci/applications/luci-app-w1700k-fancontrol/htdocs/luci-static/resources/view/fan/settings.js:162 +msgid "Fan Curve Preset" +msgstr "Ajuste preestablecido de curva del ventilador" + +#: feeds/luci/applications/luci-app-w1700k-fancontrol/htdocs/luci-static/resources/view/fan/status.js:43 +msgid "Fan Speed" +msgstr "Velocidad del ventilador" + +#: feeds/luci/applications/luci-app-w1700k-fancontrol/htdocs/luci-static/resources/view/fan/status.js:98 +msgid "Fan Status" +msgstr "Estado del ventilador" + +#: feeds/luci/applications/luci-app-w1700k-fancontrol/root/usr/share/rpcd/acl.d/luci-app-w1700k-fancontrol.json:3 +msgid "Grant access to W1700K fan control" +msgstr "Otorgar acceso al control del ventilador del W1700K" + +#: feeds/luci/applications/luci-app-w1700k-fancontrol/htdocs/luci-static/resources/view/fan/settings.js:150 +msgid "Manual (Fixed Speed)" +msgstr "Manual (velocidad fija)" + +#: feeds/luci/applications/luci-app-w1700k-fancontrol/htdocs/luci-static/resources/view/fan/settings.js:154 +msgid "Manual Fan Speed (PWM)" +msgstr "Velocidad manual del ventilador (PWM)" + +#: feeds/luci/applications/luci-app-w1700k-fancontrol/htdocs/luci-static/resources/view/fan/status.js:111 +#: feeds/luci/applications/luci-app-w1700k-fancontrol/htdocs/luci-static/resources/view/fan/status.js:174 +msgid "Manual Override" +msgstr "Anulación manual" + +#: feeds/luci/applications/luci-app-w1700k-fancontrol/htdocs/luci-static/resources/view/fan/settings.js:148 +msgid "Mode" +msgstr "Modo" + +#: feeds/luci/applications/luci-app-w1700k-fancontrol/htdocs/luci-static/resources/view/fan/settings.js:165 +msgid "Performance - Higher speeds, lower temps" +msgstr "Rendimiento: mayores velocidades, temperaturas más bajas" + +#: feeds/luci/applications/luci-app-w1700k-fancontrol/htdocs/luci-static/resources/view/fan/settings.js:202 +msgid "Point %d PWM (0-255)" +msgstr "Punto %d de PWM (0-255)" + +#: feeds/luci/applications/luci-app-w1700k-fancontrol/htdocs/luci-static/resources/view/fan/settings.js:198 +msgid "Point %d Temperature (°C)" +msgstr "Punto %d de temperatura (°C)" + +#: feeds/luci/applications/luci-app-w1700k-fancontrol/htdocs/luci-static/resources/view/fan/settings.js:163 +msgid "Quiet - Lower speeds, higher temps" +msgstr "Tranquilo - velocidades más bajas, temperaturas más altas" + +#: feeds/luci/applications/luci-app-w1700k-fancontrol/htdocs/luci-static/resources/view/fan/settings.js:155 +msgid "Set a fixed PWM value (0-255). 0 = Off, 255 = Full Speed" +msgstr "Establezca un valor PWM fijo (0-255). 0 = Apagado, 255 = Velocidad máxima" + +#: feeds/luci/applications/luci-app-w1700k-fancontrol/root/usr/share/luci/menu.d/luci-app-w1700k-fancontrol.json:22 +msgid "Settings" +msgstr "Configuraciones" + +#: feeds/luci/applications/luci-app-w1700k-fancontrol/root/usr/share/luci/menu.d/luci-app-w1700k-fancontrol.json:14 +msgid "Status" +msgstr "Estado" + +#: feeds/luci/applications/luci-app-w1700k-fancontrol/htdocs/luci-static/resources/view/fan/status.js:130 +msgid "10G PHY 2" +msgstr "PHY de 10G 2" + +#: feeds/luci/applications/luci-app-w1700k-fancontrol/htdocs/luci-static/resources/view/fan/status.js:125 +msgid "System" +msgstr "Sistema" + +#: feeds/luci/applications/luci-app-w1700k-fancontrol/htdocs/luci-static/resources/view/fan/status.js:121 +msgid "Temperatures" +msgstr "Temperaturas" + +#: feeds/luci/applications/luci-app-w1700k-fancontrol/htdocs/luci-static/resources/view/fan/status.js:135 +msgid "WiFi" +msgstr "Wi-Fi" diff --git a/package/luci-app-w1700k-fancontrol/po/templates/luci-app-w1700k-fancontrol.pot b/package/luci-app-w1700k-fancontrol/po/templates/luci-app-w1700k-fancontrol.pot new file mode 100644 index 00000000000000..a1dd1e5b6a49dc --- /dev/null +++ b/package/luci-app-w1700k-fancontrol/po/templates/luci-app-w1700k-fancontrol.pot @@ -0,0 +1,160 @@ +msgid "" +msgstr "Content-Type: text/plain; charset=UTF-8" + +#: feeds/luci/applications/luci-app-w1700k-fancontrol/htdocs/luci-static/resources/view/fan/status.js:129 +msgid "10G PHY 1" +msgstr "" + +#: feeds/luci/applications/luci-app-w1700k-fancontrol/htdocs/luci-static/resources/view/fan/status.js:137 +msgid "2.4 GHz Radio" +msgstr "" + +#: feeds/luci/applications/luci-app-w1700k-fancontrol/htdocs/luci-static/resources/view/fan/status.js:138 +msgid "5 GHz Radio" +msgstr "" + +#: feeds/luci/applications/luci-app-w1700k-fancontrol/htdocs/luci-static/resources/view/fan/status.js:139 +msgid "6 GHz Radio" +msgstr "" + +#: feeds/luci/applications/luci-app-w1700k-fancontrol/htdocs/luci-static/resources/view/fan/status.js:108 +msgid "Active Preset" +msgstr "" + +#: feeds/luci/applications/luci-app-w1700k-fancontrol/htdocs/luci-static/resources/view/fan/settings.js:149 +msgid "Automatic (Follow Curve)" +msgstr "" + +#: feeds/luci/applications/luci-app-w1700k-fancontrol/htdocs/luci-static/resources/view/fan/settings.js:164 +msgid "Balanced - Good mix of noise and cooling" +msgstr "" + +#: feeds/luci/applications/luci-app-w1700k-fancontrol/htdocs/luci-static/resources/view/fan/status.js:128 +msgid "Board (Fan Curve)" +msgstr "" + +#: feeds/luci/applications/luci-app-w1700k-fancontrol/htdocs/luci-static/resources/view/fan/status.js:127 +msgid "CPU" +msgstr "" + +#: feeds/luci/applications/luci-app-w1700k-fancontrol/htdocs/luci-static/resources/view/fan/settings.js:142 +msgid "Configure fan control mode and speed curves." +msgstr "" + +#: feeds/luci/applications/luci-app-w1700k-fancontrol/htdocs/luci-static/resources/view/fan/settings.js:145 +#: feeds/luci/applications/luci-app-w1700k-fancontrol/htdocs/luci-static/resources/view/fan/status.js:102 +msgid "Control Mode" +msgstr "" + +#: feeds/luci/applications/luci-app-w1700k-fancontrol/htdocs/luci-static/resources/view/fan/settings.js:195 +msgid "Curve Points" +msgstr "" + +#: feeds/luci/applications/luci-app-w1700k-fancontrol/htdocs/luci-static/resources/view/fan/settings.js:171 +msgid "Curve Preview" +msgstr "" + +#: feeds/luci/applications/luci-app-w1700k-fancontrol/htdocs/luci-static/resources/view/fan/settings.js:166 +msgid "Custom - Define your own curve" +msgstr "" + +#: feeds/luci/applications/luci-app-w1700k-fancontrol/htdocs/luci-static/resources/view/fan/settings.js:179 +msgid "Custom Curve Editor" +msgstr "" + +#: feeds/luci/applications/luci-app-w1700k-fancontrol/htdocs/luci-static/resources/view/fan/settings.js:180 +msgid "Define temperature thresholds and corresponding fan speeds." +msgstr "" + +#: feeds/luci/applications/luci-app-w1700k-fancontrol/root/usr/share/luci/menu.d/luci-app-w1700k-fancontrol.json:3 +msgid "Fan Control" +msgstr "" + +#: feeds/luci/applications/luci-app-w1700k-fancontrol/htdocs/luci-static/resources/view/fan/settings.js:141 +msgid "Fan Control - Settings" +msgstr "" + +#: feeds/luci/applications/luci-app-w1700k-fancontrol/htdocs/luci-static/resources/view/fan/status.js:94 +msgid "Fan Control - Status" +msgstr "" + +#: feeds/luci/applications/luci-app-w1700k-fancontrol/htdocs/luci-static/resources/view/fan/settings.js:162 +msgid "Fan Curve Preset" +msgstr "" + +#: feeds/luci/applications/luci-app-w1700k-fancontrol/htdocs/luci-static/resources/view/fan/status.js:43 +msgid "Fan Speed" +msgstr "" + +#: feeds/luci/applications/luci-app-w1700k-fancontrol/htdocs/luci-static/resources/view/fan/status.js:98 +msgid "Fan Status" +msgstr "" + +#: feeds/luci/applications/luci-app-w1700k-fancontrol/root/usr/share/rpcd/acl.d/luci-app-w1700k-fancontrol.json:3 +msgid "Grant access to W1700K fan control" +msgstr "" + +#: feeds/luci/applications/luci-app-w1700k-fancontrol/htdocs/luci-static/resources/view/fan/settings.js:150 +msgid "Manual (Fixed Speed)" +msgstr "" + +#: feeds/luci/applications/luci-app-w1700k-fancontrol/htdocs/luci-static/resources/view/fan/settings.js:154 +msgid "Manual Fan Speed (PWM)" +msgstr "" + +#: feeds/luci/applications/luci-app-w1700k-fancontrol/htdocs/luci-static/resources/view/fan/status.js:111 +#: feeds/luci/applications/luci-app-w1700k-fancontrol/htdocs/luci-static/resources/view/fan/status.js:174 +msgid "Manual Override" +msgstr "" + +#: feeds/luci/applications/luci-app-w1700k-fancontrol/htdocs/luci-static/resources/view/fan/settings.js:148 +msgid "Mode" +msgstr "" + +#: feeds/luci/applications/luci-app-w1700k-fancontrol/htdocs/luci-static/resources/view/fan/settings.js:165 +msgid "Performance - Higher speeds, lower temps" +msgstr "" + +#: feeds/luci/applications/luci-app-w1700k-fancontrol/htdocs/luci-static/resources/view/fan/settings.js:202 +msgid "Point %d PWM (0-255)" +msgstr "" + +#: feeds/luci/applications/luci-app-w1700k-fancontrol/htdocs/luci-static/resources/view/fan/settings.js:198 +msgid "Point %d Temperature (°C)" +msgstr "" + +#: feeds/luci/applications/luci-app-w1700k-fancontrol/htdocs/luci-static/resources/view/fan/settings.js:163 +msgid "Quiet - Lower speeds, higher temps" +msgstr "" + +#: feeds/luci/applications/luci-app-w1700k-fancontrol/htdocs/luci-static/resources/view/fan/settings.js:155 +msgid "Set a fixed PWM value (0-255). 0 = Off, 255 = Full Speed" +msgstr "" + +#: feeds/luci/applications/luci-app-w1700k-fancontrol/root/usr/share/luci/menu.d/luci-app-w1700k-fancontrol.json:22 +msgid "Settings" +msgstr "" + +#: feeds/luci/applications/luci-app-w1700k-fancontrol/root/usr/share/luci/menu.d/luci-app-w1700k-fancontrol.json:14 +msgid "Status" +msgstr "" + +#: feeds/luci/applications/luci-app-w1700k-fancontrol/htdocs/luci-static/resources/view/fan/status.js:130 +msgid "10G PHY 2" +msgstr "" + +#: feeds/luci/applications/luci-app-w1700k-fancontrol/htdocs/luci-static/resources/view/fan/status.js:125 +msgid "System" +msgstr "" + +#: feeds/luci/applications/luci-app-w1700k-fancontrol/htdocs/luci-static/resources/view/fan/status.js:121 +msgid "Temperatures" +msgstr "" + +#: feeds/luci/applications/luci-app-w1700k-fancontrol/htdocs/luci-static/resources/view/fan/status.js:135 +msgid "WiFi" +msgstr "" + +#: feeds/luci/applications/luci-app-w1700k-fancontrol/htdocs/luci-static/resources/view/fan/settings.js:206 +msgid "NCT7802 hardware fixes point 5 at full speed (255) — this value is stored but not written to the fan controller." +msgstr "" diff --git a/package/luci-app-w1700k-fancontrol/po/zh_Hans/luci-app-w1700k-fancontrol.po b/package/luci-app-w1700k-fancontrol/po/zh_Hans/luci-app-w1700k-fancontrol.po new file mode 100644 index 00000000000000..d939af954e6bba --- /dev/null +++ b/package/luci-app-w1700k-fancontrol/po/zh_Hans/luci-app-w1700k-fancontrol.po @@ -0,0 +1,171 @@ +msgid "" +msgstr "" +"Project-Id-Version: \n" +"POT-Creation-Date: \n" +"PO-Revision-Date: 2026-07-08 23:00+0800\n" +"Last-Translator: \n" +"Language-Team: \n" +"Language: zh_Hans\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=1; plural=0;\n" +"X-Generator: manual\n" + +#: htdocs/luci-static/resources/view/fan/status.js:129 +msgid "10G PHY 1" +msgstr "10G PHY 1" + +#: htdocs/luci-static/resources/view/fan/status.js:137 +msgid "2.4 GHz Radio" +msgstr "2.4GHz 射频" + +#: htdocs/luci-static/resources/view/fan/status.js:138 +msgid "5 GHz Radio" +msgstr "5GHz 射频" + +#: htdocs/luci-static/resources/view/fan/status.js:139 +msgid "6 GHz Radio" +msgstr "6GHz 射频" + +#: htdocs/luci-static/resources/view/fan/status.js:108 +msgid "Active Preset" +msgstr "当前预设" + +#: htdocs/luci-static/resources/view/fan/settings.js:149 +msgid "Automatic (Follow Curve)" +msgstr "自动(跟随曲线)" + +#: htdocs/luci-static/resources/view/fan/settings.js:164 +msgid "Balanced - Good mix of noise and cooling" +msgstr "均衡 — 噪音与散热平衡" + +#: htdocs/luci-static/resources/view/fan/status.js:128 +msgid "Board (Fan Curve)" +msgstr "主板(风扇曲线)" + +#: htdocs/luci-static/resources/view/fan/status.js:127 +msgid "CPU" +msgstr "CPU" + +#: htdocs/luci-static/resources/view/fan/settings.js:142 +msgid "Configure fan control mode and speed curves." +msgstr "配置风扇控制模式和转速曲线。" + +#: htdocs/luci-static/resources/view/fan/settings.js:145 +#: htdocs/luci-static/resources/view/fan/status.js:102 +msgid "Control Mode" +msgstr "控制模式" + +#: htdocs/luci-static/resources/view/fan/settings.js:195 +msgid "Curve Points" +msgstr "曲线节点" + +#: htdocs/luci-static/resources/view/fan/settings.js:171 +msgid "Curve Preview" +msgstr "曲线预览" + +#: htdocs/luci-static/resources/view/fan/settings.js:166 +msgid "Custom - Define your own curve" +msgstr "自定义 — 自行定义曲线" + +#: htdocs/luci-static/resources/view/fan/settings.js:179 +msgid "Custom Curve Editor" +msgstr "自定义曲线编辑器" + +#: htdocs/luci-static/resources/view/fan/settings.js:180 +msgid "Define temperature thresholds and corresponding fan speeds." +msgstr "定义温度阈值和对应的风扇转速。" + +#: root/usr/share/luci/menu.d/luci-app-w1700k-fancontrol.json:3 +msgid "Fan Control" +msgstr "风扇控制" + +#: htdocs/luci-static/resources/view/fan/settings.js:141 +msgid "Fan Control - Settings" +msgstr "风扇控制 — 设置" + +#: htdocs/luci-static/resources/view/fan/status.js:94 +msgid "Fan Control - Status" +msgstr "风扇控制 — 状态" + +#: htdocs/luci-static/resources/view/fan/settings.js:162 +msgid "Fan Curve Preset" +msgstr "风扇曲线预设" + +#: htdocs/luci-static/resources/view/fan/status.js:43 +msgid "Fan Speed" +msgstr "风扇转速" + +#: htdocs/luci-static/resources/view/fan/status.js:98 +msgid "Fan Status" +msgstr "风扇状态" + +#: root/usr/share/rpcd/acl.d/luci-app-w1700k-fancontrol.json:3 +msgid "Grant access to W1700K fan control" +msgstr "授予 W1700K 风扇控制访问权限" + +#: htdocs/luci-static/resources/view/fan/settings.js:150 +msgid "Manual (Fixed Speed)" +msgstr "手动(固定转速)" + +#: htdocs/luci-static/resources/view/fan/settings.js:154 +msgid "Manual Fan Speed (PWM)" +msgstr "手动风扇转速 (PWM)" + +#: htdocs/luci-static/resources/view/fan/status.js:111 +#: htdocs/luci-static/resources/view/fan/status.js:174 +msgid "Manual Override" +msgstr "手动覆盖" + +#: htdocs/luci-static/resources/view/fan/settings.js:148 +msgid "Mode" +msgstr "模式" + +#: htdocs/luci-static/resources/view/fan/settings.js:165 +msgid "Performance - Higher speeds, lower temps" +msgstr "性能 — 更高转速,更低温度" + +#: htdocs/luci-static/resources/view/fan/settings.js:202 +msgid "Point %d PWM (0-255)" +msgstr "节点 %d PWM (0-255)" + +#: htdocs/luci-static/resources/view/fan/settings.js:198 +msgid "Point %d Temperature (°C)" +msgstr "节点 %d 温度 (°C)" + +#: htdocs/luci-static/resources/view/fan/settings.js:163 +msgid "Quiet - Lower speeds, higher temps" +msgstr "静音 — 较低转速,较高温度" + +#: htdocs/luci-static/resources/view/fan/settings.js:155 +msgid "Set a fixed PWM value (0-255). 0 = Off, 255 = Full Speed" +msgstr "设置固定 PWM 值 (0-255)。0 = 关闭,255 = 全速" + +#: root/usr/share/luci/menu.d/luci-app-w1700k-fancontrol.json:22 +msgid "Settings" +msgstr "设置" + +#: root/usr/share/luci/menu.d/luci-app-w1700k-fancontrol.json:14 +msgid "Status" +msgstr "状态" + +#: htdocs/luci-static/resources/view/fan/status.js:130 +msgid "10G PHY 2" +msgstr "10G PHY 2" + +#: htdocs/luci-static/resources/view/fan/status.js:125 +msgid "System" +msgstr "系统" + +#: htdocs/luci-static/resources/view/fan/status.js:121 +msgid "Temperatures" +msgstr "温度" + +#: htdocs/luci-static/resources/view/fan/status.js:135 +msgid "WiFi" +msgstr "WiFi" + +#: htdocs/luci-static/resources/view/fan/settings.js:206 +msgid "NCT7802 hardware fixes point 5 at full speed (255) — this value is stored but not written to the fan controller." +msgstr "NCT7802 硬件将节点 5 固定为全速 (255) — 此值仅存储,不会写入风扇控制器。" diff --git a/package/luci-app-w1700k-fancontrol/root/etc/config/fan b/package/luci-app-w1700k-fancontrol/root/etc/config/fan new file mode 100644 index 00000000000000..d17069419889bc --- /dev/null +++ b/package/luci-app-w1700k-fancontrol/root/etc/config/fan @@ -0,0 +1,57 @@ +config fancontrol 'settings' + option mode 'auto' + option manual_pwm '127' + option curve_preset 'balanced' + +# Stock firmware equivalent - uses reverse-engineered PWM values +# RPM 770=PWM54, RPM1000=PWM69, RPM1400=PWM95, RPM2700=PWM199 +config curve 'quiet' + option point1_temp '45' + option point1_pwm '54' + option point2_temp '55' + option point2_pwm '69' + option point3_temp '65' + option point3_pwm '95' + option point4_temp '75' + option point4_pwm '199' + option point5_temp '85' + option point5_pwm '255' + +# Balanced - similar to stock but slightly more aggressive +config curve 'balanced' + option point1_temp '40' + option point1_pwm '54' + option point2_temp '50' + option point2_pwm '69' + option point3_temp '60' + option point3_pwm '95' + option point4_temp '70' + option point4_pwm '199' + option point5_temp '80' + option point5_pwm '255' + +# Performance - ramps up fan earlier for maximum cooling +config curve 'performance' + option point1_temp '35' + option point1_pwm '69' + option point2_temp '45' + option point2_pwm '95' + option point3_temp '55' + option point3_pwm '150' + option point4_temp '65' + option point4_pwm '199' + option point5_temp '75' + option point5_pwm '255' + +# Custom - user-editable, starts as balanced +config curve 'custom' + option point1_temp '40' + option point1_pwm '54' + option point2_temp '50' + option point2_pwm '69' + option point3_temp '60' + option point3_pwm '95' + option point4_temp '70' + option point4_pwm '199' + option point5_temp '80' + option point5_pwm '255' diff --git a/package/luci-app-w1700k-fancontrol/root/etc/init.d/fan b/package/luci-app-w1700k-fancontrol/root/etc/init.d/fan new file mode 100755 index 00000000000000..5ad82d894a798b --- /dev/null +++ b/package/luci-app-w1700k-fancontrol/root/etc/init.d/fan @@ -0,0 +1,77 @@ +#!/bin/sh /etc/rc.common + +START=99 +USE_PROCD=1 + +# Dynamically find NCT7802 fan controller hwmon device +find_nct7802() { + for hwmon in /sys/class/hwmon/hwmon*; do + if [ -f "$hwmon/name" ] && [ "$(cat "$hwmon/name" 2>/dev/null)" = "nct7802" ]; then + echo "$hwmon" + return + fi + done + echo "/sys/class/hwmon/hwmon5" # fallback +} + +HWMON=$(find_nct7802) + +apply_curve() { + local preset="$1" + local temp pwm + + # Read curve settings from UCI + # Note: point5_pwm is read-only (fixed at 255), so only set temp for point 5 + for i in 1 2 3 4 5; do + temp=$(uci -q get fan.${preset}.point${i}_temp) + pwm=$(uci -q get fan.${preset}.point${i}_pwm) + + if [ -n "$temp" ]; then + # Temperature in milli-celsius + echo $((temp * 1000)) > ${HWMON}/pwm1_auto_point${i}_temp 2>/dev/null + fi + + # PWM is writable only for points 1-4 + if [ -n "$pwm" ] && [ "$i" -lt 5 ]; then + echo "$pwm" > ${HWMON}/pwm1_auto_point${i}_pwm 2>/dev/null + fi + done +} + +apply_settings() { + local mode preset manual_pwm + + [ ! -d "$HWMON" ] && return 1 + + mode=$(uci -q get fan.settings.mode) + preset=$(uci -q get fan.settings.curve_preset) + manual_pwm=$(uci -q get fan.settings.manual_pwm) + + # Disable auto mode temporarily to write settings + echo 1 > ${HWMON}/pwm1_enable 2>/dev/null + + case "$mode" in + manual) + # Set manual PWM and keep in manual mode + [ -n "$manual_pwm" ] && echo "$manual_pwm" > ${HWMON}/pwm1 2>/dev/null + ;; + auto|*) + # Apply selected curve preset + [ -n "$preset" ] && apply_curve "$preset" + # Enable automatic mode + echo 2 > ${HWMON}/pwm1_enable 2>/dev/null + ;; + esac +} + +start_service() { + apply_settings +} + +reload_service() { + apply_settings +} + +service_triggers() { + procd_add_reload_trigger "fan" +} diff --git a/package/luci-app-w1700k-fancontrol/root/usr/libexec/rpcd/luci.fan b/package/luci-app-w1700k-fancontrol/root/usr/libexec/rpcd/luci.fan new file mode 100755 index 00000000000000..64faba25d136dc --- /dev/null +++ b/package/luci-app-w1700k-fancontrol/root/usr/libexec/rpcd/luci.fan @@ -0,0 +1,288 @@ +#!/bin/sh + +# W1700K Fan Control RPC backend for LuCI + +# Dynamically find NCT7802 fan controller hwmon device +find_nct7802() { + for hwmon in /sys/class/hwmon/hwmon*; do + if [ -f "$hwmon/name" ] && [ "$(cat "$hwmon/name" 2>/dev/null)" = "nct7802" ]; then + echo "$hwmon" + return + fi + done + echo "/sys/class/hwmon/hwmon5" # fallback +} + +# Dynamically find mt7996 WiFi hwmon devices +find_mt7996_hwmon() { + local band="$1" # 0, 1, or 2 + for hwmon in /sys/class/hwmon/hwmon*; do + if [ -f "$hwmon/name" ]; then + local name=$(cat "$hwmon/name" 2>/dev/null) + case "$name" in + mt7996_phy0."$band"|mt7996_phy0_"$band") + echo "$hwmon" + return + ;; + esac + fi + done + echo "" +} + +# Dynamically find PHY hwmon devices (mt7530 DSA — RTL8261N registers +# hwmon with NULL name, so match the device-symlink target instead). +find_phy_hwmon() { + local suffix="$1" # :05 or :08 + for hwmon in /sys/class/hwmon/hwmon*; do + local target=$(readlink -f "$hwmon/device" 2>/dev/null) + case "$target" in + *"$suffix") + echo "$hwmon" + return + ;; + esac + done + echo "" +} + +HWMON=$(find_nct7802) + +read_temp() { + local file="$1" + local temp=0 + if [ -f "$file" ]; then + temp=$(cat "$file" 2>/dev/null || echo 0) + # Convert from milli-celsius to celsius + temp=$((temp / 1000)) + fi + echo "$temp" +} + +read_value() { + local file="$1" + if [ -f "$file" ]; then + cat "$file" 2>/dev/null || echo 0 + else + echo 0 + fi +} + +get_status() { + local temp_cpu temp_board temp_phy1 temp_phy2 + local fan_rpm fan_pwm fan_mode fan_percentage + + # Read CPU temperature from thermal zone (AN7581 SoC die temp) + temp_cpu=$(read_temp "/sys/class/thermal/thermal_zone0/temp") + + # Read temperatures from NCT7802 fan controller (hwmon5) + # temp1 = board local (used by hardware fan curve), temp2 = external (disconnected), temp4 = external + temp_board=$(read_temp "${HWMON}/temp1_input") + + # Read PHY temperatures from mt7530 DSA switch sensors (dynamic lookup) + local phy1_hwmon=$(find_phy_hwmon ":05") + local phy2_hwmon=$(find_phy_hwmon ":08") + temp_phy1=$([ -n "$phy1_hwmon" ] && read_temp "$phy1_hwmon/temp1_input" || echo 0) + temp_phy2=$([ -n "$phy2_hwmon" ] && read_temp "$phy2_hwmon/temp1_input" || echo 0) + + # Read fan status + fan_rpm=$(read_value "${HWMON}/fan1_input") + fan_pwm=$(read_value "${HWMON}/pwm1") + fan_mode=$(read_value "${HWMON}/pwm1_enable") + + # Calculate percentage (0-255 -> 0-100) + fan_percentage=$((fan_pwm * 100 / 255)) + + # Get WiFi temperatures from mt7996 hwmon devices (dynamic lookup) + local wifi_24g=0 wifi_5g=0 wifi_6g=0 + local wifi_24g_hwmon=$(find_mt7996_hwmon 0) + local wifi_5g_hwmon=$(find_mt7996_hwmon 1) + local wifi_6g_hwmon=$(find_mt7996_hwmon 2) + + [ -n "$wifi_24g_hwmon" ] && wifi_24g=$(read_temp "$wifi_24g_hwmon/temp1_input") + [ -n "$wifi_5g_hwmon" ] && wifi_5g=$(read_temp "$wifi_5g_hwmon/temp1_input") + [ -n "$wifi_6g_hwmon" ] && wifi_6g=$(read_temp "$wifi_6g_hwmon/temp1_input") + + # Get current UCI settings + local uci_mode=$(uci -q get fan.settings.mode || echo "auto") + local uci_preset=$(uci -q get fan.settings.curve_preset || echo "balanced") + local uci_manual_pwm=$(uci -q get fan.settings.manual_pwm || echo "127") + + # Mode description + local mode_desc="Unknown" + case "$fan_mode" in + 0) mode_desc="Full Speed" ;; + 1) mode_desc="Manual" ;; + 2) mode_desc="Automatic" ;; + 3) mode_desc="Auto (Closed Loop)" ;; + esac + + printf '{"temp_cpu":%d,"temp_board":%d,"temp_phy1":%d,"temp_phy2":%d,"wifi_24g":%d,"wifi_5g":%d,"wifi_6g":%d,"fan_rpm":%d,"fan_pwm":%d,"fan_percentage":%d,"fan_mode":%d,"fan_mode_desc":"%s","uci_mode":"%s","uci_preset":"%s","uci_manual_pwm":%d}' \ + "$temp_cpu" "$temp_board" "$temp_phy1" "$temp_phy2" \ + "$wifi_24g" "$wifi_5g" "$wifi_6g" \ + "$fan_rpm" "$fan_pwm" "$fan_percentage" \ + "$fan_mode" "$mode_desc" \ + "$uci_mode" "$uci_preset" "$uci_manual_pwm" +} + +get_curve() { + local preset="$1" + [ -z "$preset" ] && preset="balanced" + + local points="[" + local first=1 + + for i in 1 2 3 4 5; do + local temp=$(uci -q get fan.${preset}.point${i}_temp || echo 0) + local pwm=$(uci -q get fan.${preset}.point${i}_pwm || echo 0) + + [ $first -eq 0 ] && points="${points}," + first=0 + points="${points}{\"temp\":${temp},\"pwm\":${pwm}}" + done + points="${points}]" + + printf '{"preset":"%s","points":%s}' "$preset" "$points" +} + +get_all_curves() { + local curves="{" + local first=1 + + for preset in quiet balanced performance custom; do + [ $first -eq 0 ] && curves="${curves}," + first=0 + + local points="[" + local pfirst=1 + for i in 1 2 3 4 5; do + local temp=$(uci -q get fan.${preset}.point${i}_temp || echo 0) + local pwm=$(uci -q get fan.${preset}.point${i}_pwm || echo 0) + + [ $pfirst -eq 0 ] && points="${points}," + pfirst=0 + points="${points}{\"temp\":${temp},\"pwm\":${pwm}}" + done + points="${points}]" + + curves="${curves}\"${preset}\":${points}" + done + curves="${curves}}" + + echo "$curves" +} + +set_mode() { + local mode="$1" + + case "$mode" in + manual|auto) + uci set fan.settings.mode="$mode" + uci commit fan + /etc/init.d/fan reload + echo '{"success":true}' + ;; + *) + echo '{"success":false,"error":"Invalid mode"}' + ;; + esac +} + +set_manual_pwm() { + local pwm="$1" + + # Validate PWM range + if [ "$pwm" -ge 0 ] && [ "$pwm" -le 255 ] 2>/dev/null; then + uci set fan.settings.manual_pwm="$pwm" + uci commit fan + /etc/init.d/fan reload + echo '{"success":true}' + else + echo '{"success":false,"error":"Invalid PWM value (0-255)"}' + fi +} + +set_preset() { + local preset="$1" + + case "$preset" in + quiet|balanced|performance|custom) + uci set fan.settings.curve_preset="$preset" + uci commit fan + /etc/init.d/fan reload + echo '{"success":true}' + ;; + *) + echo '{"success":false,"error":"Invalid preset"}' + ;; + esac +} + +set_custom_curve() { + # Read JSON from stdin + local json + read json + + # Parse points using jsonfilter if available + if command -v jsonfilter >/dev/null 2>&1; then + for i in 1 2 3 4 5; do + local idx=$((i - 1)) + local temp=$(echo "$json" | jsonfilter -e "@.points[${idx}].temp" 2>/dev/null) + local pwm=$(echo "$json" | jsonfilter -e "@.points[${idx}].pwm" 2>/dev/null) + + if [ -n "$temp" ] && [ -n "$pwm" ]; then + uci set fan.custom.point${i}_temp="$temp" + uci set fan.custom.point${i}_pwm="$pwm" + fi + done + uci set fan.settings.curve_preset="custom" + uci commit fan + /etc/init.d/fan reload + echo '{"success":true}' + else + echo '{"success":false,"error":"jsonfilter not available"}' + fi +} + +case "$1" in + list) + echo '{"getStatus":{},"getCurve":{"preset":"str"},"getAllCurves":{},"setMode":{"mode":"str"},"setManualPwm":{"pwm":"int"},"setPreset":{"preset":"str"},"setCustomCurve":{"points":"array"}}' + ;; + call) + case "$2" in + getStatus) + get_status + ;; + getCurve) + # Parse preset from JSON input + read input + preset=$(echo "$input" | jsonfilter -e '@.preset' 2>/dev/null || echo "balanced") + get_curve "$preset" + ;; + getAllCurves) + get_all_curves + ;; + setMode) + read input + mode=$(echo "$input" | jsonfilter -e '@.mode' 2>/dev/null) + set_mode "$mode" + ;; + setManualPwm) + read input + pwm=$(echo "$input" | jsonfilter -e '@.pwm' 2>/dev/null) + set_manual_pwm "$pwm" + ;; + setPreset) + read input + preset=$(echo "$input" | jsonfilter -e '@.preset' 2>/dev/null) + set_preset "$preset" + ;; + setCustomCurve) + set_custom_curve + ;; + *) + echo '{"error":"Invalid method"}' + ;; + esac + ;; +esac diff --git a/package/luci-app-w1700k-fancontrol/root/usr/share/luci/menu.d/luci-app-w1700k-fancontrol.json b/package/luci-app-w1700k-fancontrol/root/usr/share/luci/menu.d/luci-app-w1700k-fancontrol.json new file mode 100644 index 00000000000000..a6220b6be3745e --- /dev/null +++ b/package/luci-app-w1700k-fancontrol/root/usr/share/luci/menu.d/luci-app-w1700k-fancontrol.json @@ -0,0 +1,29 @@ +{ + "admin/system/fan": { + "title": "Fan Control", + "order": 90, + "action": { + "type": "firstchild" + }, + "depends": { + "acl": [ "luci-app-w1700k-fancontrol" ], + "uci": { "fan": true } + } + }, + "admin/system/fan/status": { + "title": "Status", + "order": 1, + "action": { + "type": "view", + "path": "fan/status" + } + }, + "admin/system/fan/settings": { + "title": "Settings", + "order": 2, + "action": { + "type": "view", + "path": "fan/settings" + } + } +} diff --git a/package/luci-app-w1700k-fancontrol/root/usr/share/rpcd/acl.d/luci-app-w1700k-fancontrol.json b/package/luci-app-w1700k-fancontrol/root/usr/share/rpcd/acl.d/luci-app-w1700k-fancontrol.json new file mode 100644 index 00000000000000..4555d283132c8d --- /dev/null +++ b/package/luci-app-w1700k-fancontrol/root/usr/share/rpcd/acl.d/luci-app-w1700k-fancontrol.json @@ -0,0 +1,20 @@ +{ + "luci-app-w1700k-fancontrol": { + "description": "Grant access to W1700K fan control", + "read": { + "ubus": { + "luci.fan": [ "getStatus", "getCurve", "getAllCurves" ] + }, + "uci": [ "fan" ], + "file": { + "/sys/class/hwmon/hwmon*/*": [ "read" ] + } + }, + "write": { + "ubus": { + "luci.fan": [ "setMode", "setManualPwm", "setPreset", "setCustomCurve" ] + }, + "uci": [ "fan" ] + } + } +} diff --git a/package/luci-app-w1700k-fancontrol/screenshots/fan_setting.png b/package/luci-app-w1700k-fancontrol/screenshots/fan_setting.png new file mode 100644 index 00000000000000..7a91f5132262df Binary files /dev/null and b/package/luci-app-w1700k-fancontrol/screenshots/fan_setting.png differ diff --git a/package/luci-app-w1700k-fancontrol/screenshots/fan_status.png b/package/luci-app-w1700k-fancontrol/screenshots/fan_status.png new file mode 100644 index 00000000000000..91c0e9fee507d9 Binary files /dev/null and b/package/luci-app-w1700k-fancontrol/screenshots/fan_status.png differ diff --git a/package/network/config/firewall4/patches/9990-fw4-resolve-bonding-lower-devices.patch b/package/network/config/firewall4/patches/9990-fw4-resolve-bonding-lower-devices.patch new file mode 100644 index 00000000000000..da190f2ca44eb1 --- /dev/null +++ b/package/network/config/firewall4/patches/9990-fw4-resolve-bonding-lower-devices.patch @@ -0,0 +1,33 @@ +From: Jihong Min +Date: Sun, 17 May 2026 20:15:00 +0900 +Subject: [PATCH] fw4: resolve bonding lower devices for flowtable offload + +fw4 resolves bridge and VLAN lower devices before creating the flowtable +device set. Treat bonding devices the same way, otherwise a bridge member +that is a bond master leaves bond0 in the hardware offload flowtable. That +prevents per-port offload drivers from seeing the real physical devices. + +Assisted-by: Codex:gpt-5.5 +Signed-off-by: Jihong Min +--- +--- a/root/usr/share/ucode/fw4.uc ++++ b/root/usr/share/ucode/fw4.uc +@@ -439,7 +439,18 @@ function resolve_lower_devices(devstatus + + break; + ++ case 'bonding': + default: ++ if (fs.access(`/sys/class/net/${devname}/bonding`)) { ++ let e; ++ ++ while ((e = dir.read()) != null) ++ if (index(e, "lower_") === 0) ++ push(devs, ...resolve_lower_devices(devstatus, substr(e, 6))); ++ ++ break; ++ } ++ + push(devs, devname); + + break; diff --git a/package/network/config/wifi-scripts/Makefile b/package/network/config/wifi-scripts/Makefile index e398bd58517f54..f9d36ce84f112d 100644 --- a/package/network/config/wifi-scripts/Makefile +++ b/package/network/config/wifi-scripts/Makefile @@ -20,7 +20,7 @@ include $(INCLUDE_DIR)/package.mk define Package/wifi-scripts SECTION:=utils CATEGORY:=Base system - DEPENDS:=+netifd +ucode +ucode-mod-nl80211 +ucode-mod-rtnl +ucode-mod-ubus +ucode-mod-uci +ucode-mod-digest + DEPENDS:=+netifd +ucode +ucode-mod-nl80211 +ucode-mod-rtnl +ucode-mod-ubus +ucode-mod-uci +ucode-mod-digest +ucode-mod-uloop TITLE:=Wi-Fi configuration scripts PKGARCH:=all endef diff --git a/package/network/config/wifi-scripts/files-ucode/lib/netifd/wireless/mac80211.sh b/package/network/config/wifi-scripts/files-ucode/lib/netifd/wireless/mac80211.sh index fa924cb1db18c6..b89357a1523268 100755 --- a/package/network/config/wifi-scripts/files-ucode/lib/netifd/wireless/mac80211.sh +++ b/package/network/config/wifi-scripts/files-ucode/lib/netifd/wireless/mac80211.sh @@ -11,9 +11,13 @@ import * as iface from 'wifi.iface'; import { find_phy } from 'wifi.utils'; import * as nl80211 from 'nl80211'; import * as fs from 'fs'; +import * as uloop from 'uloop'; global.radio = ARGV[2]; +const txpower_retry_delay = 10; +const txpower_retry_count = 12; + const mesh_param_list = [ "mesh_retry_timeout", "mesh_confirm_timeout", "mesh_holding_timeout", "mesh_max_peer_links", "mesh_max_retries", "mesh_ttl", "mesh_element_ttl", "mesh_hwmp_max_preq_retries", @@ -31,7 +35,20 @@ function phy_suffix(radio, sep) { return sep + radio; } +function txpower_token_file(phy, radio) { + return `/var/run/wifi-txpower-${phy}.${radio}`; +} + +function invalidate_radio_txpower_retry(phy, radio) { + if (radio == null || radio < 0) + return; + + fs.writefile(txpower_token_file(phy, radio), `${time()}:cancelled`); +} + function reset_config(phy, radio) { + invalidate_radio_txpower_retry(phy, radio); + let name = phy + phy_suffix(radio, "."); let prev_config = `/var/run/hostapd-${name}.conf`; @@ -106,7 +123,8 @@ function setup_phy(phy, config, data) { system(`iw phy ${phy} set antenna ${config.txantenna} ${config.rxantenna}`); } system(`iw phy ${phy} set distance ${config.distance}`); - system(`iw phy ${phy} set txpower ${config.txpower}`); + if (config.radio == null || config.radio < 0) + set_txpower([ 'iw', 'phy', phy, 'set', 'txpower' ], config.txpower); if (config.frag) system(`iw phy ${phy} set frag ${config.frag}`); @@ -114,6 +132,139 @@ function setup_phy(phy, config, data) { system(`iw phy ${phy} set rts ${config.rts}`); } +function set_txpower(cmd, txpower) { + for (let arg in split(txpower, ' ')) + push(cmd, arg); + + return system(cmd); +} + +function get_interface_info(ifname) { + return nl80211.request(nl80211.const.NL80211_CMD_GET_INTERFACE, 0, { dev: ifname }); +} + +function target_txpower(txpower) { + let val = split(txpower, ' '); + + if (val[0] != 'fixed') + return null; + + return +val[1]; +} + +function ap_interface_ready(ifname, info) { + if (info?.iftype != nl80211.const.NL80211_IFTYPE_AP) + return true; + + let carrier = trim(fs.readfile(`/sys/class/net/${ifname}/carrier`) ?? '', '\n'); + + return info.ssid != null && info.wiphy_freq != null && carrier == '1'; +} + +function txpower_configured(ifname, txpower, after_set) { + let info = get_interface_info(ifname); + + if (!info) + return false; + + if (!ap_interface_ready(ifname, info)) + return false; + + let target = target_txpower(txpower); + + return target != null ? info.wiphy_tx_power_level == target : after_set; +} + +function setup_radio_txpower(phy, config, ifnames, quiet) { + if (config.radio == null || config.radio < 0) + return; + + let configured = []; + let missing = []; + let pending = []; + let failed = []; + + for (let ifname in ifnames) { + if (!fs.access(`/sys/class/net/${ifname}`)) { + push(missing, ifname); + continue; + } + + if (txpower_configured(ifname, config.txpower, false)) { + push(configured, ifname); + continue; + } + + if (!quiet) + log(`Configuring '${phy}' radio ${config.radio} txpower on ${ifname}: ${config.txpower}`); + + if (set_txpower([ 'iw', 'dev', ifname, 'set', 'txpower' ], config.txpower)) { + push(failed, ifname); + continue; + } + + if (txpower_configured(ifname, config.txpower, true)) + push(configured, ifname); + else + push(pending, ifname); + } + + if (quiet) + return { configured, missing, pending, failed }; + + if (length(missing)) + log(`Pending txpower setup for '${phy}' radio ${config.radio} on missing interfaces: ${join(' ', missing)}`); + if (length(pending)) + log(`Pending txpower verification for '${phy}' radio ${config.radio} on interfaces: ${join(' ', pending)}`); + if (length(failed)) + log(`Failed txpower setup for '${phy}' radio ${config.radio} on interfaces: ${join(' ', failed)}`); + if (!length(configured) && !length(pending) && !length(failed)) + log(`No active interface found for '${phy}' radio ${config.radio}; skipping txpower setup`); + + return { configured, missing, pending, failed }; +} + +function pending_txpower_ifnames(status) { + let ifnames = []; + + for (let field in [ 'missing', 'pending', 'failed' ]) + for (let ifname in status[field]) + push(ifnames, ifname); + + return ifnames; +} + +function schedule_radio_txpower_retry(phy, config, ifnames) { + if (config.radio == null || config.radio < 0) + return; + + if (!length(ifnames)) + return; + + let token_file = txpower_token_file(phy, config.radio); + let token = `${time()}:${config.txpower}:${join(' ', ifnames)}`; + + fs.writefile(token_file, token); + + if (!uloop.task(() => { + let pending = ifnames; + + for (let i = 0; i < txpower_retry_count; i++) { + system([ '/bin/sleep', `${txpower_retry_delay}` ]); + + if (fs.readfile(token_file) != token) + return; + + pending = pending_txpower_ifnames(setup_radio_txpower(phy, config, pending, true)); + if (!length(pending)) + return; + } + + log(`Txpower setup for '${phy}' radio ${config.radio} still pending on interfaces: ${join(' ', pending)}`); + })) + log(`Failed to schedule txpower retry for '${phy}' radio ${config.radio}`); +} + function iw_htmode(config) { let suffix = substr(config.htmode, 3); if (suffix == "40+" || suffix == "40-") @@ -224,7 +375,7 @@ function setup() { if (v.config.encryption == 'owe' && v.config.owe_transition) { mode_idx = idx[mode]++; v.config.owe_transition_ifname = data.ifname_prefix + mode + mode_idx; - push(active_ifnames, v.config.ifname); + push(active_ifnames, v.config.owe_transition_ifname); } switch (mode) { @@ -312,8 +463,13 @@ function setup() { if (length(supplicant_data) > 0) supplicant.start(data); + let txpower_status = setup_radio_txpower(data.phy, data.config, active_ifnames); + let pending_txpower = pending_txpower_ifnames(txpower_status); + netifd.set_up(); + schedule_radio_txpower_retry(data.phy, data.config, pending_txpower); + return 0 } diff --git a/package/network/config/wifi-scripts/files-ucode/usr/share/schema/wireless.wifi-device.json b/package/network/config/wifi-scripts/files-ucode/usr/share/schema/wireless.wifi-device.json index f6dac167aa2d68..ba175cbf54eebf 100644 --- a/package/network/config/wifi-scripts/files-ucode/usr/share/schema/wireless.wifi-device.json +++ b/package/network/config/wifi-scripts/files-ucode/usr/share/schema/wireless.wifi-device.json @@ -354,6 +354,21 @@ "type": "boolean", "default": true }, + "eht_mu_beamformer": { + "description": "Whether EHT MU beamformer support is advertised.", + "type": "boolean", + "default": true + }, + "eht_su_beamformee": { + "description": "Whether EHT SU beamformee support is advertised.", + "type": "boolean", + "default": true + }, + "eht_su_beamformer": { + "description": "Whether EHT SU beamformer support is advertised.", + "type": "boolean", + "default": true + }, "hostapd_options": { "type": "array", "items": { diff --git a/package/network/config/wifi-scripts/files-ucode/usr/share/schema/wireless.wifi-iface.json b/package/network/config/wifi-scripts/files-ucode/usr/share/schema/wireless.wifi-iface.json index c32a49170cb010..25e151fd88b8ab 100644 --- a/package/network/config/wifi-scripts/files-ucode/usr/share/schema/wireless.wifi-iface.json +++ b/package/network/config/wifi-scripts/files-ucode/usr/share/schema/wireless.wifi-iface.json @@ -196,6 +196,10 @@ "description": "Specifies the path the CA certificate used for authentication", "type": "string" }, + "ca_cert2": { + "description": "Path to the inner-tunnel (phase 2) CA certificate for EAP-TLS", + "type": "string" + }, "ca_cert2_usesystem": { "type": "boolean" }, @@ -215,6 +219,10 @@ "description": "File path to client certificate file (PEM/DER)", "type": "string" }, + "client_cert2": { + "description": "File path to the inner-tunnel (phase 2) client certificate for EAP-TLS", + "type": "string" + }, "dae_client": { "type": "alias", "default": "radius_das_client" @@ -371,6 +379,10 @@ "gas_address3": { "type": "string" }, + "gcmp256": { + "description": "Advertise the GCMP-256 pairwise cipher on HE/EHT BSSes (alongside CCMP for sae/sae-mixed, in the RSNE Override 2 element for sae-compat). Mandatory for EHT, recommended otherwise. Defaults off for sae and sae-mixed because some clients and chipsets misbehave when GCMP-256 is offered, and on for sae-compat BSSes using an EHT htmode (where GCMP-256 is mandatory), carried in a separate RSN Override element that legacy clients ignore. Only advertised when the driver supports the GCMP-256 cipher.", + "type": "boolean" + }, "hidden": { "type": "alias", "default": "ignore_broadcast_ssid" @@ -841,19 +853,37 @@ "default": true }, "priv_key": { - "type": "string" + "type": "alias", + "default": "private_key" }, "priv_key_pwd": { - "type": "string" + "type": "alias", + "default": "private_key_passwd" + }, + "priv_key2": { + "type": "alias", + "default": "private_key2" + }, + "priv_key2_pwd": { + "type": "alias", + "default": "private_key2_passwd" }, "private_key": { "description": "Private key matching with the server certificate for EAP-TLS/PEAP/TTLS", "type": "string" }, + "private_key2": { + "description": "Inner-tunnel (phase 2) private key for EAP-TLS", + "type": "string" + }, "private_key_passwd": { "description": "Passphrase for private key", "type": "string" }, + "private_key2_passwd": { + "description": "Passphrase for the inner-tunnel (phase 2) private key", + "type": "string" + }, "proxy_arp": { "description": "Proxy ARP", "type": "boolean" @@ -963,9 +993,8 @@ "type": "boolean" }, "sae_ext_key": { - "description": "Advertise the SAE-EXT-KEY AKM alongside plain SAE. Mandatory for EHT, enabled by default.", - "type": "boolean", - "default": true + "description": "Advertise the SAE-EXT-KEY AKM (SAE using a group-dependent hash) alongside plain SAE. Mandatory for EHT, recommended otherwise. Defaults off for sae and sae-mixed because some clients misbehave when SAE-EXT-KEY is offered, and on for sae-compat BSSes using an EHT htmode (where SAE-EXT-KEY is mandatory), carried in a separate RSN Override element that legacy clients ignore.", + "type": "boolean" }, "sae_password_file": { "description": "External file containing VLAN SAE MAC address triplets", diff --git a/package/network/config/wifi-scripts/files-ucode/usr/share/ucode/wifi/ap.uc b/package/network/config/wifi-scripts/files-ucode/usr/share/ucode/wifi/ap.uc index 6c675b79a4d6eb..e539b00f286a9c 100644 --- a/package/network/config/wifi-scripts/files-ucode/usr/share/ucode/wifi/ap.uc +++ b/package/network/config/wifi-scripts/files-ucode/usr/share/ucode/wifi/ap.uc @@ -551,7 +551,7 @@ export function generate(interface, data, config, vlans, stas, phy_features) { config.start_disabled = data.ap_start_disabled; iface_setup(config); - iface.parse_encryption(config, data.config); + iface.parse_encryption(config, data.config, phy_features); if (data.config.band == '6g') { if (config.auth_type == 'psk-sae') config.auth_type = 'sae'; diff --git a/package/network/config/wifi-scripts/files-ucode/usr/share/ucode/wifi/hostapd.uc b/package/network/config/wifi-scripts/files-ucode/usr/share/ucode/wifi/hostapd.uc index 5f56e324e4d65b..34fe80c733fb1e 100644 --- a/package/network/config/wifi-scripts/files-ucode/usr/share/ucode/wifi/hostapd.uc +++ b/package/network/config/wifi-scripts/files-ucode/usr/share/ucode/wifi/hostapd.uc @@ -14,6 +14,8 @@ import * as fs from 'fs'; const NL80211_EXT_FEATURE_ENABLE_FTM_RESPONDER = 33; const NL80211_EXT_FEATURE_RADAR_BACKGROUND = 61; +const WLAN_CIPHER_SUITE_GCMP_256 = 0x000fac09; + let phy_features = {}; let phy_capabilities = {}; @@ -448,22 +450,28 @@ function device_htmode_append(config) { } if (wildcard(config.htmode, 'EHT*')) { - let eht_phy_cap = phy_capabilities.eht_phy_cap; - - config.ieee80211be = true; - - if (!(eht_phy_cap[0] & 0x20)) - config.eht_su_beamformer = false; - if (!(eht_phy_cap[0] & 0x40)) - config.eht_su_beamformee = false; - if (!(eht_phy_cap[7] & 0x70)) - config.eht_mu_beamformer = false; - - append_vars(config, [ - 'ieee80211be', 'eht_su_beamformer', 'eht_su_beamformee', - 'eht_mu_beamformer', - ]); - +let eht_phy_cap = phy_capabilities.eht_phy_cap; + +config.ieee80211be = true; + +/* Hostapd masks these against the hardware EHT capabilities. */ +set_default(config, 'eht_su_beamformer', true); +set_default(config, 'eht_su_beamformee', true); +set_default(config, 'eht_mu_beamformer', true); + +if (!(eht_phy_cap[0] & 0x20)) + config.eht_su_beamformer = false; +if (!(eht_phy_cap[0] & 0x40)) + config.eht_su_beamformee = false; +if (!(eht_phy_cap[7] & 0x70)) + config.eht_mu_beamformer = false; + +append_vars(config, [ + 'ieee80211be', + 'eht_su_beamformer', + 'eht_su_beamformee', + 'eht_mu_beamformer', +]); if (config.hw_mode == 'a') append_vars(config, [ 'eht_oper_chwidth', 'eht_oper_centr_freq_seg0_idx' ]); } @@ -496,6 +504,7 @@ function device_capabilities(config) { phy_features.ftm_responder = device_extended_features(phy.extended_features, NL80211_EXT_FEATURE_ENABLE_FTM_RESPONDER); phy_features.radar_background = device_extended_features(phy.extended_features, NL80211_EXT_FEATURE_RADAR_BACKGROUND); + phy_features.cipher_gcmp256 = WLAN_CIPHER_SUITE_GCMP_256 in (phy.cipher_suites ?? []); } function generate(config) { diff --git a/package/network/config/wifi-scripts/files-ucode/usr/share/ucode/wifi/iface.uc b/package/network/config/wifi-scripts/files-ucode/usr/share/ucode/wifi/iface.uc index 0193eaeef1a153..d841bf814afdaa 100644 --- a/package/network/config/wifi-scripts/files-ucode/usr/share/ucode/wifi/iface.uc +++ b/package/network/config/wifi-scripts/files-ucode/usr/share/ucode/wifi/iface.uc @@ -3,7 +3,7 @@ import { append_value, log } from 'wifi.common'; import * as fs from 'fs'; -export function parse_encryption(config, dev_config) { +export function parse_encryption(config, dev_config, phy_features) { if (!config.encryption) return; @@ -19,6 +19,19 @@ export function parse_encryption(config, dev_config) { config.auth_type = encryption[0] ?? 'none'; + /* + * GCMP-256 and the SAE-EXT-KEY (SAE-GDH) AKM are only mandatory for + * EHT/MLO and break interoperability with many clients, so only default + * them on where they are both required and safe to offer: on Compatibility + * mode (sae-compat) BSSes that run an EHT htmode, which carry them in a + * separate RSN Override element that legacy clients ignore. They stay off + * for WPA3-Personal (sae) and Transition (sae-mixed) mode and on non-EHT + * BSSes. Explicit gcmp256 and sae_ext_key options override this per BSS. + */ + let compat = (config.auth_type == 'sae-compat'); + config.gcmp256 ??= compat && wildcard(dev_config?.htmode ?? '', 'EHT*'); + config.sae_ext_key ??= compat && wildcard(dev_config?.htmode ?? '', 'EHT*'); + switch(config.auth_type) { case 'owe': config.auth_type = 'owe'; @@ -62,7 +75,7 @@ export function parse_encryption(config, dev_config) { config.wpa_pairwise = 'CCMP'; if (dev_config.band != '6g') config.rsn_override_pairwise = 'CCMP'; - if (wildcard(dev_config.htmode ?? '', 'EHT*')) + if (config.gcmp256 && phy_features?.cipher_gcmp256) config.rsn_override_pairwise_2 = 'GCMP-256'; break; @@ -107,7 +120,7 @@ export function parse_encryption(config, dev_config) { config.wpa_pairwise ??= null; else if (config.hw_mode == 'ad') config.wpa_pairwise ??= 'GCMP'; - else if (wildcard(dev_config?.htmode, 'EHT*') || wildcard(dev_config?.htmode, 'HE*')) + else if (config.gcmp256 && phy_features?.cipher_gcmp256) config.wpa_pairwise ??= 'GCMP-256 CCMP'; else config.wpa_pairwise ??= 'CCMP'; @@ -189,7 +202,7 @@ export function wpa_key_mgmt(config, band) { if (config.ieee80211r) append_value(config, 'wpa_key_mgmt', 'FT-SAE'); - if (config.sae_ext_key && config.rsn_override_pairwise_2) { + if (config.sae_ext_key) { append_value(config, 'rsn_override_key_mgmt_2', 'SAE-EXT-KEY'); if (config.ieee80211r) append_value(config, 'rsn_override_key_mgmt_2', 'FT-SAE-EXT-KEY'); @@ -205,7 +218,7 @@ export function wpa_key_mgmt(config, band) { if (config.ieee80211r) append_value(config, 'rsn_override_key_mgmt', 'FT-SAE'); - if (config.sae_ext_key && config.rsn_override_pairwise_2) { + if (config.sae_ext_key) { append_value(config, 'rsn_override_key_mgmt_2', 'SAE-EXT-KEY'); if (config.ieee80211r) append_value(config, 'rsn_override_key_mgmt_2', 'FT-SAE-EXT-KEY'); diff --git a/package/network/config/wifi-scripts/files-ucode/usr/share/ucode/wifi/supplicant.uc b/package/network/config/wifi-scripts/files-ucode/usr/share/ucode/wifi/supplicant.uc index 7f0effadd8c9c5..80c9bd91e7f8c2 100644 --- a/package/network/config/wifi-scripts/files-ucode/usr/share/ucode/wifi/supplicant.uc +++ b/package/network/config/wifi-scripts/files-ucode/usr/share/ucode/wifi/supplicant.uc @@ -164,7 +164,20 @@ function setup_sta(data, config) { case 'peap': case 'ttls': set_default(config, 'auth', 'MSCHAPV2'); - config.phase2 = `"auth=${config.auth}"`; + + let auth = config.auth; + let phase2proto = 'auth='; + if (index(auth, 'auth') == 0) { + /* user already provided a full "auth=..." spec */ + phase2proto = ''; + } else if (index(auth, 'EAP-') == 0) { + /* inner EAP method, e.g. EAP-MSCHAPV2 -> MSCHAPV2 */ + auth = substr(auth, 4); + if (config.eap_type == 'ttls') + phase2proto = 'autheap='; + } + config.phase2 = `"${phase2proto}${auth}"`; + if (config.auth == 'EAP-TLS') { if (config.ca_cert2_usesystem && fs.stat('/etc/ssl/certs/ca-certificates.crt')) config.ca_cert2 = '/etc/ssl/certs/ca-certificates.crt'; @@ -198,9 +211,24 @@ function setup_sta(data, config) { config.mcast_rate = ratestr(config.mcast_rate); + /* + * Certificate constraint lists are semicolon-separated strings in the + * wpa_supplicant config, while UCI stores them as arrays. Join them here + * so they are emitted as a single quoted value below. + */ + for (let key in [ 'altsubject_match', 'altsubject_match2', + 'domain_match', 'domain_match2', + 'domain_suffix_match', 'domain_suffix_match2' ]) + if (type(config[key]) == 'array') + config[key] = length(config[key]) ? join(';', config[key]) : null; + network_append_string_vars(config, [ 'ssid', 'identity', 'anonymous_identity', 'password', - 'ca_cert', 'ca_cert2', 'client_cert', 'client_cert2', 'subject_match', + 'ca_cert', 'ca_cert2', 'client_cert', 'client_cert2', + 'subject_match', 'subject_match2', + 'altsubject_match', 'altsubject_match2', + 'domain_match', 'domain_match2', + 'domain_suffix_match', 'domain_suffix_match2', 'private_key', 'private_key_passwd', 'private_key2', 'private_key2_passwd', ]); network_append_vars(config, [ @@ -209,7 +237,6 @@ function setup_sta(data, config) { 'proto', 'mesh_fwding', 'mesh_rssi_threshold', 'frequency', 'fixed_freq', 'disable_ht', 'disable_ht40', 'disable_vht', 'vht', 'max_oper_chwidth', 'ht40', 'beacon_int', 'ieee80211w', 'rates', 'mesh_basic_rates', 'mcast_rate', - 'altsubject_match', 'domain_match', 'domain_suffix_match', 'bssid_blacklist', 'bssid_whitelist', 'erp', 'eap', 'phase2', 'dpp_connector', 'dpp_csign', 'dpp_netaccesskey', ]); diff --git a/package/network/services/hostapd/Config.in b/package/network/services/hostapd/Config.in index 742cd78a14627b..7d8a094efbf892 100644 --- a/package/network/services/hostapd/Config.in +++ b/package/network/services/hostapd/Config.in @@ -33,15 +33,24 @@ config WPA_WOLFSSL config DRIVER_11AC_SUPPORT bool + default y if TARGET_airoha_an7581_DEVICE_gemtek_w1700k-ubi + default y if TARGET_airoha_an7581_DEVICE_econet_xr1710g-ubi + default y if TARGET_airoha_an7581_DEVICE_nokia_valyrian default n config DRIVER_11AX_SUPPORT bool + default y if TARGET_airoha_an7581_DEVICE_gemtek_w1700k-ubi + default y if TARGET_airoha_an7581_DEVICE_econet_xr1710g-ubi + default y if TARGET_airoha_an7581_DEVICE_nokia_valyrian default n select WPA_MBO_SUPPORT config DRIVER_11BE_SUPPORT bool + default y if TARGET_airoha_an7581_DEVICE_gemtek_w1700k-ubi + default y if TARGET_airoha_an7581_DEVICE_econet_xr1710g-ubi + default y if TARGET_airoha_an7581_DEVICE_nokia_valyrian default n config WPA_ENABLE_WEP diff --git a/package/network/services/hostapd/patches/602-nl80211-short-circuit-use-existing-iface.patch b/package/network/services/hostapd/patches/602-nl80211-short-circuit-use-existing-iface.patch new file mode 100644 index 00000000000000..724b694a9102bc --- /dev/null +++ b/package/network/services/hostapd/patches/602-nl80211-short-circuit-use-existing-iface.patch @@ -0,0 +1,82 @@ +From cc152837acb9cc58dd5b6c9d8899777c23e33be5 Mon Sep 17 00:00:00 2001 +From: Yanghan Ye +Date: Sun, 12 Apr 2026 18:55:57 +0800 +Subject: [PATCH] nl80211: short-circuit the use_existing interface path + +OpenWrt may pre-create AP netdevs from ucode before hostapd reaches the +driver-level interface-add path. When use_existing is already set, the +current code still sends NL80211_CMD_NEW_INTERFACE first, logs a bogus +ENFILE/"Too many open files in system" failure, and only then falls back +to the already-existing netdev. + +Short-circuit that case: if use_existing is allowed and the target +ifname already exists, skip the create attempt and immediately reuse the +interface. + +Signed-off-by: Yanghan Ye +--- + src/drivers/driver_nl80211.c | 36 +++++++++++++++++++++++------------- + 1 file changed, 23 insertions(+), 13 deletions(-) + +diff --git a/src/drivers/driver_nl80211.c b/src/drivers/driver_nl80211.c +index 413b6bd..8917204 100644 +--- a/src/drivers/driver_nl80211.c ++++ b/src/drivers/driver_nl80211.c +@@ -6686,6 +6686,24 @@ static int nl80211_create_iface_once(struct wpa_driver_nl80211_data *drv, + } + + ++static int nl80211_use_existing_iface(struct wpa_driver_nl80211_data *drv, ++ const char *ifname, ++ enum nl80211_iftype iftype, ++ const u8 *addr) ++{ ++ wpa_printf(MSG_DEBUG, "nl80211: Continue using existing interface %s", ++ ifname); ++ if (addr && iftype != NL80211_IFTYPE_MONITOR && ++ linux_set_ifhwaddr(drv->global->ioctl_sock, ifname, addr) < 0 && ++ (linux_set_iface_flags(drv->global->ioctl_sock, ifname, 0) < 0 || ++ linux_set_ifhwaddr(drv->global->ioctl_sock, ifname, addr) < 0 || ++ linux_set_iface_flags(drv->global->ioctl_sock, ifname, 1) < 0)) ++ return -1; ++ ++ return -ENFILE; ++} ++ ++ + int nl80211_create_iface(struct wpa_driver_nl80211_data *drv, + const char *ifname, enum nl80211_iftype iftype, + const u8 *addr, int wds, u32 radio_mask, +@@ -6694,25 +6712,17 @@ int nl80211_create_iface(struct wpa_driver_nl80211_data *drv, + { + int ret; + ++ if (use_existing && if_nametoindex(ifname)) ++ return nl80211_use_existing_iface(drv, ifname, iftype, addr); ++ + ret = nl80211_create_iface_once(drv, ifname, iftype, addr, wds, radio_mask, + handler, arg); + + /* if error occurred and interface exists already */ + if (ret < 0 && if_nametoindex(ifname)) { + if (use_existing) { +- wpa_printf(MSG_DEBUG, "nl80211: Continue using existing interface %s", +- ifname); +- if (addr && iftype != NL80211_IFTYPE_MONITOR && +- linux_set_ifhwaddr(drv->global->ioctl_sock, ifname, +- addr) < 0 && +- (linux_set_iface_flags(drv->global->ioctl_sock, +- ifname, 0) < 0 || +- linux_set_ifhwaddr(drv->global->ioctl_sock, ifname, +- addr) < 0 || +- linux_set_iface_flags(drv->global->ioctl_sock, +- ifname, 1) < 0)) +- return -1; +- return -ENFILE; ++ return nl80211_use_existing_iface(drv, ifname, iftype, ++ addr); + } + wpa_printf(MSG_INFO, "Try to remove and re-create %s", ifname); + +-- +2.43.0 diff --git a/package/network/services/hostapd/patches/603-hostapd-6g-band-does-not-require-dfs.patch b/package/network/services/hostapd/patches/603-hostapd-6g-band-does-not-require-dfs.patch new file mode 100644 index 00000000000000..4330d149c028c7 --- /dev/null +++ b/package/network/services/hostapd/patches/603-hostapd-6g-band-does-not-require-dfs.patch @@ -0,0 +1,23 @@ +From cd2d0f7ee500cbdf302b2042e9e4804107e06148 Mon Sep 17 00:00:00 2001 +From: Peter Chiu +Date: Mon, 13 Feb 2023 11:03:53 +0800 +Subject: [PATCH] hostapd: 6G band does not require DFS + +6 GHz operation is not radar DFS based. Do not route 6 GHz AP bring-up +through the DFS requirement path. + +Signed-off-by: Peter Chiu +--- + src/ap/dfs.c | 1 + + 1 file changed, 1 insertion(+) + +--- a/src/ap/dfs.c ++++ b/src/ap/dfs.c +@@ -1561,6 +1561,7 @@ int hostapd_is_dfs_required(struct hosta + if ((!(iface->drv_flags & WPA_DRIVER_FLAGS_DFS_OFFLOAD) && + !iface->conf->ieee80211h) || + !iface->current_mode || ++ is_6ghz_freq(iface->freq) || + iface->current_mode->mode != HOSTAPD_MODE_IEEE80211A) + return 0; + diff --git a/package/network/utils/iwinfo/Makefile b/package/network/utils/iwinfo/Makefile index a809d4cca323f5..c3750fa8de308b 100644 --- a/package/network/utils/iwinfo/Makefile +++ b/package/network/utils/iwinfo/Makefile @@ -7,7 +7,7 @@ include $(TOPDIR)/rules.mk PKG_NAME:=libiwinfo -PKG_RELEASE:=1 +PKG_RELEASE:=2 PKG_SOURCE_PROTO:=git PKG_SOURCE_URL=$(PROJECT_GIT)/project/iwinfo.git diff --git a/package/network/utils/iwinfo/patches/101-nl80211-fix-txpowerlist-wiphy-dump.patch b/package/network/utils/iwinfo/patches/101-nl80211-fix-txpowerlist-wiphy-dump.patch new file mode 100644 index 00000000000000..25647ea730ef6c --- /dev/null +++ b/package/network/utils/iwinfo/patches/101-nl80211-fix-txpowerlist-wiphy-dump.patch @@ -0,0 +1,173 @@ +From 6fa7779d216705d533435b8af9afacf17cff51f6 Mon Sep 17 00:00:00 2001 +From: YYH2913 +Date: Wed, 24 Jun 2026 17:06:39 +0800 +Subject: [PATCH] iwinfo: fix txpower list from split wiphy dumps + +Request split wiphy dumps and match the current frequency when building +txpower lists. Fall back to the old channel-based matching if the current +frequency cannot be resolved. + +Keep the nl80211 conveyor ownership clear: nl80211_send() consumes the +message, while the local nla_put_failure path frees it before send. + +Signed-off-by: YYH2913 +--- + iwinfo_nl80211.c | 104 +++++++++++++++++++++++++++++++---------------- + 1 file changed, 69 insertions(+), 35 deletions(-) + +diff --git a/iwinfo_nl80211.c b/iwinfo_nl80211.c +index adc10b9..a36a517 100644 +--- a/iwinfo_nl80211.c ++++ b/iwinfo_nl80211.c +@@ -2554,10 +2554,16 @@ static int nl80211_get_assoclist(const char *ifname, char *buf, int *len) + return -1; + } + ++struct nl80211_txpwrlist { ++ int freq; ++ int channel; ++ int dbm_max; ++}; ++ + static int nl80211_get_txpwrlist_cb(struct nl_msg *msg, void *arg) + { +- int *dbm_max = arg; +- int ch_cur, ch_cmp, bands_remain, freqs_remain; ++ struct nl80211_txpwrlist *txpwrlist = arg; ++ int freq_cur, freq_cmp, ch_cur, ch_cmp, bands_remain, freqs_remain; + + struct nlattr **attr = nl80211_parse(msg); + struct nlattr *bands[NL80211_BAND_ATTR_MAX + 1]; +@@ -2573,29 +2579,39 @@ static int nl80211_get_txpwrlist_cb(struct nl_msg *msg, void *arg) + [NL80211_FREQUENCY_ATTR_MAX_TX_POWER] = { .type = NLA_U32 }, + }; + +- ch_cur = *dbm_max; /* value int* is initialized with channel by caller */ +- *dbm_max = -1; ++ freq_cur = txpwrlist->freq; ++ ch_cur = txpwrlist->channel; ++ ++ if (!attr[NL80211_ATTR_WIPHY_BANDS]) ++ return NL_SKIP; + + nla_for_each_nested(band, attr[NL80211_ATTR_WIPHY_BANDS], bands_remain) + { + nla_parse(bands, NL80211_BAND_ATTR_MAX, nla_data(band), + nla_len(band), NULL); + ++ if (!bands[NL80211_BAND_ATTR_FREQS]) ++ continue; ++ + nla_for_each_nested(freq, bands[NL80211_BAND_ATTR_FREQS], freqs_remain) + { + nla_parse(freqs, NL80211_FREQUENCY_ATTR_MAX, + nla_data(freq), nla_len(freq), freq_policy); + +- ch_cmp = nl80211_freq2channel(nla_get_u32( +- freqs[NL80211_FREQUENCY_ATTR_FREQ])); ++ if (!freqs[NL80211_FREQUENCY_ATTR_FREQ] || ++ !freqs[NL80211_FREQUENCY_ATTR_MAX_TX_POWER]) ++ continue; ++ ++ freq_cmp = nla_get_u32(freqs[NL80211_FREQUENCY_ATTR_FREQ]); ++ ch_cmp = nl80211_freq2channel(freq_cmp); + +- if ((!ch_cur || (ch_cmp == ch_cur)) && +- freqs[NL80211_FREQUENCY_ATTR_MAX_TX_POWER]) ++ if ((freq_cur && freq_cmp == freq_cur) || ++ (!freq_cur && (!ch_cur || ch_cmp == ch_cur))) + { +- *dbm_max = (int)(0.01 * nla_get_u32( +- freqs[NL80211_FREQUENCY_ATTR_MAX_TX_POWER])); ++ txpwrlist->dbm_max = nla_get_u32( ++ freqs[NL80211_FREQUENCY_ATTR_MAX_TX_POWER]) / 100; + +- break; ++ return NL_SKIP; + } + } + } +@@ -2605,41 +2621,59 @@ static int nl80211_get_txpwrlist_cb(struct nl_msg *msg, void *arg) + + static int nl80211_get_txpwrlist(const char *ifname, char *buf, int *len) + { +- int err, ch_cur; +- int dbm_max = -1, dbm_cur, dbm_cnt; ++ struct nl80211_msg_conveyor *cv; ++ struct nl80211_txpwrlist txpwrlist = { ++ .freq = 0, ++ .channel = 0, ++ .dbm_max = -1, ++ }; ++ uint32_t features = nl80211_get_protocol_features(ifname); ++ int dbm_cur, dbm_cnt; ++ int flags; + struct iwinfo_txpwrlist_entry entry; + +- if (nl80211_get_channel(ifname, &ch_cur)) +- ch_cur = 0; ++ if (nl80211_get_frequency(ifname, &txpwrlist.freq)) { ++ txpwrlist.freq = 0; ++ if (nl80211_get_channel(ifname, &txpwrlist.channel)) ++ txpwrlist.channel = 0; ++ } + +- /* initialize the value pointer with channel for callback */ +- dbm_max = ch_cur; ++ flags = features & NL80211_PROTOCOL_FEATURE_SPLIT_WIPHY_DUMP ? NLM_F_DUMP : 0; ++ cv = nl80211_msg(ifname, NL80211_CMD_GET_WIPHY, flags); ++ if (!cv) ++ goto out; + +- err = nl80211_request(ifname, NL80211_CMD_GET_WIPHY, 0, +- nl80211_get_txpwrlist_cb, &dbm_max); ++ NLA_PUT_FLAG(cv->msg, NL80211_ATTR_SPLIT_WIPHY_DUMP); ++ if (nl80211_send(cv, nl80211_get_txpwrlist_cb, &txpwrlist)) ++ goto out; + +- if (!err) ++ if (txpwrlist.dbm_max < 0 || txpwrlist.dbm_max > 100) ++ goto out; ++ ++ for (dbm_cur = 0, dbm_cnt = 0; ++ dbm_cur < txpwrlist.dbm_max; ++ dbm_cur++, dbm_cnt++) + { +- for (dbm_cur = 0, dbm_cnt = 0; +- dbm_cur < dbm_max; +- dbm_cur++, dbm_cnt++) +- { +- entry.dbm = dbm_cur; +- entry.mw = iwinfo_dbm2mw(dbm_cur); ++ entry.dbm = dbm_cur; ++ entry.mw = iwinfo_dbm2mw(dbm_cur); + +- memcpy(&buf[dbm_cnt * sizeof(entry)], &entry, sizeof(entry)); +- } ++ memcpy(&buf[dbm_cnt * sizeof(entry)], &entry, sizeof(entry)); ++ } + +- entry.dbm = dbm_max; +- entry.mw = iwinfo_dbm2mw(dbm_max); ++ entry.dbm = txpwrlist.dbm_max; ++ entry.mw = iwinfo_dbm2mw(txpwrlist.dbm_max); + +- memcpy(&buf[dbm_cnt * sizeof(entry)], &entry, sizeof(entry)); +- dbm_cnt++; ++ memcpy(&buf[dbm_cnt * sizeof(entry)], &entry, sizeof(entry)); ++ dbm_cnt++; + +- *len = dbm_cnt * sizeof(entry); +- return 0; +- } ++ *len = dbm_cnt * sizeof(entry); ++ return 0; ++ ++nla_put_failure: ++ nl80211_free(cv); + ++out: ++ *len = 0; + return -1; + } + diff --git a/package/system/rpcd/files/rpcd.init b/package/system/rpcd/files/rpcd.init index f75d0e0f0eea4c..1ebe42701457bc 100755 --- a/package/system/rpcd/files/rpcd.init +++ b/package/system/rpcd/files/rpcd.init @@ -1,6 +1,6 @@ #!/bin/sh /etc/rc.common -START=12 +START=50 USE_PROCD=1 NAME=rpcd @@ -10,6 +10,11 @@ start_service() { local socket=$(uci -q get rpcd.@rpcd[0].socket) local timeout=$(uci -q get rpcd.@rpcd[0].timeout) + # Wait for ubusd to be fully ready. Starting rpcd before ubusd can + # accept object registrations causes plugins (file.so, luci.so, + # iwinfo.so, ...) to silently fail, leaving the ubus namespace empty. + sleep 5 + procd_open_instance procd_set_param command "$PROG" ${socket:+-s "$socket"} ${timeout:+-t "$timeout"} procd_set_param respawn diff --git a/target/linux/airoha/an7581/base-files/etc/board.d/02_network b/target/linux/airoha/an7581/base-files/etc/board.d/02_network index 107ca37046206c..cf2d6bd5083e7f 100644 --- a/target/linux/airoha/an7581/base-files/etc/board.d/02_network +++ b/target/linux/airoha/an7581/base-files/etc/board.d/02_network @@ -17,6 +17,14 @@ an7581_setup_interfaces() gemtek,w1700k-ubi) ucidef_set_interfaces_lan_wan "lan2 lan3 lan4" "wan" ;; + econet,xr1710g-ubi) + ucidef_set_interfaces_lan_wan "lan1 lan2 lan3" "wan" + ucidef_set_country "US" + ucidef_set_wireless "2g" "XR1710G" + ucidef_set_wireless "5g" "XR1710G-5G" + ucidef_set_wireless "6g" "XR1710G-6G" + ucidef_set_root_password_plain "password" + ;; nokia,valyrian) ucidef_set_interfaces_lan_wan "lan1 lan2 lan3 10g" "wan" ;; diff --git a/target/linux/airoha/an7581/base-files/etc/hotplug.d/net/50-flow-offload-wifi b/target/linux/airoha/an7581/base-files/etc/hotplug.d/net/50-flow-offload-wifi new file mode 100644 index 00000000000000..32bc7bb56aa919 --- /dev/null +++ b/target/linux/airoha/an7581/base-files/etc/hotplug.d/net/50-flow-offload-wifi @@ -0,0 +1,27 @@ +#!/bin/sh + +case "$ACTION" in + add|move) ;; + *) exit 0 ;; +esac + +DEV="${DEVICENAME:-$INTERFACE}" +[ -n "$DEV" ] || exit 0 + +case "$DEV" in + phy*-ap*|ap-mld*) ;; + *) exit 0 ;; +esac + +# Coalesce bursts of WiFi device creation / bridge attach events into +# one delayed reload so fw4 sees the final bridge topology. +[ -f /tmp/.flow-offload-wifi-pending ] && exit 0 +touch /tmp/.flow-offload-wifi-pending + +( + sleep 10 + rm -f /tmp/.flow-offload-wifi-pending + /etc/init.d/firewall reload +) 2>/dev/null & + +exit 0 diff --git a/target/linux/airoha/an7581/base-files/etc/hotplug.d/net/50-nf-bridge-call b/target/linux/airoha/an7581/base-files/etc/hotplug.d/net/50-nf-bridge-call new file mode 100644 index 00000000000000..795d631276a7f8 --- /dev/null +++ b/target/linux/airoha/an7581/base-files/etc/hotplug.d/net/50-nf-bridge-call @@ -0,0 +1,19 @@ +#!/bin/sh +# +# Enable netfilter on br-lan only, so bridged IP traffic traverses the +# netfilter forward chain where nft_flow_offload can pick it up for +# hardware PPE offload. +# +# Scoped to br-lan to avoid imposing netfilter overhead on user-created +# bridges, e.g. docker or guest networks, which may not need it. + +[ "$ACTION" = "add" ] || exit 0 +[ "$DEVICENAME" = "br-lan" ] || exit 0 + +bridge=/sys/devices/virtual/net/br-lan/bridge +[ -d "$bridge" ] || exit 0 + +echo 1 > "$bridge/nf_call_iptables" 2>/dev/null +echo 1 > "$bridge/nf_call_ip6tables" 2>/dev/null + +exit 0 diff --git a/target/linux/airoha/an7581/base-files/etc/init.d/airoha_fan b/target/linux/airoha/an7581/base-files/etc/init.d/airoha_fan index b9d5633bd1a4bb..addb0d47fc56ab 100755 --- a/target/linux/airoha/an7581/base-files/etc/init.d/airoha_fan +++ b/target/linux/airoha/an7581/base-files/etc/init.d/airoha_fan @@ -21,7 +21,7 @@ find_nct7802() boot() { case $(board_name) in - gemtek,w1700k-ubi) + gemtek,w1700k-ubi|econet,xr1710g-ubi) hwmon=$(find_nct7802) # Temporarily disable automatic fan control to configure curve @@ -45,4 +45,3 @@ boot() ;; esac } - diff --git a/target/linux/airoha/an7581/base-files/etc/uci-defaults/12_airoha-network-migrate b/target/linux/airoha/an7581/base-files/etc/uci-defaults/12_airoha-network-migrate new file mode 100644 index 00000000000000..5068dfca1f31d0 --- /dev/null +++ b/target/linux/airoha/an7581/base-files/etc/uci-defaults/12_airoha-network-migrate @@ -0,0 +1,56 @@ +#!/bin/sh + +. /lib/functions.sh +. /lib/functions/system.sh + +[ "$(board_name)" = "econet,xr1710g-ubi" ] || exit 0 + +migrated=0 + +migrate_xr1710g_bridge() { + local config="$1" + local name type ports ifname + + config_get type "$config" type + [ "$type" = "bridge" ] || return + + config_get name "$config" name + [ "$name" = "br-lan" ] || return + + config_get ports "$config" ports + config_get ifname "$config" ifname + + case " $ports $ifname " in + *" eth0.8 "*|*" eth0.1 "*|*" eth0.2 "*) + uci -q delete "network.$config.ifname" + uci -q delete "network.$config.ports" + uci add_list "network.$config.ports=lan1" + uci add_list "network.$config.ports=lan2" + uci add_list "network.$config.ports=lan3" + migrated=1 + ;; + esac +} + +migrate_xr1710g_interface() { + local config="$1" + local device + + config_get device "$config" device + case "$device" in + ae_wan) + uci set "network.$config.device=wan" + migrated=1 + ;; + esac +} + +config_load network +config_foreach migrate_xr1710g_bridge device +config_foreach migrate_xr1710g_interface interface + +[ "$migrated" -eq 1 ] || exit 0 + +uci commit network + +exit 0 diff --git a/target/linux/airoha/an7581/base-files/etc/uci-defaults/14-airoha-hw-flow-offload b/target/linux/airoha/an7581/base-files/etc/uci-defaults/14-airoha-hw-flow-offload new file mode 100644 index 00000000000000..798627a2ffa4ca --- /dev/null +++ b/target/linux/airoha/an7581/base-files/etc/uci-defaults/14-airoha-hw-flow-offload @@ -0,0 +1,18 @@ +#!/bin/sh + +# Enable routed/NAT hardware flow offload by default on AN7581 boards. +# This is required for WAN <-> LAN PPE/NPU fastpath via fw4/nft_flow_offload. + +. /lib/functions.sh + +config_load firewall + +config_get flow_offloading defaults flow_offloading +config_get flow_offloading_hw defaults flow_offloading_hw + +[ -n "$flow_offloading" ] || uci set firewall.@defaults[0].flow_offloading='1' +[ -n "$flow_offloading_hw" ] || uci set firewall.@defaults[0].flow_offloading_hw='1' + +uci commit firewall + +exit 0 diff --git a/target/linux/airoha/an7581/base-files/etc/uci-defaults/15-w1700k-flow-offload b/target/linux/airoha/an7581/base-files/etc/uci-defaults/15-w1700k-flow-offload new file mode 100644 index 00000000000000..a4f210cd8c0a53 --- /dev/null +++ b/target/linux/airoha/an7581/base-files/etc/uci-defaults/15-w1700k-flow-offload @@ -0,0 +1,37 @@ +#!/bin/sh + +# Refresh any pre-existing overlay copy of the WiFi flow-offload hotplug +# helper so sysupgrades do not keep the older variant that only reloads +# once too early and misses later WiFi bridge attach events. + +mkdir -p /etc/hotplug.d/net +cat > /etc/hotplug.d/net/50-flow-offload-wifi <<'HOTPLUG' +#!/bin/sh + +case "$ACTION" in + add|move) ;; + *) exit 0 ;; +esac + +DEV="${DEVICENAME:-$INTERFACE}" +[ -n "$DEV" ] || exit 0 + +case "$DEV" in + phy*-ap*|ap-mld*) ;; + *) exit 0 ;; +esac + +[ -f /tmp/.flow-offload-wifi-pending ] && exit 0 +touch /tmp/.flow-offload-wifi-pending + +( + sleep 10 + rm -f /tmp/.flow-offload-wifi-pending + /etc/init.d/firewall reload +) 2>/dev/null & + +exit 0 +HOTPLUG +chmod +x /etc/hotplug.d/net/50-flow-offload-wifi + +exit 0 diff --git a/target/linux/airoha/an7581/base-files/etc/uci-defaults/16-airoha-wifi-tuning b/target/linux/airoha/an7581/base-files/etc/uci-defaults/16-airoha-wifi-tuning new file mode 100644 index 00000000000000..d264bb48fb4080 --- /dev/null +++ b/target/linux/airoha/an7581/base-files/etc/uci-defaults/16-airoha-wifi-tuning @@ -0,0 +1,41 @@ +#!/bin/sh + +. /lib/functions.sh +. /lib/functions/system.sh + +case "$(board_name)" in + gemtek,w1700k-ubi|econet,xr1710g-ubi) ;; + *) exit 0 ;; +esac + +config_load wireless + +changed=0 + +tune_wifi_iface() { + local cfg="$1" + local mode + + config_get mode "$cfg" mode + [ "$mode" = "ap" ] || return 0 + + # Apple and other aggressive power-save clients are prone to bursty + # delivery and stalls on this platform when U-APSD is advertised. + if ! uci -q get wireless."$cfg".uapsd >/dev/null; then + uci set wireless."$cfg".uapsd='0' + changed=1 + fi + + # Avoid disconnecting high-MCS clients too aggressively when retries + # spike during short fades or PS state transitions. + if ! uci -q get wireless."$cfg".disassoc_low_ack >/dev/null; then + uci set wireless."$cfg".disassoc_low_ack='0' + changed=1 + fi +} + +config_foreach tune_wifi_iface wifi-iface + +[ "$changed" -eq 1 ] && uci commit wireless + +exit 0 diff --git a/target/linux/airoha/an7581/base-files/etc/uci-defaults/17-xr1710g-wifi-defaults b/target/linux/airoha/an7581/base-files/etc/uci-defaults/17-xr1710g-wifi-defaults new file mode 100755 index 00000000000000..0d4ac4ffa81801 --- /dev/null +++ b/target/linux/airoha/an7581/base-files/etc/uci-defaults/17-xr1710g-wifi-defaults @@ -0,0 +1,44 @@ +#!/bin/sh + +. /lib/functions.sh +. /lib/functions/system.sh + +[ "$(board_name)" = "econet,xr1710g-ubi" ] || exit 0 + +config_load wireless + +changed=0 + +set_xr1710g_radio_defaults() { + local cfg="$1" + local band + + config_get band "$cfg" band + + case "$band" in + 2g) + uci set wireless."$cfg".country='US' + ;; + 5g) + uci set wireless."$cfg".country='US' + uci set wireless."$cfg".channel='36' + uci set wireless."$cfg".htmode='EHT160' + ;; + 6g) + uci set wireless."$cfg".country='US' + uci set wireless."$cfg".channel='37' + uci set wireless."$cfg".htmode='EHT320' + ;; + *) + return 0 + ;; + esac + + changed=1 +} + +config_foreach set_xr1710g_radio_defaults wifi-device + +[ "$changed" -eq 1 ] && uci commit wireless + +exit 0 diff --git a/target/linux/airoha/an7581/base-files/lib/upgrade/platform.sh b/target/linux/airoha/an7581/base-files/lib/upgrade/platform.sh index 8584b84e0c6be5..9925b285f02e35 100644 --- a/target/linux/airoha/an7581/base-files/lib/upgrade/platform.sh +++ b/target/linux/airoha/an7581/base-files/lib/upgrade/platform.sh @@ -36,6 +36,11 @@ platform_do_upgrade() { nokia,xg-040g-md-ubi) fit_do_upgrade "$1" ;; + econet,xr1710g-ubi) + IGNORE_ROOTFS_DATA_MAX=1 \ + CI_REMOVE_UBIVOLS="config log" \ + fit_do_upgrade "$1" + ;; *) nand_do_upgrade "$1" ;; diff --git a/target/linux/airoha/an7581/config-6.18 b/target/linux/airoha/an7581/config-6.18 index 59f73252125d55..08829331cd3d71 100644 --- a/target/linux/airoha/an7581/config-6.18 +++ b/target/linux/airoha/an7581/config-6.18 @@ -111,6 +111,7 @@ CONFIG_FUNCTION_ALIGNMENT_4B=y CONFIG_FWNODE_MDIO=y CONFIG_FWNODE_PCS=y # CONFIG_FW_LOADER_USER_HELPER is not set +CONFIG_DEVMEM=y CONFIG_GCC_SUPPORTS_DYNAMIC_FTRACE_WITH_ARGS=y CONFIG_GENERIC_ALLOCATOR=y CONFIG_GENERIC_ARCH_TOPOLOGY=y @@ -157,6 +158,8 @@ CONFIG_HOTPLUG_CORE_SYNC=y CONFIG_HOTPLUG_CORE_SYNC_DEAD=y CONFIG_HOTPLUG_CPU=y CONFIG_HW_RANDOM=y +# CONFIG_IO_STRICT_DEVMEM is not set +# CONFIG_STRICT_DEVMEM is not set CONFIG_HW_RANDOM_AIROHA=y CONFIG_ILLEGAL_POINTER_VALUE=0xdead000000000000 CONFIG_INITRAMFS_SOURCE="" @@ -220,6 +223,7 @@ CONFIG_NEED_SG_DMA_LENGTH=y CONFIG_NET_AIROHA=y # CONFIG_NET_AIROHA_FLOW_STATS is not set CONFIG_NET_AIROHA_NPU=y +CONFIG_NET_AIROHA_SOE=y CONFIG_NET_DEVLINK=y CONFIG_NET_DSA=y CONFIG_NET_DSA_MT7530=y diff --git a/target/linux/airoha/an7581/target.mk b/target/linux/airoha/an7581/target.mk index 3d0cee97d4b6d1..7dd4cc484cb33d 100644 --- a/target/linux/airoha/an7581/target.mk +++ b/target/linux/airoha/an7581/target.mk @@ -6,9 +6,11 @@ KERNELNAME:=Image dtbs FEATURES+=pwm DEFAULT_PACKAGES += \ - airoha-en7581-npu-firmware uboot-envtools + airoha-en7581-npu-firmware uboot-envtools \ + kmod-br-netfilter \ + kmod-crypto-hw-eip93 \ + luci-proto-ipv6 define Target/Description Build firmware images for Airoha an7581 ARM based boards. endef - diff --git a/target/linux/airoha/an7583/target.mk b/target/linux/airoha/an7583/target.mk index dacbe0928e5b38..2681c79767f55f 100644 --- a/target/linux/airoha/an7583/target.mk +++ b/target/linux/airoha/an7583/target.mk @@ -5,7 +5,8 @@ CPU_TYPE:=cortex-a53 KERNELNAME:=Image dtbs FEATURES+=pwm source-only +DEFAULT_PACKAGES += kmod-crypto-hw-eip93 + define Target/Description Build firmware images for Airoha an7583 ARM based boards. endef - diff --git a/target/linux/airoha/base-files/etc/sysctl.d/13-nf-bridge.conf b/target/linux/airoha/base-files/etc/sysctl.d/13-nf-bridge.conf new file mode 100644 index 00000000000000..0454e18ae6a7d1 --- /dev/null +++ b/target/linux/airoha/base-files/etc/sysctl.d/13-nf-bridge.conf @@ -0,0 +1,2 @@ +net.bridge.bridge-nf-call-iptables=1 +net.bridge.bridge-nf-call-ip6tables=1 diff --git a/target/linux/airoha/base-files/lib/preinit/03_create_devmem b/target/linux/airoha/base-files/lib/preinit/03_create_devmem new file mode 100644 index 00000000000000..054e55d6b61f43 --- /dev/null +++ b/target/linux/airoha/base-files/lib/preinit/03_create_devmem @@ -0,0 +1,7 @@ +create_devmem_node() { + [ -c /dev/mem ] && return 0 + grep -qE '^[[:space:]]*1[[:space:]]+mem$' /proc/devices 2>/dev/null || return 0 + mknod -m 0600 /dev/mem c 1 1 2>/dev/null || true +} + +boot_hook_add preinit_essential create_devmem_node diff --git a/target/linux/airoha/dts/an7581-nokia-valyrian.dts b/target/linux/airoha/dts/an7581-nokia-valyrian.dts index 7e5facbcdac10f..da642691206582 100644 --- a/target/linux/airoha/dts/an7581-nokia-valyrian.dts +++ b/target/linux/airoha/dts/an7581-nokia-valyrian.dts @@ -468,6 +468,7 @@ &gdm4 { status = "okay"; + airoha,lan-port; managed = "in-band-status"; phy-handle = <&as21xx>; diff --git a/target/linux/airoha/dts/an7581-w1700k-ubi.dts b/target/linux/airoha/dts/an7581-w1700k-ubi.dts index d306f8f8e609e0..41c90583103755 100644 --- a/target/linux/airoha/dts/an7581-w1700k-ubi.dts +++ b/target/linux/airoha/dts/an7581-w1700k-ubi.dts @@ -322,6 +322,7 @@ &gdm4 { status = "okay"; + airoha,lan-port; managed = "in-band-status"; phy-handle = <&phy5>; diff --git a/target/linux/airoha/dts/an7581-xr1710g-ubi.dts b/target/linux/airoha/dts/an7581-xr1710g-ubi.dts new file mode 100644 index 00000000000000..8c0dda08e8fbad --- /dev/null +++ b/target/linux/airoha/dts/an7581-xr1710g-ubi.dts @@ -0,0 +1,433 @@ +// SPDX-License-Identifier: GPL-2.0-or-later OR MIT + +/dts-v1/; + +#include +#include +#include +#include +#include "an7581.dtsi" + +/ { + model = "XR1710G"; + compatible = "econet,xr1710g-ubi", "gemtek,xr1710g-ubi", + "econet,xr1710g", "gemtek,xr1710g", + "airoha,an7581", "airoha,en7581"; + + aliases { + serial0 = &uart1; + led-boot = &led_status_red; + led-failsafe = &led_status_blue; + led-upgrade = &led_status_blue; + led-running = &led_status_green; + }; + + chosen { + rootdisk = <&ubi_rootfs>; + bootargs = "console=ttyS0,115200 earlycon ubi.block=0,fit root=/dev/fit0 rootwait"; + stdout-path = "serial0:115200n8"; + }; + + keys { + compatible = "gpio-keys"; + + key-restart { + label = "reset"; + gpios = <&en7581_pinctrl 0 GPIO_ACTIVE_LOW>; + linux,code = ; + }; + }; + + leds { + compatible = "gpio-leds"; + + led_status_green: led-status-green { + color = ; + function = LED_FUNCTION_STATUS; + gpios = <&en7581_pinctrl 17 GPIO_ACTIVE_LOW>; + }; + + led_status_blue: led-status-blue { + color = ; + function = LED_FUNCTION_STATUS; + gpios = <&en7581_pinctrl 19 GPIO_ACTIVE_LOW>; + }; + + led_status_red: led-status-red { + color = ; + function = LED_FUNCTION_STATUS; + gpios = <&en7581_pinctrl 29 GPIO_ACTIVE_LOW>; + default-state = "on"; + }; + + led_status_white: led-status-white { + color = ; + function = LED_FUNCTION_STATUS; + gpios = <&en7581_pinctrl 20 GPIO_ACTIVE_LOW>; + }; + + }; +}; + +&en7581_pinctrl { + gpio-ranges = <&en7581_pinctrl 0 13 47>; + + mdio_pins: mdio-pins { + mux { + function = "mdio"; + groups = "mdio"; + }; + + conf { + pins = "gpio2"; + output-high; + }; + }; + + pcie0_rst_pins: pcie0-rst-pins { + conf { + pins = "pcie_reset0"; + drive-open-drain = <1>; + }; + }; + + pcie2_rst_pins: pcie2-rst-pins { + conf { + pins = "pcie_reset2"; + drive-open-drain = <1>; + }; + }; + + /* LAN2/LAN3 LEDs are driven directly by the internal MT7530 switch. */ + gswp1_led0_pins: gswp1-led0-pins { + mux { + function = "phy1_led0"; + pins = "gpio33"; + }; + }; + + gswp1_led1_pins: gswp1-led1-pins { + mux { + function = "phy1_led1"; + pins = "gpio43"; + }; + }; + + gswp2_led0_pins: gswp2-led0-pins { + mux { + function = "phy2_led0"; + pins = "gpio34"; + }; + }; + + gswp2_led1_pins: gswp2-led1-pins { + mux { + function = "phy2_led1"; + pins = "gpio44"; + }; + }; + + uart2_pins: uart2-pins { + mux { + function = "uart"; + groups = "uart2"; + }; + }; + + hsuart_pins: hsuart-pins { + mux { + function = "uart"; + groups = "hsuart"; + }; + }; +}; + +&snfi { + status = "okay"; +}; + +&spi_nand { + #address-cells = <1>; + #size-cells = <1>; + partitions { + compatible = "fixed-partitions"; + #address-cells = <1>; + #size-cells = <1>; + + partition@0 { + label = "vendor"; + reg = <0x00000000 0x00600000>; + read-only; + }; + + partition@600000 { + label = "chainloader"; + reg = <0x00600000 0x00100000>; + read-only; + }; + + partition@700000 { + label = "ubi"; + reg = <0x00700000 0x1d9c0000>; + compatible = "linux,ubi"; + + volumes { + ubi-volume-ubootenv { + volname = "ubootenv"; + nvmem-layout { + compatible = "u-boot,env-redundant-bool"; + }; + }; + + ubi-volume-ubootenv2 { + volname = "ubootenv2"; + nvmem-layout { + compatible = "u-boot,env-redundant-bool"; + }; + }; + + ubi_rootfs: ubi-volume-fit { + volname = "fit"; + }; + + ubi_factory: ubi-volume-factory { + volname = "factory"; + }; + }; + }; + + /* + * Reserve the final 250 eraseblocks (31.25 MiB) for the stock + * bootloader's BMT/BBT handling on the 512 MiB SPI-NAND. + * Otherwise U-Boot may erase/write into the end of the "ubi" + * partition during recovery or sysupgrade flows. + */ + reserved_bmt@1e0c0000 { + label = "reserved_bmt"; + reg = <0x1e0c0000 0x01f40000>; + read-only; + }; + }; +}; + +&ubi_factory { + nvmem-layout { + compatible = "fixed-layout"; + #address-cells = <1>; + #size-cells = <1>; + + eeprom: eeprom@0 { + reg = <0x0 0x1e00>; + }; + + wan_mac: macaddr@5000 { + reg = <0x5000 0x6>; + }; + + lan_mac: macaddr@6000 { + compatible = "mac-base"; + reg = <0x6000 0x6>; + #nvmem-cell-cells = <1>; + }; + }; +}; + +&i2c0 { + status = "okay"; + + hwmon@2e { + compatible = "nuvoton,nct7802"; + reg = <0x2e>; + }; +}; + +&pcie0 { + status = "okay"; + + airoha,x2-mode; + + pinctrl-names = "default"; + pinctrl-0 = <&pcie0_rst_pins>; + + pcie@0,0 { + reg = <0x0000 0 0 0 0>; + device_type = "pci"; + #address-cells = <3>; + #size-cells = <2>; + + mt7996@0,0 { + reg = <0x0000 0 0 0 0>; + #address-cells = <1>; + #size-cells = <0>; + nvmem-cells = <&eeprom>; + nvmem-cell-names = "eeprom"; + airoha,npu = <&npu>; + airoha,eth = <ð>; + + band@0 { + /* 2.4 GHz */ + reg = <0>; + nvmem-cells = <&lan_mac 1>; + nvmem-cell-names = "mac-address"; + }; + + band@1 { + /* 5 GHz */ + reg = <1>; + nvmem-cells = <&lan_mac 2>; + nvmem-cell-names = "mac-address"; + }; + + band@2 { + /* 6 GHz */ + reg = <2>; + nvmem-cells = <&lan_mac 3>; + nvmem-cell-names = "mac-address"; + }; + }; + }; +}; + +&pcie2 { + status = "okay"; + + pinctrl-names = "default"; + pinctrl-0 = <&pcie2_rst_pins>; +}; + +&npu { + firmware-name = "airoha/en7581_MT7996_npu_rv32.bin", + "airoha/en7581_MT7996_npu_data.bin"; + status = "okay"; +}; + +&afe { + status = "disabled"; +}; + +ð { + status = "okay"; +}; + +&gdm1 { + status = "okay"; + + nvmem-cells = <&lan_mac 0>; + nvmem-cell-names = "mac-address"; +}; + +&gdm2 { + status = "okay"; + + managed = "in-band-status"; + phy-handle = <&phy8>; + phy-mode = "usxgmii"; + + nvmem-cells = <&wan_mac 0>; + nvmem-cell-names = "mac-address"; + openwrt,netdev-name = "wan"; +}; + +&gdm4 { + status = "okay"; + airoha,lan-port; + + managed = "in-band-status"; + phy-handle = <&phy5>; + phy-mode = "usxgmii"; + + nvmem-cells = <&lan_mac 0>; + nvmem-cell-names = "mac-address"; + openwrt,netdev-name = "lan1"; +}; + +&switch { + status = "okay"; + + pinctrl-names = "default"; + pinctrl-0 = <&mdio_pins>; + + mdio { + phy5: ethernet-phy@5 { + compatible = "ethernet-phy-ieee802.3-c45"; + reg = <5>; + reset-gpios = <&en7581_pinctrl 46 GPIO_ACTIVE_LOW>; + reset-assert-us = <200000>; + reset-deassert-us = <200000>; + tx-polarity = ; + rx-polarity = ; + }; + + phy8: ethernet-phy@8 { + compatible = "ethernet-phy-ieee802.3-c45"; + reg = <8>; + reset-gpios = <&en7581_pinctrl 31 GPIO_ACTIVE_LOW>; + reset-assert-us = <200000>; + reset-deassert-us = <200000>; + tx-polarity = ; + rx-polarity = ; + }; + }; +}; + +&gsw_phy1 { + status = "okay"; + pinctrl-names = "gbe-led"; + pinctrl-0 = <&gswp1_led0_pins>, <&gswp1_led1_pins>; +}; + +&gsw_port1 { + status = "okay"; + label = "lan2"; +}; + +&gsw_phy1_led0 { + function = LED_FUNCTION_LAN; + color = ; + status = "okay"; + active-low; +}; + +&gsw_phy1_led1 { + function = LED_FUNCTION_LAN; + color = ; + status = "okay"; + active-low; +}; + +&gsw_phy2 { + status = "okay"; + pinctrl-names = "gbe-led"; + pinctrl-0 = <&gswp2_led0_pins>, <&gswp2_led1_pins>; +}; + +&gsw_port2 { + status = "okay"; + label = "lan3"; +}; + +&gsw_phy2_led0 { + function = LED_FUNCTION_LAN; + color = ; + status = "okay"; + active-low; +}; + +&gsw_phy2_led1 { + function = LED_FUNCTION_LAN; + color = ; + status = "okay"; + active-low; +}; + +&uart2 { + status = "okay"; + pinctrl-names = "default"; + pinctrl-0 = <&uart2_pins>; +}; + +&hsuart3 { + status = "okay"; + pinctrl-names = "default"; + pinctrl-0 = <&hsuart_pins>; +}; diff --git a/target/linux/airoha/dts/an7581.dtsi b/target/linux/airoha/dts/an7581.dtsi index e7bfd69165b3aa..18ac25b4aa174c 100644 --- a/target/linux/airoha/dts/an7581.dtsi +++ b/target/linux/airoha/dts/an7581.dtsi @@ -156,6 +156,9 @@ }; cpufreq: cpufreq { + reg = <0x0 0x1fa20000 0x0 0x2c0>, + <0x0 0x1efbe000 0x0 0x800>; + reg-names = "chip-scu", "mcucfg"; compatible = "airoha,en7581-cpufreq"; operating-points-v2 = <&cpu_smcc_opp_table>; @@ -466,6 +469,7 @@ pon_pcs: pcs@1fa08000 { compatible = "airoha,an7581-pcs-pon"; + #pcs-cells = <0>; reg = <0x0 0x1fa08000 0x0 0x1000>, <0x0 0x1fa80000 0x0 0x60>, <0x0 0x1fa80a00 0x0 0x164>, @@ -483,12 +487,11 @@ reset-names = "mac", "phy"; airoha,scu = <&scuclk>; - - #pcs-cells = <0>; }; eth_pcs: pcs@1fa09000 { compatible = "airoha,an7581-pcs-eth"; + #pcs-cells = <0>; reg = <0x0 0x1fa09000 0x0 0x1000>, <0x0 0x1fa70000 0x0 0x60>, <0x0 0x1fa70a00 0x0 0x164>, @@ -506,8 +509,6 @@ reset-names = "mac", "phy"; airoha,scu = <&scuclk>; - - #pcs-cells = <0>; }; pcie_pcs: pcs@1fa04000 { @@ -580,10 +581,11 @@ #reset-cells = <1>; }; - rng@1faa1000 { + rng: rng@1faa1000 { compatible = "airoha,en7581-trng"; reg = <0x0 0x1faa1000 0x0 0xc04>; interrupts = ; + airoha,scu = <&chip_scu>; }; usb0: usb@1fab0000 { @@ -639,6 +641,13 @@ interrupts = ; }; + /* Packet/IPsec SOE register window owned by the Ethernet driver. */ + soe: soe@1fbfa000 { + compatible = "airoha,an7581-soe"; + reg = <0x0 0x1fbfa000 0x0 0x268>; + interrupts = ; + }; + thermal: thermal-sensor@1efbd000 { compatible = "airoha,en7581-thermal"; reg = <0x0 0x1efbd000 0x0 0xd5c>; @@ -724,7 +733,7 @@ spi_nand: nand@0 { compatible = "spi-nand"; reg = <0>; - spi-max-frequency = <50000000>; + spi-max-frequency = <33000000>; spi-tx-bus-width = <1>; spi-rx-bus-width = <2>; }; @@ -947,7 +956,15 @@ , , , - ; + , + , + , + , + , + , + , + , + ; memory-region = <&qdma0_buf>, <&qdma1_buf>; memory-region-names = "qdma0-buf", "qdma1-buf"; diff --git a/target/linux/airoha/dts/an7583.dtsi b/target/linux/airoha/dts/an7583.dtsi index 33ac571ca317fd..fff6830b062489 100644 --- a/target/linux/airoha/dts/an7583.dtsi +++ b/target/linux/airoha/dts/an7583.dtsi @@ -572,6 +572,7 @@ pon_pcs: pcs@1fa08000 { compatible = "airoha,an7583-pcs-pon"; + #pcs-cells = <0>; reg = <0x0 0x1fa08000 0x0 0x1000>, <0x0 0x1fa80000 0x0 0x60>, <0x0 0x1fa80a00 0x0 0x164>, @@ -590,12 +591,11 @@ reset-names = "mac", "phy", "xfi"; airoha,scu = <&scuclk>; - - #pcs-cells = <0>; }; eth_pcs: pcs@1fa09000 { compatible = "airoha,an7583-pcs-eth"; + #pcs-cells = <0>; reg = <0x0 0x1fa09000 0x0 0x1000>, <0x0 0x1fa70000 0x0 0x60>, <0x0 0x1fa70a00 0x0 0x164>, @@ -613,8 +613,6 @@ reset-names = "mac", "phy"; airoha,scu = <&scuclk>; - - #pcs-cells = <0>; }; eth: ethernet@1fb50000 { @@ -671,7 +669,7 @@ gdm2: ethernet@2 { compatible = "airoha,eth-mac"; reg = <2>; - pcs = <&pon_pcs>; + pcs-handle = <&pon_pcs>; status = "disabled"; }; @@ -679,7 +677,7 @@ gdm3: ethernet@3 { compatible = "airoha,eth-mac"; reg = <3>; - pcs = <ð_pcs>; + pcs-handle = <ð_pcs>; airoha,gdm-srcport = <0x16>; status = "disabled"; diff --git a/target/linux/airoha/image/an7581.mk b/target/linux/airoha/image/an7581.mk index 87ed0e816442da..42cfb5abaa952a 100644 --- a/target/linux/airoha/image/an7581.mk +++ b/target/linux/airoha/image/an7581.mk @@ -121,6 +121,41 @@ define Device/gemtek_w1700k-ubi endef TARGET_DEVICES += gemtek_w1700k-ubi +define Device/econet_xr1710g-ubi + DEVICE_VENDOR := Econet + DEVICE_MODEL := XR1710G + DEVICE_VARIANT := UBI + DEVICE_DTS := an7581-xr1710g-ubi + DEVICE_PACKAGES := -airoha-en7581-npu-firmware \ + -kmod-input-gpio-keys-polled -kmod-leds-pwm \ + -kmod-pwm-airoha \ + airoha-en7581-mt7996-npu-firmware \ + apk-mbedtls fitblk \ + kmod-i2c-an7581 kmod-hwmon-nct7802 \ + kmod-mt7996-firmware rtl8261n-firmware \ + luci \ + luci-app-firewall luci-app-mlo \ + luci-app-package-manager \ + luci-i18n-mlo-zh-cn \ + luci-mod-admin-full \ + luci-proto-ppp luci-theme-bootstrap \ + rpcd-mod-rrdns uhttpd uhttpd-mod-ubus \ + wpad-basic-mbedtls + UBINIZE_OPTS := -E 5 + BLOCKSIZE := 128k + PAGESIZE := 2048 + UBOOTENV_IN_UBI := 1 + KERNEL_IN_UBI := 1 + KERNEL := kernel-bin | gzip + KERNEL_INITRAMFS := kernel-bin | lzma | \ + fit lzma $$(KDIR)/image-$$(firstword $$(DEVICE_DTS)).dtb with-initrd | pad-to 128k + KERNEL_INITRAMFS_SUFFIX := -recovery.itb + IMAGES := sysupgrade.itb + IMAGE/sysupgrade.itb := append-kernel | fit gzip $$(KDIR)/image-$$(firstword $$(DEVICE_DTS)).dtb external-static-with-rootfs | append-metadata + SOC := an7581 +endef +TARGET_DEVICES += econet_xr1710g-ubi + define Device/nokia_valyrian DEVICE_VENDOR := Nokia DEVICE_MODEL := Valyrian diff --git a/target/linux/airoha/patches-6.12/0401-pmdomain-airoha-fallback-to-PLL-registers-when-BL31-GET_FREQ-fails.patch b/target/linux/airoha/patches-6.12/0401-pmdomain-airoha-fallback-to-PLL-registers-when-BL31-GET_FREQ-fails.patch new file mode 100644 index 00000000000000..464b09913c2a52 --- /dev/null +++ b/target/linux/airoha/patches-6.12/0401-pmdomain-airoha-fallback-to-PLL-registers-when-BL31-GET_FREQ-fails.patch @@ -0,0 +1,100 @@ +--- a/drivers/pmdomain/mediatek/airoha-cpu-pmdomain.c 2026-04-02 12:03:41.783492903 +0800 ++++ b/drivers/pmdomain/mediatek/airoha-cpu-pmdomain.c 2026-04-02 12:04:07.772626445 +0800 +@@ -3,7 +3,9 @@ + #include + #include + #include ++#include + #include ++#include + #include + #include + #include +@@ -15,10 +17,15 @@ + FIELD_PREP(AIROHA_AVS_OP_MASK, 0x1)) + #define AIROHA_AVS_OP_GET_FREQ (AIROHA_AVS_OP_BASE | \ + FIELD_PREP(AIROHA_AVS_OP_MASK, 0x2)) ++#define AIROHA_EN7581_CPUPLL_SDM_PCW 0x1fa202b4 ++#define AIROHA_EN7581_PLL_PCW GENMASK(31, 24) ++#define AIROHA_EN7581_PLL_POSDIV GENMASK(6, 4) + + struct airoha_cpu_pmdomain_priv { + struct clk_hw hw; + struct generic_pm_domain pd; ++ struct device *dev; ++ void __iomem *en7581_pll_regs; + }; + + static long airoha_cpu_pmdomain_clk_round(struct clk_hw *hw, unsigned long rate, +@@ -30,13 +37,53 @@ + static unsigned long airoha_cpu_pmdomain_clk_get(struct clk_hw *hw, + unsigned long parent_rate) + { ++ struct airoha_cpu_pmdomain_priv *priv = ++ container_of(hw, struct airoha_cpu_pmdomain_priv, hw); + struct arm_smccc_res res; ++ s64 freq_mhz; ++ u32 pcw, posdiv; + + arm_smccc_1_1_invoke(AIROHA_SIP_AVS_HANDLE, AIROHA_AVS_OP_GET_FREQ, + 0, 0, 0, 0, 0, 0, &res); + + /* SMCCC returns freq in MHz */ +- return (int)(res.a0 * 1000 * 1000); ++ freq_mhz = (s64)res.a0; ++ if (freq_mhz > 0 && freq_mhz <= 2000) ++ return (unsigned long)freq_mhz * 1000 * 1000; ++ ++ /* ++ * Older EN7581 secure firmware exposes the AVS SMC handler but can ++ * still return 0 for GET_FREQ, which prevents cpufreq-dt from ++ * registering a policy. Fall back to the CPU PLL registers used by ++ * the vendor BL31 implementation in that case. ++ */ ++ if (!priv->en7581_pll_regs) ++ return 0; ++ ++ pcw = FIELD_GET(AIROHA_EN7581_PLL_PCW, ++ readl_relaxed(priv->en7581_pll_regs)); ++ posdiv = FIELD_GET(AIROHA_EN7581_PLL_POSDIV, ++ readl_relaxed(priv->en7581_pll_regs + sizeof(u32))); ++ ++ switch (posdiv) { ++ case 0: ++ freq_mhz = pcw * 50; ++ break; ++ case 1: ++ freq_mhz = pcw * 25; ++ break; ++ default: ++ return 0; ++ } ++ ++ if (freq_mhz < 500 || freq_mhz > 1200 || freq_mhz % 50) ++ return 0; ++ ++ dev_warn_once(priv->dev, ++ "BL31 AVS GET_FREQ returned %lld, falling back to direct PLL register decode\n", ++ (long long)(s64)res.a0); ++ ++ return (unsigned long)freq_mhz * 1000 * 1000; + } + + /* Airoha CPU clk SMCC is always enabled */ +@@ -80,6 +127,16 @@ + if (!priv) + return -ENOMEM; + ++ priv->dev = dev; ++ ++ if (of_machine_is_compatible("airoha,en7581")) { ++ priv->en7581_pll_regs = devm_ioremap(dev, ++ AIROHA_EN7581_CPUPLL_SDM_PCW, ++ sizeof(u32) * 2); ++ if (!priv->en7581_pll_regs) ++ return -ENOMEM; ++ } ++ + /* Init and register a get-only clk for Cpufreq */ + priv->hw.init = &init; + ret = devm_clk_hw_register(dev, &priv->hw); diff --git a/target/linux/airoha/patches-6.12/620-net-pcs-airoha-fix-USXGMII-rate-adaptation-on-speed.patch b/target/linux/airoha/patches-6.12/620-net-pcs-airoha-fix-USXGMII-rate-adaptation-on-speed.patch new file mode 100644 index 00000000000000..5b886a1ef16f26 --- /dev/null +++ b/target/linux/airoha/patches-6.12/620-net-pcs-airoha-fix-USXGMII-rate-adaptation-on-speed.patch @@ -0,0 +1,151 @@ +From: Ryan Chen +Date: Thu, 06 Mar 2026 12:00:00 -0600 +Subject: [PATCH 1/2] net: pcs: airoha: fix USXGMII rate adaptation on speed + changes + +The USXGMII rate adaptation code in airoha_pcs_link_up() is inside an +else branch that only executes when neg_mode is not +PHYLINK_PCS_NEG_INBAND_ENABLED. However, USXGMII uses in-band +autonegotiation, so this code path is never reached on speed changes. +The RATE_UPDATE_MODE and FORCE_RATE_ADAPT_MODE registers remain +configured for the previous speed. + +Fix this by moving the USXGMII rate adaptation block out of the +forced-mode-only branch so it runs regardless of the negotiation mode. +The SGMII in-band FIFO threshold adjustment and the SGMII/1000BASEX +forced-mode speed configuration remain gated on their respective +negotiation modes as before. + +Signed-off-by: Ryan Chen +--- + drivers/net/pcs/airoha/pcs-airoha-common.c | 127 +++++++++++--------- + 1 file changed, 69 insertions(+), 58 deletions(-) + +--- a/drivers/net/pcs/airoha/pcs-airoha-common.c ++++ b/drivers/net/pcs/airoha/pcs-airoha-common.c +@@ -635,64 +635,69 @@ + + data = priv->data; + +- if (neg_mode == PHYLINK_PCS_NEG_INBAND_ENABLED) { +- if (interface == PHY_INTERFACE_MODE_SGMII) { +- regmap_update_bits(priv->hsgmii_rate_adp, +- AIROHA_PCS_HSGMII_RATE_ADAPT_CTRL_1, +- AIROHA_PCS_HSGMII_RATE_ADAPT_RX_AFIFO_WR_THR | +- AIROHA_PCS_HSGMII_RATE_ADAPT_RX_AFIFO_RD_THR, +- FIELD_PREP(AIROHA_PCS_HSGMII_RATE_ADAPT_RX_AFIFO_WR_THR, 0x0) | +- FIELD_PREP(AIROHA_PCS_HSGMII_RATE_ADAPT_RX_AFIFO_RD_THR, 0x0)); +- udelay(1); +- regmap_update_bits(priv->hsgmii_rate_adp, +- AIROHA_PCS_HSGMII_RATE_ADAPT_CTRL_1, +- AIROHA_PCS_HSGMII_RATE_ADAPT_RX_AFIFO_WR_THR | +- AIROHA_PCS_HSGMII_RATE_ADAPT_RX_AFIFO_RD_THR, +- FIELD_PREP(AIROHA_PCS_HSGMII_RATE_ADAPT_RX_AFIFO_WR_THR, 0xf) | +- FIELD_PREP(AIROHA_PCS_HSGMII_RATE_ADAPT_RX_AFIFO_RD_THR, 0x5)); +- } +- } else { +- if (interface == PHY_INTERFACE_MODE_USXGMII || +- interface == PHY_INTERFACE_MODE_10GBASER) { +- u32 mode; +- u32 rate_adapt; +- +- switch (speed) { +- case SPEED_10000: +- rate_adapt = AIROHA_PCS_HSGMII_RATE_ADPT_FORCE_RATE_ADAPT_MODE_10000; +- mode = AIROHA_PCS_USXGMII_MODE_10000; +- break; +- case SPEED_5000: +- rate_adapt = AIROHA_PCS_HSGMII_RATE_ADPT_FORCE_RATE_ADAPT_MODE_5000; +- mode = AIROHA_PCS_USXGMII_MODE_5000; +- break; +- case SPEED_2500: +- rate_adapt = AIROHA_PCS_HSGMII_RATE_ADPT_FORCE_RATE_ADAPT_MODE_2500; +- mode = AIROHA_PCS_USXGMII_MODE_2500; +- break; +- case SPEED_1000: +- rate_adapt = AIROHA_PCS_HSGMII_RATE_ADPT_FORCE_RATE_ADAPT_MODE_1000; +- mode = AIROHA_PCS_USXGMII_MODE_1000; +- break; +- case SPEED_100: +- rate_adapt = AIROHA_PCS_HSGMII_RATE_ADPT_FORCE_RATE_ADAPT_MODE_100; +- mode = AIROHA_PCS_USXGMII_MODE_100; +- break; +- } +- +- /* Trigger USXGMII change mode and force selected speed */ +- regmap_update_bits(priv->usxgmii_pcs, AIROHA_PCS_USXGMII_PCS_AN_CONTROL_7, +- AIROHA_PCS_USXGMII_RATE_UPDATE_MODE | +- AIROHA_PCS_USXGMII_MODE, +- AIROHA_PCS_USXGMII_RATE_UPDATE_MODE | mode); +- +- regmap_update_bits(priv->hsgmii_rate_adp, AIROHA_PCS_HSGMII_RATE_ADAPT_CTRL_11, +- AIROHA_PCS_HSGMII_RATE_ADPT_FORCE_RATE_ADAPT_MODE_EN | +- AIROHA_PCS_HSGMII_RATE_ADPT_FORCE_RATE_ADAPT_MODE, +- AIROHA_PCS_HSGMII_RATE_ADPT_FORCE_RATE_ADAPT_MODE_EN | +- rate_adapt); ++ if (neg_mode == PHYLINK_PCS_NEG_INBAND_ENABLED && ++ interface == PHY_INTERFACE_MODE_SGMII) { ++ regmap_update_bits(priv->hsgmii_rate_adp, ++ AIROHA_PCS_HSGMII_RATE_ADAPT_CTRL_1, ++ AIROHA_PCS_HSGMII_RATE_ADAPT_RX_AFIFO_WR_THR | ++ AIROHA_PCS_HSGMII_RATE_ADAPT_RX_AFIFO_RD_THR, ++ FIELD_PREP(AIROHA_PCS_HSGMII_RATE_ADAPT_RX_AFIFO_WR_THR, 0x0) | ++ FIELD_PREP(AIROHA_PCS_HSGMII_RATE_ADAPT_RX_AFIFO_RD_THR, 0x0)); ++ udelay(1); ++ regmap_update_bits(priv->hsgmii_rate_adp, ++ AIROHA_PCS_HSGMII_RATE_ADAPT_CTRL_1, ++ AIROHA_PCS_HSGMII_RATE_ADAPT_RX_AFIFO_WR_THR | ++ AIROHA_PCS_HSGMII_RATE_ADAPT_RX_AFIFO_RD_THR, ++ FIELD_PREP(AIROHA_PCS_HSGMII_RATE_ADAPT_RX_AFIFO_WR_THR, 0xf) | ++ FIELD_PREP(AIROHA_PCS_HSGMII_RATE_ADAPT_RX_AFIFO_RD_THR, 0x5)); ++ } ++ ++ /* Update USXGMII rate adaptation for the current speed. ++ * This must run regardless of negotiation mode because USXGMII ++ * uses in-band autoneg but still requires the rate adaptation ++ * registers to be updated when the link speed changes. ++ */ ++ if (interface == PHY_INTERFACE_MODE_USXGMII || ++ interface == PHY_INTERFACE_MODE_10GBASER) { ++ u32 mode; ++ u32 rate_adapt; ++ ++ switch (speed) { ++ case SPEED_10000: ++ rate_adapt = AIROHA_PCS_HSGMII_RATE_ADPT_FORCE_RATE_ADAPT_MODE_10000; ++ mode = AIROHA_PCS_USXGMII_MODE_10000; ++ break; ++ case SPEED_5000: ++ rate_adapt = AIROHA_PCS_HSGMII_RATE_ADPT_FORCE_RATE_ADAPT_MODE_5000; ++ mode = AIROHA_PCS_USXGMII_MODE_5000; ++ break; ++ case SPEED_2500: ++ rate_adapt = AIROHA_PCS_HSGMII_RATE_ADPT_FORCE_RATE_ADAPT_MODE_2500; ++ mode = AIROHA_PCS_USXGMII_MODE_2500; ++ break; ++ case SPEED_1000: ++ rate_adapt = AIROHA_PCS_HSGMII_RATE_ADPT_FORCE_RATE_ADAPT_MODE_1000; ++ mode = AIROHA_PCS_USXGMII_MODE_1000; ++ break; ++ case SPEED_100: ++ rate_adapt = AIROHA_PCS_HSGMII_RATE_ADPT_FORCE_RATE_ADAPT_MODE_100; ++ mode = AIROHA_PCS_USXGMII_MODE_100; ++ break; + } + ++ regmap_update_bits(priv->usxgmii_pcs, AIROHA_PCS_USXGMII_PCS_AN_CONTROL_7, ++ AIROHA_PCS_USXGMII_RATE_UPDATE_MODE | ++ AIROHA_PCS_USXGMII_MODE, ++ AIROHA_PCS_USXGMII_RATE_UPDATE_MODE | mode); ++ ++ regmap_update_bits(priv->hsgmii_rate_adp, AIROHA_PCS_HSGMII_RATE_ADAPT_CTRL_11, ++ AIROHA_PCS_HSGMII_RATE_ADPT_FORCE_RATE_ADAPT_MODE_EN | ++ AIROHA_PCS_HSGMII_RATE_ADPT_FORCE_RATE_ADAPT_MODE, ++ AIROHA_PCS_HSGMII_RATE_ADPT_FORCE_RATE_ADAPT_MODE_EN | ++ rate_adapt); ++ } ++ ++ if (neg_mode != PHYLINK_PCS_NEG_INBAND_ENABLED) { + if (interface == PHY_INTERFACE_MODE_SGMII || + interface == PHY_INTERFACE_MODE_1000BASEX) { + u32 force_speed; diff --git a/target/linux/airoha/patches-6.12/621-net-pcs-airoha-AN7581-add-global-digital-reset-for.patch b/target/linux/airoha/patches-6.12/621-net-pcs-airoha-AN7581-add-global-digital-reset-for.patch new file mode 100644 index 00000000000000..6b1067f59c886d --- /dev/null +++ b/target/linux/airoha/patches-6.12/621-net-pcs-airoha-AN7581-add-global-digital-reset-for.patch @@ -0,0 +1,114 @@ +From: Ryan Chen +Date: Thu, 06 Mar 2026 12:00:00 -0600 +Subject: [PATCH 2/2] net: pcs: airoha: AN7581: add global digital reset for + USXGMII speed changes + +On the EN7581, when phylink renegotiates a new speed within the same +USXGMII interface mode, only link_up() is called (not pcs_config()). +The existing link_up() only resets the TXPCS, which is insufficient: +the RX data path (RXPCS, FIFOs, PMA digital logic) remains configured +for the previous speed and no traffic flows. + +Fix this by adding a global digital reset sequence to the AN7581 +link_up callback for USXGMII and 10GBASE-R interfaces. + +Guard the global reset with a needs_full_init flag set by pcs_config() +after bringup(). On the first link_up after bringup, skip the global +reset since bringup already performed full PCS/PMA initialization. +During this window the PHY firmware may still be loading and its SerDes +is unstable — a global PCS reset can corrupt the USXGMII autoneg state +permanently. On subsequent link_up calls (cable replug, speed changes), +the global reset fires normally since the PHY is fully initialized. + +Signed-off-by: Ryan Chen +--- + drivers/net/pcs/airoha/pcs-airoha-common.c | 1 + + drivers/net/pcs/airoha/pcs-airoha.h | 1 + + drivers/net/pcs/airoha/pcs-an7581.c | 55 +++++++++++++++++++++ + 3 files changed, 57 insertions(+) + +--- a/drivers/net/pcs/airoha/pcs-airoha.h ++++ b/drivers/net/pcs/airoha/pcs-airoha.h +@@ -1188,6 +1188,7 @@ + struct reset_control_bulk_data rsts[AIROHA_PCS_MAX_NUM_RSTS]; + + bool manual_rx_calib; ++ bool needs_full_init; + }; + + struct airoha_pcs_port { +--- a/drivers/net/pcs/airoha/pcs-airoha-common.c ++++ b/drivers/net/pcs/airoha/pcs-airoha-common.c +@@ -422,6 +422,7 @@ + ret = data->bringup(priv, interface); + if (ret) + return ret; ++ priv->needs_full_init = true; + } + + /* Set final configuration for various modes */ +--- a/drivers/net/pcs/airoha/pcs-an7581.c ++++ b/drivers/net/pcs/airoha/pcs-an7581.c +@@ -1363,6 +1363,29 @@ + + void an7581_pcs_phya_link_up(struct airoha_pcs_priv *priv) + { ++ u32 rst_mask = AIROHA_PCS_PMA_SW_HSG_RXPCS_RST_N | ++ AIROHA_PCS_PMA_SW_HSG_TXPCS_RST_N | ++ AIROHA_PCS_PMA_SW_XFI_RXPCS_BIST_RST_N | ++ AIROHA_PCS_PMA_SW_XFI_RXPCS_RST_N | ++ AIROHA_PCS_PMA_SW_XFI_TXPCS_RST_N | ++ AIROHA_PCS_PMA_SW_TX_FIFO_RST_N | ++ AIROHA_PCS_PMA_SW_REF_RST_N | ++ AIROHA_PCS_PMA_SW_ALLPCS_RST_N | ++ AIROHA_PCS_PMA_SW_PMA_RST_N | ++ AIROHA_PCS_PMA_SW_TX_RST_N | ++ AIROHA_PCS_PMA_SW_RX_RST_N | ++ AIROHA_PCS_PMA_SW_RX_FIFO_RST_N; ++ u32 val; ++ int i; ++ ++ /* Wait for any ongoing PCS reset to complete before proceeding. */ ++ for (i = 0; i < 50; i++) { ++ regmap_read(priv->xfi_pma, AIROHA_PCS_PMA_SW_RST_SET, &val); ++ if ((val & rst_mask) == rst_mask) ++ break; ++ usleep_range(1000, 2000); ++ } ++ + /* Reset TXPCS on link up */ + regmap_clear_bits(priv->xfi_pma, AIROHA_PCS_PMA_SW_RST_SET, + AIROHA_PCS_PMA_SW_HSG_TXPCS_RST_N); +@@ -1371,6 +1394,32 @@ + + regmap_set_bits(priv->xfi_pma, AIROHA_PCS_PMA_SW_RST_SET, + AIROHA_PCS_PMA_SW_HSG_TXPCS_RST_N); ++ ++ if (priv->interface == PHY_INTERFACE_MODE_USXGMII || ++ priv->interface == PHY_INTERFACE_MODE_10GBASER) { ++ if (priv->needs_full_init) { ++ priv->needs_full_init = false; ++ dev_dbg(priv->dev, "link_up: skipping global reset (bringup just ran)\n"); ++ return; ++ } ++ ++ dev_dbg(priv->dev, "link_up: performing global digital reset\n"); ++ an7581_pcs_cdr_reset(priv, priv->interface, false); ++ ++ /* Assert global reset (keep REF_RST_N high) */ ++ regmap_update_bits(priv->xfi_pma, AIROHA_PCS_PMA_SW_RST_SET, ++ rst_mask, AIROHA_PCS_PMA_SW_REF_RST_N); ++ ++ usleep_range(100, 200); ++ ++ /* Deassert global reset */ ++ regmap_set_bits(priv->xfi_pma, AIROHA_PCS_PMA_SW_RST_SET, ++ rst_mask); ++ ++ usleep_range(5000, 7000); ++ ++ an7581_pcs_cdr_reset(priv, priv->interface, false); ++ } + } + + static bool an7581_pcs_have_rx_signal(struct airoha_pcs_priv *priv) diff --git a/target/linux/airoha/patches-6.12/920-hwrng-airoha-fix-init-sequence-default-to-DRBG.patch b/target/linux/airoha/patches-6.12/920-hwrng-airoha-fix-init-sequence-default-to-DRBG.patch new file mode 100644 index 00000000000000..d40046b2a0086b --- /dev/null +++ b/target/linux/airoha/patches-6.12/920-hwrng-airoha-fix-init-sequence-default-to-DRBG.patch @@ -0,0 +1,151 @@ +From ef64a5e87ad086ce701e63f683886f8f06e65f65 Mon Sep 17 00:00:00 2001 +From: Hello Example +Date: Wed, 7 Jan 2026 14:41:14 +0100 +Subject: [PATCH] hwrng: airoha: fix init sequence, default to DRBG, allow RAW + +Initialize the TRNG more robustly by enabling the oscillator bit and +opening the required SCU clock gates when available via device tree. + +Default the driver to DRBG output and permit opting into raw mode with +the `airoha,raw-mode` DT property. Use quality 900 only in DRBG mode. + +Derived from OpenWRT-fanboy/OpenW1700k PR #1 patch 18/18. +Link: https://github.com/OpenWRT-fanboy/OpenW1700k/pull/1 +--- + drivers/char/hw_random/airoha-trng.c | 55 ++++++++++++++++++++++++++++------- + 1 file changed, 45 insertions(+), 10 deletions(-) + +--- a/drivers/char/hw_random/airoha-trng.c ++++ b/drivers/char/hw_random/airoha-trng.c +@@ -7,8 +7,11 @@ + #include + #include + #include + #include ++#include ++#include + #include ++#include + + #define TRNG_IP_RDY 0x800 + #define CNT_TRANS GENMASK(15, 8) +@@ -16,6 +19,7 @@ + #define TRNG_NS_SEK_AND_DAT_EN 0x804 + #define RNG_EN BIT(31) /* referenced as ring_en */ + #define RAW_DATA_EN BIT(16) ++#define RNG_OSC_EN BIT(1) + #define TRNG_HEALTH_TEST_SW_RST 0x808 + #define SW_RST BIT(0) /* Active High */ + #define TRNG_INTR_EN 0x818 +@@ -31,6 +35,16 @@ + #define RAW_DATA_VALID BIT(7) + + #define TRNG_RAW_DATA_OUT 0x828 ++#define TRNG_TEST_MODE_SHA_DONE 0x838 ++#define DRBG_DATA_RDY BIT(31) ++#define TRNG_DRBG_DATA_OUT 0x83c ++ ++#define REG_SCU_BUS_CLK_GAT 0x1e4 ++#define REG_SCU_PER_CLK_GAT_1 0x1ec ++#define REG_SCU_PER_CLK_GAT_2 0x200 ++#define SCU_TRNG_BUS_EN BIT(3) ++#define SCU_TRNG_PER1_EN BIT(21) ++#define SCU_TRNG_PER2_EN BIT(0) + + #define TRNG_CNT_TRANS_VALID 0x80 + #define BUSY_LOOP_SLEEP 10 +@@ -41,6 +53,8 @@ struct airoha_trng { + struct device *dev; + + struct completion rng_op_done; ++ struct regmap *scu; ++ bool use_drbg; + }; + + static int airoha_trng_irq_mask(struct airoha_trng *trng) +@@ -77,8 +91,14 @@ static int airoha_trng_init(struct hwrng + int ret; + u32 val; + + val = readl(trng->base + TRNG_NS_SEK_AND_DAT_EN); +- val |= RNG_EN; ++ val |= RNG_EN | RNG_OSC_EN; + writel(val, trng->base + TRNG_NS_SEK_AND_DAT_EN); ++ ++ if (trng->scu) { ++ regmap_set_bits(trng->scu, REG_SCU_BUS_CLK_GAT, SCU_TRNG_BUS_EN); ++ regmap_set_bits(trng->scu, REG_SCU_PER_CLK_GAT_1, SCU_TRNG_PER1_EN); ++ regmap_set_bits(trng->scu, REG_SCU_PER_CLK_GAT_2, SCU_TRNG_PER2_EN); ++ } + + /* Set out of SW Reset */ + airoha_trng_irq_unmask(trng); +@@ -135,15 +155,25 @@ static int airoha_trng_read(struct hwrng + { + struct airoha_trng *trng = container_of(rng, struct airoha_trng, rng); + u32 *data = buf; +- u32 status; ++ u32 status, status_reg, data_reg, rdy_mask; + int ret; + +- ret = readl_poll_timeout(trng->base + TRNG_HEALTH_TEST_STATUS, status, +- status & RAW_DATA_VALID, 10, 1000); ++ if (trng->use_drbg) { ++ status_reg = TRNG_TEST_MODE_SHA_DONE; ++ data_reg = TRNG_DRBG_DATA_OUT; ++ rdy_mask = DRBG_DATA_RDY; ++ } else { ++ status_reg = TRNG_HEALTH_TEST_STATUS; ++ data_reg = TRNG_RAW_DATA_OUT; ++ rdy_mask = RAW_DATA_VALID; ++ } ++ ++ ret = readl_poll_timeout(trng->base + status_reg, status, ++ status & rdy_mask, 10, 10000); + if (ret < 0) { +- dev_err(trng->dev, "Timeout waiting for TRNG RAW Data valid\n"); ++ dev_err(trng->dev, "Timeout waiting for TRNG data\n"); + return ret; + } + +- *data = readl(trng->base + TRNG_RAW_DATA_OUT); ++ *data = readl(trng->base + data_reg); + + return 4; + } +@@ -173,6 +203,11 @@ static int airoha_trng_probe(struct plat + if (IS_ERR(trng->base)) + return PTR_ERR(trng->base); + ++ trng->scu = syscon_regmap_lookup_by_phandle(dev->of_node, "airoha,scu"); ++ if (IS_ERR(trng->scu)) ++ trng->scu = NULL; ++ trng->use_drbg = !of_property_read_bool(dev->of_node, "airoha,raw-mode"); ++ + irq = platform_get_irq(pdev, 0); + if (irq < 0) + return irq; +@@ -191,9 +226,12 @@ static int airoha_trng_probe(struct plat + val |= RST_STARTUP_INITR_EN; + writel(val, trng->base + TRNG_INTR_EN); + +- /* Set output to raw data */ ++ /* Default to DRBG output, allow raw mode via device-tree. */ + val = readl(trng->base + TRNG_NS_SEK_AND_DAT_EN); +- val |= RAW_DATA_EN; ++ if (trng->use_drbg) ++ val &= ~RAW_DATA_EN; ++ else ++ val |= RAW_DATA_EN; + writel(val, trng->base + TRNG_NS_SEK_AND_DAT_EN); + + /* Put it in SW Reset */ +@@ -204,7 +242,7 @@ static int airoha_trng_probe(struct plat + trng->rng.init = airoha_trng_init; + trng->rng.cleanup = airoha_trng_cleanup; + trng->rng.read = airoha_trng_read; +- trng->rng.quality = 900; ++ trng->rng.quality = trng->use_drbg ? 900 : 0; + + ret = devm_hwrng_register(dev, &trng->rng); + if (ret) { diff --git a/target/linux/airoha/patches-6.12/921-net-airoha-npu-fix-firmware-loading-issue.patch b/target/linux/airoha/patches-6.12/921-net-airoha-npu-fix-firmware-loading-issue.patch new file mode 100644 index 00000000000000..28cc68a18fa3c5 --- /dev/null +++ b/target/linux/airoha/patches-6.12/921-net-airoha-npu-fix-firmware-loading-issue.patch @@ -0,0 +1,32 @@ +From 9b835526b225a2d1c34c75f360d224395432f174 Mon Sep 17 00:00:00 2001 +From: Ziyang Huang +Date: Mon, 30 Mar 2026 22:58:49 +0800 +Subject: [PATCH 1/1] net: airoha: npu: fix firmware loading issue. + +This driver is loaded before rootfs mounting. So firmware doesn't exist. +When CONFIG_FW_LOADER_USER_HELPER=n, request_firmware() will return -ENOENT, +-EPROBE_DEFER will be returned to make driver load again after rootfs mounted. + +But when CONFIG_FW_LOADER_USER_HELPER=y, request_firmware() will send uevent +and wait for 60s (the value of fw_fallback_config.loading_timeout). But no +user process is running now (including init, coldplug, hotplug), so +-ETIMEDOUT will return, making driver probe fail. + +To avoid the second cause, let's use request_firmware_direct(). + +Signed-off-by: Ziyang Huang +--- + drivers/net/ethernet/airoha/airoha_npu.c | 2 +- + 1 file changed, 1 insertion(+), 1 deletion(-) + +--- a/drivers/net/ethernet/airoha/airoha_npu.c ++++ b/drivers/net/ethernet/airoha/airoha_npu.c +@@ -202,7 +202,7 @@ static int airoha_npu_load_firmware(struct device *dev, void __iomem *addr, + const struct firmware *fw; + int ret; + +- ret = request_firmware(&fw, fw_name, dev); ++ ret = request_firmware_direct(&fw, fw_name, dev); + if (ret) + return ret == -ENOENT ? -EPROBE_DEFER : ret; + diff --git a/target/linux/airoha/patches-6.12/922-net-airoha-classify-external-lan-ports-from-DT.patch b/target/linux/airoha/patches-6.12/922-net-airoha-classify-external-lan-ports-from-DT.patch new file mode 100644 index 00000000000000..ac0f5d26c07a17 --- /dev/null +++ b/target/linux/airoha/patches-6.12/922-net-airoha-classify-external-lan-ports-from-DT.patch @@ -0,0 +1,79 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: Codex +Date: Fri, 3 Apr 2026 00:00:00 +0800 +Subject: [PATCH] net: airoha: classify external lan ports from DT + +XR1710G and W1700K expose an external PHY on GDM4, but that port is a +LAN member, not a WAN uplink. The current driver assumes only GDM1 is a +LAN port on EN7581 and therefore treats GDM4 as WAN. + +That misclassification feeds directly into the recent offload code paths: +- the port is pinned to the WAN QDMA block +- FE LAN/WAN MAC programming uses the wrong register bank +- PPE fast-path handling misses the LAN case + +As a result, NPU/PPE offload never becomes effective on these boards and +traffic falls back to CPU forwarding. + +Allow board DTS files to mark external LAN ports explicitly and keep an +OpenWrt netdev-name fallback for already named boards. + +Signed-off-by: Codex +--- + drivers/net/ethernet/airoha/airoha_eth.c | 13 +++++++++++++ + drivers/net/ethernet/airoha/airoha_eth.h | 7 ++----- + 2 files changed, 15 insertions(+), 5 deletions(-) + +--- a/drivers/net/ethernet/airoha/airoha_eth.c ++++ b/drivers/net/ethernet/airoha/airoha_eth.c +@@ -78,6 +78,19 @@ static bool airhoa_is_phy_external(struc + } + #endif + ++static bool airoha_port_is_lan(struct device_node *np, u32 id) ++{ ++ const char *name; ++ ++ if (of_property_read_bool(np, "airoha,lan-port")) ++ return true; ++ ++ if (!of_property_read_string(np, "openwrt,netdev-name", &name)) ++ return !strncmp(name, "lan", 3); ++ ++ return id == AIROHA_GDM1_IDX; ++} ++ + static void airoha_set_macaddr(struct airoha_gdm_port *port, const u8 *addr) + { + struct airoha_eth *eth = port->qdma->eth; +@@ -3100,6 +3113,7 @@ static int airoha_alloc_gdm_port(struct + spin_lock_init(&port->stats.lock); + port->eth = eth; + port->dev = dev; ++ port->lan = airoha_port_is_lan(np, id); + port->id = id; + eth->ports[p] = port; + +--- a/drivers/net/ethernet/airoha/airoha_eth.h ++++ b/drivers/net/ethernet/airoha/airoha_eth.h +@@ -537,6 +537,7 @@ struct airoha_gdm_port { + struct airoha_eth *eth; + struct net_device *dev; + int id; ++ bool lan; + + #if defined(CONFIG_PCS_AIROHA) + struct phylink *phylink; +@@ -637,11 +638,7 @@ static inline bool airoha_is_lan_gdm_por + + static inline bool airoha_is_lan_gdm_port(struct airoha_gdm_port *port) + { +- /* GDM1 port on EN7581 SoC is connected to the lan dsa switch. +- * GDM{2,3,4} can be used as wan port connected to an external +- * phy module. +- */ +- return port->id == 1; ++ return port->lan; + } + + static inline bool airoha_is_7581(struct airoha_eth *eth) diff --git a/target/linux/airoha/patches-6.18/0401-pmdomain-airoha-fallback-to-PLL-registers-when-BL31-GET_FREQ-fails.patch b/target/linux/airoha/patches-6.18/0401-pmdomain-airoha-fallback-to-PLL-registers-when-BL31-GET_FREQ-fails.patch new file mode 100644 index 00000000000000..0b924b988ed33a --- /dev/null +++ b/target/linux/airoha/patches-6.18/0401-pmdomain-airoha-fallback-to-PLL-registers-when-BL31-GET_FREQ-fails.patch @@ -0,0 +1,285 @@ +From 3e8e3301869c449365c284be9ab1edf5c7bce945 Mon Sep 17 00:00:00 2001 +From: Yanghan Ye +Date: Sun, 12 Apr 2026 18:55:52 +0800 +Subject: [PATCH] pmdomain: airoha: fall back to PLL registers when BL31 + GET_FREQ fails or ATF SMC is missing + +Some EN7581 boot chains either do not expose the Airoha ATF AVS +service at all, or return an invalid value from GET_FREQ during +cpufreq-dt setup. In both cases the CPU PM domain driver cannot provide +a sane clock implementation and cpufreq probing aborts. + +Teach the driver to fall back to direct CPU PLL decoding and +programming on EN7581 whenever the SMC path is unavailable or unusable, +while keeping the SMC path for platforms where firmware support is +present. + +Signed-off-by: Yanghan Ye +--- + .../pmdomain/mediatek/airoha-cpu-pmdomain.c | 189 +++++++++++++++++- + 1 file changed, 180 insertions(+), 9 deletions(-) + +diff --git a/drivers/pmdomain/mediatek/airoha-cpu-pmdomain.c b/drivers/pmdomain/mediatek/airoha-cpu-pmdomain.c +index 3b1d202..d64b7fa 100644 +--- a/drivers/pmdomain/mediatek/airoha-cpu-pmdomain.c ++++ b/drivers/pmdomain/mediatek/airoha-cpu-pmdomain.c +@@ -3,7 +3,10 @@ + #include + #include + #include ++#include ++#include + #include ++#include + #include + #include + #include +@@ -16,9 +19,32 @@ + #define AIROHA_AVS_OP_GET_FREQ (AIROHA_AVS_OP_BASE | \ + FIELD_PREP(AIROHA_AVS_OP_MASK, 0x2)) + ++#define CPUPLL_CLK_MUX 0x1e0 ++#define PLLRG_PROTECT 0x268 ++#define CPUPLL_SDM_PCW 0x2b4 ++#define CPUPLL_SDM_PCW_CHG 0x2b8 ++ ++#define CPUPLL_PCW_INT_MASK GENMASK(30, 24) ++#define CPUPLL_XTAL_MHZ 50 ++#define CPUPLL_CHG_BIT BIT(0) ++#define CPUPLL_CHG_POSDIV_MASK GENMASK(6, 4) ++#define PLLRG_PROTECT_MASK GENMASK(7, 0) ++#define PLLRG_PROTECT_KEY 0x12 ++ ++#define MCUCFG_CK_SWITCH_UNLOCK 0x640 ++#define MCUCFG_CK_SOURCE_SEL 0x7c0 ++ ++#define MCUCFG_CK_UNLOCK_KEY 0x12 ++#define MCUCFG_CK_SEL_MASK GENMASK(10, 9) ++#define MCUCFG_CK_SEL_ARMPLL 1 ++#define MCUCFG_CK_SEL_PLL2 3 ++ + struct airoha_cpu_pmdomain_priv { + struct clk_hw hw; + struct generic_pm_domain pd; ++ bool use_smc; ++ void __iomem *pll_base; ++ void __iomem *mcucfg; + }; + + static int airoha_cpu_pmdomain_clk_determine_rate(struct clk_hw *hw, +@@ -27,16 +53,40 @@ static int airoha_cpu_pmdomain_clk_determine_rate(struct clk_hw *hw, + return 0; + } + +-static unsigned long airoha_cpu_pmdomain_clk_get(struct clk_hw *hw, +- unsigned long parent_rate) ++static unsigned long airoha_cpu_smc_clk_get(void) + { + struct arm_smccc_res res; + + arm_smccc_1_1_invoke(AIROHA_SIP_AVS_HANDLE, AIROHA_AVS_OP_GET_FREQ, + 0, 0, 0, 0, 0, 0, &res); + +- /* SMCCC returns freq in MHz */ +- return (int)(res.a0 * 1000 * 1000); ++ return (unsigned long)(res.a0 * 1000 * 1000); ++} ++ ++static unsigned long airoha_cpu_pll_clk_get(struct airoha_cpu_pmdomain_priv *priv) ++{ ++ u32 pcw, chg; ++ unsigned int pcw_int, posdiv; ++ ++ pcw = readl(priv->pll_base + CPUPLL_SDM_PCW); ++ pcw_int = FIELD_GET(CPUPLL_PCW_INT_MASK, pcw); ++ ++ chg = readl(priv->pll_base + CPUPLL_SDM_PCW_CHG); ++ posdiv = FIELD_GET(CPUPLL_CHG_POSDIV_MASK, chg); ++ ++ return (unsigned long)pcw_int * CPUPLL_XTAL_MHZ * 1000000UL >> posdiv; ++} ++ ++static unsigned long airoha_cpu_pmdomain_clk_get(struct clk_hw *hw, ++ unsigned long parent_rate) ++{ ++ struct airoha_cpu_pmdomain_priv *priv = ++ container_of(hw, struct airoha_cpu_pmdomain_priv, hw); ++ ++ if (priv->use_smc) ++ return airoha_cpu_smc_clk_get(); ++ ++ return airoha_cpu_pll_clk_get(priv); + } + + /* Airoha CPU clk SMCC is always enabled */ +@@ -51,8 +101,7 @@ static const struct clk_ops airoha_cpu_pmdomain_clk_ops = { + .determine_rate = airoha_cpu_pmdomain_clk_determine_rate, + }; + +-static int airoha_cpu_pmdomain_set_performance_state(struct generic_pm_domain *domain, +- unsigned int state) ++static int airoha_cpu_smc_set_freq(unsigned int state) + { + struct arm_smccc_res res; + +@@ -63,6 +112,106 @@ static int airoha_cpu_pmdomain_set_performance_state(struct generic_pm_domain *d + return res.a0 & BIT(0) ? -EINVAL : 0; + } + ++static void airoha_cpu_clock_switch(void __iomem *pll_base, ++ void __iomem *mcucfg, unsigned int sel) ++{ ++ u32 val; ++ ++ if (sel != 0) { ++ val = readl(pll_base + CPUPLL_CLK_MUX); ++ writel(val | BIT(sel - 1), pll_base + CPUPLL_CLK_MUX); ++ } ++ ++ val = readl(mcucfg + MCUCFG_CK_SWITCH_UNLOCK); ++ writel((val & ~0x1f) | MCUCFG_CK_UNLOCK_KEY, ++ mcucfg + MCUCFG_CK_SWITCH_UNLOCK); ++ ++ val = readl(mcucfg + MCUCFG_CK_SOURCE_SEL); ++ val &= ~MCUCFG_CK_SEL_MASK; ++ val |= FIELD_PREP(MCUCFG_CK_SEL_MASK, sel); ++ writel(val, mcucfg + MCUCFG_CK_SOURCE_SEL); ++} ++ ++static int airoha_cpu_pll_set_freq(struct airoha_cpu_pmdomain_priv *priv, ++ unsigned int state) ++{ ++ unsigned int freq_mhz = 500 + state * 50; ++ unsigned int posdiv, pcw_int; ++ unsigned long flags; ++ u32 val, old_chg, cleared; ++ ++ if (freq_mhz < 1000) { ++ posdiv = 1; ++ pcw_int = freq_mhz / 25; ++ } else { ++ posdiv = 0; ++ pcw_int = freq_mhz / 50; ++ } ++ ++ local_irq_save(flags); ++ ++ airoha_cpu_clock_switch(priv->pll_base, priv->mcucfg, ++ MCUCFG_CK_SEL_PLL2); ++ ++ val = readl(priv->pll_base + PLLRG_PROTECT); ++ writel((val & ~PLLRG_PROTECT_MASK) | PLLRG_PROTECT_KEY, ++ priv->pll_base + PLLRG_PROTECT); ++ ++ val = readl(priv->pll_base + CPUPLL_SDM_PCW); ++ val &= ~CPUPLL_PCW_INT_MASK; ++ val |= FIELD_PREP(CPUPLL_PCW_INT_MASK, pcw_int); ++ writel(val, priv->pll_base + CPUPLL_SDM_PCW); ++ ++ old_chg = readl(priv->pll_base + CPUPLL_SDM_PCW_CHG); ++ cleared = old_chg & ~(CPUPLL_CHG_POSDIV_MASK | CPUPLL_CHG_BIT); ++ ++ if (old_chg & CPUPLL_CHG_BIT) ++ writel(cleared | FIELD_PREP(CPUPLL_CHG_POSDIV_MASK, posdiv), ++ priv->pll_base + CPUPLL_SDM_PCW_CHG); ++ else ++ writel(cleared | ++ FIELD_PREP(CPUPLL_CHG_POSDIV_MASK, posdiv) | ++ CPUPLL_CHG_BIT, ++ priv->pll_base + CPUPLL_SDM_PCW_CHG); ++ ++ udelay(20); ++ ++ airoha_cpu_clock_switch(priv->pll_base, priv->mcucfg, ++ MCUCFG_CK_SEL_ARMPLL); ++ ++ val = readl(priv->pll_base + CPUPLL_CLK_MUX); ++ writel(val & ~BIT(2), priv->pll_base + CPUPLL_CLK_MUX); ++ ++ val = readl(priv->pll_base + PLLRG_PROTECT); ++ writel(val & ~PLLRG_PROTECT_MASK, priv->pll_base + PLLRG_PROTECT); ++ ++ local_irq_restore(flags); ++ ++ return 0; ++} ++ ++static bool airoha_cpu_smc_available(void) ++{ ++ struct arm_smccc_res res; ++ ++ arm_smccc_1_1_invoke(AIROHA_SIP_AVS_HANDLE, AIROHA_AVS_OP_GET_FREQ, ++ 0, 0, 0, 0, 0, 0, &res); ++ ++ return res.a0 > 0 && res.a0 < 2000; ++} ++ ++static int airoha_cpu_pmdomain_set_performance_state(struct generic_pm_domain *domain, ++ unsigned int state) ++{ ++ struct airoha_cpu_pmdomain_priv *priv = ++ container_of(domain, struct airoha_cpu_pmdomain_priv, pd); ++ ++ if (priv->use_smc) ++ return airoha_cpu_smc_set_freq(state); ++ ++ return airoha_cpu_pll_set_freq(priv, state); ++} ++ + static int airoha_cpu_pmdomain_probe(struct platform_device *pdev) + { + struct airoha_cpu_pmdomain_priv *priv; +@@ -70,18 +219,41 @@ static int airoha_cpu_pmdomain_probe(struct platform_device *pdev) + const struct clk_init_data init = { + .name = "cpu", + .ops = &airoha_cpu_pmdomain_clk_ops, +- /* Clock with no set_rate, can't cache */ + .flags = CLK_GET_RATE_NOCACHE, + }; + struct generic_pm_domain *pd; ++ unsigned long freq_hz; + int ret; + + priv = devm_kzalloc(dev, sizeof(*priv), GFP_KERNEL); + if (!priv) + return -ENOMEM; + +- /* Init and register a get-only clk for Cpufreq */ ++ if (airoha_cpu_smc_available()) { ++ priv->use_smc = true; ++ dev_info(dev, "using ATF SMC for CPU frequency control\n"); ++ } else { ++ priv->pll_base = devm_platform_ioremap_resource(pdev, 0); ++ if (IS_ERR(priv->pll_base)) ++ return dev_err_probe(dev, PTR_ERR(priv->pll_base), ++ "ATF SMC not available and no chip-scu reg in DT\n"); ++ ++ priv->mcucfg = devm_platform_ioremap_resource(pdev, 1); ++ if (IS_ERR(priv->mcucfg)) ++ return dev_err_probe(dev, PTR_ERR(priv->mcucfg), ++ "ATF SMC not available and no mcucfg reg in DT\n"); ++ ++ dev_info(dev, "ATF SMC not available, using direct PLL programming\n"); ++ } ++ + priv->hw.init = &init; ++ freq_hz = airoha_cpu_pmdomain_clk_get(&priv->hw, 0); ++ if (!freq_hz || freq_hz > 2000000000UL) ++ return dev_err_probe(dev, -ENODEV, ++ "invalid CPU frequency: %lu Hz\n", freq_hz); ++ ++ dev_info(dev, "CPU frequency: %lu MHz\n", freq_hz / 1000000); ++ + ret = devm_clk_hw_register(dev, &priv->hw); + if (ret) + return ret; +@@ -91,7 +263,6 @@ static int airoha_cpu_pmdomain_probe(struct platform_device *pdev) + if (ret) + return ret; + +- /* Init and register a PD for CPU */ + pd = &priv->pd; + pd->name = "cpu_pd"; + pd->flags = GENPD_FLAG_ALWAYS_ON; +-- +2.43.0 diff --git a/target/linux/airoha/patches-6.18/0403-pmdomain-airoha-expose-kconfig-symbol-on-ARCH_AIROHA.patch b/target/linux/airoha/patches-6.18/0403-pmdomain-airoha-expose-kconfig-symbol-on-ARCH_AIROHA.patch new file mode 100644 index 00000000000000..e0b1787ca23fe6 --- /dev/null +++ b/target/linux/airoha/patches-6.18/0403-pmdomain-airoha-expose-kconfig-symbol-on-ARCH_AIROHA.patch @@ -0,0 +1,61 @@ +From e014d172830afbdd1feb863549c9ca91c52d79cd Mon Sep 17 00:00:00 2001 +From: Yanghan Ye +Date: Sun, 12 Apr 2026 18:55:53 +0800 +Subject: [PATCH] pmdomain: airoha: expose Kconfig symbol on ARCH_AIROHA + +OpenWrt already enables CONFIG_AIROHA_CPU_PM_DOMAIN on the 6.18 Airoha +target, but the symbol currently lives inside the MediaTek PM domains +menu gated by ARCH_MEDIATEK || COMPILE_TEST. As a result, the option is +silently dropped from the final .config and the CPU PM domain driver is +never built. + +Move the Airoha-specific symbol out of the MediaTek-only menu and gate +it on ARCH_AIROHA instead. + +Signed-off-by: Yanghan Ye +--- + drivers/pmdomain/mediatek/Kconfig | 25 +++++++++++++------------ + 1 file changed, 13 insertions(+), 12 deletions(-) + +diff --git a/drivers/pmdomain/mediatek/Kconfig b/drivers/pmdomain/mediatek/Kconfig +index 0e34a51..0d0ac4f 100644 +--- a/drivers/pmdomain/mediatek/Kconfig ++++ b/drivers/pmdomain/mediatek/Kconfig +@@ -1,5 +1,18 @@ + # SPDX-License-Identifier: GPL-2.0-only + ++config AIROHA_CPU_PM_DOMAIN ++ tristate "Airoha CPU power domain" ++ default ARCH_AIROHA ++ depends on ARCH_AIROHA || COMPILE_TEST ++ depends on HAVE_ARM_SMCCC ++ depends on PM ++ select PM_GENERIC_DOMAINS ++ help ++ Say y here to enable CPU power domain support for Airoha SoC. ++ ++ CPU frequency and power is controlled by ATF with SMC command to ++ set performance states. ++ + menu "MediaTek PM Domains" + depends on ARCH_MEDIATEK || COMPILE_TEST + +@@ -26,16 +39,4 @@ config MTK_SCPSYS_PM_DOMAINS + Control Processor System (SCPSYS) has several power management related + tasks in the system. + +-config AIROHA_CPU_PM_DOMAIN +- tristate "Airoha CPU power domain" +- default ARCH_AIROHA +- depends on HAVE_ARM_SMCCC +- depends on PM +- select PM_GENERIC_DOMAINS +- help +- Say y here to enable CPU power domain support for Airoha SoC. +- +- CPU frequency and power is controlled by ATF with SMC command to +- set performance states. +- + endmenu +-- +2.43.0 diff --git a/target/linux/airoha/patches-6.18/099-06-v6.19-net-airoha-ppe-Remove-airoha_ppe_is_enabled-where-no.patch b/target/linux/airoha/patches-6.18/099-06-v6.19-net-airoha-ppe-Remove-airoha_ppe_is_enabled-where-no.patch deleted file mode 100644 index bdbf1d9ae0cbab..00000000000000 --- a/target/linux/airoha/patches-6.18/099-06-v6.19-net-airoha-ppe-Remove-airoha_ppe_is_enabled-where-no.patch +++ /dev/null @@ -1,58 +0,0 @@ -From 41139125f5c70e0f66f0cc4ac1b3a62f5801ab42 Mon Sep 17 00:00:00 2001 -From: Lorenzo Bianconi -Date: Fri, 17 Oct 2025 11:06:16 +0200 -Subject: [PATCH 06/12] net: airoha: ppe: Remove airoha_ppe_is_enabled() where - not necessary - -Now each PPE has always PPE_STATS_NUM_ENTRIES entries so we do not need -to run airoha_ppe_is_enabled routine to check if the hash refers to -PPE1 or PPE2. - -Reviewed-by: Simon Horman -Signed-off-by: Lorenzo Bianconi -Link: https://patch.msgid.link/20251017-an7583-eth-support-v3-7-f28319666667@kernel.org -Signed-off-by: Paolo Abeni ---- - drivers/net/ethernet/airoha/airoha_ppe.c | 13 ++++--------- - 1 file changed, 4 insertions(+), 9 deletions(-) - ---- a/drivers/net/ethernet/airoha/airoha_ppe.c -+++ b/drivers/net/ethernet/airoha/airoha_ppe.c -@@ -527,10 +527,8 @@ static int airoha_ppe_foe_get_flow_stats - if (ppe_num_stats_entries < 0) - return ppe_num_stats_entries; - -- *index = hash; -- if (airoha_ppe_is_enabled(ppe->eth, 1) && -- hash >= ppe_num_stats_entries) -- *index = *index - PPE_STATS_NUM_ENTRIES; -+ *index = hash >= ppe_num_stats_entries ? hash - PPE_STATS_NUM_ENTRIES -+ : hash; - - return 0; - } -@@ -620,13 +618,11 @@ airoha_ppe_foe_get_entry_locked(struct a - - if (hash < sram_num_entries) { - u32 *hwe = ppe->foe + hash * sizeof(struct airoha_foe_entry); -+ bool ppe2 = hash >= PPE_SRAM_NUM_ENTRIES; - struct airoha_eth *eth = ppe->eth; -- bool ppe2; - u32 val; - int i; - -- ppe2 = airoha_ppe_is_enabled(ppe->eth, 1) && -- hash >= PPE_SRAM_NUM_ENTRIES; - airoha_fe_wr(ppe->eth, REG_PPE_RAM_CTRL(ppe2), - FIELD_PREP(PPE_SRAM_CTRL_ENTRY_MASK, hash) | - PPE_SRAM_CTRL_REQ_MASK); -@@ -704,8 +700,7 @@ static int airoha_ppe_foe_commit_entry(s - - if (hash < sram_num_entries) { - dma_addr_t addr = ppe->foe_dma + hash * sizeof(*hwe); -- bool ppe2 = airoha_ppe_is_enabled(eth, 1) && -- hash >= PPE_SRAM_NUM_ENTRIES; -+ bool ppe2 = hash >= PPE_SRAM_NUM_ENTRIES; - - err = npu->ops.ppe_foe_commit_entry(npu, addr, sizeof(*hwe), - hash, ppe2); diff --git a/target/linux/airoha/patches-6.18/099-07-v6.19-net-airoha-ppe-Configure-SRAM-PPE-entries-via-the-cp.patch b/target/linux/airoha/patches-6.18/099-07-v6.19-net-airoha-ppe-Configure-SRAM-PPE-entries-via-the-cp.patch deleted file mode 100644 index 02c9093e21fdcf..00000000000000 --- a/target/linux/airoha/patches-6.18/099-07-v6.19-net-airoha-ppe-Configure-SRAM-PPE-entries-via-the-cp.patch +++ /dev/null @@ -1,65 +0,0 @@ -From 306b78f5035af4bd011753c5a6b12812515caa6c Mon Sep 17 00:00:00 2001 -From: Lorenzo Bianconi -Date: Fri, 17 Oct 2025 11:06:17 +0200 -Subject: [PATCH 07/12] net: airoha: ppe: Configure SRAM PPE entries via the - cpu - -Introduce airoha_ppe_foe_commit_sram_entry routine in order to configure -the SRAM PPE entries directly via the CPU instead of using the NPU APIs. -This is a preliminary patch to enable netfilter flowtable hw offload for -AN7583 SoC. - -Reviewed-by: Simon Horman -Signed-off-by: Lorenzo Bianconi -Link: https://patch.msgid.link/20251017-an7583-eth-support-v3-8-f28319666667@kernel.org -Signed-off-by: Paolo Abeni ---- - drivers/net/ethernet/airoha/airoha_ppe.c | 30 ++++++++++++++++++------ - 1 file changed, 23 insertions(+), 7 deletions(-) - ---- a/drivers/net/ethernet/airoha/airoha_ppe.c -+++ b/drivers/net/ethernet/airoha/airoha_ppe.c -@@ -669,6 +669,27 @@ static bool airoha_ppe_foe_compare_entry - return !memcmp(&e->data.d, &hwe->d, len - sizeof(hwe->ib1)); - } - -+static int airoha_ppe_foe_commit_sram_entry(struct airoha_ppe *ppe, u32 hash) -+{ -+ struct airoha_foe_entry *hwe = ppe->foe + hash * sizeof(*hwe); -+ bool ppe2 = hash >= PPE_SRAM_NUM_ENTRIES; -+ u32 *ptr = (u32 *)hwe, val; -+ int i; -+ -+ for (i = 0; i < sizeof(*hwe) / sizeof(*ptr); i++) -+ airoha_fe_wr(ppe->eth, REG_PPE_RAM_ENTRY(ppe2, i), ptr[i]); -+ -+ wmb(); -+ airoha_fe_wr(ppe->eth, REG_PPE_RAM_CTRL(ppe2), -+ FIELD_PREP(PPE_SRAM_CTRL_ENTRY_MASK, hash) | -+ PPE_SRAM_CTRL_WR_MASK | PPE_SRAM_CTRL_REQ_MASK); -+ -+ return read_poll_timeout_atomic(airoha_fe_rr, val, -+ val & PPE_SRAM_CTRL_ACK_MASK, -+ 10, 100, false, ppe->eth, -+ REG_PPE_RAM_CTRL(ppe2)); -+} -+ - static int airoha_ppe_foe_commit_entry(struct airoha_ppe *ppe, - struct airoha_foe_entry *e, - u32 hash, bool rx_wlan) -@@ -698,13 +719,8 @@ static int airoha_ppe_foe_commit_entry(s - if (!rx_wlan) - airoha_ppe_foe_flow_stats_update(ppe, npu, hwe, hash); - -- if (hash < sram_num_entries) { -- dma_addr_t addr = ppe->foe_dma + hash * sizeof(*hwe); -- bool ppe2 = hash >= PPE_SRAM_NUM_ENTRIES; -- -- err = npu->ops.ppe_foe_commit_entry(npu, addr, sizeof(*hwe), -- hash, ppe2); -- } -+ if (hash < sram_num_entries) -+ err = airoha_ppe_foe_commit_sram_entry(ppe, hash); - unlock: - rcu_read_unlock(); - diff --git a/target/linux/airoha/patches-6.18/099-08-v6.19-net-airoha-ppe-Flush-PPE-SRAM-table-during-PPE-setup.patch b/target/linux/airoha/patches-6.18/099-08-v6.19-net-airoha-ppe-Flush-PPE-SRAM-table-during-PPE-setup.patch deleted file mode 100644 index dd4bda60013d20..00000000000000 --- a/target/linux/airoha/patches-6.18/099-08-v6.19-net-airoha-ppe-Flush-PPE-SRAM-table-during-PPE-setup.patch +++ /dev/null @@ -1,71 +0,0 @@ -From 620d7b91aadbd4eb930895b07e34f0a155a9d3c1 Mon Sep 17 00:00:00 2001 -From: Lorenzo Bianconi -Date: Fri, 17 Oct 2025 11:06:18 +0200 -Subject: [PATCH 08/12] net: airoha: ppe: Flush PPE SRAM table during PPE setup - -Rely on airoha_ppe_foe_commit_sram_entry routine to flush SRAM PPE table -entries. This patch allow moving PPE SRAM flush during PPE setup and -avoid dumping uninitialized values via the debugfs if no entries are -offloaded yet. - -Reviewed-by: Simon Horman -Signed-off-by: Lorenzo Bianconi -Link: https://patch.msgid.link/20251017-an7583-eth-support-v3-9-f28319666667@kernel.org -Signed-off-by: Paolo Abeni ---- - drivers/net/ethernet/airoha/airoha_ppe.c | 24 ++++++++++++++---------- - 1 file changed, 14 insertions(+), 10 deletions(-) - ---- a/drivers/net/ethernet/airoha/airoha_ppe.c -+++ b/drivers/net/ethernet/airoha/airoha_ppe.c -@@ -1304,18 +1304,22 @@ static int airoha_ppe_flow_offload_cmd(s - return -EOPNOTSUPP; - } - --static int airoha_ppe_flush_sram_entries(struct airoha_ppe *ppe, -- struct airoha_npu *npu) -+static int airoha_ppe_flush_sram_entries(struct airoha_ppe *ppe) - { - u32 sram_num_entries = airoha_ppe_get_total_sram_num_entries(ppe); - struct airoha_foe_entry *hwe = ppe->foe; -- int i; -+ int i, err = 0; -+ -+ for (i = 0; i < sram_num_entries; i++) { -+ int err; - -- for (i = 0; i < PPE_SRAM_NUM_ENTRIES; i++) - memset(&hwe[i], 0, sizeof(*hwe)); -+ err = airoha_ppe_foe_commit_sram_entry(ppe, i); -+ if (err) -+ break; -+ } - -- return npu->ops.ppe_flush_sram_entries(npu, ppe->foe_dma, -- sram_num_entries); -+ return err; - } - - static struct airoha_npu *airoha_ppe_npu_get(struct airoha_eth *eth) -@@ -1380,10 +1384,6 @@ static int airoha_ppe_offload_setup(stru - } - - airoha_ppe_hw_init(ppe); -- err = airoha_ppe_flush_sram_entries(ppe, npu); -- if (err) -- goto error_npu_put; -- - airoha_ppe_foe_flow_stats_reset(ppe, npu); - - rcu_assign_pointer(eth->npu, npu); -@@ -1554,6 +1554,10 @@ int airoha_ppe_init(struct airoha_eth *e - if (!ppe->foe_check_time) - return -ENOMEM; - -+ err = airoha_ppe_flush_sram_entries(ppe); -+ if (err) -+ return err; -+ - err = rhashtable_init(ð->flow_table, &airoha_flow_table_params); - if (err) - return err; diff --git a/target/linux/airoha/patches-6.18/099-09-v6.19-net-airoha-Select-default-ppe-cpu-port-in-airoha_dev.patch b/target/linux/airoha/patches-6.18/099-09-v6.19-net-airoha-Select-default-ppe-cpu-port-in-airoha_dev.patch deleted file mode 100644 index e9a82de897c2d2..00000000000000 --- a/target/linux/airoha/patches-6.18/099-09-v6.19-net-airoha-Select-default-ppe-cpu-port-in-airoha_dev.patch +++ /dev/null @@ -1,91 +0,0 @@ -From c71a7a861ef02aa2bebb18c2f3385aa3f19094e0 Mon Sep 17 00:00:00 2001 -From: Lorenzo Bianconi -Date: Fri, 17 Oct 2025 11:06:19 +0200 -Subject: [PATCH 09/12] net: airoha: Select default ppe cpu port in - airoha_dev_init() - -Select the PPE default cpu port in airoha_dev_init routine. -This patch allows to distribute the load between the two available cpu -ports (FE_PSE_PORT_CDM1 and FE_PSE_PORT_CDM2) if the device is running a -single PPE module (e.g. 7583) selecting the cpu port based on the use -QDMA device. For multi-PPE device (e.g. 7581) assign FE_PSE_PORT_CDM1 to -PPE1 and FE_PSE_PORT_CDM2 to PPE2. - -Reviewed-by: Simon Horman -Signed-off-by: Lorenzo Bianconi -Link: https://patch.msgid.link/20251017-an7583-eth-support-v3-10-f28319666667@kernel.org -Signed-off-by: Paolo Abeni ---- - drivers/net/ethernet/airoha/airoha_eth.c | 38 ++++++++++-------------- - 1 file changed, 16 insertions(+), 22 deletions(-) - ---- a/drivers/net/ethernet/airoha/airoha_eth.c -+++ b/drivers/net/ethernet/airoha/airoha_eth.c -@@ -521,25 +521,6 @@ static int airoha_fe_init(struct airoha_ - /* disable IFC by default */ - airoha_fe_clear(eth, REG_FE_CSR_IFC_CFG, FE_IFC_EN_MASK); - -- airoha_fe_wr(eth, REG_PPE_DFT_CPORT0(0), -- FIELD_PREP(DFT_CPORT_MASK(7), FE_PSE_PORT_CDM1) | -- FIELD_PREP(DFT_CPORT_MASK(6), FE_PSE_PORT_CDM1) | -- FIELD_PREP(DFT_CPORT_MASK(5), FE_PSE_PORT_CDM1) | -- FIELD_PREP(DFT_CPORT_MASK(4), FE_PSE_PORT_CDM1) | -- FIELD_PREP(DFT_CPORT_MASK(3), FE_PSE_PORT_CDM1) | -- FIELD_PREP(DFT_CPORT_MASK(2), FE_PSE_PORT_CDM1) | -- FIELD_PREP(DFT_CPORT_MASK(1), FE_PSE_PORT_CDM1) | -- FIELD_PREP(DFT_CPORT_MASK(0), FE_PSE_PORT_CDM1)); -- airoha_fe_wr(eth, REG_PPE_DFT_CPORT0(1), -- FIELD_PREP(DFT_CPORT_MASK(7), FE_PSE_PORT_CDM2) | -- FIELD_PREP(DFT_CPORT_MASK(6), FE_PSE_PORT_CDM2) | -- FIELD_PREP(DFT_CPORT_MASK(5), FE_PSE_PORT_CDM2) | -- FIELD_PREP(DFT_CPORT_MASK(4), FE_PSE_PORT_CDM2) | -- FIELD_PREP(DFT_CPORT_MASK(3), FE_PSE_PORT_CDM2) | -- FIELD_PREP(DFT_CPORT_MASK(2), FE_PSE_PORT_CDM2) | -- FIELD_PREP(DFT_CPORT_MASK(1), FE_PSE_PORT_CDM2) | -- FIELD_PREP(DFT_CPORT_MASK(0), FE_PSE_PORT_CDM2)); -- - /* enable 1:N vlan action, init vlan table */ - airoha_fe_set(eth, REG_MC_VLAN_EN, MC_VLAN_EN_MASK); - -@@ -1856,8 +1837,10 @@ static int airhoha_set_gdm2_loopback(str - static int airoha_dev_init(struct net_device *dev) - { - struct airoha_gdm_port *port = netdev_priv(dev); -- struct airoha_eth *eth = port->qdma->eth; -- u32 pse_port; -+ struct airoha_qdma *qdma = port->qdma; -+ struct airoha_eth *eth = qdma->eth; -+ u32 pse_port, fe_cpu_port; -+ u8 ppe_id; - - airoha_set_macaddr(port, dev->dev_addr); - -@@ -1875,16 +1858,27 @@ static int airoha_dev_init(struct net_de - fallthrough; - case AIROHA_GDM2_IDX: - if (airoha_ppe_is_enabled(eth, 1)) { -+ /* For PPE2 always use secondary cpu port. */ -+ fe_cpu_port = FE_PSE_PORT_CDM2; - pse_port = FE_PSE_PORT_PPE2; - break; - } - fallthrough; -- default: -+ default: { -+ u8 qdma_id = qdma - ð->qdma[0]; -+ -+ /* For PPE1 select cpu port according to the running QDMA. */ -+ fe_cpu_port = qdma_id ? FE_PSE_PORT_CDM2 : FE_PSE_PORT_CDM1; - pse_port = FE_PSE_PORT_PPE1; - break; - } -+ } - - airoha_set_gdm_port_fwd_cfg(eth, REG_GDM_FWD_CFG(port->id), pse_port); -+ ppe_id = pse_port == FE_PSE_PORT_PPE2 ? 1 : 0; -+ airoha_fe_rmw(eth, REG_PPE_DFT_CPORT0(ppe_id), -+ DFT_CPORT_MASK(port->id), -+ fe_cpu_port << __ffs(DFT_CPORT_MASK(port->id))); - - return 0; - } diff --git a/target/linux/airoha/patches-6.18/099-11-v6.19-net-airoha-ppe-Do-not-use-magic-numbers-in-airoha_pp.patch b/target/linux/airoha/patches-6.18/099-11-v6.19-net-airoha-ppe-Do-not-use-magic-numbers-in-airoha_pp.patch deleted file mode 100644 index 3cea4a4a60b0f9..00000000000000 --- a/target/linux/airoha/patches-6.18/099-11-v6.19-net-airoha-ppe-Do-not-use-magic-numbers-in-airoha_pp.patch +++ /dev/null @@ -1,29 +0,0 @@ -From 63f283d36b1fb06b55ae609a1f679544f5f66057 Mon Sep 17 00:00:00 2001 -From: Lorenzo Bianconi -Date: Fri, 17 Oct 2025 11:06:21 +0200 -Subject: [PATCH 11/12] net: airoha: ppe: Do not use magic numbers in - airoha_ppe_foe_get_entry_locked() - -Explicit the size of entries pointed by hwe pointer in -airoha_ppe_foe_get_entry_locked routine - -Reviewed-by: Simon Horman -Signed-off-by: Lorenzo Bianconi -Link: https://patch.msgid.link/20251017-an7583-eth-support-v3-12-f28319666667@kernel.org -Signed-off-by: Paolo Abeni ---- - drivers/net/ethernet/airoha/airoha_ppe.c | 3 ++- - 1 file changed, 2 insertions(+), 1 deletion(-) - ---- a/drivers/net/ethernet/airoha/airoha_ppe.c -+++ b/drivers/net/ethernet/airoha/airoha_ppe.c -@@ -632,7 +632,8 @@ airoha_ppe_foe_get_entry_locked(struct a - REG_PPE_RAM_CTRL(ppe2))) - return NULL; - -- for (i = 0; i < sizeof(struct airoha_foe_entry) / 4; i++) -+ for (i = 0; i < sizeof(struct airoha_foe_entry) / sizeof(*hwe); -+ i++) - hwe[i] = airoha_fe_rr(eth, - REG_PPE_RAM_ENTRY(ppe2, i)); - } diff --git a/target/linux/airoha/patches-6.18/117-03-airoha_npu_eagle_add_ser.patch b/target/linux/airoha/patches-6.18/117-03-airoha_npu_eagle_add_ser.patch new file mode 100644 index 00000000000000..4a1cecf8dfa643 --- /dev/null +++ b/target/linux/airoha/patches-6.18/117-03-airoha_npu_eagle_add_ser.patch @@ -0,0 +1,15 @@ +--- + drivers/net/ethernet/airoha/airoha_npu.c | 2 +- + 1 file changed, 1 insertion(+), 1 deletion(-) + +--- a/drivers/net/ethernet/airoha/airoha_npu.c ++++ b/drivers/net/ethernet/airoha/airoha_npu.c +@@ -183,7 +183,7 @@ static int airoha_npu_send_msg(struct ai + ret = regmap_read_poll_timeout_atomic(npu->regmap, + REG_CR_MBQ0_CTRL(3) + offset, + val, (val & MBOX_MSG_DONE), +- 100, 100 * MSEC_PER_SEC); ++ 100, 1000 * MSEC_PER_SEC); + if (!ret && FIELD_GET(MBOX_MSG_STATUS, val) != NPU_MBOX_SUCCESS) + ret = -EINVAL; + diff --git a/target/linux/airoha/patches-6.18/125-v7.1-net-airoha-Rely-__field_prep-for-non-constant-masks.patch b/target/linux/airoha/patches-6.18/125-v7.1-net-airoha-Rely-__field_prep-for-non-constant-masks.patch index ac7e6ebdb1c5fd..326451a7cf8432 100644 --- a/target/linux/airoha/patches-6.18/125-v7.1-net-airoha-Rely-__field_prep-for-non-constant-masks.patch +++ b/target/linux/airoha/patches-6.18/125-v7.1-net-airoha-Rely-__field_prep-for-non-constant-masks.patch @@ -15,7 +15,7 @@ Signed-off-by: Jakub Kicinski --- a/drivers/net/ethernet/airoha/airoha_eth.c +++ b/drivers/net/ethernet/airoha/airoha_eth.c -@@ -1824,7 +1824,7 @@ static int airhoha_set_gdm2_loopback(str +@@ -1821,7 +1821,7 @@ static int airhoha_set_gdm2_loopback(str airoha_fe_rmw(eth, REG_SP_DFT_CPORT(src_port >> fls(SP_CPORT_DFT_MASK)), SP_CPORT_MASK(val), @@ -24,16 +24,7 @@ Signed-off-by: Jakub Kicinski if (port->id != AIROHA_GDM3_IDX && airoha_is_7581(eth)) airoha_fe_rmw(eth, REG_SRC_PORT_FC_MAP6, -@@ -1878,7 +1878,7 @@ static int airoha_dev_init(struct net_de - ppe_id = pse_port == FE_PSE_PORT_PPE2 ? 1 : 0; - airoha_fe_rmw(eth, REG_PPE_DFT_CPORT0(ppe_id), - DFT_CPORT_MASK(port->id), -- fe_cpu_port << __ffs(DFT_CPORT_MASK(port->id))); -+ __field_prep(DFT_CPORT_MASK(port->id), fe_cpu_port)); - - return 0; - } -@@ -2238,7 +2238,7 @@ static int airoha_qdma_set_chan_tx_sched +@@ -2235,7 +2235,7 @@ static int airoha_qdma_set_chan_tx_sched airoha_qdma_rmw(port->qdma, REG_CHAN_QOS_MODE(channel >> 3), CHAN_QOS_MODE_MASK(channel), diff --git a/target/linux/airoha/patches-6.18/127-v7.1-net-airoha-Move-GDM-forward-port-configuration-in-nd.patch b/target/linux/airoha/patches-6.18/127-v7.1-net-airoha-Move-GDM-forward-port-configuration-in-nd.patch index e44102acde5b6d..8f2b2de6e02a4f 100644 --- a/target/linux/airoha/patches-6.18/127-v7.1-net-airoha-Move-GDM-forward-port-configuration-in-nd.patch +++ b/target/linux/airoha/patches-6.18/127-v7.1-net-airoha-Move-GDM-forward-port-configuration-in-nd.patch @@ -18,7 +18,7 @@ Signed-off-by: Paolo Abeni --- a/drivers/net/ethernet/airoha/airoha_eth.c +++ b/drivers/net/ethernet/airoha/airoha_eth.c -@@ -1711,6 +1711,7 @@ static int airoha_dev_open(struct net_de +@@ -1708,6 +1708,7 @@ static int airoha_dev_open(struct net_de int err, len = ETH_HLEN + dev->mtu + ETH_FCS_LEN; struct airoha_gdm_port *port = netdev_priv(dev); struct airoha_qdma *qdma = port->qdma; @@ -26,7 +26,7 @@ Signed-off-by: Paolo Abeni netif_tx_start_all_queues(dev); err = airoha_set_vip_for_gdm_port(port, true); -@@ -1734,6 +1735,14 @@ static int airoha_dev_open(struct net_de +@@ -1731,6 +1732,14 @@ static int airoha_dev_open(struct net_de GLOBAL_CFG_RX_DMA_EN_MASK); atomic_inc(&qdma->users); @@ -41,7 +41,7 @@ Signed-off-by: Paolo Abeni return 0; } -@@ -1748,6 +1757,9 @@ static int airoha_dev_stop(struct net_de +@@ -1745,6 +1754,9 @@ static int airoha_dev_stop(struct net_de for (i = 0; i < dev->num_tx_queues; i++) netdev_tx_reset_subqueue(dev, i); @@ -51,36 +51,30 @@ Signed-off-by: Paolo Abeni if (atomic_dec_and_test(&qdma->users)) { airoha_qdma_clear(qdma, REG_QDMA_GLOBAL_CFG, GLOBAL_CFG_TX_DMA_EN_MASK | -@@ -1841,7 +1853,7 @@ static int airoha_dev_init(struct net_de +@@ -1850,7 +1862,6 @@ static int airoha_dev_init(struct net_de + { struct airoha_gdm_port *port = netdev_priv(dev); - struct airoha_qdma *qdma = port->qdma; - struct airoha_eth *eth = qdma->eth; -- u32 pse_port, fe_cpu_port; -+ u32 fe_cpu_port; - u8 ppe_id; + struct airoha_eth *eth = port->qdma->eth; +- u32 pse_port; airoha_set_macaddr(port, dev->dev_addr); -@@ -1862,7 +1874,7 @@ static int airoha_dev_init(struct net_de - if (airoha_ppe_is_enabled(eth, 1)) { - /* For PPE2 always use secondary cpu port. */ - fe_cpu_port = FE_PSE_PORT_CDM2; -- pse_port = FE_PSE_PORT_PPE2; -+ ppe_id = 1; - break; + +@@ -1867,18 +1878,7 @@ static int airoha_dev_init(struct net_de + return err; } fallthrough; -@@ -1871,13 +1883,11 @@ static int airoha_dev_init(struct net_de - - /* For PPE1 select cpu port according to the running QDMA. */ - fe_cpu_port = qdma_id ? FE_PSE_PORT_CDM2 : FE_PSE_PORT_CDM1; +- case AIROHA_GDM2_IDX: +- if (airoha_ppe_is_enabled(eth, 1)) { +- pse_port = FE_PSE_PORT_PPE2; +- break; +- } +- fallthrough; +- default: - pse_port = FE_PSE_PORT_PPE1; -+ ppe_id = 0; - break; +- break; } - } - +- - airoha_set_gdm_port_fwd_cfg(eth, REG_GDM_FWD_CFG(port->id), pse_port); -- ppe_id = pse_port == FE_PSE_PORT_PPE2 ? 1 : 0; - airoha_fe_rmw(eth, REG_PPE_DFT_CPORT0(ppe_id), - DFT_CPORT_MASK(port->id), - __field_prep(DFT_CPORT_MASK(port->id), fe_cpu_port)); + + return 0; + } diff --git a/target/linux/airoha/patches-6.18/129-v7.1-net-airoha-select-QDMA-block-according-LAN-WAN-confi.patch b/target/linux/airoha/patches-6.18/129-v7.1-net-airoha-select-QDMA-block-according-LAN-WAN-confi.patch index 2119016a60cac0..98898b6e5d1afc 100644 --- a/target/linux/airoha/patches-6.18/129-v7.1-net-airoha-select-QDMA-block-according-LAN-WAN-confi.patch +++ b/target/linux/airoha/patches-6.18/129-v7.1-net-airoha-select-QDMA-block-according-LAN-WAN-confi.patch @@ -39,15 +39,12 @@ Signed-off-by: Paolo Abeni --- a/drivers/net/ethernet/airoha/airoha_eth.c +++ b/drivers/net/ethernet/airoha/airoha_eth.c -@@ -1851,11 +1851,13 @@ static int airhoha_set_gdm2_loopback(str +@@ -1850,8 +1850,11 @@ static int airoha_dev_init(struct net_de static int airoha_dev_init(struct net_device *dev) { struct airoha_gdm_port *port = netdev_priv(dev); -- struct airoha_qdma *qdma = port->qdma; -- struct airoha_eth *eth = qdma->eth; +- struct airoha_eth *eth = port->qdma->eth; + struct airoha_eth *eth = port->eth; - u32 fe_cpu_port; - u8 ppe_id; + /* QDMA0 is used for lan ports while QDMA1 is used for WAN ports */ + port->qdma = ð->qdma[!airoha_is_lan_gdm_port(port)]; @@ -55,16 +52,7 @@ Signed-off-by: Paolo Abeni airoha_set_macaddr(port, dev->dev_addr); switch (port->id) { -@@ -1879,7 +1881,7 @@ static int airoha_dev_init(struct net_de - } - fallthrough; - default: { -- u8 qdma_id = qdma - ð->qdma[0]; -+ u8 qdma_id = port->qdma - ð->qdma[0]; - - /* For PPE1 select cpu port according to the running QDMA. */ - fe_cpu_port = qdma_id ? FE_PSE_PORT_CDM2 : FE_PSE_PORT_CDM1; -@@ -2966,11 +2968,10 @@ bool airoha_is_valid_gdm_port(struct air +@@ -2963,11 +2965,10 @@ bool airoha_is_valid_gdm_port(struct air } static int airoha_alloc_gdm_port(struct airoha_eth *eth, @@ -77,7 +65,7 @@ Signed-off-by: Paolo Abeni struct net_device *dev; int err, p; u32 id; -@@ -3001,7 +3002,6 @@ static int airoha_alloc_gdm_port(struct +@@ -2998,7 +2999,6 @@ static int airoha_alloc_gdm_port(struct return -ENOMEM; } @@ -85,7 +73,7 @@ Signed-off-by: Paolo Abeni dev->netdev_ops = &airoha_netdev_ops; dev->ethtool_ops = &airoha_ethtool_ops; dev->max_mtu = AIROHA_MAX_MTU; -@@ -3013,7 +3013,6 @@ static int airoha_alloc_gdm_port(struct +@@ -3010,7 +3010,6 @@ static int airoha_alloc_gdm_port(struct dev->features |= dev->hw_features; dev->vlan_features = dev->hw_features; dev->dev.of_node = np; @@ -93,7 +81,7 @@ Signed-off-by: Paolo Abeni SET_NETDEV_DEV(dev, eth->dev); /* reserve hw queues for HTB offloading */ -@@ -3034,7 +3033,7 @@ static int airoha_alloc_gdm_port(struct +@@ -3031,7 +3030,7 @@ static int airoha_alloc_gdm_port(struct port = netdev_priv(dev); u64_stats_init(&port->stats.syncp); spin_lock_init(&port->stats.lock); @@ -102,7 +90,7 @@ Signed-off-by: Paolo Abeni port->dev = dev; port->id = id; /* XXX: Read nbq from DTS */ -@@ -3136,7 +3135,6 @@ static int airoha_probe(struct platform_ +@@ -3133,7 +3132,6 @@ static int airoha_probe(struct platform_ for (i = 0; i < ARRAY_SIZE(eth->qdma); i++) airoha_qdma_start_napi(ð->qdma[i]); @@ -110,7 +98,7 @@ Signed-off-by: Paolo Abeni for_each_child_of_node(pdev->dev.of_node, np) { if (!of_device_is_compatible(np, "airoha,eth-mac")) continue; -@@ -3144,7 +3142,7 @@ static int airoha_probe(struct platform_ +@@ -3141,7 +3139,7 @@ static int airoha_probe(struct platform_ if (!of_device_is_available(np)) continue; diff --git a/target/linux/airoha/patches-6.18/132-v7.1-net-airoha-Reset-PPE-default-cput-port-in-airoha_ppe.patch b/target/linux/airoha/patches-6.18/132-v7.1-net-airoha-Reset-PPE-default-cput-port-in-airoha_ppe.patch index 6b28275fb44560..30d2e62784f6a1 100644 --- a/target/linux/airoha/patches-6.18/132-v7.1-net-airoha-Reset-PPE-default-cput-port-in-airoha_ppe.patch +++ b/target/linux/airoha/patches-6.18/132-v7.1-net-airoha-Reset-PPE-default-cput-port-in-airoha_ppe.patch @@ -32,50 +32,48 @@ Signed-off-by: Lorenzo Bianconi --- a/drivers/net/ethernet/airoha/airoha_eth.c +++ b/drivers/net/ethernet/airoha/airoha_eth.c -@@ -1852,8 +1852,7 @@ static int airoha_dev_init(struct net_de +@@ -667,7 +667,7 @@ static int airoha_fe_init(struct airoha_ + /* disable IFC by default */ + airoha_fe_clear(eth, REG_FE_CSR_IFC_CFG, FE_IFC_EN_MASK); + +- airoha_fe_wr(eth, REG_PPE_DFT_CPORT0(0), ++ airoha_fe_wr(eth, REG_PPE_DFT_CPORT(0, 0), + FIELD_PREP(DFT_CPORT_MASK(7), FE_PSE_PORT_CDM1) | + FIELD_PREP(DFT_CPORT_MASK(6), FE_PSE_PORT_CDM1) | + FIELD_PREP(DFT_CPORT_MASK(5), FE_PSE_PORT_CDM1) | +@@ -676,7 +676,7 @@ static int airoha_fe_init(struct airoha_ + FIELD_PREP(DFT_CPORT_MASK(2), FE_PSE_PORT_CDM1) | + FIELD_PREP(DFT_CPORT_MASK(1), FE_PSE_PORT_CDM1) | + FIELD_PREP(DFT_CPORT_MASK(0), FE_PSE_PORT_CDM1)); +- airoha_fe_wr(eth, REG_PPE_DFT_CPORT0(1), ++ airoha_fe_wr(eth, REG_PPE_DFT_CPORT(1, 0), + FIELD_PREP(DFT_CPORT_MASK(7), FE_PSE_PORT_CDM2) | + FIELD_PREP(DFT_CPORT_MASK(6), FE_PSE_PORT_CDM2) | + FIELD_PREP(DFT_CPORT_MASK(5), FE_PSE_PORT_CDM2) | +@@ -1850,6 +1850,7 @@ static int airoha_dev_init(struct net_de { struct airoha_gdm_port *port = netdev_priv(dev); - struct airoha_eth *eth = port->eth; -- u32 fe_cpu_port; -- u8 ppe_id; + struct airoha_eth *eth = port->eth; + int i; /* QDMA0 is used for lan ports while QDMA1 is used for WAN ports */ port->qdma = ð->qdma[!airoha_is_lan_gdm_port(port)]; -@@ -1871,28 +1870,13 @@ static int airoha_dev_init(struct net_de +@@ -1870,8 +1871,13 @@ static int airoha_dev_init(struct net_de if (err) return err; } - fallthrough; -- case AIROHA_GDM2_IDX: -- if (airoha_ppe_is_enabled(eth, 1)) { -- /* For PPE2 always use secondary cpu port. */ -- fe_cpu_port = FE_PSE_PORT_CDM2; -- ppe_id = 1; -- break; -- } -- fallthrough; -- default: { -- u8 qdma_id = port->qdma - ð->qdma[0]; -- -- /* For PPE1 select cpu port according to the running QDMA. */ -- fe_cpu_port = qdma_id ? FE_PSE_PORT_CDM2 : FE_PSE_PORT_CDM1; -- ppe_id = 0; - break; -- } ++ break; + default: + break; } -- airoha_fe_rmw(eth, REG_PPE_DFT_CPORT0(ppe_id), -- DFT_CPORT_MASK(port->id), -- __field_prep(DFT_CPORT_MASK(port->id), fe_cpu_port)); + for (i = 0; i < eth->soc->num_ppe; i++) + airoha_ppe_set_cpu_port(port, i); - ++ return 0; } -@@ -1995,7 +1979,7 @@ static u32 airoha_get_dsa_tag(struct sk_ +@@ -1992,7 +1976,7 @@ static u32 airoha_get_dsa_tag(struct sk_ #endif } diff --git a/target/linux/airoha/patches-6.18/330-net-airoha-use-device_set_node-helper-to-setup-GDM-node.patch b/target/linux/airoha/patches-6.18/330-net-airoha-use-device_set_node-helper-to-setup-GDM-node.patch new file mode 100644 index 00000000000000..7d039a99b8529a --- /dev/null +++ b/target/linux/airoha/patches-6.18/330-net-airoha-use-device_set_node-helper-to-setup-GDM-node.patch @@ -0,0 +1,38 @@ +From: Christian Marangi +To: Lorenzo Bianconi , + Andrew Lunn , + "David S. Miller" , + Eric Dumazet , + Jakub Kicinski , Paolo Abeni , + Russell King , + linux-arm-kernel@lists.infradead.org, + linux-mediatek@lists.infradead.org, netdev@vger.kernel.org, + linux-kernel@vger.kernel.org +Cc: Christian Marangi +Subject: [net-next PATCH v2 1/2] net: airoha: use device_set_node helper to setup GDM node +Date: Thu, 23 Oct 2025 16:58:48 +0200 [thread overview] +Message-ID: <20251023145850.28459-2-ansuelsmth@gmail.com> (raw) +In-Reply-To: <20251023145850.28459-1-ansuelsmth@gmail.com> + +Use device_set_node helper to setup GDM node instead of manually setting +the dev.of_node to also fill the fwnode entry. + +This is to address some API that use fwnode instead of of_node (for +example phylink_create) + +Signed-off-by: Christian Marangi +--- + drivers/net/ethernet/airoha/airoha_eth.c | 2 +- + 1 file changed, 1 insertion(+), 1 deletion(-) + +--- a/drivers/net/ethernet/airoha/airoha_eth.c ++++ b/drivers/net/ethernet/airoha/airoha_eth.c +@@ -3436,7 +3436,7 @@ static int airoha_alloc_gdm_device(struct airoha_eth *eth, + return -EINVAL; + } + +- netdev->dev.of_node = of_node_get(np); ++ device_set_node(&netdev->dev, of_fwnode_handle(of_node_get(np))); + dev = netdev_priv(netdev); + u64_stats_init(&dev->stats.syncp); + dev->port = port; diff --git a/target/linux/airoha/patches-6.18/600-12-v6.19-clk-en7523-Add-reset-controller-support-for-EN7523-S.patch b/target/linux/airoha/patches-6.18/600-12-clk-en7523-Add-reset-controller-support-for-EN7523-S.patch similarity index 100% rename from target/linux/airoha/patches-6.18/600-12-v6.19-clk-en7523-Add-reset-controller-support-for-EN7523-S.patch rename to target/linux/airoha/patches-6.18/600-12-clk-en7523-Add-reset-controller-support-for-EN7523-S.patch diff --git a/target/linux/airoha/patches-6.18/620-net-pcs-airoha-fix-USXGMII-rate-adaptation-on-speed.patch b/target/linux/airoha/patches-6.18/620-net-pcs-airoha-fix-USXGMII-rate-adaptation-on-speed.patch new file mode 100644 index 00000000000000..70e824aa21e0df --- /dev/null +++ b/target/linux/airoha/patches-6.18/620-net-pcs-airoha-fix-USXGMII-rate-adaptation-on-speed.patch @@ -0,0 +1,152 @@ +From: Ryan Chen +Date: Thu, 06 Mar 2026 12:00:00 -0600 +Subject: [PATCH 1/2] net: pcs: airoha: fix USXGMII rate adaptation on speed + changes + +The USXGMII rate adaptation code in airoha_pcs_link_up() is inside an +else branch that only executes when neg_mode is not +PHYLINK_PCS_NEG_INBAND_ENABLED. However, USXGMII uses in-band +autonegotiation, so this code path is never reached on speed changes. +The RATE_UPDATE_MODE and FORCE_RATE_ADAPT_MODE registers remain +configured for the previous speed. + +Fix this by moving the USXGMII rate adaptation block out of the +forced-mode-only branch so it runs regardless of the negotiation mode. +The SGMII in-band FIFO threshold adjustment and the SGMII/1000BASEX +forced-mode speed configuration remain gated on their respective +negotiation modes as before. + +Signed-off-by: Ryan Chen +--- + drivers/net/pcs/airoha/pcs-airoha-common.c | 128 +++++++++++--------- + 1 file changed, 70 insertions(+), 58 deletions(-) + +--- a/drivers/net/pcs/airoha/pcs-airoha-common.c ++++ b/drivers/net/pcs/airoha/pcs-airoha-common.c +@@ -755,63 +755,71 @@ static void airoha_pcs_link_up(struct ph + maps = &priv->maps[port->index]; + data = priv->data; + +- if (neg_mode == PHYLINK_PCS_NEG_INBAND_ENABLED) { +- if (interface == PHY_INTERFACE_MODE_SGMII) { +- regmap_update_bits(maps->hsgmii_rate_adp, +- AIROHA_PCS_HSGMII_RATE_ADAPT_CTRL_1, +- AIROHA_PCS_HSGMII_RATE_ADAPT_RX_AFIFO_WR_THR | +- AIROHA_PCS_HSGMII_RATE_ADAPT_RX_AFIFO_RD_THR, +- FIELD_PREP(AIROHA_PCS_HSGMII_RATE_ADAPT_RX_AFIFO_WR_THR, 0x0) | +- FIELD_PREP(AIROHA_PCS_HSGMII_RATE_ADAPT_RX_AFIFO_RD_THR, 0x0)); +- udelay(1); +- regmap_update_bits(maps->hsgmii_rate_adp, +- AIROHA_PCS_HSGMII_RATE_ADAPT_CTRL_1, +- AIROHA_PCS_HSGMII_RATE_ADAPT_RX_AFIFO_WR_THR | +- AIROHA_PCS_HSGMII_RATE_ADAPT_RX_AFIFO_RD_THR, +- FIELD_PREP(AIROHA_PCS_HSGMII_RATE_ADAPT_RX_AFIFO_WR_THR, 0xf) | +- FIELD_PREP(AIROHA_PCS_HSGMII_RATE_ADAPT_RX_AFIFO_RD_THR, 0x5)); +- } +- } else { +- if (interface == PHY_INTERFACE_MODE_USXGMII || +- interface == PHY_INTERFACE_MODE_10GBASER) { +- u32 mode; +- u32 rate_adapt; +- +- switch (speed) { +- case SPEED_10000: +- rate_adapt = AIROHA_PCS_HSGMII_RATE_ADPT_FORCE_RATE_ADAPT_MODE_10000; +- mode = AIROHA_PCS_USXGMII_MODE_10000; +- break; +- case SPEED_5000: +- rate_adapt = AIROHA_PCS_HSGMII_RATE_ADPT_FORCE_RATE_ADAPT_MODE_5000; +- mode = AIROHA_PCS_USXGMII_MODE_5000; +- break; +- case SPEED_2500: +- rate_adapt = AIROHA_PCS_HSGMII_RATE_ADPT_FORCE_RATE_ADAPT_MODE_2500; +- mode = AIROHA_PCS_USXGMII_MODE_2500; +- break; +- case SPEED_1000: +- rate_adapt = AIROHA_PCS_HSGMII_RATE_ADPT_FORCE_RATE_ADAPT_MODE_1000; +- mode = AIROHA_PCS_USXGMII_MODE_1000; +- break; +- case SPEED_100: +- rate_adapt = AIROHA_PCS_HSGMII_RATE_ADPT_FORCE_RATE_ADAPT_MODE_100; +- mode = AIROHA_PCS_USXGMII_MODE_100; +- break; +- } +- +- /* Force USXGMII to selected speed */ +- regmap_update_bits(maps->usxgmii_pcs, AIROHA_PCS_USXGMII_PCS_AN_CONTROL_7, +- AIROHA_PCS_USXGMII_MODE, mode); +- +- if (interface == PHY_INTERFACE_MODE_10GBASER) +- regmap_update_bits(maps->hsgmii_rate_adp, AIROHA_PCS_HSGMII_RATE_ADAPT_CTRL_11, +- AIROHA_PCS_HSGMII_RATE_ADPT_FORCE_RATE_ADAPT_MODE_EN | +- AIROHA_PCS_HSGMII_RATE_ADPT_FORCE_RATE_ADAPT_MODE, +- AIROHA_PCS_HSGMII_RATE_ADPT_FORCE_RATE_ADAPT_MODE_EN | +- rate_adapt); ++ if (neg_mode == PHYLINK_PCS_NEG_INBAND_ENABLED && ++ interface == PHY_INTERFACE_MODE_SGMII) { ++ regmap_update_bits(maps->hsgmii_rate_adp, ++ AIROHA_PCS_HSGMII_RATE_ADAPT_CTRL_1, ++ AIROHA_PCS_HSGMII_RATE_ADAPT_RX_AFIFO_WR_THR | ++ AIROHA_PCS_HSGMII_RATE_ADAPT_RX_AFIFO_RD_THR, ++ FIELD_PREP(AIROHA_PCS_HSGMII_RATE_ADAPT_RX_AFIFO_WR_THR, 0x0) | ++ FIELD_PREP(AIROHA_PCS_HSGMII_RATE_ADAPT_RX_AFIFO_RD_THR, 0x0)); ++ udelay(1); ++ regmap_update_bits(maps->hsgmii_rate_adp, ++ AIROHA_PCS_HSGMII_RATE_ADAPT_CTRL_1, ++ AIROHA_PCS_HSGMII_RATE_ADAPT_RX_AFIFO_WR_THR | ++ AIROHA_PCS_HSGMII_RATE_ADAPT_RX_AFIFO_RD_THR, ++ FIELD_PREP(AIROHA_PCS_HSGMII_RATE_ADAPT_RX_AFIFO_WR_THR, 0xf) | ++ FIELD_PREP(AIROHA_PCS_HSGMII_RATE_ADAPT_RX_AFIFO_RD_THR, 0x5)); ++ } ++ ++ /* Update USXGMII rate adaptation for the current speed. ++ * This must run regardless of negotiation mode because USXGMII ++ * uses in-band autoneg but still requires the rate adaptation ++ * registers to be updated when the link speed changes. ++ */ ++ if (interface == PHY_INTERFACE_MODE_USXGMII || ++ interface == PHY_INTERFACE_MODE_10GBASER) { ++ u32 mode; ++ u32 rate_adapt; ++ ++ switch (speed) { ++ case SPEED_10000: ++ rate_adapt = AIROHA_PCS_HSGMII_RATE_ADPT_FORCE_RATE_ADAPT_MODE_10000; ++ mode = AIROHA_PCS_USXGMII_MODE_10000; ++ break; ++ case SPEED_5000: ++ rate_adapt = AIROHA_PCS_HSGMII_RATE_ADPT_FORCE_RATE_ADAPT_MODE_5000; ++ mode = AIROHA_PCS_USXGMII_MODE_5000; ++ break; ++ case SPEED_2500: ++ rate_adapt = AIROHA_PCS_HSGMII_RATE_ADPT_FORCE_RATE_ADAPT_MODE_2500; ++ mode = AIROHA_PCS_USXGMII_MODE_2500; ++ break; ++ case SPEED_1000: ++ rate_adapt = AIROHA_PCS_HSGMII_RATE_ADPT_FORCE_RATE_ADAPT_MODE_1000; ++ mode = AIROHA_PCS_USXGMII_MODE_1000; ++ break; ++ case SPEED_100: ++ rate_adapt = AIROHA_PCS_HSGMII_RATE_ADPT_FORCE_RATE_ADAPT_MODE_100; ++ mode = AIROHA_PCS_USXGMII_MODE_100; ++ break; + } + ++ regmap_update_bits(maps->usxgmii_pcs, ++ AIROHA_PCS_USXGMII_PCS_AN_CONTROL_7, ++ AIROHA_PCS_USXGMII_RATE_UPDATE_MODE | ++ AIROHA_PCS_USXGMII_MODE, ++ AIROHA_PCS_USXGMII_RATE_UPDATE_MODE | mode); ++ ++ regmap_update_bits(maps->hsgmii_rate_adp, ++ AIROHA_PCS_HSGMII_RATE_ADAPT_CTRL_11, ++ AIROHA_PCS_HSGMII_RATE_ADPT_FORCE_RATE_ADAPT_MODE_EN | ++ AIROHA_PCS_HSGMII_RATE_ADPT_FORCE_RATE_ADAPT_MODE, ++ AIROHA_PCS_HSGMII_RATE_ADPT_FORCE_RATE_ADAPT_MODE_EN | ++ rate_adapt); ++ } ++ ++ if (neg_mode != PHYLINK_PCS_NEG_INBAND_ENABLED) { + if (interface == PHY_INTERFACE_MODE_SGMII || + interface == PHY_INTERFACE_MODE_1000BASEX) { + u32 force_speed; diff --git a/target/linux/airoha/patches-6.18/621-net-pcs-airoha-AN7581-add-global-digital-reset-for.patch b/target/linux/airoha/patches-6.18/621-net-pcs-airoha-AN7581-add-global-digital-reset-for.patch new file mode 100644 index 00000000000000..249927b2d1f942 --- /dev/null +++ b/target/linux/airoha/patches-6.18/621-net-pcs-airoha-AN7581-add-global-digital-reset-for.patch @@ -0,0 +1,105 @@ +From: Ryan Chen +Date: Thu, 06 Mar 2026 12:00:00 -0600 +Subject: [PATCH 2/2] net: pcs: airoha: AN7581: add global digital reset for + USXGMII speed changes + +On the EN7581, when phylink renegotiates a new speed within the same +USXGMII interface mode, only link_up() is called (not pcs_config()). +The existing link_up() only resets the TXPCS, which is insufficient: +the RX data path (RXPCS, FIFOs, PMA digital logic) remains configured +for the previous speed and no traffic flows. + +Fix this by adding a global digital reset sequence to the AN7581 +link_up callback for USXGMII and 10GBASE-R interfaces. + +Guard the global reset with a needs_full_init flag set by pcs_config() +after bringup(). On the first link_up after bringup, skip the global +reset since bringup already performed full PCS/PMA initialization. +During this window the PHY firmware may still be loading and its SerDes +is unstable — a global PCS reset can corrupt the USXGMII autoneg state +permanently. On subsequent link_up calls (cable replug, speed changes), +the global reset fires normally since the PHY is fully initialized. + +Signed-off-by: Ryan Chen +--- + drivers/net/pcs/airoha/pcs-airoha-common.c | 1 + + drivers/net/pcs/airoha/pcs-airoha.h | 1 + + drivers/net/pcs/airoha/pcs-an7581.c | 55 +++++++++++++++++++++ + 3 files changed, 57 insertions(+) + +--- a/drivers/net/pcs/airoha/pcs-airoha.h ++++ b/drivers/net/pcs/airoha/pcs-airoha.h +@@ -1663,2 +1663,3 @@ struct airoha_pcs_priv { + bool manual_rx_calib; ++ bool needs_full_init[2]; + }; +--- a/drivers/net/pcs/airoha/pcs-airoha-common.c ++++ b/drivers/net/pcs/airoha/pcs-airoha-common.c +@@ -513,6 +513,7 @@ static int airoha_pcs_config(struct phyl + ret = data->bringup(priv, index, interface); + if (ret) + return ret; ++ priv->needs_full_init[index] = true; + } + + /* Set final configuration for various modes */ +--- a/drivers/net/pcs/airoha/pcs-an7581.c ++++ b/drivers/net/pcs/airoha/pcs-an7581.c +@@ -1997,4 +1997,27 @@ void an7581_pcs_phya_link_up(struct airo + { + struct regmap *pcs_pma = priv->pcs_pma[index]; ++ u32 rst_mask = AIROHA_PCS_PMA_SW_HSG_RXPCS_RST_N | ++ AIROHA_PCS_PMA_SW_HSG_TXPCS_RST_N | ++ AIROHA_PCS_PMA_SW_XFI_RXPCS_BIST_RST_N | ++ AIROHA_PCS_PMA_SW_XFI_RXPCS_RST_N | ++ AIROHA_PCS_PMA_SW_XFI_TXPCS_RST_N | ++ AIROHA_PCS_PMA_SW_TX_FIFO_RST_N | ++ AIROHA_PCS_PMA_SW_REF_RST_N | ++ AIROHA_PCS_PMA_SW_ALLPCS_RST_N | ++ AIROHA_PCS_PMA_SW_PMA_RST_N | ++ AIROHA_PCS_PMA_SW_TX_RST_N | ++ AIROHA_PCS_PMA_SW_RX_RST_N | ++ AIROHA_PCS_PMA_SW_RX_FIFO_RST_N; ++ u32 val; ++ int i; ++ ++ /* Wait for any ongoing PCS reset to complete before proceeding. */ ++ for (i = 0; i < 50; i++) { ++ regmap_read(pcs_pma, AIROHA_PCS_PMA_SW_RST_SET, &val); ++ if ((val & rst_mask) == rst_mask) ++ break; ++ usleep_range(1000, 2000); ++ } ++ + + /* Reset TXPCS on link up */ +@@ -2007,3 +2030,29 @@ void an7581_pcs_phya_link_up(struct airo + regmap_set_bits(pcs_pma, AIROHA_PCS_PMA_SW_RST_SET, + AIROHA_PCS_PMA_SW_HSG_TXPCS_RST_N); ++ ++ if (priv->interface == PHY_INTERFACE_MODE_USXGMII || ++ priv->interface == PHY_INTERFACE_MODE_10GBASER) { ++ if (priv->needs_full_init[index]) { ++ priv->needs_full_init[index] = false; ++ dev_dbg(priv->dev, "link_up: skipping global reset (bringup just ran)\n"); ++ return; ++ } ++ ++ dev_dbg(priv->dev, "link_up: performing global digital reset\n"); ++ an7581_pcs_cdr_reset(priv, index, priv->interface, false); ++ ++ /* Assert global reset (keep REF_RST_N high) */ ++ regmap_update_bits(pcs_pma, AIROHA_PCS_PMA_SW_RST_SET, ++ rst_mask, AIROHA_PCS_PMA_SW_REF_RST_N); ++ ++ usleep_range(100, 200); ++ ++ /* Deassert global reset */ ++ regmap_set_bits(pcs_pma, AIROHA_PCS_PMA_SW_RST_SET, ++ rst_mask); ++ ++ usleep_range(5000, 7000); ++ ++ an7581_pcs_cdr_reset(priv, index, priv->interface, false); ++ } + } diff --git a/target/linux/airoha/patches-6.18/745-net-pcs-airoha-extend-manual-rx-calib-to-E2-silicon.patch b/target/linux/airoha/patches-6.18/745-net-pcs-airoha-extend-manual-rx-calib-to-E2-silicon.patch new file mode 100644 index 00000000000000..0f8a5d6091f85d --- /dev/null +++ b/target/linux/airoha/patches-6.18/745-net-pcs-airoha-extend-manual-rx-calib-to-E2-silicon.patch @@ -0,0 +1,40 @@ +From: Gilly +Date: Tue, 6 May 2026 13:30:00 +1000 +Subject: [PATCH] net: pcs: airoha: extend manual RX calibration to E2 silicon + +The AN7581 PCS driver skips manual RX calibration for SoC revisions +>= E2, on the assumption that E2+ silicon autocalibrates correctly. +In practice E2 silicon (PRODUCT_ID = 2) on Gemtek W1700K HW.1.1 +boards leaves RX permanently stuck on ~80% of cold boots when manual +calibration is skipped, while HW.1.0 (PRODUCT_ID = 1) is reliable. + +The manual calibration path retries up to AIROHA_PCS_MAX_CALIBRATION_TRY +times when the FreqDet module fails to lock; the autocalibration path +has no equivalent retry. + +Extend the threshold to include E2 silicon. + +Signed-off-by: Gilly +--- +--- a/drivers/net/pcs/airoha/pcs-airoha-common.c ++++ b/drivers/net/pcs/airoha/pcs-airoha-common.c +@@ -1046,8 +1046,8 @@ + return dev_err_probe(dev, PTR_ERR(priv->xfi_rst), "failed to get xfi reset lines\n"); + + /* For Ethernet PCS, read the AN7581 SoC revision to check if +- * manual rx calibration is needed. This is only limited to +- * any SoC revision before E2. ++ * manual rx calibration is needed. Extend the manual-calibration ++ * threshold to include E2 silicon. + */ + if (device_is_compatible(dev, "airoha,an7581-pcs-eth") && + data->port_type == AIROHA_PCS_ETH) { +@@ -1057,7 +1057,7 @@ + if (ret) + return ret; + +- if (FIELD_GET(AIROHA_SCU_PRODUCT_ID, val) < 0x2) ++ if (FIELD_GET(AIROHA_SCU_PRODUCT_ID, val) < 0x3) + priv->manual_rx_calib = true; + } + diff --git a/target/linux/airoha/patches-6.18/746-net-dsa-mt7530-pre-deassert-phy-reset-gpios-before-mdio-scan.patch b/target/linux/airoha/patches-6.18/746-net-dsa-mt7530-pre-deassert-phy-reset-gpios-before-mdio-scan.patch new file mode 100644 index 00000000000000..122b21f338161a --- /dev/null +++ b/target/linux/airoha/patches-6.18/746-net-dsa-mt7530-pre-deassert-phy-reset-gpios-before-mdio-scan.patch @@ -0,0 +1,56 @@ +From: Gilly +Date: Thu, 8 May 2026 00:00:00 +1000 +Subject: [PATCH] net: dsa: mt7530: deassert PHY reset GPIOs before MDIO bus scan + +--- +--- a/drivers/net/dsa/mt7530.c ++++ b/drivers/net/dsa/mt7530.c +@@ -2272,6 +2272,38 @@ static void mt7530_free_irq(struct mt7530_priv *priv) + mt7530_free_irq_common(priv); + } + ++static void ++mt7530_mdio_predeassert_phy_resets(struct device *dev, struct device_node *mnp) ++{ ++ struct device_node *child; ++ u32 deassert_us; ++ ++ for_each_available_child_of_node(mnp, child) { ++ struct gpio_desc *gpiod; ++ ++ gpiod = fwnode_gpiod_get_index(of_fwnode_handle(child), ++ "reset", 0, GPIOD_OUT_LOW, ++ "mt7530-mdio-scan"); ++ if (IS_ERR_OR_NULL(gpiod)) ++ continue; ++ ++ /* fwnode_gpiod_get_index with GPIOD_OUT_LOW already drove ++ * the line low (deasserted, since reset-gpios in DTS is ++ * GPIO_ACTIVE_LOW). Honour the declared settle time before ++ * releasing so phylib can re-acquire the line for its own ++ * probe-time reset cycle. ++ */ ++ if (of_property_read_u32(child, "reset-deassert-us", ++ &deassert_us) == 0 && deassert_us) ++ fsleep(deassert_us); ++ ++ gpiod_put(gpiod); ++ ++ dev_dbg(dev, "pre-deasserted reset for PHY %pOFn\n", ++ child); ++ } ++} ++ + static int + mt7530_setup_mdio(struct mt7530_priv *priv) + { +@@ -2306,6 +2338,9 @@ mt7530_setup_mdio(struct mt7530_priv *priv) + if (priv->irq && !mnp) + mt7530_setup_mdio_irq(priv); + ++ if (mnp) ++ mt7530_mdio_predeassert_phy_resets(dev, mnp); ++ + ret = devm_of_mdiobus_register(dev, bus, mnp); + if (ret) { + dev_err(dev, "failed to register MDIO bus: %d\n", ret); diff --git a/target/linux/airoha/patches-6.18/910-02-usb-pcie.patch b/target/linux/airoha/patches-6.18/910-02-usb-pcie.patch new file mode 100644 index 00000000000000..6d1de706d418ce --- /dev/null +++ b/target/linux/airoha/patches-6.18/910-02-usb-pcie.patch @@ -0,0 +1,24 @@ +--- + drivers/clk/clk-en7523.c | 4 +++- + 1 file changed, 3 insertions(+), 1 deletion(-) + +--- a/drivers/clk/clk-en7523.c ++++ b/drivers/clk/clk-en7523.c +@@ -42,6 +42,7 @@ + #define REG_NP_SCU_SSTR 0x9c + #define REG_PCIE_XSI0_SEL_MASK GENMASK(14, 13) + #define REG_PCIE_XSI1_SEL_MASK GENMASK(12, 11) ++#define REG_USB_PCIE_SEL BIT(3) + #define REG_CRYPTO_CLKSRC2 0x20c + + #define REG_RST_CTRL2 0x830 +@@ -1128,7 +1129,8 @@ static int en7581_clk_hw_init(struct pla + return err; + + regmap_clear_bits(clk_map, REG_NP_SCU_SSTR, +- REG_PCIE_XSI0_SEL_MASK | REG_PCIE_XSI1_SEL_MASK); ++ REG_PCIE_XSI0_SEL_MASK | REG_PCIE_XSI1_SEL_MASK | ++ REG_USB_PCIE_SEL); + regmap_update_bits(clk_map, REG_NP_SCU_PCIC, REG_PCIE_CTRL, + FIELD_PREP(REG_PCIE_CTRL, 3)); + diff --git a/target/linux/airoha/patches-6.18/915-01-net-netfilter-flowtable-Add-the-capability-to-offloa.patch b/target/linux/airoha/patches-6.18/915-01-net-netfilter-flowtable-Add-the-capability-to-offloa.patch index 5229eb47987b35..dc25d8d95ceece 100644 --- a/target/linux/airoha/patches-6.18/915-01-net-netfilter-flowtable-Add-the-capability-to-offloa.patch +++ b/target/linux/airoha/patches-6.18/915-01-net-netfilter-flowtable-Add-the-capability-to-offloa.patch @@ -102,10 +102,11 @@ Signed-off-by: Lorenzo Bianconi key->tcp.flags = 0; --- a/net/netfilter/nft_flow_offload.c +++ b/net/netfilter/nft_flow_offload.c -@@ -8,6 +8,7 @@ +@@ -8,7 +8,8 @@ #include #include #include + #include +#include #include #include diff --git a/target/linux/airoha/patches-6.18/920-cpufreq-airoha-handle-positive-pm-domain-attach-count.patch b/target/linux/airoha/patches-6.18/920-cpufreq-airoha-handle-positive-pm-domain-attach-count.patch new file mode 100644 index 00000000000000..b38a40802a9e44 --- /dev/null +++ b/target/linux/airoha/patches-6.18/920-cpufreq-airoha-handle-positive-pm-domain-attach-count.patch @@ -0,0 +1,29 @@ +From 28a85072c6e58f820ce69935939c1468e3632bda Mon Sep 17 00:00:00 2001 +From: Yanghan Ye +Date: Sun, 12 Apr 2026 18:55:55 +0800 +Subject: [PATCH] cpufreq: airoha: handle positive PM domain attach count + +dev_pm_domain_attach_list() returns the number of attached PM domains on +success, not zero. Treat only negative values as failures, otherwise the +driver aborts probe as soon as the CPU PM domain is available. + +Signed-off-by: Yanghan Ye +--- + drivers/cpufreq/airoha-cpufreq.c | 2 +- + 1 file changed, 1 insertion(+), 1 deletion(-) + +diff --git a/drivers/cpufreq/airoha-cpufreq.c b/drivers/cpufreq/airoha-cpufreq.c +index e667f59..f88a44e 100644 +--- a/drivers/cpufreq/airoha-cpufreq.c ++++ b/drivers/cpufreq/airoha-cpufreq.c +@@ -65,7 +65,7 @@ static int airoha_cpufreq_probe(struct platform_device *pdev) + /* Attach PM for OPP */ + ret = dev_pm_domain_attach_list(cpu_dev, &attach_data, + &priv->pd_list); +- if (ret) ++ if (ret < 0) + goto clear_opp_config; + + cpufreq_dt = platform_device_register_simple("cpufreq-dt", -1, NULL, 0); +-- +2.43.0 diff --git a/target/linux/airoha/patches-6.18/920-hwrng-airoha-fix-init-sequence-default-to-DRBG.patch b/target/linux/airoha/patches-6.18/920-hwrng-airoha-fix-init-sequence-default-to-DRBG.patch new file mode 100644 index 00000000000000..4a373de91e1817 --- /dev/null +++ b/target/linux/airoha/patches-6.18/920-hwrng-airoha-fix-init-sequence-default-to-DRBG.patch @@ -0,0 +1,255 @@ +From ef64a5e87ad086ce701e63f683886f8f06e65f65 Mon Sep 17 00:00:00 2001 +From: Hello Example +Date: Wed, 7 Jan 2026 14:41:14 +0100 +Subject: [PATCH] hwrng: airoha: fix init sequence, default to DRBG, allow RAW + +Initialize the TRNG more robustly by enabling the oscillator bit and +opening the required SCU clock gates when available via device tree. + +Default the driver to DRBG output and permit opting into raw mode with +the `airoha,raw-mode` DT property. Use quality 900 only in DRBG mode. + +Derived from OpenWRT-fanboy/OpenW1700k PR #1 patch 18/18. +Link: https://github.com/OpenWRT-fanboy/OpenW1700k/pull/1 +Signed-off-by: Yanghan Ye +--- + drivers/char/hw_random/airoha-trng.c | 58 ++++++++++++++++++++++++++++++-------- + 1 file changed, 49 insertions(+), 9 deletions(-) + +--- a/drivers/char/hw_random/airoha-trng.c ++++ b/drivers/char/hw_random/airoha-trng.c +@@ -1,63 +1,79 @@ + // SPDX-License-Identifier: GPL-2.0 + /* Copyright (C) 2024 Christian Marangi */ + + #include + #include + #include + #include + #include + #include + #include + #include + #include ++#include ++#include + #include ++#include + + #define TRNG_IP_RDY 0x800 + #define CNT_TRANS GENMASK(15, 8) + #define SAMPLE_RDY BIT(0) + #define TRNG_NS_SEK_AND_DAT_EN 0x804 + #define RNG_EN BIT(31) /* referenced as ring_en */ + #define RAW_DATA_EN BIT(16) ++#define RNG_OSC_EN BIT(1) + #define TRNG_HEALTH_TEST_SW_RST 0x808 + #define SW_RST BIT(0) /* Active High */ + #define TRNG_INTR_EN 0x818 + #define INTR_MASK BIT(16) + #define CONTINUOUS_HEALTH_INITR_EN BIT(2) + #define SW_STARTUP_INITR_EN BIT(1) + #define RST_STARTUP_INITR_EN BIT(0) + /* Notice that Health Test are done only out of Reset and with RNG_EN */ + #define TRNG_HEALTH_TEST_STATUS 0x824 + #define CONTINUOUS_HEALTH_AP_TEST_FAIL BIT(23) + #define CONTINUOUS_HEALTH_RC_TEST_FAIL BIT(22) + #define SW_STARTUP_TEST_DONE BIT(21) + #define SW_STARTUP_AP_TEST_FAIL BIT(20) + #define SW_STARTUP_RC_TEST_FAIL BIT(19) + #define RST_STARTUP_TEST_DONE BIT(18) + #define RST_STARTUP_AP_TEST_FAIL BIT(17) + #define RST_STARTUP_RC_TEST_FAIL BIT(16) + #define RAW_DATA_VALID BIT(7) + + #define TRNG_RAW_DATA_OUT 0x828 ++#define TRNG_TEST_MODE_SHA_DONE 0x838 ++#define DRBG_DATA_RDY BIT(31) ++#define TRNG_DRBG_DATA_OUT 0x83c ++ ++#define REG_SCU_BUS_CLK_GAT 0x1e4 ++#define REG_SCU_PER_CLK_GAT_1 0x1ec ++#define REG_SCU_PER_CLK_GAT_2 0x200 ++#define SCU_TRNG_BUS_EN BIT(3) ++#define SCU_TRNG_PER1_EN BIT(21) ++#define SCU_TRNG_PER2_EN BIT(0) + + #define TRNG_CNT_TRANS_VALID 0x80 + #define BUSY_LOOP_SLEEP 10 + #define BUSY_LOOP_TIMEOUT (BUSY_LOOP_SLEEP * 10000) + + struct airoha_trng { + void __iomem *base; + struct hwrng rng; + struct device *dev; + + struct completion rng_op_done; ++ struct regmap *scu; ++ bool use_drbg; + }; + + static int airoha_trng_irq_mask(struct airoha_trng *trng) + { + u32 val; + + val = readl(trng->base + TRNG_INTR_EN); + val |= INTR_MASK; + writel(val, trng->base + TRNG_INTR_EN); + + return 0; + } +@@ -71,27 +87,33 @@ + writel(val, trng->base + TRNG_INTR_EN); + + return 0; + } + + static int airoha_trng_init(struct hwrng *rng) + { + struct airoha_trng *trng = container_of(rng, struct airoha_trng, rng); + int ret; + u32 val; + + val = readl(trng->base + TRNG_NS_SEK_AND_DAT_EN); +- val |= RNG_EN; ++ val |= RNG_EN | RNG_OSC_EN; + writel(val, trng->base + TRNG_NS_SEK_AND_DAT_EN); + ++ if (trng->scu) { ++ regmap_set_bits(trng->scu, REG_SCU_BUS_CLK_GAT, SCU_TRNG_BUS_EN); ++ regmap_set_bits(trng->scu, REG_SCU_PER_CLK_GAT_1, SCU_TRNG_PER1_EN); ++ regmap_set_bits(trng->scu, REG_SCU_PER_CLK_GAT_2, SCU_TRNG_PER2_EN); ++ } ++ + /* Set out of SW Reset */ + airoha_trng_irq_unmask(trng); + writel(0, trng->base + TRNG_HEALTH_TEST_SW_RST); + + ret = wait_for_completion_timeout(&trng->rng_op_done, BUSY_LOOP_TIMEOUT); + if (ret <= 0) { + dev_err(trng->dev, "Timeout waiting for Health Check\n"); + airoha_trng_irq_mask(trng); + return -ENODEV; + } + + /* Check if Health Test Failed */ +@@ -130,35 +152,45 @@ + val = readl(trng->base + TRNG_NS_SEK_AND_DAT_EN); + val &= ~RNG_EN; + writel(val, trng->base + TRNG_NS_SEK_AND_DAT_EN); + + /* Put it in SW Reset */ + writel(SW_RST, trng->base + TRNG_HEALTH_TEST_SW_RST); + } + + static int airoha_trng_read(struct hwrng *rng, void *buf, size_t max, bool wait) + { + struct airoha_trng *trng = container_of(rng, struct airoha_trng, rng); + u32 *data = buf; +- u32 status; ++ u32 status, status_reg, data_reg, rdy_mask; + int ret; + +- ret = readl_poll_timeout(trng->base + TRNG_HEALTH_TEST_STATUS, status, +- status & RAW_DATA_VALID, 10, 1000); ++ if (trng->use_drbg) { ++ status_reg = TRNG_TEST_MODE_SHA_DONE; ++ data_reg = TRNG_DRBG_DATA_OUT; ++ rdy_mask = DRBG_DATA_RDY; ++ } else { ++ status_reg = TRNG_HEALTH_TEST_STATUS; ++ data_reg = TRNG_RAW_DATA_OUT; ++ rdy_mask = RAW_DATA_VALID; ++ } ++ ++ ret = readl_poll_timeout(trng->base + status_reg, status, ++ status & rdy_mask, 10, 10000); + if (ret < 0) { +- dev_err(trng->dev, "Timeout waiting for TRNG RAW Data valid\n"); ++ dev_err(trng->dev, "Timeout waiting for TRNG data\n"); + return ret; + } + +- *data = readl(trng->base + TRNG_RAW_DATA_OUT); ++ *data = readl(trng->base + data_reg); + + return 4; + } + + static irqreturn_t airoha_trng_irq(int irq, void *priv) + { + struct airoha_trng *trng = (struct airoha_trng *)priv; + + airoha_trng_irq_mask(trng); + /* Just complete the task, we will read the value later */ + complete(&trng->rng_op_done); + +@@ -171,57 +203,65 @@ + struct airoha_trng *trng; + int irq, ret; + u32 val; + + trng = devm_kzalloc(dev, sizeof(*trng), GFP_KERNEL); + if (!trng) + return -ENOMEM; + + trng->base = devm_platform_ioremap_resource(pdev, 0); + if (IS_ERR(trng->base)) + return PTR_ERR(trng->base); + ++ trng->scu = syscon_regmap_lookup_by_phandle(dev->of_node, "airoha,scu"); ++ if (IS_ERR(trng->scu)) ++ trng->scu = NULL; ++ trng->use_drbg = !of_property_read_bool(dev->of_node, "airoha,raw-mode"); ++ + irq = platform_get_irq(pdev, 0); + if (irq < 0) + return irq; + + airoha_trng_irq_mask(trng); + ret = devm_request_irq(&pdev->dev, irq, airoha_trng_irq, 0, + pdev->name, (void *)trng); + if (ret) { + dev_err(dev, "Can't get interrupt working.\n"); + return ret; + } + + init_completion(&trng->rng_op_done); + + /* Enable interrupt for SW reset Health Check */ + val = readl(trng->base + TRNG_INTR_EN); + val |= RST_STARTUP_INITR_EN; + writel(val, trng->base + TRNG_INTR_EN); + +- /* Set output to raw data */ ++ /* Default to DRBG output, allow raw mode via device-tree. */ + val = readl(trng->base + TRNG_NS_SEK_AND_DAT_EN); +- val |= RAW_DATA_EN; ++ if (trng->use_drbg) ++ val &= ~RAW_DATA_EN; ++ else ++ val |= RAW_DATA_EN; + writel(val, trng->base + TRNG_NS_SEK_AND_DAT_EN); + + /* Put it in SW Reset */ + writel(SW_RST, trng->base + TRNG_HEALTH_TEST_SW_RST); + + trng->dev = dev; + trng->rng.name = pdev->name; + trng->rng.init = airoha_trng_init; + trng->rng.cleanup = airoha_trng_cleanup; + trng->rng.read = airoha_trng_read; +- trng->rng.quality = 900; ++ trng->rng.quality = trng->use_drbg ? 900 : 0; + + ret = devm_hwrng_register(dev, &trng->rng); + if (ret) { + dev_err(dev, "failed to register rng device: %d\n", ret); + return ret; + } + + return 0; + } + + static const struct of_device_id airoha_trng_of_match[] = { + { .compatible = "airoha,en7581-trng", }, diff --git a/target/linux/airoha/patches-6.18/921-net-airoha-npu-fix-firmware-loading-issue.patch b/target/linux/airoha/patches-6.18/921-net-airoha-npu-fix-firmware-loading-issue.patch new file mode 100644 index 00000000000000..28cc68a18fa3c5 --- /dev/null +++ b/target/linux/airoha/patches-6.18/921-net-airoha-npu-fix-firmware-loading-issue.patch @@ -0,0 +1,32 @@ +From 9b835526b225a2d1c34c75f360d224395432f174 Mon Sep 17 00:00:00 2001 +From: Ziyang Huang +Date: Mon, 30 Mar 2026 22:58:49 +0800 +Subject: [PATCH 1/1] net: airoha: npu: fix firmware loading issue. + +This driver is loaded before rootfs mounting. So firmware doesn't exist. +When CONFIG_FW_LOADER_USER_HELPER=n, request_firmware() will return -ENOENT, +-EPROBE_DEFER will be returned to make driver load again after rootfs mounted. + +But when CONFIG_FW_LOADER_USER_HELPER=y, request_firmware() will send uevent +and wait for 60s (the value of fw_fallback_config.loading_timeout). But no +user process is running now (including init, coldplug, hotplug), so +-ETIMEDOUT will return, making driver probe fail. + +To avoid the second cause, let's use request_firmware_direct(). + +Signed-off-by: Ziyang Huang +--- + drivers/net/ethernet/airoha/airoha_npu.c | 2 +- + 1 file changed, 1 insertion(+), 1 deletion(-) + +--- a/drivers/net/ethernet/airoha/airoha_npu.c ++++ b/drivers/net/ethernet/airoha/airoha_npu.c +@@ -202,7 +202,7 @@ static int airoha_npu_load_firmware(struct device *dev, void __iomem *addr, + const struct firmware *fw; + int ret; + +- ret = request_firmware(&fw, fw_name, dev); ++ ret = request_firmware_direct(&fw, fw_name, dev); + if (ret) + return ret == -ENOENT ? -EPROBE_DEFER : ret; + diff --git a/target/linux/airoha/patches-6.18/922-net-airoha-classify-external-lan-ports-from-DT.patch b/target/linux/airoha/patches-6.18/922-net-airoha-classify-external-lan-ports-from-DT.patch new file mode 100644 index 00000000000000..bd76f5e6a7f205 --- /dev/null +++ b/target/linux/airoha/patches-6.18/922-net-airoha-classify-external-lan-ports-from-DT.patch @@ -0,0 +1,53 @@ +From 86e1f84e8c9a15c61985f2329e20462ff3d55ca0 Mon Sep 17 00:00:00 2001 +From: Yanghan Ye +Date: Sun, 12 Apr 2026 18:55:56 +0800 +Subject: [PATCH] net: airoha: classify external lan ports from DT + +XR1710G and W1700K expose an external PHY on GDM4, but that port is a +LAN member, not a WAN uplink. The current driver assumes only GDM1 is a +LAN port on EN7581 and therefore treats GDM4 as WAN. + +That misclassification feeds directly into the recent offload code paths: +- the port is pinned to the WAN QDMA block +- FE LAN/WAN MAC programming uses the wrong register bank +- PPE fast-path handling misses the LAN case + +As a result, NPU/PPE offload never becomes effective on these boards and +traffic falls back to CPU forwarding. + +Allow board DTS files to mark external LAN ports explicitly and keep an +OpenWrt netdev-name fallback for already named boards. + +Signed-off-by: Yanghan Ye +--- + drivers/net/ethernet/airoha/airoha_eth.c | 16 ++++++++++++++++ + 1 file changed, 16 insertions(+) + +--- a/drivers/net/ethernet/airoha/airoha_eth.c ++++ b/drivers/net/ethernet/airoha/airoha_eth.c +@@ -75,4 +75,17 @@ static void airoha_qdma_irq_disable(stru + airoha_qdma_set_irqmask(irq_bank, index, mask, 0); + } + ++static bool airoha_dev_is_lan(struct device_node *np, u32 id) ++{ ++ const char *name; ++ ++ if (of_property_read_bool(np, "airoha,lan-port")) ++ return true; ++ ++ if (!of_property_read_string(np, "openwrt,netdev-name", &name)) ++ return !strncmp(name, "lan", 3); ++ ++ return id == AIROHA_GDM1_IDX; ++} ++ + static int airoha_set_macaddr(struct airoha_gdm_dev *dev, const u8 *addr) +@@ -3894,4 +3907,7 @@ static int airoha_alloc_gdm_device(struc + dev->port = port; + dev->eth = eth; + dev->nbq = nbq; ++ if (!airoha_dev_is_lan(np, port->id)) ++ dev->flags |= AIROHA_PRIV_F_WAN; ++ + port->devs[index] = dev; diff --git a/target/linux/airoha/patches-6.18/990-01-smartrg-netfilter-nf_flow_table-invalidate-flows-on-bridge-FDB-roaming.patch b/target/linux/airoha/patches-6.18/990-01-smartrg-netfilter-nf_flow_table-invalidate-flows-on-bridge-FDB-roaming.patch new file mode 100644 index 00000000000000..bb78776c312300 --- /dev/null +++ b/target/linux/airoha/patches-6.18/990-01-smartrg-netfilter-nf_flow_table-invalidate-flows-on-bridge-FDB-roaming.patch @@ -0,0 +1,273 @@ +From: Chad Monroe +Date: Sun, 02 Feb 2026 10:30:00 -0800 +Subject: [PATCH] netfilter: nf_flow_table: invalidate flows on bridge FDB roaming + +When a client roams between bridge ports (WiFi APs, wired to wireless, +etc.), offloaded flows with stale destination MACs continue forwarding +to the wrong port until they time out. + +Register a switchdev notifier to detect FDB deletions and tear down +affected flows immediately. Workqueue defers cleanup from atomic +notifier context. + +Optimizations to reduce CPU overhead under FDB churn: +- Track per-flowtable direct-xmit flow count; skip flowtables with none +- Skip multicast/broadcast FDB entries + +Signed-off-by: Chad Monroe +--- + include/net/netfilter/nf_flow_table.h | 12 ++ + net/netfilter/nf_flow_table_core.c | 139 ++++++++++++++++++++++++++ + 2 files changed, 151 insertions(+) + +--- a/net/netfilter/nf_flow_table_core.c ++++ b/net/netfilter/nf_flow_table_core.c +@@ -5,6 +5,7 @@ + #include + #include + #include ++#include + #include + #include + #include +@@ -12,10 +13,23 @@ + #include + #include + #include ++#if IS_ENABLED(CONFIG_NET_SWITCHDEV) ++#include ++#endif + + static DEFINE_MUTEX(flowtable_lock); + static LIST_HEAD(flowtables); + ++#if IS_ENABLED(CONFIG_NET_SWITCHDEV) ++static struct workqueue_struct *flow_mac_cleanup_wq; ++ ++struct flow_mac_cleanup { ++ struct work_struct work; ++ struct net *net; ++ unsigned char mac[ETH_ALEN]; ++}; ++#endif ++ + static void + flow_offload_fill_dir(struct flow_offload *flow, + enum flow_offload_tuple_dir dir) +@@ -295,6 +309,12 @@ int flow_offload_add(struct nf_flowtable + + nf_ct_offload_timeout(flow->ct); + ++#if IS_ENABLED(CONFIG_NET_SWITCHDEV) ++ if (flow->tuplehash[0].tuple.xmit_type == FLOW_OFFLOAD_XMIT_DIRECT || ++ flow->tuplehash[1].tuple.xmit_type == FLOW_OFFLOAD_XMIT_DIRECT) ++ atomic_inc(&flow_table->direct_xmit_count); ++#endif ++ + if (nf_flowtable_hw_offload(flow_table)) { + __set_bit(NF_FLOW_HW, &flow->flags); + nf_flow_offload_add(flow_table, flow); +@@ -330,6 +350,11 @@ static inline bool nf_flow_has_expired(c + static void flow_offload_del(struct nf_flowtable *flow_table, + struct flow_offload *flow) + { ++#if IS_ENABLED(CONFIG_NET_SWITCHDEV) ++ if (flow->tuplehash[0].tuple.xmit_type == FLOW_OFFLOAD_XMIT_DIRECT || ++ flow->tuplehash[1].tuple.xmit_type == FLOW_OFFLOAD_XMIT_DIRECT) ++ atomic_dec(&flow_table->direct_xmit_count); ++#endif + rhashtable_remove_fast(&flow_table->rhashtable, + &flow->tuplehash[FLOW_OFFLOAD_DIR_ORIGINAL].node, + nf_flow_offload_rhash_params); +@@ -544,6 +569,9 @@ int nf_flow_table_init(struct nf_flowtab + INIT_DELAYED_WORK(&flowtable->gc_work, nf_flow_offload_work_gc); + flow_block_init(&flowtable->flow_block); + init_rwsem(&flowtable->flow_block_lock); ++#if IS_ENABLED(CONFIG_NET_SWITCHDEV) ++ atomic_set(&flowtable->direct_xmit_count, 0); ++#endif + + err = rhashtable_init(&flowtable->rhashtable, + &nf_flow_offload_rhash_params); +@@ -596,6 +624,115 @@ void nf_flow_table_cleanup(struct net_de + } + EXPORT_SYMBOL_GPL(nf_flow_table_cleanup); + ++#if IS_ENABLED(CONFIG_NET_SWITCHDEV) ++static void nf_flow_table_do_cleanup_mac(struct nf_flowtable *flowtable, ++ struct flow_offload *flow, void *data) ++{ ++ struct flow_mac_cleanup *cleanup = data; ++ struct flow_offload_tuple *t = &flow->tuplehash[0].tuple; ++ ++ if (!net_eq(nf_ct_net(flow->ct), cleanup->net)) ++ return; ++ ++ if ((flow->tuplehash[0].tuple.xmit_type == FLOW_OFFLOAD_XMIT_DIRECT && ++ ether_addr_equal(flow->tuplehash[0].tuple.out.h_dest, cleanup->mac)) || ++ (flow->tuplehash[1].tuple.xmit_type == FLOW_OFFLOAD_XMIT_DIRECT && ++ ether_addr_equal(flow->tuplehash[1].tuple.out.h_dest, cleanup->mac))) { ++ if (t->l3proto == NFPROTO_IPV4) ++ pr_debug("nf_flow_table: teardown flow %pI4:%u->%pI4:%u proto=%u for roaming mac=%pM\n", ++ &t->src_v4, ntohs(t->src_port), ++ &t->dst_v4, ntohs(t->dst_port), ++ t->l4proto, cleanup->mac); ++ else ++ pr_debug("nf_flow_table: teardown flow %pI6c:%u->%pI6c:%u proto=%u for roaming mac=%pM\n", ++ &t->src_v6, ntohs(t->src_port), ++ &t->dst_v6, ntohs(t->dst_port), ++ t->l4proto, cleanup->mac); ++ flow_offload_teardown(flow); ++ } ++} ++ ++static void nf_flow_table_mac_cleanup_work(struct work_struct *work) ++{ ++ struct flow_mac_cleanup *cleanup = ++ container_of(work, struct flow_mac_cleanup, work); ++ struct nf_flowtable *flowtable; ++ ++ pr_debug("nf_flow_table: processing roaming cleanup for mac=%pM\n", ++ cleanup->mac); ++ ++ mutex_lock(&flowtable_lock); ++ list_for_each_entry(flowtable, &flowtables, list) { ++ if (nf_flowtable_has_direct_xmit(flowtable)) ++ nf_flow_table_iterate(flowtable, ++ nf_flow_table_do_cleanup_mac, cleanup); ++ ++ /* do this implicitly.. it seems to help expire software path ++ * entries faster but still need to get concrete data */ ++ flush_delayed_work(&flowtable->gc_work); ++ nf_flow_table_offload_flush(flowtable); ++ } ++ mutex_unlock(&flowtable_lock); ++ ++ pr_debug("nf_flow_table: roaming cleanup complete for mac=%pM\n", ++ cleanup->mac); ++ ++ put_net(cleanup->net); ++ kfree(cleanup); ++} ++ ++static void nf_flow_table_cleanup_mac(struct net *net, const unsigned char *mac) ++{ ++ struct flow_mac_cleanup *cleanup; ++ ++ cleanup = kmalloc(sizeof(*cleanup), GFP_ATOMIC); ++ if (!cleanup) ++ return; ++ ++ cleanup->net = get_net(net); ++ memcpy(cleanup->mac, mac, ETH_ALEN); ++ INIT_WORK(&cleanup->work, nf_flow_table_mac_cleanup_work); ++ queue_work(flow_mac_cleanup_wq, &cleanup->work); ++} ++ ++static int nf_flow_table_switchdev_event(struct notifier_block *nb, ++ unsigned long event, void *ptr) ++{ ++ struct switchdev_notifier_fdb_info *fdb_info = ptr; ++ struct net_device *dev; ++ ++ if (event != SWITCHDEV_FDB_DEL_TO_DEVICE) ++ return NOTIFY_DONE; ++ ++ if (!fdb_info || !fdb_info->addr || !fdb_info->info.dev) ++ return NOTIFY_DONE; ++ ++ dev = fdb_info->info.dev; ++ ++ if (is_multicast_ether_addr(fdb_info->addr)) ++ return NOTIFY_DONE; ++ ++ if (fdb_info->is_local) ++ return NOTIFY_DONE; ++ ++ if (netif_is_bridge_master(dev)) ++ return NOTIFY_DONE; ++ ++ if (!netif_oper_up(dev)) ++ return NOTIFY_DONE; ++ ++ pr_debug("nf_flow_table: FDB roaming detected del mac=%pM from port=%s\n", ++ fdb_info->addr, dev->name); ++ ++ nf_flow_table_cleanup_mac(dev_net(dev), fdb_info->addr); ++ return NOTIFY_DONE; ++} ++ ++static struct notifier_block flow_offload_switchdev_notifier = { ++ .notifier_call = nf_flow_table_switchdev_event, ++}; ++#endif ++ + void nf_flow_table_free(struct nf_flowtable *flow_table) + { + mutex_lock(&flowtable_lock); +@@ -694,8 +831,27 @@ static int __init nf_flow_table_module_i + if (ret) + goto out_bpf; + ++#if IS_ENABLED(CONFIG_NET_SWITCHDEV) ++ flow_mac_cleanup_wq = alloc_workqueue("nf_flow_mac_cleanup", ++ WQ_UNBOUND, 0); ++ if (!flow_mac_cleanup_wq) { ++ ret = -ENOMEM; ++ goto out_wq; ++ } ++ ++ ret = register_switchdev_notifier(&flow_offload_switchdev_notifier); ++ if (ret) ++ goto out_switchdev; ++#endif ++ + return 0; + ++#if IS_ENABLED(CONFIG_NET_SWITCHDEV) ++out_switchdev: ++ destroy_workqueue(flow_mac_cleanup_wq); ++out_wq: ++ unregister_netdevice_notifier(&flow_offload_netdev_notifier); ++#endif + out_bpf: + nf_flow_table_offload_exit(); + out_offload: +@@ -705,6 +861,11 @@ out_offload: + + static void __exit nf_flow_table_module_exit(void) + { ++#if IS_ENABLED(CONFIG_NET_SWITCHDEV) ++ unregister_switchdev_notifier(&flow_offload_switchdev_notifier); ++ flush_workqueue(flow_mac_cleanup_wq); ++ destroy_workqueue(flow_mac_cleanup_wq); ++#endif + unregister_netdevice_notifier(&flow_offload_netdev_notifier); + nf_flow_table_offload_exit(); + unregister_pernet_subsys(&nf_flow_table_net_ops); +--- a/include/net/netfilter/nf_flow_table.h ++++ b/include/net/netfilter/nf_flow_table.h +@@ -84,6 +84,9 @@ struct nf_flowtable { + struct flow_block flow_block; + struct rw_semaphore flow_block_lock; /* Guards flow_block */ + possible_net_t net; ++#if IS_ENABLED(CONFIG_NET_SWITCHDEV) ++ atomic_t direct_xmit_count; ++#endif + }; + + static inline bool nf_flowtable_hw_offload(struct nf_flowtable *flowtable) +@@ -91,6 +94,13 @@ static inline bool nf_flowtable_hw_offlo + return flowtable->flags & NF_FLOWTABLE_HW_OFFLOAD; + } + ++#if IS_ENABLED(CONFIG_NET_SWITCHDEV) ++static inline bool nf_flowtable_has_direct_xmit(struct nf_flowtable *flowtable) ++{ ++ return atomic_read(&flowtable->direct_xmit_count) > 0; ++} ++#endif ++ + enum flow_offload_tuple_dir { + FLOW_OFFLOAD_DIR_ORIGINAL = IP_CT_DIR_ORIGINAL, + FLOW_OFFLOAD_DIR_REPLY = IP_CT_DIR_REPLY, diff --git a/target/linux/airoha/patches-6.18/991-net-neigh-Reallocate-headroom-if-necessary-in-neigh_.patch b/target/linux/airoha/patches-6.18/991-net-neigh-Reallocate-headroom-if-necessary-in-neigh_.patch new file mode 100644 index 00000000000000..7da0d76aca7405 --- /dev/null +++ b/target/linux/airoha/patches-6.18/991-net-neigh-Reallocate-headroom-if-necessary-in-neigh_.patch @@ -0,0 +1,133 @@ +From d2fd58be96aa24ae82c951c5a56c53ea703dcd5c Mon Sep 17 00:00:00 2001 +Message-ID: +From: Lorenzo Bianconi +Date: Thu, 7 May 2026 15:07:49 +0200 +Subject: [PATCH] net: neigh: Reallocate headroom if necessary in + neigh_hh_bridge() + +neigh_hh_bridge() assumes the skb always has sufficient headroom to copy +the aligned L2 header. This assumption can trigger the crash reported +below using the following netfilter setup: + +$modprobe br_netfilter +$sysctl -w net.bridge.bridge-nf-call-iptables=1 + +$root@OpenWrt:~# nft list ruleset +table ip nat { + chain prerouting { + type nat hook prerouting priority dstnat; policy accept; + ip daddr 192.168.83.123 dnat to 192.168.83.120 + } +} + +- iperf3 client (192.168.83.119) --> bridge (192.168.83.118) --> iperf3 server (192.168.83.120) + +the iperf3 client is sending packet for 192.168.83.123 to the bridge device. + +[ 1579.036575] Unable to handle kernel write to read-only memory at virtual address ffffff8004d76ffe +[ 1579.045482] Mem abort info: +[ 1579.048273] ESR = 0x000000009600004f +[ 1579.052024] EC = 0x25: DABT (current EL), IL = 32 bits +[ 1579.057363] SET = 0, FnV = 0 +[ 1579.060417] EA = 0, S1PTW = 0 +[ 1579.063550] FSC = 0x0f: level 3 permission fault +[ 1579.068345] Data abort info: +[ 1579.071224] ISV = 0, ISS = 0x0000004f, ISS2 = 0x00000000 +[ 1579.076720] CM = 0, WnR = 1, TnD = 0, TagAccess = 0 +[ 1579.081770] GCS = 0, Overlay = 0, DirtyBit = 0, Xs = 0 +[ 1579.087092] swapper pgtable: 4k pages, 39-bit VAs, pgdp=0000000080dc4000 +[ 1579.093794] [ffffff8004d76ffe] pgd=180000009ffff003, p4d=180000009ffff003, pud=180000009ffff003, pmd=180000009ffe3003, pte=0060000084d76787 +[ 1579.106343] Internal error: Oops: 000000009600004f [#1] SMP +[ 1579.193824] CPU: 0 UID: 0 PID: 235 Comm: napi/qdma_eth-3 Tainted: G O 6.12.57 #0 +[ 1579.202614] Tainted: [O]=OOT_MODULE +[ 1579.206102] Hardware name: Airoha AN7581 Evaluation Board (DT) +[ 1579.211929] pstate: 60400005 (nZCv daif +PAN -UAO -TCO -DIT -SSBS BTYPE=--) +[ 1579.218889] pc : br_nf_pre_routing_finish_bridge+0x1ac/0xcc8 [br_netfilter] +[ 1579.225859] lr : br_nf_pre_routing_finish_bridge+0x18c/0xcc8 [br_netfilter] +[ 1579.232822] sp : ffffffc0817cba20 +[ 1579.236128] x29: ffffffc0817cba20 x28: 0000000000000000 x27: ffffff8002b89000 +[ 1579.243273] x26: ffffff8004d7700e x25: 0000000000000008 x24: 0000000000000000 +[ 1579.250416] x23: ffffffc08179d4c0 x22: 0000000000000000 x21: ffffffc08179d4c0 +[ 1579.257561] x20: ffffff8004d9b800 x19: ffffff8015010000 x18: 0000000000000014 +[ 1579.264704] x17: ffffffbf9e930000 x16: ffffffc0817c8000 x15: 0000000000000070 +[ 1579.271848] x14: 0000000000000080 x13: 0000000000000001 x12: 0000000000000000 +[ 1579.278993] x11: ffffffc0798caae0 x10: ffffff8014db6fd8 x9 : 0000000000000000 +[ 1579.286136] x8 : 0000000000000003 x7 : ffffffc08171f628 x6 : 000000001a3b83d3 +[ 1579.293281] x5 : 0000000000000000 x4 : 1beb76f22fee0000 x3 : ffffff8004d7700e +[ 1579.300425] x2 : 0000000000000000 x1 : ffffff8004d9b8bc x0 : ffffff80026ed000 +[ 1579.307570] Call trace: +[ 1579.310018] br_nf_pre_routing_finish_bridge+0x1ac/0xcc8 [br_netfilter] +[ 1579.316632] br_nf_hook_thresh+0xd4/0x14bc [br_netfilter] +[ 1579.322032] br_nf_hook_thresh+0x250/0x14bc [br_netfilter] +[ 1579.327517] br_nf_hook_thresh+0x76c/0x14bc [br_netfilter] +[ 1579.333003] br_handle_frame+0x180/0x480 +[ 1579.336935] __netif_receive_skb_core.constprop.0+0x540/0xf40 +[ 1579.342682] __netif_receive_skb_one_core+0x28/0x50 +[ 1579.347561] process_backlog+0x98/0x1e0 +[ 1579.351398] __napi_poll+0x34/0x1c4 +[ 1579.354887] net_rx_action+0x178/0x330 +[ 1579.358638] handle_softirqs+0x108/0x2d4 +[ 1579.362560] __do_softirq+0x10/0x18 +[ 1579.366051] ____do_softirq+0xc/0x20 +[ 1579.369627] call_on_irq_stack+0x30/0x4c +[ 1579.373550] do_softirq_own_stack+0x18/0x20 +[ 1579.377734] do_softirq+0x4c/0x60 +[ 1579.381050] __local_bh_enable_ip+0x88/0x98 +[ 1579.385234] napi_threaded_poll_loop+0x188/0x21c +[ 1579.389853] napi_threaded_poll+0x70/0x80 +[ 1579.393863] kthread+0xd8/0xdc +[ 1579.396918] ret_from_fork+0x10/0x20 +[ 1579.400499] Code: 88dffc22 3707ffc2 f9406663 f9406684 (f81f0064) +[ 1579.406589] ---[ end trace 0000000000000000 ]--- +[ 1579.411209] Kernel panic - not syncing: Oops: Fatal exception in interrupt +[ 1579.418083] SMP: stopping secondary CPUs +[ 1579.422012] Kernel Offset: disabled + +Fix the issue reallocating the skb headroom if necessary in neigh_hh_bridge routine. + +Fixes: e179e6322ac33 ("netfilter: bridge-netfilter: Fix MAC header handling with IP DNAT") +Reviewed-by: Ido Schimmel +Signed-off-by: Lorenzo Bianconi +--- + include/net/neighbour.h | 8 ++++++-- + net/bridge/br_netfilter_hooks.c | 6 +++++- + 2 files changed, 11 insertions(+), 3 deletions(-) + +--- a/include/net/neighbour.h ++++ b/include/net/neighbour.h +@@ -489,11 +489,15 @@ static inline int neigh_event_send(struc + #if IS_ENABLED(CONFIG_BRIDGE_NETFILTER) + static inline int neigh_hh_bridge(struct hh_cache *hh, struct sk_buff *skb) + { +- unsigned int seq, hh_alen; ++ unsigned int seq, hh_alen = HH_DATA_ALIGN(ETH_HLEN); ++ int err; ++ ++ err = skb_cow_head(skb, hh_alen); ++ if (err) ++ return err; + + do { + seq = read_seqbegin(&hh->hh_lock); +- hh_alen = HH_DATA_ALIGN(ETH_HLEN); + memcpy(skb->data - hh_alen, hh->hh_data, ETH_ALEN + hh_alen - ETH_HLEN); + } while (read_seqretry(&hh->hh_lock, seq)); + return 0; +--- a/net/bridge/br_netfilter_hooks.c ++++ b/net/bridge/br_netfilter_hooks.c +@@ -297,7 +297,11 @@ int br_nf_pre_routing_finish_bridge(stru + goto free_skb; + } + +- neigh_hh_bridge(&neigh->hh, skb); ++ if (neigh_hh_bridge(&neigh->hh, skb)) { ++ neigh_release(neigh); ++ goto free_skb; ++ } ++ + skb->dev = br_indev; + + ret = br_handle_frame_finish(net, sk, skb); +-- +2.54.0 + diff --git a/target/linux/airoha/patches-6.18/999-dsa-01-add-mt7531-netlink-support.patch b/target/linux/airoha/patches-6.18/999-dsa-01-add-mt7531-netlink-support.patch new file mode 100644 index 00000000000000..64c7d566a786be --- /dev/null +++ b/target/linux/airoha/patches-6.18/999-dsa-01-add-mt7531-netlink-support.patch @@ -0,0 +1,564 @@ +From 8700a460e6be1ff4dc657390c597ae6b918204ef Mon Sep 17 00:00:00 2001 +From: "neal.yen" +Date: Wed, 22 Oct 2025 11:17:31 +0800 +Subject: [PATCH] net: dsa: add mt7531 netlink support for switch app + + +diff --git a/drivers/net/dsa/Makefile b/drivers/net/dsa/Makefile +index 1e9d01e..fd740d0 100644 +--- a/drivers/net/dsa/Makefile ++++ b/drivers/net/dsa/Makefile +@@ -8,7 +8,8 @@ endif + bcm-sf2-objs := bcm_sf2.o bcm_sf2_cfp.o + obj-$(CONFIG_NET_DSA_LOOP) += dsa_loop.o + obj-$(CONFIG_NET_DSA_KS8995) += ks8995.o +-obj-$(CONFIG_NET_DSA_MT7530) += mt7530.o ++obj-$(CONFIG_NET_DSA_MT7530) += mt7530-dsa.o ++mt7530-dsa-objs := mt7530.o mt7530_nl.o + obj-$(CONFIG_NET_DSA_MT7530_MDIO) += mt7530-mdio.o + obj-$(CONFIG_NET_DSA_MT7530_MMIO) += mt7530-mmio.o + obj-$(CONFIG_NET_DSA_MV88E6060) += mv88e6060.o +diff --git a/drivers/net/dsa/mt7530-mdio.c b/drivers/net/dsa/mt7530-mdio.c +index 6163be7..5a99a68 100644 +--- a/drivers/net/dsa/mt7530-mdio.c ++++ b/drivers/net/dsa/mt7530-mdio.c +@@ -14,6 +14,7 @@ + #include + + #include "mt7530.h" ++#include "mt7530_nl.h" + + static int + mt7530_regmap_write(void *context, unsigned int reg, unsigned int val) +@@ -212,7 +213,20 @@ mt7530_probe(struct mdio_device *mdiodev) + if (priv->id == ID_MT7531) + priv->create_sgmii = mt7531_create_sgmii; + +- return dsa_register_switch(priv->ds); ++ ret = dsa_register_switch(priv->ds); ++ if (ret) { ++ dev_err(&mdiodev->dev, "Failed to register DSA switch: %d\n", ret); ++ return ret; ++ } ++ ++ ret = mt7530_nl_init(&priv); ++ if (ret) { ++ dev_err(&mdiodev->dev, "Failed to initialize netlink with DSA: %d\n", ret); ++ dsa_unregister_switch(priv->ds); ++ return ret; ++ } ++ ++ return 0; + } + + static void +@@ -235,6 +249,7 @@ mt7530_remove(struct mdio_device *mdiodev) + ret); + + mt7530_remove_common(priv); ++ mt7530_nl_exit(); + + for (i = 0; i < 2; ++i) + mtk_pcs_lynxi_destroy(priv->ports[5 + i].sgmii_pcs); +diff --git a/drivers/net/dsa/mt7530-mmio.c b/drivers/net/dsa/mt7530-mmio.c +index 10dc499..7e7a033 100644 +--- a/drivers/net/dsa/mt7530-mmio.c ++++ b/drivers/net/dsa/mt7530-mmio.c +@@ -9,6 +9,7 @@ + #include + + #include "mt7530.h" ++#include "mt7530_nl.h" + + static const struct of_device_id mt7988_of_match[] = { + { .compatible = "airoha,en7581-switch", .data = &mt753x_table[ID_EN7581], }, +@@ -61,7 +62,20 @@ mt7988_probe(struct platform_device *pdev) + if (IS_ERR(priv->regmap)) + return PTR_ERR(priv->regmap); + +- return dsa_register_switch(priv->ds); ++ ret = dsa_register_switch(priv->ds); ++ if (ret) { ++ dev_err(&pdev->dev, "Failed to register DSA switch: %d\n", ret); ++ return ret; ++ } ++ ++ ret = mt7530_nl_init(&priv); ++ if (ret) { ++ dev_err(&pdev->dev, "Failed to initialize netlink with DSA: %d\n", ret); ++ dsa_unregister_switch(priv->ds); ++ return ret; ++ } ++ ++ return 0; + } + + static void mt7988_remove(struct platform_device *pdev) +@@ -70,6 +84,8 @@ static void mt7988_remove(struct platform_device *pdev) + + if (priv) + mt7530_remove_common(priv); ++ ++ mt7530_nl_exit(); + } + + static void mt7988_shutdown(struct platform_device *pdev) +diff --git a/drivers/net/dsa/mt7530.c b/drivers/net/dsa/mt7530.c +index 93bf085..f8e6ca3 100644 +--- a/drivers/net/dsa/mt7530.c ++++ b/drivers/net/dsa/mt7530.c +@@ -209,8 +209,7 @@ mt7530_mii_read(struct mt7530_priv *priv, u32 reg) + return val; + } + +-static void +-mt7530_write(struct mt7530_priv *priv, u32 reg, u32 val) ++void mt7530_write(struct mt7530_priv *priv, u32 reg, u32 val) + { + mt7530_mutex_lock(priv); + +@@ -239,8 +238,7 @@ _mt7530_read(struct mt7530_dummy_poll *p) + return val; + } + +-static u32 +-mt7530_read(struct mt7530_priv *priv, u32 reg) ++u32 mt7530_read(struct mt7530_priv *priv, u32 reg) + { + struct mt7530_dummy_poll p; + +@@ -573,8 +571,7 @@ static int mt7530_phy_write_c45(struct mt7530_priv *priv, int port, int devad, + return mdiobus_c45_write_nested(priv->bus, port, devad, regnum, val); + } + +-static int +-mt7531_ind_c45_phy_read(struct mt7530_priv *priv, int port, int devad, ++int mt7531_ind_c45_phy_read(struct mt7530_priv *priv, int port, int devad, + int regnum) + { + struct mt7530_dummy_poll p; +@@ -621,8 +618,7 @@ mt7531_ind_c45_phy_read(struct mt7530_priv *priv, int port, int devad, + return ret; + } + +-static int +-mt7531_ind_c45_phy_write(struct mt7530_priv *priv, int port, int devad, ++int mt7531_ind_c45_phy_write(struct mt7530_priv *priv, int port, int devad, + int regnum, u16 data) + { + struct mt7530_dummy_poll p; +@@ -668,8 +664,7 @@ mt7531_ind_c45_phy_write(struct mt7530_priv *priv, int port, int devad, + return ret; + } + +-static int +-mt7531_ind_c22_phy_read(struct mt7530_priv *priv, int port, int regnum) ++int mt7531_ind_c22_phy_read(struct mt7530_priv *priv, int port, int regnum) + { + struct mt7530_dummy_poll p; + int ret; +@@ -705,8 +700,7 @@ mt7531_ind_c22_phy_read(struct mt7530_priv *priv, int port, int regnum) + return ret; + } + +-static int +-mt7531_ind_c22_phy_write(struct mt7530_priv *priv, int port, int regnum, ++int mt7531_ind_c22_phy_write(struct mt7530_priv *priv, int port, int regnum, + u16 data) + { + struct mt7530_dummy_poll p; +diff --git a/drivers/net/dsa/mt7530.h b/drivers/net/dsa/mt7530.h +index 6ad33a9..2aac404 100644 +--- a/drivers/net/dsa/mt7530.h ++++ b/drivers/net/dsa/mt7530.h +@@ -874,6 +874,13 @@ static inline void INIT_MT7530_DUMMY_POLL(struct mt7530_dummy_poll *p, + + int mt7530_probe_common(struct mt7530_priv *priv); + void mt7530_remove_common(struct mt7530_priv *priv); ++void mt7530_write(struct mt7530_priv *priv, u32 reg, u32 val); ++void mt7530_write(struct mt7530_priv *priv, u32 reg, u32 val); ++u32 mt7530_read(struct mt7530_priv *priv, u32 reg); ++int mt7531_ind_c45_phy_read(struct mt7530_priv *priv, int port, int devad, int regnum); ++int mt7531_ind_c45_phy_write(struct mt7530_priv *priv, int port, int devad, int regnum, u16 data); ++int mt7531_ind_c22_phy_read(struct mt7530_priv *priv, int port, int regnum); ++int mt7531_ind_c22_phy_write(struct mt7530_priv *priv, int port, int regnum, u16 data); + + extern const struct dsa_switch_ops mt7530_switch_ops; + extern const struct mt753x_info mt753x_table[]; +diff --git a/drivers/net/dsa/mt7530_nl.c b/drivers/net/dsa/mt7530_nl.c +new file mode 100644 +index 0000000..63e49d7 +--- /dev/null ++++ b/drivers/net/dsa/mt7530_nl.c +@@ -0,0 +1,314 @@ ++// SPDX-License-Identifier: GPL-2.0 ++/* ++ * Copyright (c) 2018 MediaTek Inc. ++ * Author: Sirui Zhao ++ */ ++ ++#include ++#include ++#include ++#include ++#include ++#include ++#include ++#include ++ ++#include "mt7530.h" ++#include "mt7530_nl.h" ++ ++struct mt7530_nl_cmd_item { ++ enum mt7530_cmd cmd; ++ bool require_dev; ++ int (*process)(struct genl_info *info); ++ u32 nr_required_attrs; ++ const enum mt7530_attr *required_attrs; ++}; ++ ++struct mt7530_priv *sw_priv; ++ ++static DEFINE_MUTEX(mt7530_devs_lock); ++ ++static void mt7530_put(void) ++{ ++ mutex_unlock(&mt7530_devs_lock); ++} ++ ++static void mt7530_lock(void) ++{ ++ mutex_lock(&mt7530_devs_lock); ++} ++ ++static int mt7530_nl_response(struct sk_buff *skb, struct genl_info *info); ++ ++static const struct nla_policy mt7530_nl_cmd_policy[] = { ++ [MT7530_ATTR_TYPE_MESG] = { .type = NLA_STRING }, ++ [MT7530_ATTR_TYPE_PHY] = { .type = NLA_S32 }, ++ [MT7530_ATTR_TYPE_REG] = { .type = NLA_S32 }, ++ [MT7530_ATTR_TYPE_VAL] = { .type = NLA_S32 }, ++ [MT7530_ATTR_TYPE_DEV_NAME] = { .type = NLA_S32 }, ++ [MT7530_ATTR_TYPE_DEV_ID] = { .type = NLA_S32 }, ++ [MT7530_ATTR_TYPE_DEVAD] = { .type = NLA_S32 }, ++}; ++ ++static const struct genl_ops mt7530_nl_ops[] = { ++ { ++ .cmd = MT7530_CMD_REQUEST, ++ .doit = mt7530_nl_response, ++ .flags = GENL_ADMIN_PERM, ++ }, { ++ .cmd = MT7530_CMD_READ, ++ .doit = mt7530_nl_response, ++ .flags = GENL_ADMIN_PERM, ++ }, { ++ .cmd = MT7530_CMD_WRITE, ++ .doit = mt7530_nl_response, ++ .flags = GENL_ADMIN_PERM, ++ }, ++}; ++ ++static struct genl_family mt7530_nl_family = { ++ .name = MT7530_DSA_GENL_NAME, ++ .version = MT7530_GENL_VERSION, ++ .maxattr = MT7530_NR_ATTR_TYPE, ++ .ops = mt7530_nl_ops, ++ .n_ops = ARRAY_SIZE(mt7530_nl_ops), ++ .policy = mt7530_nl_cmd_policy, ++}; ++ ++static int mt7530_nl_prepare_reply(struct genl_info *info, u8 cmd, ++ struct sk_buff **skbp) ++{ ++ struct sk_buff *msg; ++ void *reply; ++ ++ if (!info) ++ return -EINVAL; ++ ++ msg = genlmsg_new(NLMSG_GOODSIZE, GFP_KERNEL); ++ if (!msg) ++ return -ENOMEM; ++ ++ /* Construct send-back message header */ ++ reply = genlmsg_put(msg, info->snd_portid, info->snd_seq, ++ &mt7530_nl_family, 0, cmd); ++ if (!reply) { ++ nlmsg_free(msg); ++ return -EINVAL; ++ } ++ ++ *skbp = msg; ++ return 0; ++} ++ ++static int mt7530_nl_send_reply(struct sk_buff *skb, struct genl_info *info) ++{ ++ struct genlmsghdr *genlhdr = nlmsg_data(nlmsg_hdr(skb)); ++ void *reply = genlmsg_data(genlhdr); ++ ++ /* Finalize a generic netlink message (update message header) */ ++ genlmsg_end(skb, reply); ++ ++ /* reply to a request */ ++ return genlmsg_reply(skb, info); ++} ++ ++static s32 mt7530_nl_get_s32(struct genl_info *info, enum mt7530_attr attr, ++ s32 defval) ++{ ++ struct nlattr *na; ++ ++ na = info->attrs[attr]; ++ if (na) ++ return nla_get_s32(na); ++ ++ return defval; ++} ++ ++static int mt7530_nl_get_u32(struct genl_info *info, enum mt7530_attr attr, ++ u32 *val) ++{ ++ struct nlattr *na; ++ ++ na = info->attrs[attr]; ++ if (na) { ++ *val = nla_get_u32(na); ++ return 0; ++ } ++ ++ return -1; ++} ++ ++static int mt7530_nl_reply_read(struct genl_info *info) ++{ ++ struct sk_buff *rep_skb = NULL; ++ s32 phy, devad, reg; ++ int value; ++ int ret = 0; ++ ++ phy = mt7530_nl_get_s32(info, MT7530_ATTR_TYPE_PHY, -1); ++ devad = mt7530_nl_get_s32(info, MT7530_ATTR_TYPE_DEVAD, -1); ++ reg = mt7530_nl_get_s32(info, MT7530_ATTR_TYPE_REG, -1); ++ ++ if (reg < 0) ++ goto err; ++ ++ ret = mt7530_nl_prepare_reply(info, MT7530_CMD_READ, &rep_skb); ++ if (ret < 0) ++ goto err; ++ ++ if (phy >= 0) { ++ if (devad < 0) ++ value = mt7531_ind_c22_phy_read(sw_priv, phy, reg); ++ else ++ value = mt7531_ind_c45_phy_read(sw_priv, phy, devad, reg); ++ } else ++ value = mt7530_read(sw_priv, reg); ++ ++ ret = nla_put_s32(rep_skb, MT7530_ATTR_TYPE_REG, reg); ++ if (ret < 0) ++ goto err; ++ ++ ret = nla_put_s32(rep_skb, MT7530_ATTR_TYPE_VAL, value); ++ if (ret < 0) ++ goto err; ++ ++ return mt7530_nl_send_reply(rep_skb, info); ++ ++err: ++ if (rep_skb) ++ nlmsg_free(rep_skb); ++ ++ return ret; ++} ++ ++static int mt7530_nl_reply_write(struct genl_info *info) ++{ ++ struct sk_buff *rep_skb = NULL; ++ s32 phy, devad, reg; ++ u32 value; ++ int ret = 0; ++ ++ phy = mt7530_nl_get_s32(info, MT7530_ATTR_TYPE_PHY, -1); ++ devad = mt7530_nl_get_s32(info, MT7530_ATTR_TYPE_DEVAD, -1); ++ reg = mt7530_nl_get_s32(info, MT7530_ATTR_TYPE_REG, -1); ++ ++ if (mt7530_nl_get_u32(info, MT7530_ATTR_TYPE_VAL, &value)) ++ goto err; ++ ++ if (reg < 0) ++ goto err; ++ ++ ret = mt7530_nl_prepare_reply(info, MT7530_CMD_WRITE, &rep_skb); ++ if (ret < 0) ++ goto err; ++ ++ if (phy >= 0) { ++ if (devad < 0) ++ mt7531_ind_c22_phy_write(sw_priv, phy, reg, value); ++ else ++ mt7531_ind_c45_phy_write(sw_priv, phy, devad, reg, value); ++ } else ++ mt7530_write(sw_priv, reg, value); ++ ++ ret = nla_put_s32(rep_skb, MT7530_ATTR_TYPE_REG, reg); ++ if (ret < 0) ++ goto err; ++ ++ ret = nla_put_s32(rep_skb, MT7530_ATTR_TYPE_VAL, value); ++ if (ret < 0) ++ goto err; ++ ++ return mt7530_nl_send_reply(rep_skb, info); ++ ++err: ++ if (rep_skb) ++ nlmsg_free(rep_skb); ++ ++ return ret; ++} ++ ++static const enum mt7530_attr mt7530_nl_cmd_read_attrs[] = { ++ MT7530_ATTR_TYPE_REG ++}; ++ ++static const enum mt7530_attr mt7530_nl_cmd_write_attrs[] = { ++ MT7530_ATTR_TYPE_REG, ++ MT7530_ATTR_TYPE_VAL ++}; ++ ++static const struct mt7530_nl_cmd_item mt7530_nl_cmds[] = { ++ { ++ .cmd = MT7530_CMD_READ, ++ .require_dev = true, ++ .process = mt7530_nl_reply_read, ++ .required_attrs = mt7530_nl_cmd_read_attrs, ++ .nr_required_attrs = ARRAY_SIZE(mt7530_nl_cmd_read_attrs), ++ }, { ++ .cmd = MT7530_CMD_WRITE, ++ .require_dev = true, ++ .process = mt7530_nl_reply_write, ++ .required_attrs = mt7530_nl_cmd_write_attrs, ++ .nr_required_attrs = ARRAY_SIZE(mt7530_nl_cmd_write_attrs), ++ } ++}; ++ ++static int mt7530_nl_response(struct sk_buff *skb, struct genl_info *info) ++{ ++ struct genlmsghdr *hdr = nlmsg_data(info->nlhdr); ++ const struct mt7530_nl_cmd_item *cmditem = NULL; ++ u32 sat_req_attrs = 0; ++ int i, ret; ++ ++ for (i = 0; i < ARRAY_SIZE(mt7530_nl_cmds); i++) { ++ if (hdr->cmd == mt7530_nl_cmds[i].cmd) { ++ cmditem = &mt7530_nl_cmds[i]; ++ break; ++ } ++ } ++ ++ if (!cmditem) { ++ pr_info("mt7530-nl: unknown cmd %u\n", hdr->cmd); ++ return -EINVAL; ++ } ++ ++ for (i = 0; i < cmditem->nr_required_attrs; i++) { ++ if (info->attrs[cmditem->required_attrs[i]]) ++ sat_req_attrs++; ++ } ++ ++ if (sat_req_attrs != cmditem->nr_required_attrs) { ++ pr_info("mt7530-nl: missing required attr(s) for cmd %u\n", ++ hdr->cmd); ++ return -EINVAL; ++ } ++ ++ mt7530_lock(); ++ ret = cmditem->process(info); ++ mt7530_put(); ++ ++ return ret; ++} ++ ++int mt7530_nl_init(struct mt7530_priv **priv) ++{ ++ int ret; ++ ++ pr_info("mt7530-nl: genl_register_family_with_ops \n"); ++ ++ sw_priv = *priv; ++ ret = genl_register_family(&mt7530_nl_family); ++ if (ret) { ++ return ret; ++ } ++ ++ return 0; ++} ++EXPORT_SYMBOL_GPL(mt7530_nl_init); ++ ++void mt7530_nl_exit(void) ++{ ++ sw_priv = NULL; ++ genl_unregister_family(&mt7530_nl_family); ++} ++EXPORT_SYMBOL_GPL(mt7530_nl_exit); ++ +diff --git a/drivers/net/dsa/mt7530_nl.h b/drivers/net/dsa/mt7530_nl.h +new file mode 100644 +index 0000000..1093e83 +--- /dev/null ++++ b/drivers/net/dsa/mt7530_nl.h +@@ -0,0 +1,49 @@ ++/* SPDX-License-Identifier: GPL-2.0-only */ ++/* ++ * Copyright (c) 2018 MediaTek Inc. ++ * Author: Sirui Zhao ++ */ ++ ++#ifndef _MT753x_NL_H_ ++#define _MT753x_NL_H_ ++ ++#define MT7530_DSA_GENL_NAME "mt753x_dsa" ++#define MT7530_GENL_VERSION 0x1 ++ ++enum mt7530_cmd { ++ MT7530_CMD_UNSPEC = 0, ++ MT7530_CMD_REQUEST, ++ MT7530_CMD_REPLY, ++ MT7530_CMD_READ, ++ MT7530_CMD_WRITE, ++ ++ __MT7530_CMD_MAX, ++}; ++ ++enum mt7530_attr { ++ MT7530_ATTR_TYPE_UNSPEC = 0, ++ MT7530_ATTR_TYPE_MESG, ++ MT7530_ATTR_TYPE_PHY, ++ MT7530_ATTR_TYPE_DEVAD, ++ MT7530_ATTR_TYPE_REG, ++ MT7530_ATTR_TYPE_VAL, ++ MT7530_ATTR_TYPE_DEV_NAME, ++ MT7530_ATTR_TYPE_DEV_ID, ++ ++ __MT7530_ATTR_TYPE_MAX, ++}; ++ ++#define MT7530_NR_ATTR_TYPE (__MT7530_ATTR_TYPE_MAX - 1) ++ ++struct mt7530_info { ++ struct mii_bus *bus; ++ void __iomem *base; ++ int direct_access; ++}; ++ ++#ifdef __KERNEL__ ++int mt7530_nl_init(struct mt7530_priv **priv); ++void mt7530_nl_exit(void); ++#endif /* __KERNEL__ */ ++ ++#endif /* _MT7530_NL_H_ */ +-- +2.45.2 diff --git a/target/linux/airoha/patches-6.18/9990-net-airoha-bind-WLAN-bound-flows-on-PPE-driver-L2-cache-miss.patch b/target/linux/airoha/patches-6.18/9990-net-airoha-bind-WLAN-bound-flows-on-PPE-driver-L2-cache-miss.patch new file mode 100644 index 00000000000000..095a7d4a1a5a30 --- /dev/null +++ b/target/linux/airoha/patches-6.18/9990-net-airoha-bind-WLAN-bound-flows-on-PPE-driver-L2-cache-miss.patch @@ -0,0 +1,219 @@ +From: Jihong Min +Date: Mon, 25 May 2026 07:09:47 +0900 +Subject: [PATCH] net: airoha: bind WLAN-bound flows on PPE driver L2 cache miss + +The Linux bridge FDB can resolve a destination station to WDMA even when +the Airoha PPE driver's L2 offload cache has no entry for that MAC pair. +The normal bind path only checks the PPE driver's L2 offload cache, so an +unbound PPE hit for WLAN egress can stay unbound even though the bridge +already knows the right output path, unless a later offload event fills +that PPE driver cache. + +This matters for bridge-visible WLAN egress, such as wired-to-WLAN +forwarding or WLAN peer forwarding across another BSS, radio or MLO link. +Same-link or same-radio intra-BSS forwarding can stay inside the WLAN +datapath and is not covered. + +Before touching the PPE table, resolve the destination MAC through the +bridge device above the ingress netdev. If the PPE driver's L2 offload +cache lookup misses, bind the hardware flow to the resolved CDM4/WDMA +path. + +Assisted-by: Codex:gpt-5.5 +Signed-off-by: Jihong Min +--- +--- a/drivers/net/ethernet/airoha/airoha_ppe.c ++++ b/drivers/net/ethernet/airoha/airoha_ppe.c +@@ -708,65 +708,162 @@ + } + + static int +-airoha_ppe_foe_commit_subflow_entry(struct airoha_ppe *ppe, +- struct airoha_flow_table_entry *e, +- u32 hash, bool rx_wlan) ++airoha_ppe_foe_commit_subflow(struct airoha_ppe *ppe, ++ const struct airoha_foe_entry *bridge, ++ u32 hash, bool rx_wlan) + { + u32 mask = AIROHA_FOE_IB1_BIND_PACKET_TYPE | AIROHA_FOE_IB1_BIND_UDP; + struct airoha_foe_entry *hwe_p, hwe; +- struct airoha_flow_table_entry *f; + int type; + + hwe_p = airoha_ppe_foe_get_entry_locked(ppe, hash); + if (!hwe_p) + return -EINVAL; + +- f = kzalloc(sizeof(*f), GFP_ATOMIC); +- if (!f) +- return -ENOMEM; +- +- hlist_add_head(&f->l2_subflow_node, &e->l2_flows); +- f->type = FLOW_TYPE_L2_SUBFLOW; +- f->hash = hash; +- + memcpy(&hwe, hwe_p, sizeof(*hwe_p)); +- hwe.ib1 = (hwe.ib1 & mask) | (e->data.ib1 & ~mask); ++ hwe.ib1 = (hwe.ib1 & mask) | (bridge->ib1 & ~mask); + + type = FIELD_GET(AIROHA_FOE_IB1_BIND_PACKET_TYPE, hwe.ib1); + if (type >= PPE_PKT_TYPE_IPV6_ROUTE_3T) { +- memcpy(&hwe.ipv6.l2, &e->data.bridge.l2, sizeof(hwe.ipv6.l2)); +- hwe.ipv6.ib2 = e->data.bridge.ib2; ++ memcpy(&hwe.ipv6.l2, &bridge->bridge.l2, ++ sizeof(hwe.ipv6.l2)); ++ hwe.ipv6.ib2 = bridge->bridge.ib2; + /* setting smac_id to 0xf instruct the hw to keep original + * source mac address + */ + hwe.ipv6.l2.src_mac_hi = FIELD_PREP(AIROHA_FOE_MAC_SMAC_ID, + 0xf); + } else { +- memcpy(&hwe.bridge.l2, &e->data.bridge.l2, ++ memcpy(&hwe.bridge.l2, &bridge->bridge.l2, + sizeof(hwe.bridge.l2)); +- hwe.bridge.ib2 = e->data.bridge.ib2; ++ hwe.bridge.ib2 = bridge->bridge.ib2; + if (type == PPE_PKT_TYPE_IPV4_HNAPT) + memcpy(&hwe.ipv4.new_tuple, &hwe.ipv4.orig_tuple, + sizeof(hwe.ipv4.new_tuple)); + } + +- hwe.bridge.data = e->data.bridge.data; +- airoha_ppe_foe_commit_entry(ppe, &hwe, hash, rx_wlan); ++ hwe.bridge.data = bridge->bridge.data; ++ return airoha_ppe_foe_commit_entry(ppe, &hwe, hash, rx_wlan); ++} ++ ++static int ++airoha_ppe_foe_commit_subflow_entry(struct airoha_ppe *ppe, ++ struct airoha_flow_table_entry *e, ++ u32 hash, bool rx_wlan) ++{ ++ struct airoha_flow_table_entry *f; ++ int err; ++ ++ f = kzalloc(sizeof(*f), GFP_ATOMIC); ++ if (!f) ++ return -ENOMEM; ++ ++ err = airoha_ppe_foe_commit_subflow(ppe, &e->data, hash, rx_wlan); ++ if (err) { ++ kfree(f); ++ return err; ++ } ++ ++ hlist_add_head(&f->l2_subflow_node, &e->l2_flows); ++ f->type = FLOW_TYPE_L2_SUBFLOW; ++ f->hash = hash; + + return 0; + } + ++static bool ++airoha_ppe_foe_prepare_wdma_subflow_dev(struct airoha_ppe *ppe, ++ struct net_device *dev, ++ struct airoha_flow_data *data, ++ struct airoha_foe_entry *hwe) ++{ ++ u32 pse_port; ++ int err; ++ ++ err = airoha_ppe_foe_entry_prepare(ppe->eth, hwe, dev, ++ PPE_PKT_TYPE_BRIDGE, data, 0, 0); ++ if (err) ++ return false; ++ ++ pse_port = FIELD_GET(AIROHA_FOE_IB2_PSE_PORT, hwe->bridge.ib2); ++ if (pse_port != FE_PSE_PORT_CDM4) ++ return false; ++ ++ return true; ++} ++ ++static struct net_device * ++airoha_ppe_foe_get_bridge_master(struct net_device *dev) ++{ ++ struct net_device *master = NULL; ++ ++ rcu_read_lock(); ++ master = netdev_master_upper_dev_get_rcu(dev); ++ if (master && netif_is_bridge_master(master)) ++ dev_hold(master); ++ else ++ master = NULL; ++ rcu_read_unlock(); ++ ++ return master; ++} ++ ++static bool ++airoha_ppe_foe_prepare_wdma_subflow(struct airoha_ppe *ppe, ++ struct sk_buff *skb, ++ struct airoha_foe_entry *hwe) ++{ ++ struct ethhdr *eh = eth_hdr(skb); ++ struct airoha_flow_data data = {}; ++ struct net_device *master; ++ ++ if (!is_valid_ether_addr(eh->h_source) || ++ !is_valid_ether_addr(eh->h_dest)) ++ return false; ++ ++ ether_addr_copy(data.eth.h_dest, eh->h_dest); ++ ether_addr_copy(data.eth.h_source, eh->h_source); ++ ++ if (!skb->dev) ++ return false; ++ ++ /* WLAN egress unbound hits can arrive before flowtable creates the ++ * L2 master flow normally used for subflow binding. Resolve only ++ * through the bridge master so dev_fill_forward_path() must use the ++ * bridge FDB for the destination MAC. Calling the ingress AP netdev ++ * directly can describe the source station's WDMA path and would ++ * corrupt Wi-Fi-to-wired flows whose real egress is not WDMA. ++ */ ++ master = airoha_ppe_foe_get_bridge_master(skb->dev); ++ if (!master) ++ return false; ++ ++ if (airoha_ppe_foe_prepare_wdma_subflow_dev(ppe, master, &data, ++ hwe)) { ++ dev_put(master); ++ return true; ++ } ++ ++ dev_put(master); ++ return false; ++} ++ + static void airoha_ppe_foe_insert_entry(struct airoha_ppe *ppe, + struct sk_buff *skb, + u32 hash, bool rx_wlan) + { ++ struct airoha_foe_entry wdma_hwe = {}; + struct airoha_flow_table_entry *e; + struct airoha_foe_bridge br = {}; + struct airoha_foe_entry *hwe; + bool commit_done = false; ++ bool wdma_ready = false; + struct hlist_node *n; + u32 index, state; + ++ wdma_ready = airoha_ppe_foe_prepare_wdma_subflow(ppe, skb, ++ &wdma_hwe); ++ + spin_lock_bh(&ppe_lock); + + hwe = airoha_ppe_foe_get_entry_locked(ppe, hash); +@@ -799,8 +883,10 @@ + airoha_ppe_foe_set_bridge_addrs(&br, eth_hdr(skb)); + e = rhashtable_lookup_fast(&ppe->l2_flows, &br, + airoha_l2_flow_table_params); + if (e) + airoha_ppe_foe_commit_subflow_entry(ppe, e, hash, rx_wlan); ++ else if (wdma_ready) ++ airoha_ppe_foe_commit_subflow(ppe, &wdma_hwe, hash, rx_wlan); + unlock: + spin_unlock_bh(&ppe_lock); + } diff --git a/target/linux/airoha/patches-6.18/9999-01-net-airoha-add-soe-kconfig-option.patch b/target/linux/airoha/patches-6.18/9999-01-net-airoha-add-soe-kconfig-option.patch new file mode 100644 index 00000000000000..a354c9343030df --- /dev/null +++ b/target/linux/airoha/patches-6.18/9999-01-net-airoha-add-soe-kconfig-option.patch @@ -0,0 +1,33 @@ +From: Jihong Min +Date: Mon, 1 Jun 2026 13:30:00 +0900 +Subject: [PATCH] net: airoha: add SOE Kconfig option + +Add the explicit NET_AIROHA_SOE option used to gate the Airoha Secure +Offload Engine packet-offload provider. The provider is still advertised +at runtime only when the hardware block and packet path are available. + +Assisted-by: Codex:gpt-5.5 +Signed-off-by: Jihong Min +--- +diff --git a/drivers/net/ethernet/airoha/Kconfig b/drivers/net/ethernet/airoha/Kconfig +index 38dcc76..b548b5a 100644 +--- a/drivers/net/ethernet/airoha/Kconfig ++++ b/drivers/net/ethernet/airoha/Kconfig +@@ -32,4 +32,17 @@ config NET_AIROHA_FLOW_STATS + help + Enable Aiorha flowtable statistic counters. + ++config NET_AIROHA_SOE ++ bool "Airoha SOE ESP offload support" ++ depends on NET_AIROHA ++ depends on INET ++ select XFRM ++ select XFRM_OFFLOAD ++ help ++ Enable support for the Airoha Secure Offload Engine used by ++ the Ethernet driver for ESP packet offload. This option only ++ adds the provider and netdev plumbing; ESP offload is still ++ advertised at runtime only when the SOE block and required ++ packet offload path are available. ++ + endif #NET_VENDOR_AIROHA diff --git a/target/linux/airoha/patches-6.18/9999-02-net-airoha-add-soe-register-definitions.patch b/target/linux/airoha/patches-6.18/9999-02-net-airoha-add-soe-register-definitions.patch new file mode 100644 index 00000000000000..57816c8d0f0476 --- /dev/null +++ b/target/linux/airoha/patches-6.18/9999-02-net-airoha-add-soe-register-definitions.patch @@ -0,0 +1,75 @@ +From: Jihong Min +Date: Mon, 1 Jun 2026 13:30:00 +0900 +Subject: [PATCH] net: airoha: add SOE register definitions + +Add the FE, QDMA and PPE register definitions used by the SOE packet +path. Later Ethernet, PPE and provider patches consume these symbolic +fields instead of open-coding offsets and bit layouts. + +Assisted-by: Codex:gpt-5.5 +Signed-off-by: Jihong Min +--- +diff --git a/drivers/net/ethernet/airoha/airoha_regs.h b/drivers/net/ethernet/airoha/airoha_regs.h +index a13a3db..700ba69 100644 +--- a/drivers/net/ethernet/airoha/airoha_regs.h ++++ b/drivers/net/ethernet/airoha/airoha_regs.h +@@ -71,6 +71,9 @@ + #define REG_FE_PSE_QUEUE_CFG_VAL 0x0084 + #define PSE_CFG_OQ_RSV_MASK GENMASK(13, 0) + ++/* PSE selector readback used while validating SOE output queue routing. */ ++#define REG_FE_PSE_OQUEUE_EN 0x0088 ++ + #define PSE_FQ_CFG 0x008c + #define PSE_FQ_LIMIT_MASK GENMASK(14, 0) + +@@ -82,6 +85,10 @@ + #define PSE_SHARE_USED_MTHD_MASK GENMASK(31, 16) + #define PSE_SHARE_USED_HTHD_MASK GENMASK(15, 0) + ++/* TDMA/SOE port 7 needs shared-buffer flow control enabled in the PSE. */ ++#define REG_PSE_FC_CFG 0x0098 ++#define PSE_TDMA_SHARE_BUF_DIS_MASK BIT(23) ++ + #define REG_GDM_MISC_CFG 0x0148 + #define GDM2_RDM_ACK_WAIT_PREF_MASK BIT(9) + #define GDM2_CHN_VLD_MODE_MASK BIT(5) +@@ -266,6 +273,8 @@ + #define PPE_GLO_CFG_EN_MASK BIT(0) + + #define REG_PPE_PPE_FLOW_CFG(_n) (((_n) ? PPE2_BASE : PPE1_BASE) + 0x204) ++#define PPE_FLOW_CFG_IP6_IPSEC_MASK BIT(28) ++#define PPE_FLOW_CFG_IP4_IPSEC_MASK BIT(27) + #define PPE_FLOW_CFG_IP6_HASH_GRE_KEY_MASK BIT(20) + #define PPE_FLOW_CFG_IP4_HASH_GRE_KEY_MASK BIT(19) + #define PPE_FLOW_CFG_IP4_HASH_FLOW_LABEL_MASK BIT(18) +@@ -881,6 +890,8 @@ + #define QDMA_DESC_NEXT_ID_MASK GENMASK(15, 0) + /* TX MSG0 */ + #define QDMA_ETH_TXMSG_MIC_IDX_MASK BIT(30) ++/* SOE submit metadata: msg0 carries SA index, msg1 selects port 7 OQ8/OQ9. */ ++#define QDMA_ETH_TXMSG_SOE_SA_MASK GENMASK(29, 24) + #define QDMA_ETH_TXMSG_SP_TAG_MASK GENMASK(29, 14) + #define QDMA_ETH_TXMSG_ICO_MASK BIT(13) + #define QDMA_ETH_TXMSG_UCO_MASK BIT(12) +@@ -903,6 +914,11 @@ + + /* RX MSG0 */ + #define QDMA_ETH_RXMSG_SPTAG GENMASK(21, 14) ++/* SOE completion metadata can use the full 16-bit SP tag word. */ ++#define QDMA_ETH_RXMSG_SPTAG_FULL GENMASK(29, 14) ++/* SOE completion metadata returned by the QDMA RX descriptor. */ ++#define QDMA_ETH_RXMSG_SOE_MASK BIT(10) ++#define QDMA_ETH_RXMSG_HOP_FLAGS_MASK GENMASK(2, 0) + /* RX MSG1 */ + #define QDMA_ETH_RXMSG_DEI_MASK BIT(31) + #define QDMA_ETH_RXMSG_IP6_MASK BIT(30) +@@ -914,6 +930,8 @@ + #define QDMA_ETH_RXMSG_CRSN_MASK GENMASK(20, 16) + #define QDMA_ETH_RXMSG_PPE_ENTRY_MASK GENMASK(15, 0) + /* RX MSG2 */ ++/* SW_UDF carries the SA index for SOE completion frames. */ ++#define QDMA_ETH_RXMSG_SW_UDF_MASK GENMASK(31, 24) + #define QDMA_ETH_RXMSG_AGG_COUNT_MASK GENMASK(31, 24) + #define QDMA_ETH_RXMSG_L2_LEN_MASK GENMASK(6, 0) + /* RX MSG3 */ diff --git a/target/linux/airoha/patches-6.18/9999-03-net-airoha-add-soe-driver-state.patch b/target/linux/airoha/patches-6.18/9999-03-net-airoha-add-soe-driver-state.patch new file mode 100644 index 00000000000000..4775713a1a2292 --- /dev/null +++ b/target/linux/airoha/patches-6.18/9999-03-net-airoha-add-soe-driver-state.patch @@ -0,0 +1,117 @@ +From: Jihong Min +Date: Mon, 1 Jun 2026 13:30:00 +0900 +Subject: [PATCH] net: airoha: add SOE driver state + +Extend the Ethernet driver state with the data needed to wire SOE into +the existing QDMA and PPE paths. This keeps ownership in airoha_eth +while allowing the SOE provider to submit packets, track completion +context and manage the dedicated SOE RX ring. + +Assisted-by: Codex:gpt-5.5 +Signed-off-by: Jihong Min +--- +diff --git a/drivers/net/ethernet/airoha/airoha_eth.h b/drivers/net/ethernet/airoha/airoha_eth.h +index eef8234..08d0936 100644 +--- a/drivers/net/ethernet/airoha/airoha_eth.h ++++ b/drivers/net/ethernet/airoha/airoha_eth.h +@@ -16,6 +16,8 @@ + #include + #include + ++struct airoha_soe; ++ + #define AIROHA_MAX_NUM_GDM_PORTS 4 + #define AIROHA_MAX_NUM_GDM_DEVS 2 + #define AIROHA_MAX_NUM_QDMA 2 +@@ -200,18 +202,31 @@ struct airoha_queue { + spinlock_t lock; + struct airoha_queue_entry *entry; + struct airoha_qdma_desc *desc; ++ /* Preserved for RX ring reprogramming; dmam_alloc_coherent() owns it. */ ++ dma_addr_t desc_dma; + u16 head; + u16 tail; + + int queued; + int ndesc; ++ /* SOE RX rings may use coherent slots instead of page_pool fragments. */ ++ int rx_alloc_ndesc; + int free_thr; + int buf_size; + bool txq_stopped; ++ bool rx_coherent; ++ bool rx_drop_chain; ++ void *rx_coherent_buf; ++ dma_addr_t rx_coherent_dma; ++ size_t rx_coherent_buf_size; + + struct napi_struct napi; + struct page_pool *page_pool; + struct sk_buff *skb; ++ /* First SOE descriptor metadata is kept while a scattered frame is built. */ ++ int rx_frame_descs; ++ u32 soe_rx_msg0; ++ u32 soe_rx_msg2; + + struct list_head tx_list; + }; +@@ -462,6 +482,16 @@ struct airoha_foe_stats64 { + u64 packets; + }; + ++struct airoha_ppe_soe_meta { ++ unsigned long expires; ++ u32 key_hash; ++ u16 foe_hash; ++ u8 valid; ++ u8 sa_index; ++ u8 hop; ++ u8 seen; ++}; ++ + struct airoha_flow_data { + struct ethhdr eth; + +@@ -569,6 +584,11 @@ struct airoha_gdm_dev { + + u32 flags; + int nbq; ++ /* Prevent toggling NETIF_F_HW_ESP while programmed SAs still exist. */ ++ atomic_t soe_xfrm_state_count; ++ /* Private SOE submit path into this GDM's active QDMA instance. */ ++ int (*soe_xmit_skb)(struct airoha_gdm_dev *dev, struct sk_buff *skb, ++ u32 msg0, u32 msg1, u32 msg2); + + struct airoha_hw_stats stats; + }; +@@ -620,6 +650,7 @@ struct airoha_ppe { + + struct hlist_head *foe_flow; + u16 *foe_check_time; ++ struct airoha_ppe_soe_meta *soe_meta; + + struct airoha_foe_stats *foe_stats; + dma_addr_t foe_stats_dma; +@@ -638,6 +658,7 @@ struct airoha_eth { + + struct airoha_qdma qdma[AIROHA_MAX_NUM_QDMA]; + struct airoha_gdm_port *ports[AIROHA_MAX_NUM_GDM_PORTS]; ++ struct airoha_soe *soe; + }; + + u32 airoha_rr(void __iomem *base, u32 offset); +@@ -705,6 +726,14 @@ static inline bool airoha_qdma_is_lro_queue(struct airoha_queue *q) + int airoha_get_fe_port(struct airoha_gdm_dev *dev); + bool airoha_is_valid_gdm_dev(struct airoha_eth *eth, + struct airoha_gdm_dev *dev); ++int airoha_qdma_xmit_skb(struct airoha_gdm_dev *dev, struct sk_buff *skb, ++ u32 msg0, u32 msg1, u32 msg2); ++void airoha_ppe_soe_mark_skb(struct airoha_ppe_dev *dev, struct sk_buff *skb, ++ u16 hash, u8 sa_index, u8 hop); ++bool airoha_ppe_soe_skb_marked(struct sk_buff *skb); ++void airoha_ppe_soe_xmit_skb(struct airoha_ppe_dev *dev, struct sk_buff *skb, ++ struct net_device *netdev); ++void airoha_ppe_soe_flush_sa(struct airoha_ppe *ppe, u8 sa_index); + + void airoha_set_port_mtu(struct airoha_eth *eth, struct airoha_gdm_port *port); + void airoha_ppe_set_cpu_port(struct airoha_gdm_dev *dev, u8 ppe_id, u8 fport); diff --git a/target/linux/airoha/patches-6.18/9999-04-net-airoha-add-soe-provider-header.patch b/target/linux/airoha/patches-6.18/9999-04-net-airoha-add-soe-provider-header.patch new file mode 100644 index 00000000000000..84f490240d4d41 --- /dev/null +++ b/target/linux/airoha/patches-6.18/9999-04-net-airoha-add-soe-provider-header.patch @@ -0,0 +1,146 @@ +From: Jihong Min +Date: Mon, 1 Jun 2026 13:30:00 +0900 +Subject: [PATCH] net: airoha: add SOE provider header + +Introduce the private interface between airoha_eth and the SOE provider. +The header keeps the XFRM, SA lifetime and packet submission contracts +local to the driver while providing stubs for builds without +CONFIG_NET_AIROHA_SOE. + +Assisted-by: Codex:gpt-5.5 +Signed-off-by: Jihong Min +--- +diff --git a/drivers/net/ethernet/airoha/airoha_soe.h b/drivers/net/ethernet/airoha/airoha_soe.h +new file mode 100644 +index 0000000..bfaa9ef +--- /dev/null ++++ b/drivers/net/ethernet/airoha/airoha_soe.h +@@ -0,0 +1,128 @@ ++/* SPDX-License-Identifier: GPL-2.0-only */ ++/* Ethernet-facing declarations for the Airoha Secure Offload Engine (SOE) ++ * packet offload provider. ++ * ++ * airoha_eth owns SOE lifetime and calls these helpers to expose xfrm ++ * ESP/NAT-T offload on its netdevs. When CONFIG_NET_AIROHA_SOE is disabled, ++ * the stubs keep the Ethernet driver buildable without SOE support. ++ */ ++ ++#ifndef AIROHA_SOE_H ++#define AIROHA_SOE_H ++ ++#include ++#include ++#include ++#include ++#include ++ ++struct airoha_soe; ++struct airoha_soe_sa; ++struct airoha_eth; ++struct airoha_gdm_dev; ++struct device; ++struct dst_entry; ++struct net_device; ++struct netlink_ext_ack; ++struct sk_buff; ++struct xfrm_state; ++ ++#define AIROHA_SOE_FEATURE_ESP BIT(0) ++ ++typedef int (*airoha_soe_xmit_skb_t)(struct airoha_gdm_dev *dev, ++ struct sk_buff *skb, u32 msg0, u32 msg1, ++ u32 msg2); ++ ++#if IS_ENABLED(CONFIG_NET_AIROHA_SOE) ++int airoha_soe_init(struct airoha_eth *eth); ++void airoha_soe_deinit(struct airoha_eth *eth); ++bool airoha_soe_available(struct airoha_soe *soe); ++u32 airoha_soe_features(struct airoha_soe *soe); ++void airoha_soe_build_netdev(struct net_device *dev, ++ airoha_soe_xmit_skb_t xmit_skb); ++void airoha_soe_teardown_netdev(struct net_device *dev); ++int airoha_soe_set_features(struct net_device *dev, ++ netdev_features_t features); ++bool airoha_soe_rx_skb(struct airoha_soe *soe, struct sk_buff *skb, ++ unsigned int sa_index, u32 hop_flags); ++bool airoha_soe_rx_plain_skb(struct airoha_gdm_dev *dev, ++ struct sk_buff *skb, struct net_device *rx_dev, ++ u16 foe_hash, u32 foe_reason, bool foe_valid); ++bool airoha_soe_has_pending_rx(struct airoha_soe *soe); ++int airoha_soe_xfrm_ppe_info(const struct dst_entry *dst, ++ struct net_device *lower_dev, u8 *sa_index, ++ u8 *hop); ++int airoha_soe_xmit(struct airoha_soe_sa *sa, struct airoha_gdm_dev *dev, ++ struct sk_buff *skb, struct xfrm_state *x); ++#else ++static inline int airoha_soe_init(struct airoha_eth *eth) ++{ ++ return 0; ++} ++ ++static inline void airoha_soe_deinit(struct airoha_eth *eth) ++{ ++} ++ ++static inline bool airoha_soe_available(struct airoha_soe *soe) ++{ ++ return false; ++} ++ ++static inline u32 airoha_soe_features(struct airoha_soe *soe) ++{ ++ return 0; ++} ++ ++static inline void airoha_soe_build_netdev(struct net_device *dev, ++ airoha_soe_xmit_skb_t xmit_skb) ++{ ++} ++ ++static inline void airoha_soe_teardown_netdev(struct net_device *dev) ++{ ++} ++ ++static inline int airoha_soe_set_features(struct net_device *dev, ++ netdev_features_t features) ++{ ++ return 0; ++} ++ ++static inline bool airoha_soe_rx_skb(struct airoha_soe *soe, ++ struct sk_buff *skb, ++ unsigned int sa_index, u32 hop_flags) ++{ ++ return false; ++} ++ ++static inline bool airoha_soe_rx_plain_skb(struct airoha_gdm_dev *dev, ++ struct sk_buff *skb, ++ struct net_device *rx_dev, ++ u16 foe_hash, u32 foe_reason, ++ bool foe_valid) ++{ ++ return false; ++} ++ ++static inline bool airoha_soe_has_pending_rx(struct airoha_soe *soe) ++{ ++ return false; ++} ++ ++static inline int airoha_soe_xfrm_ppe_info(const struct dst_entry *dst, ++ struct net_device *lower_dev, ++ u8 *sa_index, u8 *hop) ++{ ++ return -EOPNOTSUPP; ++} ++ ++static inline int airoha_soe_xmit(struct airoha_soe_sa *sa, ++ struct airoha_gdm_dev *dev, ++ struct sk_buff *skb, struct xfrm_state *x) ++{ ++ return -EOPNOTSUPP; ++} ++#endif ++ ++#endif /* AIROHA_SOE_H */ diff --git a/target/linux/airoha/patches-6.18/9999-05-net-airoha-add-soe-provider-implementation.patch b/target/linux/airoha/patches-6.18/9999-05-net-airoha-add-soe-provider-implementation.patch new file mode 100644 index 00000000000000..210b1415841cb1 --- /dev/null +++ b/target/linux/airoha/patches-6.18/9999-05-net-airoha-add-soe-provider-implementation.patch @@ -0,0 +1,1946 @@ +From: Jihong Min +Date: Mon, 1 Jun 2026 13:30:00 +0900 +Subject: [PATCH] net: airoha: add SOE provider implementation + +Add the AN7581 SOE packet offload provider used by airoha_eth. The +provider programs SOE SAs from XFRM state, builds the hop descriptor +consumed by the QDMA path, tracks the DSA proxy netdevs needed for +bridge-free offload and feeds SOE RX packets back into the stack with +the original XFRM context. + +Assisted-by: Codex:gpt-5.5 +Signed-off-by: Jihong Min +--- +diff --git a/drivers/net/ethernet/airoha/airoha_soe.c b/drivers/net/ethernet/airoha/airoha_soe.c +new file mode 100644 +index 0000000..a591a7a +--- /dev/null ++++ b/drivers/net/ethernet/airoha/airoha_soe.c +@@ -0,0 +1,1927 @@ ++// SPDX-License-Identifier: GPL-2.0-only ++/* Airoha Secure Offload Engine (SOE) provider for the Ethernet driver. ++ * ++ * This file owns the AN7581 SOE packet-offload glue used by airoha_eth: ++ * xfrm state programming, hop-descriptor TX metadata, SOE RX completion ++ * decoding, and DSA proxy netdev binding. The SOE block is reached through ++ * the FE/QDMA packet fabric and is initialized by the Ethernet driver rather ++ * than by a separate platform driver. ++ */ ++ ++#include ++#include ++#include ++#include ++#include ++#include ++#include ++#include ++#include ++#include ++#include ++#include ++#include ++#include ++#include ++#include ++#include ++#include ++#include ++#include ++#include ++#include ++#include ++ ++#include ++#include ++#include ++#include ++#include ++#include ++ ++#include "airoha_eth.h" ++#include "airoha_regs.h" ++#include "airoha_soe.h" ++ ++#define AIROHA_SOE_NUM_SA 32 ++#define AIROHA_SOE_QDMA_HOP_DESC_LEN 32 ++#define AIROHA_SOE_KEY_WORDS 8 ++#define AIROHA_SOE_ADDR_WORDS 4 ++#define AIROHA_SOE_SA_TIMEOUT_US 1000 ++#define AIROHA_SOE_SA_FREE_TIMEOUT HZ ++#define AIROHA_SOE_HOP_DESC0_ENCRYPT 0xffffff81ULL ++#define AIROHA_SOE_HOP_DESC0_DECRYPT 0xffffff82ULL ++#define AIROHA_SOE_HOP_DESC1 0x1ff00000000ULL ++#define AIROHA_SOE_QDMA_TX_RING 2 ++#define AIROHA_SOE_TXMSG2_DEFAULT 0xff00ffff ++ ++/* This is the packet/IPsec SOE window at 0x1fbfa000. AN7581 E2 exposes ++ * this register block for packet processing, not standalone crypto tests. ++ */ ++#define AIROHA_SOE_GLB_CFG 0x000 ++#define AIROHA_SOE_GLB_CFG_ENC_EN BIT(0) ++#define AIROHA_SOE_GLB_CFG_DEC_EN BIT(1) ++#define AIROHA_SOE_CONT_ICV_CTRL 0x004 ++#define AIROHA_SOE_INT_EN 0x020 ++#define AIROHA_SOE_INT_STS 0x024 ++#define AIROHA_SOE_INT_ALL GENMASK(15, 0) ++#define AIROHA_SOE_CNT_CLR 0x04c ++#define AIROHA_SOE_CNT_CLR_ALL BIT(0) ++#define AIROHA_SOE_SA_CTRL 0x100 ++#define AIROHA_SOE_SA_DONE 0x104 ++#define AIROHA_SOE_SA_CMD 0x110 ++#define AIROHA_SOE_BCNT_THSHD_32_SOFT 0x114 ++#define AIROHA_SOE_BCNT_THSHD_64_SOFT 0x118 ++#define AIROHA_SOE_SA_SPI 0x11c ++#define AIROHA_SOE_SA_UDP_PORT 0x120 ++#define AIROHA_SOE_SA_ENC_KEY(n) (0x124 + (n) * 4) ++#define AIROHA_SOE_SA_HMAC_KEY(n) (0x144 + (n) * 4) ++#define AIROHA_SOE_SA_SRC_ADDR(n) (0x164 + (n) * 4) ++#define AIROHA_SOE_SA_DST_ADDR(n) (0x174 + (n) * 4) ++#define AIROHA_SOE_ICV_OK_LO_CNT 0x184 ++#define AIROHA_SOE_ICV_OK_HI_CNT 0x188 ++#define AIROHA_SOE_ICV_FAIL_LO_CNT 0x18c ++#define AIROHA_SOE_ICV_FAIL_HI_CNT 0x190 ++#define AIROHA_SOE_CON_ICV_FAIL_CNT 0x194 ++#define AIROHA_SOE_SEQ_NUM_LO 0x198 ++#define AIROHA_SOE_SEQ_NUM_HI 0x19c ++#define AIROHA_SOE_BCNT_LO 0x1a0 ++#define AIROHA_SOE_BCNT_HI 0x1a4 ++#define AIROHA_SOE_FLOW_LAB_DSCP 0x1a8 ++#define AIROHA_SOE_BCNT_80 0x1ac ++#define AIROHA_SOE_BCNT_THSHD_80 0x1b0 ++#define AIROHA_SOE_BCNT_THSHD_32_HARD 0x1b4 ++#define AIROHA_SOE_BCNT_THSHD_64_HARD 0x1b8 ++#define AIROHA_SOE_SEQ_THSHD_32_SOFT 0x1bc ++#define AIROHA_SOE_SEQ_THSHD_64_SOFT 0x1c0 ++#define AIROHA_SOE_SEQ_THSHD_32_HARD 0x1c4 ++#define AIROHA_SOE_SEQ_THSHD_64_HARD 0x1c8 ++#define AIROHA_SOE_SA_CTRL_WR BIT(0) ++#define AIROHA_SOE_SA_CTRL_IDX GENMASK(15, 8) ++#define AIROHA_SOE_SA_DONE_W1C BIT(0) ++ ++#define AIROHA_SOE_SA_CMD_ENC BIT(0) ++#define AIROHA_SOE_SA_CMD_CIPHER GENMASK(3, 1) ++#define AIROHA_SOE_SA_CMD_HASH GENMASK(6, 4) ++#define AIROHA_SOE_SA_CMD_AES_KEY_LEN GENMASK(8, 7) ++#define AIROHA_SOE_SA_CMD_ESN_EN BIT(9) ++#define AIROHA_SOE_SA_CMD_OUT_IPV6 BIT(10) ++#define AIROHA_SOE_SA_CMD_ESP_MODE BIT(11) /* 0=tunnel, 1=transport */ ++#define AIROHA_SOE_SA_CMD_NAT_EN BIT(12) ++#define AIROHA_SOE_SA_CMD_ANTI_RPLY_EN BIT(13) ++#define AIROHA_SOE_SA_CMD_ANTI_RPLY_WDW GENMASK(15, 14) ++#define AIROHA_SOE_SA_CMD_SN_ERR_DROP BIT(16) ++#define AIROHA_SOE_SA_CMD_PAD_ERR_DROP BIT(17) ++#define AIROHA_SOE_SA_CMD_ICV_ERR_DROP BIT(18) ++#define AIROHA_SOE_SA_CMD_GCM_ICV_LEN GENMASK(25, 24) ++#define AIROHA_SOE_SA_CMD_DEC_UDP_PARSER_EN BIT(29) ++#define AIROHA_SOE_SA_CMD_VLD BIT(31) ++ ++#define AIROHA_SOE_CIPHER_AES_CBC 1 ++#define AIROHA_SOE_CIPHER_AES_GCM 2 ++#define AIROHA_SOE_HASH_HMAC_SHA1_96 1 ++#define AIROHA_SOE_HASH_HMAC_SHA256_128 2 ++#define AIROHA_SOE_AES_KEY_128 0 ++#define AIROHA_SOE_AES_KEY_192 1 ++#define AIROHA_SOE_AES_KEY_256 2 ++#define AIROHA_SOE_QDMA_QUEUE_ENCRYPT 8 ++#define AIROHA_SOE_QDMA_QUEUE_DECRYPT 9 ++#define AIROHA_SOE_NATT_PORT 4500 ++#define AIROHA_SOE_HOP_FLAG_ENCRYPTED 3 ++#define AIROHA_SOE_HOP_FLAG_DECRYPTED 4 ++#define AIROHA_SOE_HOP_FLAG_ERROR_BASE 5 ++#define AIROHA_SOE_HOP_INFO_ENCRYPT 2 ++#define AIROHA_SOE_HOP_INFO_DECRYPT 3 ++ ++enum airoha_soe_ctx_dir { ++ AIROHA_SOE_CTX_OUT, ++ AIROHA_SOE_CTX_IN, ++}; ++ ++struct airoha_soe_ctx { ++ struct list_head list; ++ enum airoha_soe_ctx_dir dir; ++ union { ++ struct dst_entry *dst; ++ struct { ++ struct xfrm_state *x; ++ struct airoha_gdm_dev *gdm_dev; ++ struct net_device *dev; ++ __be32 saddr; ++ __be16 sport; ++ u16 foe_hash; ++ u32 foe_reason; ++ u8 sa_index; ++ bool foe_valid; ++ u32 mark; ++ } rx; ++ }; ++}; ++ ++struct airoha_soe_sa { ++ struct airoha_soe *soe; ++ unsigned int index; ++ u32 cmd; ++ u32 spi; ++ ++ spinlock_t lock; /* Protects in-flight context queues and dead. */ ++ struct list_head tx_queue; ++ struct list_head rx_queue; ++ struct completion idle; ++ unsigned int inflight; ++ bool dead; ++}; ++ ++struct airoha_soe_xfrm_state { ++ struct airoha_gdm_dev *dev; ++ struct airoha_soe *soe; ++ struct airoha_soe_sa *sa; ++ bool counted; ++}; ++ ++struct airoha_soe_sa_cfg { ++ u32 cmd; ++ u32 spi; ++ u32 udp_port; ++ u32 enc_key[AIROHA_SOE_KEY_WORDS]; ++ u32 hmac_key[AIROHA_SOE_KEY_WORDS]; ++ u32 src_addr[AIROHA_SOE_ADDR_WORDS]; ++ u32 dst_addr[AIROHA_SOE_ADDR_WORDS]; ++ u64 soft_byte_limit; ++ u64 hard_byte_limit; ++ u64 soft_packet_limit; ++ u64 hard_packet_limit; ++}; ++ ++struct airoha_soe_rx_info { ++ int packet_len; ++ bool encap; ++ __be16 sport; ++ __be16 dport; ++ __be32 spi; ++}; ++ ++struct airoha_soe { ++ struct device *dev; ++ void __iomem *base; ++ ++ /* Serialize SA table programming and software slot ownership. */ ++ struct mutex sa_lock; ++ unsigned long sa_map; ++ struct airoha_soe_sa __rcu *sa[AIROHA_SOE_NUM_SA]; ++ atomic_t pending_rx; ++ ++ spinlock_t state_lock; /* Protects dead against concurrent users. */ ++ refcount_t refcnt; ++ struct completion released; ++ bool dead; ++}; ++ ++static const struct xfrmdev_ops airoha_soe_xfrmdev_ops; ++static const struct xfrmdev_ops airoha_soe_dsa_xfrmdev_ops; ++ ++static struct airoha_soe *airoha_soe_get_ref(struct airoha_soe *soe) ++{ ++ unsigned long flags; ++ bool alive; ++ ++ if (!soe) ++ return NULL; ++ ++ spin_lock_irqsave(&soe->state_lock, flags); ++ alive = !soe->dead && refcount_inc_not_zero(&soe->refcnt); ++ spin_unlock_irqrestore(&soe->state_lock, flags); ++ ++ return alive ? soe : NULL; ++} ++ ++static void airoha_soe_put_ref(struct airoha_soe *soe) ++{ ++ if (soe && refcount_dec_and_test(&soe->refcnt)) ++ complete(&soe->released); ++} ++ ++bool airoha_soe_available(struct airoha_soe *soe) ++{ ++ unsigned long flags; ++ bool available; ++ ++ if (!soe) ++ return false; ++ ++ spin_lock_irqsave(&soe->state_lock, flags); ++ available = !soe->dead; ++ spin_unlock_irqrestore(&soe->state_lock, flags); ++ ++ return available; ++} ++ ++u32 airoha_soe_features(struct airoha_soe *soe) ++{ ++ return airoha_soe_available(soe) ? AIROHA_SOE_FEATURE_ESP : 0; ++} ++ ++static u64 airoha_soe_limit(u64 limit) ++{ ++ return limit == XFRM_INF ? U64_MAX : limit; ++} ++ ++static int airoha_soe_wait_sa_done(struct airoha_soe *soe) ++{ ++ u32 done; ++ int err; ++ ++ err = readl_poll_timeout(soe->base + AIROHA_SOE_SA_DONE, done, ++ done & AIROHA_SOE_SA_DONE_W1C, 1, ++ AIROHA_SOE_SA_TIMEOUT_US); ++ writel(0, soe->base + AIROHA_SOE_SA_CTRL); ++ writel(AIROHA_SOE_SA_DONE_W1C, soe->base + AIROHA_SOE_SA_DONE); ++ ++ return err; ++} ++ ++static int airoha_soe_commit_sa(struct airoha_soe *soe, unsigned int index) ++{ ++ u32 ctrl; ++ ++ /* SA registers are a single staging window committed by index. */ ++ writel(AIROHA_SOE_SA_DONE_W1C, soe->base + AIROHA_SOE_SA_DONE); ++ ctrl = FIELD_PREP(AIROHA_SOE_SA_CTRL_IDX, index) | ++ AIROHA_SOE_SA_CTRL_WR; ++ writel(ctrl, soe->base + AIROHA_SOE_SA_CTRL); ++ ++ return airoha_soe_wait_sa_done(soe); ++} ++ ++static void airoha_soe_write_key(void __iomem *base, u32 reg, const u32 *key) ++{ ++ unsigned int i; ++ ++ for (i = 0; i < AIROHA_SOE_KEY_WORDS; i++) ++ writel(key[i], base + reg + i * sizeof(u32)); ++} ++ ++static void airoha_soe_write_addr(void __iomem *base, u32 reg, const u32 *addr) ++{ ++ unsigned int i; ++ ++ for (i = 0; i < AIROHA_SOE_ADDR_WORDS; i++) ++ writel(addr[i], base + reg + i * sizeof(u32)); ++} ++ ++static int airoha_soe_program_sa_locked(struct airoha_soe *soe, ++ unsigned int index, ++ const struct airoha_soe_sa_cfg *cfg) ++{ ++ void __iomem *base = soe->base; ++ ++ writel(cfg->cmd | AIROHA_SOE_SA_CMD_VLD, base + AIROHA_SOE_SA_CMD); ++ writel(lower_32_bits(cfg->soft_byte_limit), ++ base + AIROHA_SOE_BCNT_THSHD_32_SOFT); ++ writel(upper_32_bits(cfg->soft_byte_limit), ++ base + AIROHA_SOE_BCNT_THSHD_64_SOFT); ++ writel(cfg->spi, base + AIROHA_SOE_SA_SPI); ++ writel(cfg->udp_port, base + AIROHA_SOE_SA_UDP_PORT); ++ airoha_soe_write_key(base, AIROHA_SOE_SA_ENC_KEY(0), cfg->enc_key); ++ airoha_soe_write_key(base, AIROHA_SOE_SA_HMAC_KEY(0), cfg->hmac_key); ++ airoha_soe_write_addr(base, AIROHA_SOE_SA_SRC_ADDR(0), cfg->src_addr); ++ airoha_soe_write_addr(base, AIROHA_SOE_SA_DST_ADDR(0), cfg->dst_addr); ++ ++ writel(0, base + AIROHA_SOE_ICV_OK_LO_CNT); ++ writel(0, base + AIROHA_SOE_ICV_OK_HI_CNT); ++ writel(0, base + AIROHA_SOE_ICV_FAIL_LO_CNT); ++ writel(0, base + AIROHA_SOE_ICV_FAIL_HI_CNT); ++ writel(0, base + AIROHA_SOE_CON_ICV_FAIL_CNT); ++ writel(0, base + AIROHA_SOE_SEQ_NUM_LO); ++ writel(0, base + AIROHA_SOE_SEQ_NUM_HI); ++ writel(0, base + AIROHA_SOE_BCNT_LO); ++ writel(0, base + AIROHA_SOE_BCNT_HI); ++ writel(0, base + AIROHA_SOE_FLOW_LAB_DSCP); ++ writel(0, base + AIROHA_SOE_BCNT_80); ++ writel(0xffffffff, base + AIROHA_SOE_BCNT_THSHD_80); ++ writel(lower_32_bits(cfg->hard_byte_limit), ++ base + AIROHA_SOE_BCNT_THSHD_32_HARD); ++ writel(upper_32_bits(cfg->hard_byte_limit), ++ base + AIROHA_SOE_BCNT_THSHD_64_HARD); ++ writel(lower_32_bits(cfg->soft_packet_limit), ++ base + AIROHA_SOE_SEQ_THSHD_32_SOFT); ++ writel(upper_32_bits(cfg->soft_packet_limit), ++ base + AIROHA_SOE_SEQ_THSHD_64_SOFT); ++ writel(lower_32_bits(cfg->hard_packet_limit), ++ base + AIROHA_SOE_SEQ_THSHD_32_HARD); ++ writel(upper_32_bits(cfg->hard_packet_limit), ++ base + AIROHA_SOE_SEQ_THSHD_64_HARD); ++ ++ return airoha_soe_commit_sa(soe, index); ++} ++ ++static int airoha_soe_clear_sa_locked(struct airoha_soe *soe, ++ unsigned int index) ++{ ++ struct airoha_soe_sa_cfg cfg = {}; ++ ++ return airoha_soe_program_sa_locked(soe, index, &cfg); ++} ++ ++static void airoha_soe_copy_words(u32 *dst, const u8 *src, unsigned int bits) ++{ ++ unsigned int words = bits / (BITS_PER_BYTE * sizeof(u32)); ++ unsigned int i; ++ ++ for (i = 0; i < words && i < AIROHA_SOE_KEY_WORDS; i++) ++ dst[i] = get_unaligned_be32(src + i * sizeof(u32)); ++} ++ ++static int airoha_soe_aes_key_len(unsigned int bits, ++ struct netlink_ext_ack *extack, u32 *val) ++{ ++ switch (bits) { ++ case 128: ++ *val = AIROHA_SOE_AES_KEY_128; ++ return 0; ++ case 192: ++ *val = AIROHA_SOE_AES_KEY_192; ++ return 0; ++ case 256: ++ *val = AIROHA_SOE_AES_KEY_256; ++ return 0; ++ default: ++ NL_SET_ERR_MSG_MOD(extack, ++ "SOE supports AES-128/192/256 keys only"); ++ return -EOPNOTSUPP; ++ } ++} ++ ++static int airoha_soe_build_algo(struct xfrm_state *x, ++ struct airoha_soe_sa_cfg *cfg, ++ struct netlink_ext_ack *extack) ++{ ++ u32 key_len; ++ u32 field; ++ int err; ++ ++ if (x->aead) { ++ if (strcmp(x->aead->alg_name, "rfc4106(gcm(aes))")) { ++ NL_SET_ERR_MSG_MOD(extack, ++ "SOE supports rfc4106(gcm(aes)) AEAD only"); ++ return -EOPNOTSUPP; ++ } ++ ++ if (x->aead->alg_key_len < 32) { ++ NL_SET_ERR_MSG_MOD(extack, "invalid AEAD key length"); ++ return -EINVAL; ++ } ++ ++ key_len = x->aead->alg_key_len - 32; ++ err = airoha_soe_aes_key_len(key_len, extack, &field); ++ if (err) ++ return err; ++ ++ cfg->cmd |= FIELD_PREP(AIROHA_SOE_SA_CMD_CIPHER, ++ AIROHA_SOE_CIPHER_AES_GCM); ++ cfg->cmd |= FIELD_PREP(AIROHA_SOE_SA_CMD_AES_KEY_LEN, field); ++ switch (x->aead->alg_icv_len) { ++ case 64: ++ field = 0; ++ break; ++ case 96: ++ field = 1; ++ break; ++ case 128: ++ field = 2; ++ break; ++ default: ++ NL_SET_ERR_MSG_MOD(extack, ++ "SOE supports 64/96/128-bit GCM ICV only"); ++ return -EOPNOTSUPP; ++ } ++ cfg->cmd |= FIELD_PREP(AIROHA_SOE_SA_CMD_GCM_ICV_LEN, field); ++ airoha_soe_copy_words(cfg->enc_key, x->aead->alg_key, key_len); ++ cfg->hmac_key[0] = ++ get_unaligned_be32(x->aead->alg_key + key_len / 8); ++ return 0; ++ } ++ ++ if (!x->ealg || strcmp(x->ealg->alg_name, "cbc(aes)")) { ++ NL_SET_ERR_MSG_MOD(extack, ++ "SOE supports cbc(aes) encryption only"); ++ return -EOPNOTSUPP; ++ } ++ ++ err = airoha_soe_aes_key_len(x->ealg->alg_key_len, extack, &field); ++ if (err) ++ return err; ++ ++ cfg->cmd |= ++ FIELD_PREP(AIROHA_SOE_SA_CMD_CIPHER, AIROHA_SOE_CIPHER_AES_CBC); ++ cfg->cmd |= FIELD_PREP(AIROHA_SOE_SA_CMD_AES_KEY_LEN, field); ++ airoha_soe_copy_words(cfg->enc_key, x->ealg->alg_key, ++ x->ealg->alg_key_len); ++ ++ if (!x->aalg) { ++ NL_SET_ERR_MSG_MOD(extack, ++ "SOE CBC mode requires HMAC authentication"); ++ return -EOPNOTSUPP; ++ } ++ ++ if (!strcmp(x->aalg->alg_name, "hmac(sha1)")) { ++ if (x->aalg->alg_key_len != 160 || ++ x->aalg->alg_trunc_len != 96) { ++ NL_SET_ERR_MSG_MOD(extack, ++ "SOE supports HMAC-SHA1-96 only"); ++ return -EOPNOTSUPP; ++ } ++ field = AIROHA_SOE_HASH_HMAC_SHA1_96; ++ } else if (!strcmp(x->aalg->alg_name, "hmac(sha256)")) { ++ if (x->aalg->alg_key_len != 256 || ++ x->aalg->alg_trunc_len != 128) { ++ NL_SET_ERR_MSG_MOD(extack, ++ "SOE supports HMAC-SHA256-128 only"); ++ return -EOPNOTSUPP; ++ } ++ field = AIROHA_SOE_HASH_HMAC_SHA256_128; ++ } else { ++ NL_SET_ERR_MSG_MOD(extack, ++ "SOE supports HMAC-SHA1/SHA256 only"); ++ return -EOPNOTSUPP; ++ } ++ ++ cfg->cmd |= FIELD_PREP(AIROHA_SOE_SA_CMD_HASH, field); ++ airoha_soe_copy_words(cfg->hmac_key, x->aalg->alg_key, ++ x->aalg->alg_key_len); ++ ++ return 0; ++} ++ ++static int airoha_soe_build_replay(struct xfrm_state *x, ++ struct airoha_soe_sa_cfg *cfg, ++ struct netlink_ext_ack *extack) ++{ ++ u32 window; ++ ++ if ((x->props.flags & XFRM_STATE_ESN) || ++ x->repl_mode == XFRM_REPLAY_MODE_ESN) { ++ NL_SET_ERR_MSG_MOD(extack, "SOE ESN is not supported yet"); ++ return -EOPNOTSUPP; ++ } ++ ++ window = x->replay_esn ? x->replay_esn->replay_window : ++ x->props.replay_window; ++ if (!window) ++ return 0; ++ ++ cfg->cmd |= AIROHA_SOE_SA_CMD_ANTI_RPLY_EN; ++ cfg->cmd |= FIELD_PREP(AIROHA_SOE_SA_CMD_ANTI_RPLY_WDW, ++ min_t(u32, (window - 1) / 64, 3)); ++ ++ return 0; ++} ++ ++static int airoha_soe_build_sa(struct xfrm_state *x, ++ struct airoha_soe_sa_cfg *cfg, ++ struct netlink_ext_ack *extack) ++{ ++ int err; ++ ++ if (x->xso.type != XFRM_DEV_OFFLOAD_PACKET) { ++ NL_SET_ERR_MSG_MOD(extack, ++ "SOE supports XFRM packet offload only"); ++ return -EOPNOTSUPP; ++ } ++ ++ if (x->xso.dir != XFRM_DEV_OFFLOAD_OUT && ++ x->xso.dir != XFRM_DEV_OFFLOAD_IN) { ++ NL_SET_ERR_MSG_MOD(extack, "SOE supports in/out SAs only"); ++ return -EOPNOTSUPP; ++ } ++ ++ if (x->id.proto != IPPROTO_ESP) { ++ NL_SET_ERR_MSG_MOD(extack, "SOE supports ESP only"); ++ return -EOPNOTSUPP; ++ } ++ ++ if (x->props.family != AF_INET || x->outer_mode.family != AF_INET) { ++ NL_SET_ERR_MSG_MOD(extack, ++ "SOE bring-up supports IPv4 outer tunnel only"); ++ return -EOPNOTSUPP; ++ } ++ ++ if (x->props.mode != XFRM_MODE_TUNNEL) { ++ NL_SET_ERR_MSG_MOD(extack, "SOE supports tunnel mode only"); ++ return -EOPNOTSUPP; ++ } ++ ++ if (x->encap && x->encap->encap_type != UDP_ENCAP_ESPINUDP) { ++ NL_SET_ERR_MSG_MOD(extack, ++ "SOE supports native ESP or UDP_ENCAP_ESPINUDP"); ++ return -EOPNOTSUPP; ++ } ++ ++ if (x->tfcpad) { ++ NL_SET_ERR_MSG_MOD(extack, "SOE does not support TFC padding"); ++ return -EOPNOTSUPP; ++ } ++ ++ cfg->cmd = AIROHA_SOE_SA_CMD_SN_ERR_DROP | ++ AIROHA_SOE_SA_CMD_PAD_ERR_DROP | ++ AIROHA_SOE_SA_CMD_ICV_ERR_DROP; ++ if (x->xso.dir == XFRM_DEV_OFFLOAD_OUT) { ++ cfg->cmd |= AIROHA_SOE_SA_CMD_ENC; ++ if (x->encap) ++ cfg->cmd |= AIROHA_SOE_SA_CMD_NAT_EN; ++ cfg->src_addr[0] = be32_to_cpu(x->props.saddr.a4); ++ cfg->dst_addr[0] = be32_to_cpu(x->id.daddr.a4); ++ } else if (x->encap) { ++ /* RX submit passes the full UDP/4500 packet to SOE. Ask the ++ * decrypt parser to consume the UDP header before ESP decap. ++ */ ++ cfg->cmd |= AIROHA_SOE_SA_CMD_DEC_UDP_PARSER_EN; ++ } ++ ++ err = airoha_soe_build_algo(x, cfg, extack); ++ if (err) ++ return err; ++ ++ err = airoha_soe_build_replay(x, cfg, extack); ++ if (err) ++ return err; ++ ++ cfg->spi = be32_to_cpu(x->id.spi); ++ if (x->encap) { ++ /* The NAT-T port word stores dport above sport. */ ++ cfg->udp_port = (u32)ntohs(x->encap->encap_dport) << 16 | ++ ntohs(x->encap->encap_sport); ++ } ++ cfg->soft_byte_limit = airoha_soe_limit(x->lft.soft_byte_limit); ++ cfg->hard_byte_limit = airoha_soe_limit(x->lft.hard_byte_limit); ++ cfg->soft_packet_limit = airoha_soe_limit(x->lft.soft_packet_limit); ++ cfg->hard_packet_limit = airoha_soe_limit(x->lft.hard_packet_limit); ++ ++ return 0; ++} ++ ++static int airoha_soe_alloc_sa(struct airoha_soe *soe, struct xfrm_state *x, ++ struct netlink_ext_ack *extack, ++ struct airoha_soe_sa **sa) ++{ ++ struct airoha_soe_sa_cfg cfg = {}; ++ struct airoha_soe_sa *new_sa; ++ unsigned int i; ++ int err; ++ ++ if (!soe || !sa || !airoha_soe_available(soe)) { ++ NL_SET_ERR_MSG_MOD(extack, "SOE provider is unavailable"); ++ return -ENODEV; ++ } ++ ++ err = airoha_soe_build_sa(x, &cfg, extack); ++ if (err) ++ return err; ++ ++ new_sa = kzalloc(sizeof(*new_sa), GFP_KERNEL); ++ if (!new_sa) ++ return -ENOMEM; ++ ++ mutex_lock(&soe->sa_lock); ++ for (i = 0; i < AIROHA_SOE_NUM_SA; i++) { ++ if (!(soe->sa_map & BIT(i))) ++ break; ++ } ++ if (i == AIROHA_SOE_NUM_SA) { ++ mutex_unlock(&soe->sa_lock); ++ kfree(new_sa); ++ return -ENOSPC; ++ } ++ ++ err = airoha_soe_program_sa_locked(soe, i, &cfg); ++ if (err) { ++ mutex_unlock(&soe->sa_lock); ++ kfree(new_sa); ++ return err; ++ } ++ ++ new_sa->soe = soe; ++ new_sa->index = i; ++ new_sa->cmd = cfg.cmd; ++ new_sa->spi = cfg.spi; ++ spin_lock_init(&new_sa->lock); ++ INIT_LIST_HEAD(&new_sa->tx_queue); ++ INIT_LIST_HEAD(&new_sa->rx_queue); ++ init_completion(&new_sa->idle); ++ rcu_assign_pointer(soe->sa[i], new_sa); ++ soe->sa_map |= BIT(i); ++ mutex_unlock(&soe->sa_lock); ++ ++ *sa = new_sa; ++ return 0; ++} ++ ++static void airoha_soe_mark_sa_dead(struct airoha_soe_sa *sa) ++{ ++ if (!sa) ++ return; ++ ++ spin_lock_bh(&sa->lock); ++ sa->dead = true; ++ if (!sa->inflight) ++ complete(&sa->idle); ++ spin_unlock_bh(&sa->lock); ++} ++ ++static void airoha_soe_free_ctx(struct airoha_soe_ctx *ctx) ++{ ++ if (!ctx) ++ return; ++ ++ if (ctx->dir == AIROHA_SOE_CTX_OUT) ++ dst_release(ctx->dst); ++ else ++ xfrm_state_put(ctx->rx.x); ++ kfree(ctx); ++} ++ ++static void airoha_soe_purge_ctx_list(struct list_head *head) ++{ ++ struct airoha_soe_ctx *ctx, *tmp; ++ ++ list_for_each_entry_safe(ctx, tmp, head, list) { ++ list_del(&ctx->list); ++ airoha_soe_free_ctx(ctx); ++ } ++} ++ ++static void airoha_soe_forget_rx_ctx_list(struct airoha_soe_sa *sa) ++{ ++ if (!list_empty(&sa->rx_queue)) ++ atomic_sub((int)list_count_nodes(&sa->rx_queue), ++ &sa->soe->pending_rx); ++} ++ ++static void airoha_soe_abort_sa(struct airoha_soe_sa *sa) ++{ ++ LIST_HEAD(rx_queue); ++ LIST_HEAD(tx_queue); ++ ++ if (!sa) ++ return; ++ ++ spin_lock_bh(&sa->lock); ++ sa->dead = true; ++ airoha_soe_forget_rx_ctx_list(sa); ++ list_splice_init(&sa->tx_queue, &tx_queue); ++ list_splice_init(&sa->rx_queue, &rx_queue); ++ sa->inflight = 0; ++ complete(&sa->idle); ++ spin_unlock_bh(&sa->lock); ++ ++ airoha_soe_purge_ctx_list(&tx_queue); ++ airoha_soe_purge_ctx_list(&rx_queue); ++} ++ ++static void airoha_soe_free_sa(struct airoha_soe_sa *sa) ++{ ++ LIST_HEAD(rx_queue); ++ LIST_HEAD(tx_queue); ++ struct airoha_soe *soe; ++ ++ if (!sa) ++ return; ++ ++ soe = sa->soe; ++ airoha_soe_mark_sa_dead(sa); ++ if (!wait_for_completion_timeout(&sa->idle, AIROHA_SOE_SA_FREE_TIMEOUT)) ++ dev_warn(soe->dev, ++ "timed out waiting for SOE SA%u in-flight packets\n", ++ sa->index); ++ ++ mutex_lock(&soe->sa_lock); ++ if (sa->index < AIROHA_SOE_NUM_SA && ++ rcu_access_pointer(soe->sa[sa->index]) == sa) { ++ airoha_soe_clear_sa_locked(soe, sa->index); ++ RCU_INIT_POINTER(soe->sa[sa->index], NULL); ++ soe->sa_map &= ~BIT(sa->index); ++ } ++ mutex_unlock(&soe->sa_lock); ++ synchronize_rcu(); ++ ++ spin_lock_bh(&sa->lock); ++ airoha_soe_forget_rx_ctx_list(sa); ++ list_splice_init(&sa->tx_queue, &tx_queue); ++ list_splice_init(&sa->rx_queue, &rx_queue); ++ spin_unlock_bh(&sa->lock); ++ airoha_soe_purge_ctx_list(&tx_queue); ++ airoha_soe_purge_ctx_list(&rx_queue); ++ ++ kfree(sa); ++} ++ ++static struct airoha_soe_ctx *airoha_soe_pop_ctx(struct airoha_soe_sa *sa, ++ enum airoha_soe_ctx_dir dir) ++{ ++ struct list_head *head; ++ struct airoha_soe_ctx *ctx = NULL; ++ ++ head = dir == AIROHA_SOE_CTX_OUT ? &sa->tx_queue : &sa->rx_queue; ++ ++ spin_lock_bh(&sa->lock); ++ if (!list_empty(head)) { ++ ctx = list_first_entry(head, struct airoha_soe_ctx, list); ++ list_del(&ctx->list); ++ if (dir == AIROHA_SOE_CTX_IN) ++ atomic_dec(&sa->soe->pending_rx); ++ } ++ ++ if (ctx && !WARN_ON_ONCE(!sa->inflight)) { ++ sa->inflight--; ++ if (sa->dead && !sa->inflight) ++ complete(&sa->idle); ++ } ++ spin_unlock_bh(&sa->lock); ++ ++ return ctx; ++} ++ ++static int airoha_soe_prepare_ip_headers(struct sk_buff *skb) ++{ ++ unsigned int hdr_len; ++ ++ if (!pskb_may_pull(skb, 1)) ++ return -EINVAL; ++ ++ switch (skb->data[0] & 0xf0) { ++ case 0x40: ++ hdr_len = sizeof(struct iphdr); ++ skb->protocol = htons(ETH_P_IP); ++ break; ++ case 0x60: ++ hdr_len = sizeof(struct ipv6hdr); ++ skb->protocol = htons(ETH_P_IPV6); ++ break; ++ default: ++ return -EINVAL; ++ } ++ ++ if (!pskb_may_pull(skb, hdr_len)) ++ return -EINVAL; ++ ++ skb_reset_network_header(skb); ++ skb_set_transport_header(skb, hdr_len); ++ ++ return 0; ++} ++ ++static int airoha_soe_push_l2_header(struct sk_buff *skb) ++{ ++ static const u8 ipv4_l2_header[ETH_HLEN] = { ++ 0x00, 0x0c, 0xe7, 0x20, 0x21, 0x12, 0x00, ++ 0x0c, 0xe7, 0x20, 0x22, 0x62, 0x08, 0x00, ++ }; ++ static const u8 ipv6_l2_header[ETH_HLEN] = { ++ 0x00, 0x0c, 0xe7, 0x20, 0x21, 0x12, 0x00, ++ 0x0c, 0xe7, 0x20, 0x22, 0x62, 0x86, 0xdd, ++ }; ++ const u8 *l2_header; ++ int err; ++ ++ err = airoha_soe_prepare_ip_headers(skb); ++ if (err) ++ return err; ++ ++ if (skb->protocol == htons(ETH_P_IP)) ++ l2_header = ipv4_l2_header; ++ else ++ l2_header = ipv6_l2_header; ++ ++ /* TDMA/SOE port 7 expects an Ethernet-looking frame before the SOE hop. */ ++ memcpy(skb_push(skb, ETH_HLEN), l2_header, ETH_HLEN); ++ ++ return 0; ++} ++ ++static void airoha_soe_push_hop_desc(struct sk_buff *skb, unsigned int sa_index, ++ bool encrypt, int foe_idx) ++{ ++ u32 hop_direction = encrypt ? AIROHA_SOE_HOP_INFO_ENCRYPT : ++ AIROHA_SOE_HOP_INFO_DECRYPT; ++ u64 desc3 = ((u64)(u16)((hop_direction << 4) | 0x80) << 48) | ++ ((u64)(sa_index & 0x3f) << 40) | 0x05dc0000ULL; ++ u64 desc2 = 0; ++ __le64 desc[4] = {}; ++ ++ if (foe_idx >= 0) ++ desc2 = (u64)(foe_idx & 0xffff) << 32; ++ ++ desc[0] = cpu_to_le64(encrypt ? AIROHA_SOE_HOP_DESC0_ENCRYPT : ++ AIROHA_SOE_HOP_DESC0_DECRYPT); ++ desc[1] = cpu_to_le64(AIROHA_SOE_HOP_DESC1); ++ desc[2] = cpu_to_le64(desc2); ++ desc[3] = cpu_to_le64(desc3); ++ ((u8 *)desc)[28] = sa_index; ++ ++ /* The FE/QDMA hop descriptor is consumed by PSE port 7 before SOE. */ ++ memcpy(skb_push(skb, AIROHA_SOE_QDMA_HOP_DESC_LEN), desc, sizeof(desc)); ++} ++ ++static int airoha_soe_submit_skb(struct airoha_soe_sa *sa, ++ struct airoha_gdm_dev *dev, ++ struct sk_buff *skb, ++ struct airoha_soe_ctx *ctx) ++{ ++ u32 queue = ctx->dir == AIROHA_SOE_CTX_OUT ? ++ AIROHA_SOE_QDMA_QUEUE_ENCRYPT : ++ AIROHA_SOE_QDMA_QUEUE_DECRYPT; ++ bool encrypt = ctx->dir == AIROHA_SOE_CTX_OUT; ++ unsigned int headroom = AIROHA_SOE_QDMA_HOP_DESC_LEN + ETH_HLEN; ++ struct list_head *head; ++ u32 msg0, msg1; ++ int foe_idx = -1; ++ int err; ++ ++ if (skb->ip_summed == CHECKSUM_PARTIAL) { ++ err = skb_checksum_help(skb); ++ if (err) ++ return err; ++ } ++ ++ err = skb_cow_head(skb, headroom); ++ if (err) ++ return err; ++ ++ err = airoha_soe_push_l2_header(skb); ++ if (err) ++ return err; ++ ++ msg0 = FIELD_PREP(QDMA_ETH_TXMSG_SOE_SA_MASK, sa->index & 0x3f); ++ msg1 = FIELD_PREP(QDMA_ETH_TXMSG_METER_MASK, 0x7f) | ++ FIELD_PREP(QDMA_ETH_TXMSG_FPORT_MASK, 7) | ++ FIELD_PREP(QDMA_ETH_TXMSG_NBOQ_MASK, queue) | ++ QDMA_ETH_TXMSG_HOP_MASK | ++ FIELD_PREP(QDMA_ETH_TXMSG_ACNT_G1_MASK, 0x1f) | ++ FIELD_PREP(QDMA_ETH_TXMSG_ACNT_G0_MASK, 0x3f); ++ ++ if (ctx->dir == AIROHA_SOE_CTX_IN && ctx->rx.foe_valid && ++ ctx->rx.foe_hash != AIROHA_RXD4_FOE_ENTRY) ++ foe_idx = ctx->rx.foe_hash; ++ ++ airoha_soe_push_hop_desc(skb, sa->index, encrypt, foe_idx); ++ ++ skb->dev = netdev_from_priv(dev); ++ skb_set_queue_mapping(skb, AIROHA_SOE_QDMA_TX_RING); ++ ++ if (!dev->soe_xmit_skb) ++ return -ENODEV; ++ ++ head = ctx->dir == AIROHA_SOE_CTX_OUT ? &sa->tx_queue : &sa->rx_queue; ++ spin_lock_bh(&sa->lock); ++ if (sa->dead) { ++ spin_unlock_bh(&sa->lock); ++ return -ENOENT; ++ } ++ ++ /* Completion descriptors carry only SA/hop flags, so keep skb context here. */ ++ list_add_tail(&ctx->list, head); ++ sa->inflight++; ++ if (ctx->dir == AIROHA_SOE_CTX_IN) ++ atomic_inc(&sa->soe->pending_rx); ++ reinit_completion(&sa->idle); ++ ++ err = dev->soe_xmit_skb(dev, skb, msg0, msg1, ++ AIROHA_SOE_TXMSG2_DEFAULT); ++ if (err) { ++ list_del(&ctx->list); ++ if (ctx->dir == AIROHA_SOE_CTX_IN) ++ atomic_dec(&sa->soe->pending_rx); ++ sa->inflight--; ++ if (sa->dead && !sa->inflight) ++ complete(&sa->idle); ++ } ++ spin_unlock_bh(&sa->lock); ++ ++ return err; ++} ++ ++int airoha_soe_xmit(struct airoha_soe_sa *sa, struct airoha_gdm_dev *dev, ++ struct sk_buff *skb, struct xfrm_state *x) ++{ ++ struct airoha_soe_ctx *ctx; ++ struct dst_entry *path; ++ struct dst_entry *dst; ++ int err; ++ ++ if (!sa || !dev || !skb || !x || x->xso.dir != XFRM_DEV_OFFLOAD_OUT) ++ return -EINVAL; ++ ++ if (skb_is_gso(skb)) ++ return -EOPNOTSUPP; ++ ++ dst = skb_dst(skb); ++ if (!dst) ++ return -EHOSTUNREACH; ++ ++ path = xfrm_dst_path(dst); ++ if (!path) ++ return -EHOSTUNREACH; ++ ++ ctx = kzalloc(sizeof(*ctx), GFP_ATOMIC); ++ if (!ctx) ++ return -ENOMEM; ++ ++ ctx->dir = AIROHA_SOE_CTX_OUT; ++ dst_hold(path); ++ ctx->dst = path; ++ ++ err = airoha_soe_submit_skb(sa, dev, skb, ctx); ++ if (err) { ++ airoha_soe_free_ctx(ctx); ++ return err; ++ } ++ ++ return 0; ++} ++ ++static bool airoha_soe_rx_parse_ipv4(struct sk_buff *skb, ++ struct airoha_soe_rx_info *info) ++{ ++ struct ip_esp_hdr *esph; ++ struct udphdr *uh; ++ struct iphdr *iph; ++ int iphlen; ++ int udp_len; ++ int packet_len; ++ ++ if (skb->protocol != htons(ETH_P_IP)) { ++ if (!pskb_may_pull(skb, 1) || (skb->data[0] >> 4) != 4) ++ return false; ++ ++ skb->protocol = htons(ETH_P_IP); ++ } ++ ++ if (!pskb_may_pull(skb, sizeof(*iph))) ++ return false; ++ ++ iph = ip_hdr(skb); ++ if (iph->version != 4 || ip_is_fragment(iph)) ++ return false; ++ ++ iphlen = iph->ihl * 4; ++ packet_len = ntohs(iph->tot_len); ++ if (iphlen < sizeof(*iph) || packet_len > skb->len) ++ return false; ++ ++ if (iph->protocol == IPPROTO_ESP) { ++ if (packet_len <= iphlen + sizeof(*esph) || ++ !pskb_may_pull(skb, iphlen + sizeof(*esph))) ++ return false; ++ ++ esph = (struct ip_esp_hdr *)(skb->data + iphlen); ++ if (!esph->spi) ++ return false; ++ ++ info->packet_len = packet_len; ++ info->encap = false; ++ info->sport = 0; ++ info->dport = 0; ++ info->spi = esph->spi; ++ ++ return true; ++ } ++ ++ if (iph->protocol != IPPROTO_UDP || ++ !pskb_may_pull(skb, iphlen + sizeof(*uh) + sizeof(*esph))) ++ return false; ++ ++ uh = (struct udphdr *)(skb->data + iphlen); ++ udp_len = ntohs(uh->len); ++ if (uh->dest != htons(AIROHA_SOE_NATT_PORT) || ++ udp_len <= sizeof(*uh) + sizeof(*esph) || ++ iphlen + udp_len != packet_len || packet_len > skb->len) ++ return false; ++ ++ esph = (struct ip_esp_hdr *)(skb->data + iphlen + sizeof(*uh)); ++ if (!esph->spi) ++ return false; ++ ++ info->packet_len = packet_len; ++ info->encap = true; ++ info->sport = uh->source; ++ info->dport = uh->dest; ++ info->spi = esph->spi; ++ ++ return true; ++} ++ ++/* Plain ESP/NAT-T first arrives as normal RX, then is bounced to SOE decrypt. */ ++bool airoha_soe_rx_plain_skb(struct airoha_gdm_dev *dev, struct sk_buff *skb, ++ struct net_device *rx_dev, u16 foe_hash, ++ u32 foe_reason, bool foe_valid) ++{ ++ struct airoha_soe_xfrm_state *state; ++ struct airoha_soe_rx_info info = {}; ++ struct airoha_soe_ctx *ctx; ++ xfrm_address_t daddr = {}; ++ unsigned long handle; ++ struct xfrm_state *x; ++ int err; ++ ++ if (!dev || !skb || !rx_dev) ++ return false; ++ ++ if (!dev->eth->soe || !(rx_dev->features & NETIF_F_HW_ESP)) ++ return false; ++ ++ if (!atomic_read(&dev->soe_xfrm_state_count)) ++ return false; ++ ++ /* The packet is still in the driver RX path after eth_type_trans(). */ ++ skb_reset_network_header(skb); ++ if (!airoha_soe_rx_parse_ipv4(skb, &info)) ++ return false; ++ ++ if (skb->len != info.packet_len && pskb_trim(skb, info.packet_len)) ++ return false; ++ ++ daddr.a4 = ip_hdr(skb)->daddr; ++ x = xfrm_input_state_lookup(dev_net(rx_dev), skb->mark, &daddr, ++ info.spi, IPPROTO_ESP, AF_INET); ++ if (!x) ++ return false; ++ ++ if (x->xso.dir != XFRM_DEV_OFFLOAD_IN) ++ goto put_state; ++ if (x->xso.type != XFRM_DEV_OFFLOAD_PACKET) ++ goto put_state; ++ handle = xfrm_dev_state_lower_handle(x, rx_dev); ++ if (!handle) ++ goto put_state; ++ if ((info.encap && ++ (!x->encap || x->encap->encap_type != UDP_ENCAP_ESPINUDP)) || ++ (!info.encap && x->encap)) ++ goto put_state; ++ ++ if (info.encap && info.dport != x->encap->encap_dport) ++ goto put_state; ++ ++ state = (struct airoha_soe_xfrm_state *)handle; ++ /* SOE SA state is global to the Ethernet engine. With LAG packet ++ * offload, the bond may hand back a sibling lower-device handle when ++ * the stack has lost the exact ingress slave identity. Use the live RX ++ * GDM device for submission, but require the resolved SA to belong to ++ * the same Ethernet/SOE instance. ++ */ ++ if (!state || !state->dev || state->dev->eth != dev->eth || ++ !state->sa) ++ goto put_state; ++ ++ ctx = kzalloc(sizeof(*ctx), GFP_ATOMIC); ++ if (!ctx) ++ goto put_state; ++ ++ ctx->dir = AIROHA_SOE_CTX_IN; ++ ctx->rx.x = x; ++ ctx->rx.gdm_dev = dev; ++ ctx->rx.dev = rx_dev; ++ ctx->rx.saddr = ip_hdr(skb)->saddr; ++ ctx->rx.sport = info.sport; ++ ctx->rx.foe_hash = foe_hash; ++ ctx->rx.foe_reason = foe_reason; ++ ctx->rx.sa_index = state->sa->index; ++ ctx->rx.foe_valid = foe_valid; ++ ctx->rx.mark = skb->mark; ++ ++ err = airoha_soe_submit_skb(state->sa, dev, skb, ctx); ++ if (err) { ++ airoha_soe_free_ctx(ctx); ++ goto drop_state; ++ } ++ ++ return true; ++ ++drop_state: ++ kfree_skb(skb); ++ return true; ++put_state: ++ xfrm_state_put(x); ++ return false; ++} ++ ++static bool airoha_soe_complete_out(struct sk_buff *skb, ++ struct airoha_soe_ctx *ctx) ++{ ++ struct dst_entry *dst = ctx->dst; ++ struct net *net; ++ int err; ++ ++ ctx->dst = NULL; ++ if (!pskb_may_pull(skb, ETH_HLEN + 1)) ++ goto drop; ++ skb_pull(skb, ETH_HLEN); ++ ++ err = airoha_soe_prepare_ip_headers(skb); ++ if (err) ++ goto drop; ++ ++ /* SOE returns an already transformed outer IP packet. Rebuild the IP ++ * output bookkeeping before handing it to the route dst so local_out() ++ * can refresh length/checksum state without running another XFRM lookup. ++ */ ++ if (skb->protocol == htons(ETH_P_IP)) { ++ memset(IPCB(skb), 0, sizeof(*IPCB(skb))); ++ IPCB(skb)->flags |= IPSKB_XFRM_TRANSFORMED; ++ } else if (skb->protocol == htons(ETH_P_IPV6)) { ++ memset(IP6CB(skb), 0, sizeof(*IP6CB(skb))); ++ IP6CB(skb)->flags |= IP6SKB_XFRM_TRANSFORMED; ++ } ++ skb_dst_drop(skb); ++ skb_dst_set(skb, dst); ++ skb->ignore_df = 1; ++ net = dev_net(dst->dev); ++ nf_reset_ct(skb); ++ err = skb_dst(skb)->ops->local_out(net, NULL, skb); ++ if (unlikely(err != 1)) ++ goto drop_skb; ++ kfree(ctx); ++ dst_output(net, NULL, skb); ++ ++ return true; ++ ++drop_skb: ++ kfree(ctx); ++ kfree_skb(skb); ++ return true; ++ ++drop: ++ dst_release(dst); ++ kfree(ctx); ++ kfree_skb(skb); ++ return true; ++} ++ ++static bool airoha_soe_complete_in(struct sk_buff *skb, ++ struct airoha_soe_ctx *ctx) ++{ ++ struct xfrm_state *x = ctx->rx.x; ++ struct net_device *rx_dev = ctx->rx.dev; ++ struct xfrm_offload *xo; ++ struct sec_path *sp; ++ int err; ++ ++ if (!pskb_may_pull(skb, ETH_HLEN + 1)) ++ goto drop; ++ skb_pull(skb, ETH_HLEN); ++ ++ err = airoha_soe_prepare_ip_headers(skb); ++ if (err) ++ goto drop; ++ ++ skb->dev = rx_dev; ++ skb->mark = ctx->rx.mark; ++ skb->ip_summed = CHECKSUM_NONE; ++ skb_reset_mac_header(skb); ++ skb_reset_mac_len(skb); ++ skb->pkt_type = PACKET_HOST; ++ skb->encapsulation = 0; ++ skb_dst_drop(skb); ++ ++ if (x->encap && x->encap->encap_type == UDP_ENCAP_ESPINUDP && ++ (ctx->rx.saddr != x->props.saddr.a4 || ++ ctx->rx.sport != x->encap->encap_sport)) { ++ xfrm_address_t ipaddr = { ++ .a4 = ctx->rx.saddr, ++ }; ++ ++ km_new_mapping(x, &ipaddr, ctx->rx.sport); ++ } ++ ++ /* Tell xfrm_input() equivalent consumers that hardware already decrypted. */ ++ sp = secpath_set(skb); ++ if (!sp) ++ goto drop; ++ ++ if (sp->len == XFRM_MAX_DEPTH) { ++ secpath_reset(skb); ++ goto drop; ++ } ++ ++ sp->xvec[sp->len++] = x; ++ sp->olen++; ++ ctx->rx.x = NULL; ++ xo = xfrm_offload(skb); ++ if (!xo) { ++ secpath_reset(skb); ++ goto drop; ++ } ++ ++ xo->flags = CRYPTO_DONE; ++ xo->status = CRYPTO_SUCCESS; ++ ++ /* SOE decrypt completion reaches the CPU before the routed plaintext ++ * packet has selected its final egress port. Preserve the original FOE ++ * hash and SA hop until the Ethernet xmit path can bind that decrypt ++ * entry with the completed L2/PSE descriptor. ++ */ ++ if (ctx->rx.foe_valid) ++ airoha_ppe_soe_mark_skb(&ctx->rx.gdm_dev->eth->ppe->dev, skb, ++ ctx->rx.foe_hash, ctx->rx.sa_index, ++ AIROHA_SOE_HOP_INFO_DECRYPT); ++ ++ kfree(ctx); ++ netif_rx(skb); ++ ++ return true; ++ ++drop: ++ airoha_soe_free_ctx(ctx); ++ kfree_skb(skb); ++ return true; ++} ++ ++bool airoha_soe_rx_skb(struct airoha_soe *soe, struct sk_buff *skb, ++ unsigned int sa_index, u32 hop_flags) ++{ ++ struct airoha_soe_ctx *ctx; ++ struct airoha_soe_sa *sa; ++ ++ if (!soe || !skb || sa_index >= AIROHA_SOE_NUM_SA) ++ return false; ++ ++ rcu_read_lock(); ++ sa = rcu_dereference(soe->sa[sa_index]); ++ if (!sa) { ++ rcu_read_unlock(); ++ return false; ++ } ++ ++ if (hop_flags >= AIROHA_SOE_HOP_FLAG_ERROR_BASE) { ++ ctx = airoha_soe_pop_ctx(sa, AIROHA_SOE_CTX_OUT); ++ if (!ctx) ++ ctx = airoha_soe_pop_ctx(sa, AIROHA_SOE_CTX_IN); ++ rcu_read_unlock(); ++ airoha_soe_free_ctx(ctx); ++ kfree_skb(skb); ++ return true; ++ } ++ ++ if (hop_flags == AIROHA_SOE_HOP_FLAG_ENCRYPTED) { ++ ctx = airoha_soe_pop_ctx(sa, AIROHA_SOE_CTX_OUT); ++ rcu_read_unlock(); ++ if (!ctx) { ++ kfree_skb(skb); ++ return true; ++ } ++ return airoha_soe_complete_out(skb, ctx); ++ } ++ ++ if (hop_flags == AIROHA_SOE_HOP_FLAG_DECRYPTED) { ++ ctx = airoha_soe_pop_ctx(sa, AIROHA_SOE_CTX_IN); ++ rcu_read_unlock(); ++ if (!ctx) { ++ kfree_skb(skb); ++ return true; ++ } ++ return airoha_soe_complete_in(skb, ctx); ++ } ++ ++ rcu_read_unlock(); ++ return false; ++} ++ ++bool airoha_soe_has_pending_rx(struct airoha_soe *soe) ++{ ++ if (!soe) ++ return false; ++ ++ return !!atomic_read(&soe->pending_rx); ++} ++ ++int airoha_soe_xfrm_ppe_info(const struct dst_entry *dst, ++ struct net_device *lower_dev, u8 *sa_index, ++ u8 *hop) ++{ ++ struct airoha_soe_xfrm_state *state; ++ struct net_device *state_dev; ++ struct xfrm_state *x; ++ unsigned long handle; ++ ++ if (!dst || !sa_index || !hop) ++ return -EINVAL; ++ ++ x = dst_xfrm(dst); ++ if (!x || x->xso.type != XFRM_DEV_OFFLOAD_PACKET) ++ return -EOPNOTSUPP; ++ ++ if (!lower_dev) ++ lower_dev = x->xso.dev; ++ ++ handle = xfrm_dev_state_lower_handle(x, lower_dev); ++ state = (struct airoha_soe_xfrm_state *)handle; ++ if (!state || !state->sa) ++ return -ENODEV; ++ ++ if (!state->dev || !state->dev->eth) ++ return -ENODEV; ++ ++ state_dev = netdev_from_priv(state->dev); ++ if (!(state_dev->features & NETIF_F_HW_ESP)) ++ return -ENODEV; ++ ++ if (state_dev != lower_dev) { ++ struct airoha_gdm_dev *lower_gdm; ++ ++ /* Bonding keeps per-slave replicated XFRM handles. When the ++ * flowtable selected a sibling slave, the SA is still valid for ++ * this SOE instance because the hardware SA table is shared by ++ * all Airoha GDM netdevs attached to the same Ethernet block. ++ */ ++ if (!lower_dev || !netif_is_lag_port(lower_dev) || ++ lower_dev->netdev_ops != state_dev->netdev_ops || ++ !(lower_dev->features & NETIF_F_HW_ESP)) ++ return -ENODEV; ++ ++ lower_gdm = netdev_priv(lower_dev); ++ if (!airoha_is_valid_gdm_dev(state->dev->eth, lower_gdm)) ++ return -ENODEV; ++ } ++ ++ switch (x->xso.dir) { ++ case XFRM_DEV_OFFLOAD_OUT: ++ *hop = AIROHA_SOE_HOP_INFO_ENCRYPT; ++ break; ++ case XFRM_DEV_OFFLOAD_IN: ++ *hop = AIROHA_SOE_HOP_INFO_DECRYPT; ++ break; ++ default: ++ return -EOPNOTSUPP; ++ } ++ ++ *sa_index = state->sa->index; ++ ++ return 0; ++} ++ ++static int airoha_soe_xfrm_state_add(struct net_device *dev, ++ struct xfrm_state *x, ++ struct netlink_ext_ack *extack) ++{ ++ struct airoha_soe_xfrm_state *state; ++ struct airoha_gdm_dev *gdm_dev; ++ struct airoha_soe *soe; ++ gfp_t gfp; ++ int err; ++ ++ if (dev->xfrmdev_ops != &airoha_soe_xfrmdev_ops || ++ !(dev->features & NETIF_F_HW_ESP)) ++ return -EOPNOTSUPP; ++ ++ gdm_dev = netdev_priv(dev); ++ soe = airoha_soe_get_ref(gdm_dev->eth->soe); ++ if (!soe) ++ return -ENODEV; ++ ++ gfp = (x->xso.flags & XFRM_DEV_OFFLOAD_FLAG_ACQ) ? GFP_ATOMIC : ++ GFP_KERNEL; ++ state = kzalloc(sizeof(*state), gfp); ++ if (!state) { ++ airoha_soe_put_ref(soe); ++ return -ENOMEM; ++ } ++ ++ state->dev = gdm_dev; ++ state->soe = soe; ++ ++ if (x->xso.flags & XFRM_DEV_OFFLOAD_FLAG_ACQ) ++ goto out; ++ ++ err = airoha_soe_alloc_sa(soe, x, extack, &state->sa); ++ if (err) ++ goto err_free; ++ ++ atomic_inc(&gdm_dev->soe_xfrm_state_count); ++ state->counted = true; ++out: ++ x->xso.offload_handle = (unsigned long)state; ++ return 0; ++ ++err_free: ++ kfree(state); ++ airoha_soe_put_ref(soe); ++ return err; ++} ++ ++static void airoha_soe_xfrm_state_delete(struct net_device *dev, ++ struct xfrm_state *x) ++{ ++ struct airoha_soe_xfrm_state *state; ++ ++ state = (struct airoha_soe_xfrm_state *)x->xso.offload_handle; ++ if (state && state->sa) { ++ airoha_ppe_soe_flush_sa(state->dev->eth->ppe, state->sa->index); ++ airoha_soe_abort_sa(state->sa); ++ } ++} ++ ++static void airoha_soe_xfrm_state_free(struct net_device *dev, ++ struct xfrm_state *x) ++{ ++ struct airoha_soe_xfrm_state *state; ++ ++ state = (struct airoha_soe_xfrm_state *)xchg(&x->xso.offload_handle, 0); ++ if (!state) ++ return; ++ ++ if (state->sa) { ++ airoha_ppe_soe_flush_sa(state->dev->eth->ppe, ++ state->sa->index); ++ airoha_soe_free_sa(state->sa); ++ } ++ if (state->counted) ++ atomic_dec(&state->dev->soe_xfrm_state_count); ++ airoha_soe_put_ref(state->soe); ++ kfree(state); ++} ++ ++static bool airoha_soe_xfrm_offload_ok(struct sk_buff *skb, ++ struct xfrm_state *x) ++{ ++ struct airoha_soe_xfrm_state *state; ++ struct net_device *dev = x->xso.dev; ++ ++ if (!dev || !(dev->features & NETIF_F_HW_ESP)) ++ return false; ++ ++ if (x->xso.type != XFRM_DEV_OFFLOAD_PACKET || ++ x->xso.dir != XFRM_DEV_OFFLOAD_OUT) ++ return false; ++ ++ state = (struct airoha_soe_xfrm_state *)x->xso.offload_handle; ++ ++ return state && state->sa; ++} ++ ++static int airoha_soe_xfrm_packet_xmit_gso(struct sk_buff *skb, ++ struct xfrm_state *x, ++ struct airoha_soe_xfrm_state *state) ++{ ++ struct sk_buff *segs, *nskb; ++ int err; ++ ++ segs = skb_gso_segment(skb, 0); ++ if (IS_ERR(segs)) { ++ XFRM_INC_STATS(xs_net(x), LINUX_MIB_XFRMOUTERROR); ++ kfree_skb(skb); ++ return PTR_ERR(segs); ++ } ++ ++ consume_skb(skb); ++ ++ skb_list_walk_safe(segs, skb, nskb) { ++ skb_mark_not_on_list(skb); ++ err = airoha_soe_xmit(state->sa, state->dev, skb, x); ++ if (err) { ++ XFRM_INC_STATS(xs_net(x), LINUX_MIB_XFRMOUTERROR); ++ kfree_skb(skb); ++ kfree_skb_list(nskb); ++ return err; ++ } ++ } ++ ++ return 0; ++} ++ ++static int airoha_soe_xfrm_packet_xmit(struct sk_buff *skb, ++ struct xfrm_state *x) ++{ ++ struct airoha_soe_xfrm_state *state; ++ struct net_device *lower_dev = x->xso.dev; ++ struct net_device *state_dev; ++ unsigned long handle; ++ int err = -EHOSTUNREACH; ++ ++ if (!lower_dev || lower_dev->xfrmdev_ops != &airoha_soe_xfrmdev_ops) ++ lower_dev = skb->dev; ++ ++ handle = xfrm_dev_state_lower_handle(x, lower_dev); ++ state = (struct airoha_soe_xfrm_state *)handle; ++ if (!state || !state->sa || !state->dev) ++ goto drop; ++ ++ state_dev = netdev_from_priv(state->dev); ++ if (state_dev->xfrmdev_ops != &airoha_soe_xfrmdev_ops || ++ !(state_dev->features & NETIF_F_HW_ESP)) ++ goto drop; ++ ++ if (skb_is_gso(skb)) ++ return airoha_soe_xfrm_packet_xmit_gso(skb, x, state); ++ ++ err = airoha_soe_xmit(state->sa, state->dev, skb, x); ++ if (err) ++ goto drop; ++ ++ return 0; ++ ++drop: ++ XFRM_INC_STATS(xs_net(x), LINUX_MIB_XFRMOUTERROR); ++ kfree_skb(skb); ++ return err; ++} ++ ++static int airoha_soe_xfrm_policy_add(struct xfrm_policy *x, ++ struct netlink_ext_ack *extack) ++{ ++ if (x->xdo.type != XFRM_DEV_OFFLOAD_PACKET) { ++ NL_SET_ERR_MSG_MOD(extack, ++ "SOE supports XFRM packet policies only"); ++ return -EOPNOTSUPP; ++ } ++ ++ if (xfrm_policy_id2dir(x->index) >= XFRM_POLICY_MAX) { ++ NL_SET_ERR_MSG_MOD(extack, ++ "SOE does not offload socket policies"); ++ return -EOPNOTSUPP; ++ } ++ ++ if (x->xfrm_nr != 1 || ++ x->xfrm_vec[0].id.proto != IPPROTO_ESP) { ++ NL_SET_ERR_MSG_MOD(extack, ++ "SOE offloads ESP policies only"); ++ return -EOPNOTSUPP; ++ } ++ ++ if (!x->xdo.dev || !(x->xdo.dev->features & NETIF_F_HW_ESP)) { ++ NL_SET_ERR_MSG_MOD(extack, "SOE ESP offload is disabled"); ++ return -EOPNOTSUPP; ++ } ++ ++ switch (x->xdo.dir) { ++ case XFRM_DEV_OFFLOAD_IN: ++ case XFRM_DEV_OFFLOAD_OUT: ++ return 0; ++ default: ++ NL_SET_ERR_MSG_MOD(extack, "SOE supports in/out policies only"); ++ return -EOPNOTSUPP; ++ } ++} ++ ++static struct net_device *airoha_soe_dsa_conduit_get(struct net_device *dev) ++{ ++ struct net_device *conduit; ++ struct dsa_port *dp; ++ ++ if (!dsa_user_dev_check(dev)) ++ return NULL; ++ ++ /* DSA users expose XFRM, but SOE is attached to their CPU conduit. */ ++ dp = dsa_port_from_netdev(dev); ++ if (IS_ERR(dp) || !dp->cpu_dp) ++ return NULL; ++ ++ conduit = dsa_port_to_conduit(dp); ++ if (!conduit || conduit->xfrmdev_ops != &airoha_soe_xfrmdev_ops) ++ return NULL; ++ ++ dev_hold(conduit); ++ ++ return conduit; ++} ++ ++static int airoha_soe_dsa_xfrm_state_add(struct net_device *dev, ++ struct xfrm_state *x, ++ struct netlink_ext_ack *extack) ++{ ++ struct net_device *conduit; ++ int err; ++ ++ conduit = airoha_soe_dsa_conduit_get(dev); ++ if (!conduit) { ++ NL_SET_ERR_MSG_MOD(extack, "SOE DSA conduit is unavailable"); ++ return -EOPNOTSUPP; ++ } ++ ++ err = airoha_soe_xfrm_state_add(conduit, x, extack); ++ dev_put(conduit); ++ ++ return err; ++} ++ ++static void airoha_soe_dsa_xfrm_state_delete(struct net_device *dev, ++ struct xfrm_state *x) ++{ ++ airoha_soe_xfrm_state_delete(dev, x); ++} ++ ++static void airoha_soe_dsa_xfrm_state_free(struct net_device *dev, ++ struct xfrm_state *x) ++{ ++ airoha_soe_xfrm_state_free(dev, x); ++} ++ ++static bool airoha_soe_dsa_xfrm_offload_ok(struct sk_buff *skb, ++ struct xfrm_state *x) ++{ ++ return airoha_soe_xfrm_offload_ok(skb, x); ++} ++ ++static int airoha_soe_dsa_xfrm_policy_add(struct xfrm_policy *x, ++ struct netlink_ext_ack *extack) ++{ ++ return airoha_soe_xfrm_policy_add(x, extack); ++} ++ ++static int airoha_soe_dsa_xfrm_packet_xmit(struct sk_buff *skb, ++ struct xfrm_state *x) ++{ ++ return airoha_soe_xfrm_packet_xmit(skb, x); ++} ++ ++static const struct xfrmdev_ops airoha_soe_xfrmdev_ops = { ++ .xdo_dev_state_add = airoha_soe_xfrm_state_add, ++ .xdo_dev_state_delete = airoha_soe_xfrm_state_delete, ++ .xdo_dev_state_free = airoha_soe_xfrm_state_free, ++ .xdo_dev_offload_ok = airoha_soe_xfrm_offload_ok, ++ .xdo_dev_policy_add = airoha_soe_xfrm_policy_add, ++ .xdo_dev_packet_xmit = airoha_soe_xfrm_packet_xmit, ++ .flags = XFRMDEV_OPS_F_LOWER_HANDLE, ++}; ++ ++static const struct xfrmdev_ops airoha_soe_dsa_xfrmdev_ops = { ++ .xdo_dev_state_add = airoha_soe_dsa_xfrm_state_add, ++ .xdo_dev_state_delete = airoha_soe_dsa_xfrm_state_delete, ++ .xdo_dev_state_free = airoha_soe_dsa_xfrm_state_free, ++ .xdo_dev_offload_ok = airoha_soe_dsa_xfrm_offload_ok, ++ .xdo_dev_policy_add = airoha_soe_dsa_xfrm_policy_add, ++ .xdo_dev_packet_xmit = airoha_soe_dsa_xfrm_packet_xmit, ++ .flags = XFRMDEV_OPS_F_LOWER_HANDLE, ++}; ++ ++static void airoha_soe_dsa_proxy_enable(struct net_device *dev) ++{ ++ struct net_device *conduit; ++ ++ conduit = airoha_soe_dsa_conduit_get(dev); ++ if (!conduit) ++ return; ++ ++ if (dev->xfrmdev_ops && dev->xfrmdev_ops != &airoha_soe_dsa_xfrmdev_ops) ++ goto out; ++ ++ /* Mirror ESP capability onto DSA users while programming SAs on the conduit. */ ++ dev->xfrmdev_ops = &airoha_soe_dsa_xfrmdev_ops; ++ dev->hw_features |= NETIF_F_HW_ESP; ++ dev->hw_enc_features |= NETIF_F_HW_ESP; ++ dev->wanted_features |= NETIF_F_HW_ESP; ++ ++ conduit->wanted_features |= NETIF_F_HW_ESP; ++ netdev_update_features(conduit); ++ netdev_update_features(dev); ++out: ++ dev_put(conduit); ++} ++ ++static void airoha_soe_dsa_proxy_clear(struct net_device *dev) ++{ ++ if (dev->xfrmdev_ops != &airoha_soe_dsa_xfrmdev_ops) ++ return; ++ ++ dev->wanted_features &= ~NETIF_F_HW_ESP; ++ dev->hw_features &= ~NETIF_F_HW_ESP; ++ dev->hw_enc_features &= ~NETIF_F_HW_ESP; ++ netdev_update_features(dev); ++ dev->xfrmdev_ops = NULL; ++} ++ ++static void airoha_soe_dsa_proxy_scan(bool enable) ++{ ++ struct net_device *dev; ++ ++ for_each_netdev(&init_net, dev) { ++ if (enable) ++ airoha_soe_dsa_proxy_enable(dev); ++ else ++ airoha_soe_dsa_proxy_clear(dev); ++ } ++} ++ ++static int airoha_soe_netdev_event(struct notifier_block *nb, ++ unsigned long event, void *ptr) ++{ ++ switch (event) { ++ case NETDEV_REGISTER: ++ case NETDEV_CHANGENAME: ++ airoha_soe_dsa_proxy_scan(true); ++ break; ++ } ++ ++ return NOTIFY_DONE; ++} ++ ++static struct notifier_block airoha_soe_netdev_notifier = { ++ .notifier_call = airoha_soe_netdev_event, ++}; ++ ++void airoha_soe_build_netdev(struct net_device *netdev, ++ airoha_soe_xmit_skb_t xmit_skb) ++{ ++ struct airoha_gdm_dev *dev = netdev_priv(netdev); ++ ++ atomic_set(&dev->soe_xfrm_state_count, 0); ++ dev->soe_xmit_skb = xmit_skb; ++ ++ if (!xmit_skb || ++ !(airoha_soe_features(dev->eth->soe) & AIROHA_SOE_FEATURE_ESP)) ++ return; ++ ++ netdev->xfrmdev_ops = &airoha_soe_xfrmdev_ops; ++ netdev->hw_features |= NETIF_F_HW_ESP; ++ netdev->hw_enc_features |= NETIF_F_HW_ESP; ++} ++ ++void airoha_soe_teardown_netdev(struct net_device *netdev) ++{ ++ struct airoha_gdm_dev *dev = netdev_priv(netdev); ++ ++ if (netdev->xfrmdev_ops == &airoha_soe_xfrmdev_ops) ++ netdev->xfrmdev_ops = NULL; ++ dev->soe_xmit_skb = NULL; ++} ++ ++int airoha_soe_set_features(struct net_device *netdev, ++ netdev_features_t features) ++{ ++ netdev_features_t changed = (netdev->features ^ features) & ++ NETIF_F_HW_ESP; ++ struct airoha_gdm_dev *dev = netdev_priv(netdev); ++ ++ if (!changed) ++ return 0; ++ ++ if ((features & NETIF_F_HW_ESP) && ++ !(airoha_soe_features(dev->eth->soe) & AIROHA_SOE_FEATURE_ESP)) ++ return -EOPNOTSUPP; ++ ++ if (atomic_read(&dev->soe_xfrm_state_count)) { ++ netdev_err(netdev, ++ "cannot change ESP features with active SAs\n"); ++ return -EBUSY; ++ } ++ ++ return 0; ++} ++ ++static struct device_node *airoha_soe_find_node(struct airoha_eth *eth) ++{ ++ struct device_node *parent, *np; ++ ++ if (!eth->dev->of_node) ++ return NULL; ++ ++ parent = of_get_parent(eth->dev->of_node); ++ if (!parent) ++ return NULL; ++ ++ /* SOE is a sibling DT node; Ethernet owns the provider lifetime. */ ++ for_each_child_of_node(parent, np) { ++ if (!of_device_is_available(np) || ++ !of_device_is_compatible(np, "airoha,an7581-soe")) ++ continue; ++ ++ of_node_put(parent); ++ return np; ++ } ++ ++ of_node_put(parent); ++ ++ return NULL; ++} ++ ++int airoha_soe_init(struct airoha_eth *eth) ++{ ++ struct device *dev = eth->dev; ++ struct device_node *np; ++ struct resource res; ++ struct airoha_soe *soe; ++ void __iomem *base; ++ int err; ++ ++ np = airoha_soe_find_node(eth); ++ if (!np) ++ return 0; ++ ++ err = of_address_to_resource(np, 0, &res); ++ if (err) ++ goto put_node; ++ ++ base = devm_ioremap_resource(dev, &res); ++ if (IS_ERR(base)) { ++ err = PTR_ERR(base); ++ goto put_node; ++ } ++ ++ soe = devm_kzalloc(dev, sizeof(*soe), GFP_KERNEL); ++ if (!soe) { ++ err = -ENOMEM; ++ goto put_node; ++ } ++ ++ soe->dev = dev; ++ soe->base = base; ++ mutex_init(&soe->sa_lock); ++ spin_lock_init(&soe->state_lock); ++ refcount_set(&soe->refcnt, 1); ++ init_completion(&soe->released); ++ ++ /* Enable the packet engines; reset leaves SOE present but idle. */ ++ writel(AIROHA_SOE_INT_ALL, base + AIROHA_SOE_INT_STS); ++ writel(AIROHA_SOE_CNT_CLR_ALL, base + AIROHA_SOE_CNT_CLR); ++ writel(AIROHA_SOE_INT_ALL, base + AIROHA_SOE_INT_EN); ++ writel(AIROHA_SOE_GLB_CFG_ENC_EN | AIROHA_SOE_GLB_CFG_DEC_EN, ++ base + AIROHA_SOE_GLB_CFG); ++ ++ err = register_netdevice_notifier(&airoha_soe_netdev_notifier); ++ if (err) ++ goto disable_soe; ++ ++ eth->soe = soe; ++ ++ rtnl_lock(); ++ airoha_soe_dsa_proxy_scan(true); ++ rtnl_unlock(); ++ ++ of_node_put(np); ++ ++ return 0; ++ ++disable_soe: ++ writel(0, base + AIROHA_SOE_GLB_CFG); ++ writel(0, base + AIROHA_SOE_INT_EN); ++ writel(0xffffffff, base + AIROHA_SOE_INT_STS); ++put_node: ++ of_node_put(np); ++ ++ return err; ++} ++ ++void airoha_soe_deinit(struct airoha_eth *eth) ++{ ++ struct airoha_soe *soe = eth->soe; ++ unsigned long flags; ++ ++ if (!soe) ++ return; ++ ++ eth->soe = NULL; ++ ++ spin_lock_irqsave(&soe->state_lock, flags); ++ soe->dead = true; ++ spin_unlock_irqrestore(&soe->state_lock, flags); ++ ++ rtnl_lock(); ++ airoha_soe_dsa_proxy_scan(false); ++ rtnl_unlock(); ++ unregister_netdevice_notifier(&airoha_soe_netdev_notifier); ++ ++ airoha_soe_put_ref(soe); ++ wait_for_completion(&soe->released); ++ ++ writel(0, soe->base + AIROHA_SOE_GLB_CFG); ++ writel(0, soe->base + AIROHA_SOE_INT_EN); ++ writel(0xffffffff, soe->base + AIROHA_SOE_INT_STS); ++} diff --git a/target/linux/airoha/patches-6.18/9999-06-net-airoha-add-soe-ethernet-path.patch b/target/linux/airoha/patches-6.18/9999-06-net-airoha-add-soe-ethernet-path.patch new file mode 100644 index 00000000000000..7537f4b5f6d0f1 --- /dev/null +++ b/target/linux/airoha/patches-6.18/9999-06-net-airoha-add-soe-ethernet-path.patch @@ -0,0 +1,955 @@ +From: Jihong Min +Date: Mon, 1 Jun 2026 13:30:00 +0900 +Subject: [PATCH] net: airoha: add SOE ethernet packet path + +Teach the Ethernet/QDMA path how to carry packets to and from the SOE +block. This adds the SOE RX ring setup, coherent and scatter RX buffer +handling, DSA conduit preservation, plain ESP/NAT-T receive interception +and the QDMA TX helper used by the SOE provider. + +Assisted-by: Codex:gpt-5.5 +Signed-off-by: Jihong Min +--- +diff --git a/drivers/net/ethernet/airoha/airoha_eth.c b/drivers/net/ethernet/airoha/airoha_eth.c +--- a/drivers/net/ethernet/airoha/airoha_eth.c ++++ b/drivers/net/ethernet/airoha/airoha_eth.c +@@ -6,11 +6,13 @@ + #include + #include + #include ++#include + #include + #include + #include + #include + #include ++#include + #include + #include + #include +@@ -20,6 +22,67 @@ + + #include "airoha_regs.h" + #include "airoha_eth.h" ++#include "airoha_soe.h" ++ ++/* QDMA1 uses a different RX IRQ bank layout than QDMA0 on AN7581. */ ++#define AIROHA_QDMA_WAN_RX_IRQ0_BANK_PIN_MASK 0x0000839f ++#define AIROHA_QDMA_WAN_RX_IRQ1_BANK_PIN_MASK 0x7f800400 ++#define AIROHA_QDMA_WAN_RX_IRQ2_BANK_PIN_MASK 0x00000000 ++#define AIROHA_QDMA_WAN_RX_IRQ3_BANK_PIN_MASK 0x00000040 ++ ++static unsigned int airoha_qdma0_rx_irq_bank_mask[AIROHA_MAX_NUM_IRQ_BANKS] = { ++ RX_IRQ0_BANK_PIN_MASK, ++ RX_IRQ1_BANK_PIN_MASK, ++ RX_IRQ2_BANK_PIN_MASK, ++ RX_IRQ3_BANK_PIN_MASK, ++}; ++ ++static unsigned int airoha_qdma1_rx_irq_bank_mask[AIROHA_MAX_NUM_IRQ_BANKS] = { ++ AIROHA_QDMA_WAN_RX_IRQ0_BANK_PIN_MASK, ++ AIROHA_QDMA_WAN_RX_IRQ1_BANK_PIN_MASK, ++ AIROHA_QDMA_WAN_RX_IRQ2_BANK_PIN_MASK, ++ AIROHA_QDMA_WAN_RX_IRQ3_BANK_PIN_MASK, ++}; ++ ++#if IS_REACHABLE(CONFIG_NET_AIROHA_SOE) ++#define AIROHA_SOE_RX_RING 10 ++#define AIROHA_SOE_RX_ALLOC_NDESC 2048 ++#define AIROHA_SOE_RX_NDESC_DEFAULT 512 ++#define AIROHA_SOE_RX_BUF_SIZE 4096 ++#define AIROHA_SOE_RX_BUF_SIZE_MIN 128 ++#define AIROHA_SOE_RX_BUF_SIZE_MAX 16384 ++#define AIROHA_SOE_DECRYPT_FOE_REASON_MASK \ ++ (BIT(PPE_CPU_REASON_UN_HIT) | BIT(PPE_CPU_REASON_HIT_UNBIND) | \ ++ BIT(PPE_CPU_REASON_HIT_UNBIND_RATE_REACHED)) ++ ++static int airoha_soe_rx_poll_desc_budget = AIROHA_SOE_RX_NDESC_DEFAULT; ++static int airoha_soe_rx_max_frame_descs = 128; ++static int airoha_soe_probe_rx_ndesc = AIROHA_SOE_RX_NDESC_DEFAULT; ++static int airoha_soe_probe_rx_buf_size = AIROHA_SOE_RX_BUF_SIZE; ++static bool airoha_soe_probe_rx_coherent = true; ++static int airoha_soe_probe_rx_scatter = 1; ++ ++module_param_named(soe_rx_poll_desc_budget, airoha_soe_rx_poll_desc_budget, int, ++ 0600); ++MODULE_PARM_DESC(soe_rx_poll_desc_budget, ++ "Maximum SOE RX descriptors consumed per poll"); ++module_param_named(soe_rx_max_frame_descs, airoha_soe_rx_max_frame_descs, int, ++ 0600); ++MODULE_PARM_DESC(soe_rx_max_frame_descs, ++ "Maximum descriptors per SOE RX frame before dropping the chain"); ++module_param_named(soe_probe_rx_ndesc, airoha_soe_probe_rx_ndesc, int, 0600); ++MODULE_PARM_DESC(soe_probe_rx_ndesc, "SOE RX descriptor count"); ++module_param_named(soe_probe_rx_buf_size, airoha_soe_probe_rx_buf_size, int, ++ 0600); ++MODULE_PARM_DESC(soe_probe_rx_buf_size, "SOE RX buffer size"); ++module_param_named(soe_probe_rx_coherent, airoha_soe_probe_rx_coherent, bool, ++ 0600); ++MODULE_PARM_DESC(soe_probe_rx_coherent, "Use coherent SOE RX buffers"); ++module_param_named(soe_probe_rx_scatter, airoha_soe_probe_rx_scatter, int, ++ 0600); ++MODULE_PARM_DESC(soe_probe_rx_scatter, ++ "SOE RX scatter mode: 0 disabled, 1 enabled"); ++#endif + + u32 airoha_rr(void __iomem *base, u32 offset) + { +@@ -75,6 +138,100 @@ + airoha_qdma_set_irqmask(irq_bank, index, mask, 0); + } + ++static unsigned int *airoha_qdma_rx_irq_bank_masks(struct airoha_qdma *qdma) ++{ ++ struct airoha_eth *eth = qdma->eth; ++ int id = qdma - ð->qdma[0]; ++ ++ return id ? airoha_qdma1_rx_irq_bank_mask : ++ airoha_qdma0_rx_irq_bank_mask; ++} ++ ++static u32 airoha_qdma_rx_irq_bank_mask(struct airoha_qdma *qdma, int bank) ++{ ++ unsigned int *masks = airoha_qdma_rx_irq_bank_masks(qdma); ++ ++ if (bank < 0 || bank >= AIROHA_MAX_NUM_IRQ_BANKS) ++ return 0; ++ ++ return READ_ONCE(masks[bank]); ++} ++ ++#if IS_REACHABLE(CONFIG_NET_AIROHA_SOE) ++static u32 airoha_qdma_rx_irq_all_bank_mask(struct airoha_qdma *qdma) ++{ ++ u32 mask = 0; ++ int i; ++ ++ for (i = 0; i < ARRAY_SIZE(qdma->irq_banks); i++) ++ mask |= airoha_qdma_rx_irq_bank_mask(qdma, i); ++ ++ return mask; ++} ++#endif ++ ++static void airoha_qdma_apply_rx_irq_bank_masks(struct airoha_qdma *qdma) ++{ ++ int i; ++ ++ for (i = 0; i < ARRAY_SIZE(qdma->irq_banks); i++) { ++ struct airoha_irq_bank *irq_bank = &qdma->irq_banks[i]; ++ u32 mask = airoha_qdma_rx_irq_bank_mask(qdma, i); ++ ++ airoha_qdma_set_irqmask(irq_bank, QDMA_INT_REG_IDX0, ++ RX_COHERENT_LOW_INT_MASK, ++ INT_RX0_MASK(mask)); ++ airoha_qdma_set_irqmask(irq_bank, QDMA_INT_REG_IDX1, ++ RX_NO_CPU_DSCP_LOW_INT_MASK | ++ RX_DONE_LOW_INT_MASK, ++ INT_RX1_MASK(mask)); ++ airoha_qdma_set_irqmask(irq_bank, QDMA_INT_REG_IDX2, ++ RX_NO_CPU_DSCP_HIGH_INT_MASK | ++ RX_DONE_HIGH_INT_MASK, ++ INT_RX2_MASK(mask)); ++ airoha_qdma_set_irqmask(irq_bank, QDMA_INT_REG_IDX3, ++ RX_COHERENT_HIGH_INT_MASK, ++ INT_RX3_MASK(mask)); ++ } ++} ++ ++static void airoha_qdma_set_rx_done_irq(struct airoha_qdma *qdma, int qid, ++ bool enable) ++{ ++ int i, intr_reg; ++ u32 mask; ++ ++ intr_reg = qid < RX_DONE_HIGH_OFFSET ? QDMA_INT_REG_IDX1 : ++ QDMA_INT_REG_IDX2; ++ mask = BIT(qid % RX_DONE_HIGH_OFFSET); ++ ++ for (i = 0; i < ARRAY_SIZE(qdma->irq_banks); i++) { ++ if (!(BIT(qid) & airoha_qdma_rx_irq_bank_mask(qdma, i))) ++ continue; ++ ++ if (enable) ++ airoha_qdma_irq_enable(&qdma->irq_banks[i], intr_reg, ++ mask); ++ else ++ airoha_qdma_irq_disable(&qdma->irq_banks[i], intr_reg, ++ mask); ++ } ++ ++#if IS_REACHABLE(CONFIG_NET_AIROHA_SOE) ++ if (qid != AIROHA_SOE_RX_RING) ++ return; ++ ++ if (BIT(qid) & airoha_qdma_rx_irq_all_bank_mask(qdma)) ++ return; ++ ++ /* SOE RX10 may not be covered by the shared bank mask; use bank 0. */ ++ if (enable) ++ airoha_qdma_irq_enable(&qdma->irq_banks[0], intr_reg, mask); ++ else ++ airoha_qdma_irq_disable(&qdma->irq_banks[0], intr_reg, mask); ++#endif ++} ++ + static bool airoha_dev_is_lan(struct device_node *np, u32 id) + { + const char *name; +@@ -621,6 +778,11 @@ + airoha_fe_wr(eth, REG_FE_CDM1_OQ_MAP2, BIT(4)); + airoha_fe_wr(eth, REG_FE_CDM1_OQ_MAP3, BIT(28)); + ++ /* SOE/TDMA output depends on PSE shared-buffer flow control instead ++ * of leaving port-local sharing disabled. ++ */ ++ airoha_fe_clear(eth, REG_PSE_FC_CFG, PSE_TDMA_SHARE_BUF_DIS_MASK); ++ + airoha_fe_vip_setup(eth); + airoha_fe_pse_ports_init(eth); + +@@ -707,20 +869,29 @@ + int offset; + u32 val; + +- page = page_pool_dev_alloc_frag(q->page_pool, &offset, +- q->buf_size); +- if (!page) +- break; ++ if (q->rx_coherent) { ++ /* Coherent slots avoid page_pool recycling for SOE frames. */ ++ offset = q->head * q->buf_size; ++ e->buf = q->rx_coherent_buf + offset; ++ e->dma_addr = q->rx_coherent_dma + offset; ++ e->dma_len = q->buf_size; ++ } else { ++ page = page_pool_dev_alloc_frag(q->page_pool, &offset, ++ q->buf_size); ++ if (!page) ++ break; ++ ++ offset += AIROHA_RX_HEADROOM; ++ e->buf = page_address(page) + offset; ++ e->dma_addr = page_pool_get_dma_addr(page) + offset; ++ e->dma_len = ++ SKB_WITH_OVERHEAD(AIROHA_RX_LEN(q->buf_size)); ++ } + + q->head = (q->head + 1) % q->ndesc; + q->queued++; + nframes++; + +- offset += AIROHA_RX_HEADROOM; +- e->buf = page_address(page) + offset; +- e->dma_addr = page_pool_get_dma_addr(page) + offset; +- e->dma_len = SKB_WITH_OVERHEAD(AIROHA_RX_LEN(q->buf_size)); +- + WRITE_ONCE(desc->tcp_ts_reply, 0); + val = FIELD_PREP(QDMA_DESC_LEN_MASK, e->dma_len); + WRITE_ONCE(desc->ctrl, cpu_to_le32(val)); +@@ -922,81 +1093,216 @@ + return skb; + } + ++#if IS_REACHABLE(CONFIG_NET_AIROHA_SOE) ++static bool airoha_qdma_rx_is_soe(u32 msg0) ++{ ++ u32 hop_flags = FIELD_GET(QDMA_ETH_RXMSG_HOP_FLAGS_MASK, msg0); ++ ++ return hop_flags >= 3 && hop_flags <= 7; ++} ++#endif ++ + static int airoha_qdma_rx_process(struct airoha_queue *q, int budget) + { +- enum dma_data_direction dir = page_pool_get_dma_dir(q->page_pool); +- struct airoha_eth *eth = q->qdma->eth; ++ enum dma_data_direction dir; ++ struct airoha_qdma *qdma = q->qdma; ++ struct airoha_eth *eth = qdma->eth; ++ int qid = q - &qdma->q_rx[0]; ++ int desc_budget = q->ndesc; ++ int desc_done = 0; + int done = 0; + +- while (done < budget) { ++ dir = q->rx_coherent ? DMA_FROM_DEVICE : ++ page_pool_get_dma_dir(q->page_pool); ++ ++#if IS_REACHABLE(CONFIG_NET_AIROHA_SOE) ++ if (airoha_soe_rx_poll_desc_budget > 0) ++ desc_budget = min(airoha_soe_rx_poll_desc_budget, q->ndesc); ++#endif ++ ++ while (q->queued > 0 && done < budget && desc_done < desc_budget) { + struct airoha_queue_entry *e = &q->entry[q->tail]; + struct airoha_qdma_desc *desc = &q->desc[q->tail]; +- u32 hash, reason, msg1, desc_ctrl; +- struct airoha_gdm_dev *dev; +- struct net_device *netdev; ++ u32 hash, reason, msg0, msg1, msg2, desc_ctrl; ++ struct airoha_gdm_dev *dev = NULL; ++ struct net_device *rx_dev = NULL; ++ struct net_device *netdev = NULL; ++#if IS_REACHABLE(CONFIG_NET_AIROHA_SOE) ++ /* Scattered SOE completions only tag the first descriptor. */ ++ bool partial_soe = q->skb && !q->skb->dev; ++#endif ++ bool soe_pkt = false; + int data_len, len; +- struct page *page; ++ struct page *page = NULL; + + desc_ctrl = le32_to_cpu(READ_ONCE(desc->ctrl)); + if (!(desc_ctrl & QDMA_DESC_DONE_MASK)) + break; + + dma_rmb(); ++ desc_done++; + + q->tail = (q->tail + 1) % q->ndesc; + q->queued--; + +- dma_sync_single_for_cpu(eth->dev, e->dma_addr, e->dma_len, +- dir); ++ if (!q->rx_coherent) ++ dma_sync_single_for_cpu(eth->dev, e->dma_addr, ++ e->dma_len, dir); ++ ++ if (!q->rx_coherent) ++ page = virt_to_head_page(e->buf); ++ ++ if (q->rx_drop_chain) { ++ if (!FIELD_GET(QDMA_DESC_MORE_MASK, desc_ctrl)) { ++ q->rx_drop_chain = false; ++ q->rx_frame_descs = 0; ++ done++; ++ } ++ if (!q->rx_coherent) ++ page_pool_put_full_page(q->page_pool, page, ++ true); ++ continue; ++ } + +- page = virt_to_head_page(e->buf); + len = FIELD_GET(QDMA_DESC_LEN_MASK, desc_ctrl); +- data_len = q->skb ? AIROHA_RX_LEN(q->buf_size) : e->dma_len; ++ data_len = q->skb && !q->rx_coherent ? ++ AIROHA_RX_LEN(q->buf_size) : ++ e->dma_len; + if (!len || data_len < len) + goto free_frag; + +- dev = airoha_qdma_get_gdm_dev(eth, desc); +- if (IS_ERR(dev)) +- goto free_frag; ++ msg0 = le32_to_cpu(READ_ONCE(desc->msg0)); ++ msg1 = le32_to_cpu(READ_ONCE(desc->msg1)); ++ msg2 = le32_to_cpu(READ_ONCE(desc->msg2)); ++#if IS_REACHABLE(CONFIG_NET_AIROHA_SOE) ++ soe_pkt = partial_soe || airoha_qdma_rx_is_soe(msg0); ++#endif ++ if (!soe_pkt) { ++ dev = airoha_qdma_get_gdm_dev(eth, desc); ++ if (IS_ERR(dev)) ++ goto free_frag; ++ ++ netdev = netdev_from_priv(dev); ++ rx_dev = netdev; ++ } + +- netdev = netdev_from_priv(dev); + if (!q->skb) { /* first buffer */ +- q->skb = airoha_qdma_build_rx_skb(q, desc, e, netdev); ++ if (soe_pkt) { ++ if (q->rx_coherent) { ++ q->skb = napi_alloc_skb(&q->napi, len); ++ if (q->skb) ++ skb_put_data(q->skb, e->buf, len); ++ } else { ++ void *buf = e->buf - AIROHA_RX_HEADROOM; ++ ++ q->skb = napi_build_skb(buf, q->buf_size); ++ if (q->skb) { ++ skb_reserve(q->skb, ++ AIROHA_RX_HEADROOM); ++ __skb_put(q->skb, len); ++ skb_mark_for_recycle(q->skb); ++ } ++ } ++ if (q->skb) { ++ q->skb->dev = NULL; ++ q->skb->ip_summed = CHECKSUM_NONE; ++ skb_record_rx_queue(q->skb, qid); ++ q->soe_rx_msg0 = msg0; ++ q->soe_rx_msg2 = msg2; ++ } ++ } else { ++ q->skb = airoha_qdma_build_rx_skb(q, desc, e, ++ netdev); ++ } + if (!q->skb) + goto free_frag; +- } else { /* scattered frame */ +- struct skb_shared_info *shinfo = skb_shinfo(q->skb); +- int nr_frags = shinfo->nr_frags; + +- if (nr_frags >= ARRAY_SIZE(shinfo->frags)) ++ q->rx_drop_chain = false; ++ q->rx_frame_descs = 1; ++ } else { /* scattered frame */ ++#if IS_REACHABLE(CONFIG_NET_AIROHA_SOE) ++ if (soe_pkt && airoha_soe_rx_max_frame_descs > 0 && ++ q->rx_frame_descs >= ++ airoha_soe_rx_max_frame_descs) { ++ q->rx_drop_chain = ++ FIELD_GET(QDMA_DESC_MORE_MASK, desc_ctrl); + goto free_frag; +- +- skb_add_rx_frag(q->skb, nr_frags, page, +- e->buf - page_address(page), len, +- q->buf_size); ++ } ++#endif ++ q->rx_frame_descs++; ++ if (q->rx_coherent) { ++ if (skb_tailroom(q->skb) < len) { ++ unsigned int needed; ++ ++ needed = len - skb_tailroom(q->skb); ++ if (pskb_expand_head(q->skb, 0, needed, ++ GFP_ATOMIC)) ++ goto free_frag; ++ } ++ skb_put_data(q->skb, e->buf, len); ++ } else { ++ struct skb_shared_info *shinfo = ++ skb_shinfo(q->skb); ++ int nr_frags = shinfo->nr_frags; ++ ++ if (nr_frags >= ARRAY_SIZE(shinfo->frags)) ++ goto free_frag; ++ ++ skb_add_rx_frag(q->skb, nr_frags, page, ++ e->buf - page_address(page), ++ len, q->buf_size); ++ } + } + + if (FIELD_GET(QDMA_DESC_MORE_MASK, desc_ctrl)) + continue; + ++#if IS_REACHABLE(CONFIG_NET_AIROHA_SOE) ++ if (soe_pkt) { ++ u32 hop_flags = FIELD_GET(QDMA_ETH_RXMSG_HOP_FLAGS_MASK, ++ q->soe_rx_msg0); ++ u32 sa_index = FIELD_GET(QDMA_ETH_RXMSG_SW_UDF_MASK, ++ q->soe_rx_msg2); ++ ++ q->rx_drop_chain = false; ++ q->rx_frame_descs = 0; ++ done++; ++ if (!airoha_soe_rx_skb(eth->soe, q->skb, sa_index, ++ hop_flags)) ++ dev_kfree_skb(q->skb); ++ q->skb = NULL; ++ continue; ++ } ++#endif ++ ++ q->rx_drop_chain = false; ++ q->rx_frame_descs = 0; ++ + if (netdev_uses_dsa(netdev)) { + struct airoha_gdm_port *port = dev->port; ++ u32 sptag = FIELD_GET(QDMA_ETH_RXMSG_SPTAG, msg0); ++ struct dsa_port *cpu_dp = netdev->dsa_ptr; + + /* PPE module requires untagged packets to work + * properly and it provides DSA port index via the + * DMA descriptor. Report DSA tag to the DSA stack + * via skb dst info. + */ +- u32 msg0 = le32_to_cpu(READ_ONCE(desc->msg0)); +- u32 sptag = FIELD_GET(QDMA_ETH_RXMSG_SPTAG, msg0); +- + if (sptag < ARRAY_SIZE(port->dsa_meta) && + port->dsa_meta[sptag]) + skb_dst_set_noref(q->skb, + &port->dsa_meta[sptag]->dst); ++ ++ if (cpu_dp && cpu_dp->ds) { ++ struct dsa_port *dp = ++ dsa_to_port(cpu_dp->ds, sptag); ++ ++ if (dp && dsa_port_is_user(dp) && ++ dp->cpu_dp == cpu_dp && dp->user) ++ rx_dev = dp->user; ++ } + } + +- msg1 = le32_to_cpu(READ_ONCE(desc->msg1)); + hash = FIELD_GET(AIROHA_RXD4_FOE_ENTRY, msg1); + if (hash != AIROHA_RXD4_FOE_ENTRY) + skb_set_hash(q->skb, jhash_1word(hash, 0), +@@ -1008,18 +1314,46 @@ + false); + + done++; ++#if IS_REACHABLE(CONFIG_NET_AIROHA_SOE) ++ /* Probe plain ESP/NAT-T before GRO so SOE can decrypt in hardware. */ ++ if (hash != AIROHA_RXD4_FOE_ENTRY) { ++ bool foe_valid = false; ++ ++ if (reason < 32) ++ foe_valid = AIROHA_SOE_DECRYPT_FOE_REASON_MASK & ++ BIT(reason); ++ if (airoha_soe_rx_plain_skb(dev, q->skb, rx_dev, hash, ++ reason, foe_valid)) { ++ q->skb = NULL; ++ continue; ++ } ++ } else if (airoha_soe_rx_plain_skb(dev, q->skb, rx_dev, hash, ++ reason, false)) { ++ q->skb = NULL; ++ continue; ++ } ++#endif + napi_gro_receive(&q->napi, q->skb); + q->skb = NULL; + continue; + free_frag: ++ q->rx_drop_chain = !!FIELD_GET(QDMA_DESC_MORE_MASK, desc_ctrl); ++ q->rx_frame_descs = 0; ++ done++; + if (q->skb) { + dev_kfree_skb(q->skb); + q->skb = NULL; + } +- page_pool_put_full_page(q->page_pool, page, true); ++ if (!q->rx_coherent) ++ page_pool_put_full_page(q->page_pool, page, true); + } + airoha_qdma_fill_rx_queue(q); + ++#if IS_REACHABLE(CONFIG_NET_AIROHA_SOE) ++ if (desc_done && !done) ++ return 1; ++#endif ++ + return done; + } + +@@ -1035,17 +1369,9 @@ + + if (done < budget && napi_complete(napi)) { + struct airoha_qdma *qdma = q->qdma; +- int i, qid = q - &qdma->q_rx[0]; +- int intr_reg = qid < RX_DONE_HIGH_OFFSET ? QDMA_INT_REG_IDX1 +- : QDMA_INT_REG_IDX2; ++ int qid = q - &qdma->q_rx[0]; + +- for (i = 0; i < ARRAY_SIZE(qdma->irq_banks); i++) { +- if (!(BIT(qid) & RX_IRQ_BANK_PIN_MASK(i))) +- continue; +- +- airoha_qdma_irq_enable(&qdma->irq_banks[i], intr_reg, +- BIT(qid % RX_DONE_HIGH_OFFSET)); +- } ++ airoha_qdma_set_rx_done_irq(qdma, qid, true); + } + + return done; +@@ -1067,11 +1393,24 @@ + dma_addr_t dma_addr; + bool lro_q; + ++#if IS_REACHABLE(CONFIG_NET_AIROHA_SOE) ++ if (qid == AIROHA_SOE_RX_RING) { ++ ndesc = max(ndesc, AIROHA_SOE_RX_ALLOC_NDESC); ++ if (airoha_soe_probe_rx_ndesc > 0) ++ ndesc = clamp(airoha_soe_probe_rx_ndesc, 1, ++ AIROHA_SOE_RX_ALLOC_NDESC); ++ } ++#endif ++ + q->qdma = qdma; + lro_q = airoha_qdma_is_lro_queue(q); ++#if IS_REACHABLE(CONFIG_NET_AIROHA_SOE) ++ if (qid == AIROHA_SOE_RX_RING) ++ q->rx_coherent = airoha_soe_probe_rx_coherent; ++#endif + +- q->entry = devm_kzalloc(eth->dev, ndesc * sizeof(*q->entry), +- GFP_KERNEL); ++ q->entry = ++ devm_kzalloc(eth->dev, ndesc * sizeof(*q->entry), GFP_KERNEL); + if (!q->entry) + return -ENOMEM; + +@@ -1079,25 +1418,49 @@ + &dma_addr, GFP_KERNEL); + if (!q->desc) + return -ENOMEM; ++ q->desc_dma = dma_addr; + + pp_params.order = lro_q ? AIROHA_LRO_PAGE_ORDER : 0; + pp_params.max_len = PAGE_SIZE << pp_params.order; + +- q->page_pool = page_pool_create(&pp_params); +- if (IS_ERR(q->page_pool)) { +- int err = PTR_ERR(q->page_pool); ++ if (!q->rx_coherent) { ++ q->page_pool = page_pool_create(&pp_params); ++ if (IS_ERR(q->page_pool)) { ++ int err = PTR_ERR(q->page_pool); + +- q->page_pool = NULL; +- return err; ++ q->page_pool = NULL; ++ return err; ++ } + } + + q->buf_size = lro_q ? pp_params.max_len : pp_params.max_len / 2; + q->ndesc = ndesc; ++ q->rx_alloc_ndesc = ndesc; ++#if IS_REACHABLE(CONFIG_NET_AIROHA_SOE) ++ if (qid == AIROHA_SOE_RX_RING) { ++ size_t buf_size; ++ int max_buf_size; ++ ++ /* Coherent mode reassembles SOE frames in skb linear data. */ ++ max_buf_size = q->rx_coherent ? AIROHA_SOE_RX_BUF_SIZE_MAX : ++ PAGE_SIZE; ++ q->buf_size = clamp(airoha_soe_probe_rx_buf_size, ++ AIROHA_SOE_RX_BUF_SIZE_MIN, max_buf_size); ++ if (q->rx_coherent) { ++ buf_size = (size_t)ndesc * q->buf_size; ++ q->rx_coherent_buf = ++ dmam_alloc_coherent(eth->dev, buf_size, ++ &q->rx_coherent_dma, GFP_KERNEL); ++ if (!q->rx_coherent_buf) ++ return -ENOMEM; ++ q->rx_coherent_buf_size = buf_size; ++ } ++ } ++#endif + netif_napi_add(eth->napi_dev, &q->napi, airoha_qdma_rx_napi_poll); + +- airoha_qdma_wr(qdma, REG_RX_RING_BASE(qid), dma_addr); +- airoha_qdma_rmw(qdma, REG_RX_RING_SIZE(qid), +- RX_RING_SIZE_MASK, ++ airoha_qdma_wr(qdma, REG_RX_RING_BASE(qid), q->desc_dma); ++ airoha_qdma_rmw(qdma, REG_RX_RING_SIZE(qid), RX_RING_SIZE_MASK, + FIELD_PREP(RX_RING_SIZE_MASK, ndesc)); + + thr = clamp(ndesc >> 3, 1, 32); +@@ -1105,6 +1468,12 @@ + FIELD_PREP(RX_RING_THR_MASK, thr)); + airoha_qdma_rmw(qdma, REG_RX_DMA_IDX(qid), RX_RING_DMA_IDX_MASK, + FIELD_PREP(RX_RING_DMA_IDX_MASK, q->head)); ++#if IS_REACHABLE(CONFIG_NET_AIROHA_SOE) ++ if (qid == AIROHA_SOE_RX_RING && !airoha_soe_probe_rx_scatter) ++ airoha_qdma_clear(qdma, REG_RX_SCATTER_CFG(qid), ++ RX_RING_SG_EN_MASK); ++ else ++#endif + if (lro_q) + airoha_qdma_clear(qdma, REG_RX_SCATTER_CFG(qid), + RX_RING_SG_EN_MASK); +@@ -1126,11 +1495,14 @@ + while (q->queued) { + struct airoha_queue_entry *e = &q->entry[q->tail]; + struct airoha_qdma_desc *desc = &q->desc[q->tail]; +- struct page *page = virt_to_head_page(e->buf); + +- dma_sync_single_for_cpu(eth->dev, e->dma_addr, e->dma_len, +- page_pool_get_dma_dir(q->page_pool)); +- page_pool_put_full_page(q->page_pool, page, false); ++ if (!q->rx_coherent) { ++ struct page *page = virt_to_head_page(e->buf); ++ ++ dma_sync_single_for_cpu(eth->dev, e->dma_addr, e->dma_len, ++ page_pool_get_dma_dir(q->page_pool)); ++ page_pool_put_full_page(q->page_pool, page, false); ++ } + /* Reset DMA descriptor */ + WRITE_ONCE(desc->tcp_ts_reply, 0); + WRITE_ONCE(desc->ctrl, 0); +@@ -1313,8 +1685,24 @@ + airoha_qdma_rmw(qdma, REG_IRQ_CLEAR_LEN(id), + IRQ_CLEAR_LEN_MASK, 0x80); + airoha_qdma_rmw(qdma, REG_IRQ_CLEAR_LEN(id), +- IRQ_CLEAR_LEN_MASK, (done & 0x7f)); ++ IRQ_CLEAR_LEN_MASK, ++ (done & 0x7f)); ++ } ++ ++#if IS_REACHABLE(CONFIG_NET_AIROHA_SOE) ++ if (done && airoha_soe_has_pending_rx(eth->soe)) { ++ int i; ++ ++ /* SOE decrypt completions can lag behind TX cleanup IRQs. */ ++ for (i = 0; i < ARRAY_SIZE(eth->qdma); i++) { ++ struct airoha_queue *rxq = ++ ð->qdma[i].q_rx[AIROHA_SOE_RX_RING]; ++ ++ if (rxq->ndesc) ++ napi_schedule(&rxq->napi); ++ } + } ++#endif + + if (done < budget && napi_complete(napi)) + airoha_qdma_irq_enable(&qdma->irq_banks[0], QDMA_INT_REG_IDX0, +@@ -1614,16 +2002,11 @@ + for (i = 0; i < ARRAY_SIZE(qdma->irq_banks); i++) { + /* clear pending irqs */ + airoha_qdma_wr(qdma, REG_INT_STATUS(i), 0xffffffff); +- /* setup rx irqs */ +- airoha_qdma_irq_enable(&qdma->irq_banks[i], QDMA_INT_REG_IDX0, +- INT_RX0_MASK(RX_IRQ_BANK_PIN_MASK(i))); +- airoha_qdma_irq_enable(&qdma->irq_banks[i], QDMA_INT_REG_IDX1, +- INT_RX1_MASK(RX_IRQ_BANK_PIN_MASK(i))); +- airoha_qdma_irq_enable(&qdma->irq_banks[i], QDMA_INT_REG_IDX2, +- INT_RX2_MASK(RX_IRQ_BANK_PIN_MASK(i))); +- airoha_qdma_irq_enable(&qdma->irq_banks[i], QDMA_INT_REG_IDX3, +- INT_RX3_MASK(RX_IRQ_BANK_PIN_MASK(i))); + } ++ airoha_qdma_apply_rx_irq_bank_masks(qdma); ++#if IS_REACHABLE(CONFIG_NET_AIROHA_SOE) ++ airoha_qdma_set_rx_done_irq(qdma, AIROHA_SOE_RX_RING, true); ++#endif + /* setup tx irqs */ + airoha_qdma_irq_enable(&qdma->irq_banks[0], QDMA_INT_REG_IDX0, + TX_COHERENT_LOW_INT_MASK | INT_TX_MASK); +@@ -2577,6 +2960,11 @@ + struct airoha_gdm_dev *dev = netdev_priv(netdev); + struct airoha_qdma *qdma = airoha_qdma_deref(dev); + int qdma_id = qdma - &dev->eth->qdma[0]; ++ int err; ++ ++ err = airoha_soe_set_features(netdev, features); ++ if (err) ++ return err; + + if (!(diff & NETIF_F_GRO_HW)) + return 0; +@@ -2589,6 +2977,120 @@ + return 0; + } + ++#if IS_REACHABLE(CONFIG_NET_AIROHA_SOE) ++int airoha_qdma_xmit_skb(struct airoha_gdm_dev *dev, struct sk_buff *skb, ++ u32 msg0, u32 msg1, u32 msg2) ++{ ++ struct net_device *netdev = netdev_from_priv(dev); ++ struct airoha_qdma *qdma; ++ u32 nr_frags, len; ++ struct airoha_queue_entry *e; ++ struct netdev_queue *txq; ++ struct airoha_queue *q; ++ LIST_HEAD(tx_list); ++ int i = 0, qid; ++ void *data; ++ u16 index; ++ ++ /* Submit through the active QDMA for this netdev; inactive QDMA rings ++ * can accept CPU index writes without the DMA engine consuming them. ++ */ ++ rcu_read_lock(); ++ qdma = rcu_dereference(dev->qdma); ++ qid = airoha_qdma_get_txq(qdma, skb_get_queue_mapping(skb)); ++ q = &qdma->q_tx[qid]; ++ if (WARN_ON_ONCE(!q->ndesc)) { ++ rcu_read_unlock(); ++ return -ENODEV; ++ } ++ ++ spin_lock_bh(&q->lock); ++ ++ txq = skb_get_tx_queue(netdev, skb); ++ nr_frags = 1 + skb_shinfo(skb)->nr_frags; ++ if (q->queued + nr_frags >= q->ndesc) { ++ netif_tx_stop_queue(txq); ++ q->txq_stopped = true; ++ spin_unlock_bh(&q->lock); ++ rcu_read_unlock(); ++ return -EBUSY; ++ } ++ ++ len = skb_headlen(skb); ++ data = skb->data; ++ ++ e = list_first_entry(&q->tx_list, struct airoha_queue_entry, list); ++ index = e - q->entry; ++ ++ while (true) { ++ struct airoha_qdma_desc *desc = &q->desc[index]; ++ skb_frag_t *frag = &skb_shinfo(skb)->frags[i]; ++ dma_addr_t addr; ++ u32 val; ++ ++ addr = dma_map_single(netdev->dev.parent, data, len, ++ DMA_TO_DEVICE); ++ if (unlikely(dma_mapping_error(netdev->dev.parent, addr))) ++ goto error_unmap; ++ ++ list_move_tail(&e->list, &tx_list); ++ e->skb = i == nr_frags - 1 ? skb : NULL; ++ e->dma_addr = addr; ++ e->dma_len = len; ++ ++ e = list_first_entry(&q->tx_list, struct airoha_queue_entry, ++ list); ++ index = e - q->entry; ++ ++ val = FIELD_PREP(QDMA_DESC_LEN_MASK, len); ++ if (i < nr_frags - 1) ++ val |= FIELD_PREP(QDMA_DESC_MORE_MASK, 1); ++ WRITE_ONCE(desc->ctrl, cpu_to_le32(val)); ++ WRITE_ONCE(desc->addr, cpu_to_le32(addr)); ++ val = FIELD_PREP(QDMA_DESC_NEXT_ID_MASK, index); ++ WRITE_ONCE(desc->data, cpu_to_le32(val)); ++ WRITE_ONCE(desc->msg0, cpu_to_le32(msg0)); ++ WRITE_ONCE(desc->msg1, cpu_to_le32(msg1)); ++ WRITE_ONCE(desc->msg2, cpu_to_le32(msg2)); ++ ++ if (++i == nr_frags) ++ break; ++ ++ data = skb_frag_address(frag); ++ len = skb_frag_size(frag); ++ } ++ q->queued += i; ++ ++ skb_tx_timestamp(skb); ++ netdev_tx_sent_queue(txq, skb->len); ++ if (q->ndesc - q->queued < q->free_thr) { ++ netif_tx_stop_queue(txq); ++ q->txq_stopped = true; ++ } ++ ++ /* SOE submits do not run in the regular ndo_start_xmit batching path. */ ++ airoha_qdma_rmw(qdma, REG_TX_CPU_IDX(qid), TX_RING_CPU_IDX_MASK, ++ FIELD_PREP(TX_RING_CPU_IDX_MASK, index)); ++ ++ spin_unlock_bh(&q->lock); ++ rcu_read_unlock(); ++ ++ return 0; ++ ++error_unmap: ++ list_for_each_entry(e, &tx_list, list) { ++ dma_unmap_single(netdev->dev.parent, e->dma_addr, e->dma_len, ++ DMA_TO_DEVICE); ++ e->dma_addr = 0; ++ } ++ list_splice(&tx_list, &q->tx_list); ++ spin_unlock_bh(&q->lock); ++ rcu_read_unlock(); ++ ++ return -ENOMEM; ++} ++#endif ++ + static netdev_tx_t airoha_dev_xmit(struct sk_buff *skb, + struct net_device *netdev) + { +@@ -2598,6 +3100,7 @@ + struct airoha_qdma *qdma; + struct netdev_queue *txq; + struct airoha_queue *q; ++ bool soe_decrypt_skb = false; + LIST_HEAD(tx_list); + int i = 0, qid; + void *data; +@@ -2638,6 +3141,11 @@ + FIELD_PREP(QDMA_ETH_TXMSG_FPORT_MASK, fport) | + FIELD_PREP(QDMA_ETH_TXMSG_METER_MASK, 0x7f); + ++#if IS_REACHABLE(CONFIG_NET_AIROHA_SOE) ++ if (dev->eth->ppe) ++ soe_decrypt_skb = airoha_ppe_soe_skb_marked(skb); ++#endif ++ + q = &qdma->q_tx[qid]; + if (WARN_ON_ONCE(!q->ndesc)) + goto error; +@@ -2710,12 +3218,24 @@ + q->txq_stopped = true; + } + +- if (netif_xmit_stopped(txq) || !netdev_xmit_more()) ++ if (netif_xmit_stopped(txq) || !netdev_xmit_more() || ++ soe_decrypt_skb) + airoha_qdma_rmw(qdma, REG_TX_CPU_IDX(qid), + TX_RING_CPU_IDX_MASK, + FIELD_PREP(TX_RING_CPU_IDX_MASK, index)); + + spin_unlock_bh(&q->lock); ++ ++ /* SOE decrypt flow binding needs the final egress netdev and QDMA ++ * descriptor context. The SOE RX path marks only candidate packets; bind ++ * only after the current plaintext packet has been queued and kicked so ++ * the newly bound decrypt entry cannot race the CPU-transmitted packet. ++ */ ++#if IS_REACHABLE(CONFIG_NET_AIROHA_SOE) ++ if (soe_decrypt_skb) ++ airoha_ppe_soe_xmit_skb(&dev->eth->ppe->dev, skb, netdev); ++#endif ++ + rcu_read_unlock(); + + return NETDEV_TX_OK; +@@ -3941,6 +4461,7 @@ + dev->flags |= AIROHA_PRIV_F_WAN; + + port->devs[index] = dev; ++ airoha_soe_build_netdev(netdev, airoha_qdma_xmit_skb); + + return airoha_setup_phylink(netdev); + } +@@ -4120,6 +4641,10 @@ + strscpy(eth->napi_dev->name, "qdma_eth", sizeof(eth->napi_dev->name)); + platform_set_drvdata(pdev, eth); + ++ err = airoha_soe_init(eth); ++ if (err) ++ goto error_netdev_free; ++ + err = airoha_hw_init(pdev, eth); + if (err) + goto error_netdev_free; +@@ -4168,6 +4693,7 @@ + netdev = netdev_from_priv(dev); + if (netdev->reg_state == NETREG_REGISTERED) + unregister_netdev(netdev); ++ airoha_soe_teardown_netdev(netdev); + if (dev->phylink) + phylink_destroy(dev->phylink); + of_node_put(netdev->dev.of_node); +@@ -4176,6 +4702,7 @@ + } + airoha_hw_cleanup(eth); + error_netdev_free: ++ airoha_soe_deinit(eth); + free_netdev(eth->napi_dev); + platform_set_drvdata(pdev, NULL); + +@@ -4206,12 +4733,14 @@ + + netdev = netdev_from_priv(dev); + unregister_netdev(netdev); ++ airoha_soe_teardown_netdev(netdev); + phylink_destroy(dev->phylink); + of_node_put(netdev->dev.of_node); + } + airoha_metadata_dst_free(port); + } + airoha_hw_cleanup(eth); ++ airoha_soe_deinit(eth); + + free_netdev(eth->napi_dev); + platform_set_drvdata(pdev, NULL); diff --git a/target/linux/airoha/patches-6.18/9999-07-net-airoha-extend-offload-metadata-for-soe.patch b/target/linux/airoha/patches-6.18/9999-07-net-airoha-extend-offload-metadata-for-soe.patch new file mode 100644 index 00000000000000..622b472fb02924 --- /dev/null +++ b/target/linux/airoha/patches-6.18/9999-07-net-airoha-extend-offload-metadata-for-soe.patch @@ -0,0 +1,28 @@ +From: Jihong Min +Date: Mon, 1 Jun 2026 13:30:00 +0900 +Subject: [PATCH] net: airoha: extend offload metadata for SOE + +Extend the Airoha offload metadata contract with the XFRM/SOE fields +required by the PPE driver. The PPE patch consumes these fields when +committing inline ESP flow entries. + +Assisted-by: Codex:gpt-5.5 +Signed-off-by: Jihong Min +--- +diff --git a/include/linux/soc/airoha/airoha_offload.h b/include/linux/soc/airoha/airoha_offload.h +index d01ef4a..2227a48 100644 +--- a/include/linux/soc/airoha/airoha_offload.h ++++ b/include/linux/soc/airoha/airoha_offload.h +@@ -11,7 +11,12 @@ + #include + + enum { ++ PPE_CPU_REASON_UN_HIT = 0x0d, ++ PPE_CPU_REASON_HIT_UNBIND = 0x0e, + PPE_CPU_REASON_HIT_UNBIND_RATE_REACHED = 0x0f, ++ PPE_CPU_REASON_HIT_BIND_FORCE_CPU = 0x16, ++ PPE_CPU_REASON_HIT_BIND_EXCEED_MTU = 0x1c, ++ PPE_CPU_REASON_NOT_THROUGH_PPE = 0x1e, + }; + + struct airoha_ppe_dev { diff --git a/target/linux/airoha/patches-6.18/9999-08-net-airoha-add-soe-ppe-flow-support.patch b/target/linux/airoha/patches-6.18/9999-08-net-airoha-add-soe-ppe-flow-support.patch new file mode 100644 index 00000000000000..5969a9dca7d16c --- /dev/null +++ b/target/linux/airoha/patches-6.18/9999-08-net-airoha-add-soe-ppe-flow-support.patch @@ -0,0 +1,812 @@ +From: Jihong Min +Date: Mon, 1 Jun 2026 13:30:00 +0900 +Subject: [PATCH] net: airoha: add SOE PPE flow support + +Program PPE flow entries with the SOE tunnel metadata needed for inline +ESP encapsulation and decapsulation. This also keeps the bind-rate and +SOE commit probe-window knobs exposed so hardware flow binding behavior +can be tuned without rebuilding the kernel. + +Assisted-by: Codex:gpt-5.5 +Signed-off-by: Jihong Min +--- +diff --git a/drivers/net/ethernet/airoha/airoha_ppe.c b/drivers/net/ethernet/airoha/airoha_ppe.c +--- a/drivers/net/ethernet/airoha/airoha_ppe.c ++++ b/drivers/net/ethernet/airoha/airoha_ppe.c +@@ -6,16 +6,23 @@ + + #include + #include ++#include ++#include + #include + #include + #include +-#include ++#include ++#include ++#include + #include ++#include ++#include + #include + #include + + #include "airoha_regs.h" + #include "airoha_eth.h" ++#include "airoha_soe.h" + + /* Serialize airoha_gdm_dev flags, QDMA pointer and PPE CPU port + * configuration. +@@ -23,6 +30,47 @@ + DEFINE_MUTEX(flow_offload_mutex); + static DEFINE_SPINLOCK(ppe_lock); + ++#define AIROHA_FOE_HOP0 GENMASK(31, 29) ++#define AIROHA_FOE_HOP1 GENMASK(28, 26) ++#define AIROHA_FOE_HOP2 GENMASK(25, 23) ++#define AIROHA_FOE_HOP3 GENMASK(22, 20) ++#define AIROHA_FOE_HOP_MASK \ ++ (AIROHA_FOE_HOP0 | AIROHA_FOE_HOP1 | AIROHA_FOE_HOP2 | AIROHA_FOE_HOP3) ++#define AIROHA_PPE_SOE_DEFAULT_TUNNEL_MTU 1500 ++#define AIROHA_PPE_SOE_MAGIC_IPSEC 0x72a1 ++#define AIROHA_PPE_SOE_PORT_AG 0x3f ++#define AIROHA_PPE_SOE_CHANNEL 2 ++#define AIROHA_PPE_SOE_META_TIMEOUT_MS 1000 ++#define AIROHA_PPE_SOE_MAGIC_GDM4 0x729a ++#define AIROHA_PPE_SOE_MARK_MAGIC 0x5e000000 ++#define AIROHA_PPE_SOE_MARK_MAGIC_MASK 0xff000000 ++#define AIROHA_PPE_SOE_MARK_HASH_MASK 0x00ffff00 ++ ++struct airoha_ppe_soe_tuple_info { ++ unsigned int tunnel_mtu; ++ u8 sa_index; ++ u8 hop; ++}; ++ ++/* Default hardware bind threshold used by the original PPE init path. */ ++static unsigned int airoha_ppe_bind_rate = 0x1e; ++static unsigned int airoha_ppe_soe_inline_force_commit_probe_window = 8; ++static struct airoha_ppe *airoha_ppe_active; ++ ++static int airoha_ppe_set_bind_rate(const char *val, ++ const struct kernel_param *kp); ++static int airoha_ppe_get_bind_rate(char *buf, const struct kernel_param *kp); ++ ++module_param_named(soe_inline_force_commit_probe_window, ++ airoha_ppe_soe_inline_force_commit_probe_window, uint, 0600); ++MODULE_PARM_DESC(soe_inline_force_commit_probe_window, ++ "Near-hash slots to probe when committing SOE XFRM entries"); ++module_param_call(ppe_bind_rate, airoha_ppe_set_bind_rate, ++ airoha_ppe_get_bind_rate, NULL, 0600); ++__MODULE_PARM_TYPE(ppe_bind_rate, "uint"); ++MODULE_PARM_DESC(ppe_bind_rate, ++ "PPE bind-rate threshold for L2B and bind fields"); ++ + static const struct rhashtable_params airoha_flow_table_params = { + .head_offset = offsetof(struct airoha_flow_table_entry, node), + .key_offset = offsetof(struct airoha_flow_table_entry, cookie), +@@ -83,6 +131,17 @@ + return airoha_fe_rr(eth, REG_PPE_GLO_CFG(index)) & PPE_GLO_CFG_EN_MASK; + } + ++static void airoha_ppe_apply_bind_rate(struct airoha_eth *eth, int ppe_idx) ++{ ++ u32 rate = min_t(u32, READ_ONCE(airoha_ppe_bind_rate), ++ FIELD_MAX(PPE_BIND_RATE_BIND_MASK)); ++ ++ airoha_fe_rmw(eth, REG_PPE_BIND_RATE(ppe_idx), ++ PPE_BIND_RATE_L2B_BIND_MASK | PPE_BIND_RATE_BIND_MASK, ++ FIELD_PREP(PPE_BIND_RATE_L2B_BIND_MASK, rate) | ++ FIELD_PREP(PPE_BIND_RATE_BIND_MASK, rate)); ++} ++ + static u32 airoha_ppe_get_timestamp(struct airoha_ppe *ppe) + { + return airoha_fe_get(ppe->eth, REG_FE_FOE_TS, +@@ -194,15 +253,14 @@ + FIELD_PREP(PPE_DRAM_TB_NUM_ENTRY_MASK, + dram_num_entries)); + +- airoha_fe_rmw(eth, REG_PPE_BIND_RATE(i), +- PPE_BIND_RATE_L2B_BIND_MASK | +- PPE_BIND_RATE_BIND_MASK, +- FIELD_PREP(PPE_BIND_RATE_L2B_BIND_MASK, 0x1e) | +- FIELD_PREP(PPE_BIND_RATE_BIND_MASK, 0x1e)); ++ airoha_ppe_apply_bind_rate(eth, i); + + airoha_fe_wr(eth, REG_PPE_HASH_SEED(i), PPE_HASH_SEED); + airoha_fe_clear(eth, REG_PPE_PPE_FLOW_CFG(i), + PPE_FLOW_CFG_IP6_6RD_MASK); ++ airoha_fe_set(eth, REG_PPE_PPE_FLOW_CFG(i), ++ PPE_FLOW_CFG_IP4_IPSEC_MASK | ++ PPE_FLOW_CFG_IP6_IPSEC_MASK); + } + + for (i = 0; i < ARRAY_SIZE(eth->ports); i++) { +@@ -542,6 +600,164 @@ + return 0; + } + ++static int airoha_ppe_soe_fill_inner_ipv4_data(struct sk_buff *skb, ++ struct airoha_flow_data *data, ++ int *type, int *l4proto) ++{ ++ unsigned int ip_offset = ETH_HLEN; ++ union { ++ struct tcphdr tcp; ++ struct udphdr udp; ++ } ports; ++ struct iphdr iph_buf, *iph; ++ unsigned int l4_offset; ++ struct udphdr *udp; ++ struct tcphdr *tcp; ++ ++ if (skb_headlen(skb) < ETH_HLEN) ++ return -EINVAL; ++ ++ memcpy(&data->eth, skb->data, ETH_HLEN); ++ if (data->eth.h_proto != htons(ETH_P_IP)) ++ return -EAFNOSUPPORT; ++ ++ iph = skb_header_pointer(skb, ip_offset, sizeof(iph_buf), &iph_buf); ++ if (!iph || iph->ihl < 5 || iph->version != 4) ++ return -EINVAL; ++ ++ l4_offset = ip_offset + iph->ihl * 4; ++ data->v4.src_addr = iph->saddr; ++ data->v4.dst_addr = iph->daddr; ++ *l4proto = iph->protocol; ++ ++ switch (iph->protocol) { ++ case IPPROTO_TCP: ++ tcp = skb_header_pointer(skb, l4_offset, sizeof(ports.tcp), ++ &ports.tcp); ++ if (!tcp) ++ return -EINVAL; ++ ++ data->src_port = tcp->source; ++ data->dst_port = tcp->dest; ++ *type = PPE_PKT_TYPE_IPV4_HNAPT; ++ break; ++ case IPPROTO_UDP: ++ udp = skb_header_pointer(skb, l4_offset, sizeof(ports.udp), ++ &ports.udp); ++ if (!udp) ++ return -EINVAL; ++ ++ data->src_port = udp->source; ++ data->dst_port = udp->dest; ++ *type = PPE_PKT_TYPE_IPV4_HNAPT; ++ break; ++ default: ++ *type = PPE_PKT_TYPE_IPV4_ROUTE; ++ break; ++ } ++ ++ return 0; ++} ++ ++static int airoha_ppe_foe_entry_set_soe_fields(struct airoha_foe_entry *hwe, ++ u8 sa_index, u8 hop, ++ unsigned int tunnel_mtu) ++{ ++ int type; ++ ++ if (hop > FIELD_MAX(AIROHA_FOE_HOP0)) ++ return -ERANGE; ++ ++ type = FIELD_GET(AIROHA_FOE_IB1_BIND_PACKET_TYPE, hwe->ib1); ++ switch (type) { ++ case PPE_PKT_TYPE_IPV4_HNAPT: ++ case PPE_PKT_TYPE_IPV4_ROUTE: ++ break; ++ default: ++ return -EOPNOTSUPP; ++ } ++ ++ tunnel_mtu = min_t(unsigned int, tunnel_mtu, ++ FIELD_MAX(AIROHA_FOE_TUNNEL_MTU)); ++ ++ /* SOE FOE entries store the hop selector and SA index here. */ ++ hwe->ipv4.rsv[0] &= ~AIROHA_FOE_HOP_MASK; ++ hwe->ipv4.rsv[0] |= FIELD_PREP(AIROHA_FOE_HOP0, hop); ++ ++ hwe->ipv4.data &= ~(AIROHA_FOE_ACTDP | AIROHA_FOE_TUNNEL_ID); ++ hwe->ipv4.data |= AIROHA_FOE_TUNNEL | ++ FIELD_PREP(AIROHA_FOE_ACTDP, sa_index); ++ ++ hwe->ipv4.l2.meter &= ~AIROHA_FOE_TUNNEL_MTU; ++ hwe->ipv4.l2.meter |= FIELD_PREP(AIROHA_FOE_TUNNEL_MTU, tunnel_mtu); ++ ++ hwe->ib1 &= ~AIROHA_FOE_IB1_BIND_TUNNEL_DECAP; ++ ++ return 0; ++} ++ ++static int ++airoha_ppe_soe_get_tuple_info(const struct flow_offload_tuple *tuple, ++ struct net_device *lower_dev, ++ struct airoha_ppe_soe_tuple_info *info) ++{ ++ int err; ++ ++ if (!tuple || !info) ++ return -EINVAL; ++ if (tuple->xmit_type != FLOW_OFFLOAD_XMIT_XFRM) ++ return -EOPNOTSUPP; ++ ++ err = airoha_soe_xfrm_ppe_info(tuple->dst_cache, lower_dev, ++ &info->sa_index, &info->hop); ++ if (err) ++ return err; ++ ++ info->tunnel_mtu = tuple->mtu ? tuple->mtu : ++ AIROHA_PPE_SOE_DEFAULT_TUNNEL_MTU; ++ ++ return 0; ++} ++ ++static int ++airoha_ppe_foe_entry_set_soe_info(struct airoha_foe_entry *hwe, ++ const struct flow_offload_tuple *tuple, ++ struct net_device *lower_dev) ++{ ++ struct airoha_ppe_soe_tuple_info info; ++ int err; ++ ++ if (!tuple) ++ return 0; ++ if (tuple->xmit_type != FLOW_OFFLOAD_XMIT_XFRM) ++ return 0; ++ ++ err = airoha_ppe_soe_get_tuple_info(tuple, lower_dev, &info); ++ if (err) ++ return err; ++ ++ err = airoha_ppe_foe_entry_set_soe_fields(hwe, info.sa_index, ++ info.hop, info.tunnel_mtu); ++ if (err) ++ return err; ++ ++ /* XFRM packet-offload entries are not plain Ethernet/IP entries: ++ * the PPE must tag them as SOE/IPsec work and submit them through the ++ * SOE-facing channel/port aggregation path. Without these fields the ++ * entry can still become BND, but traffic falls back to the slow path ++ * instead of the inline encrypt/decrypt datapath. ++ */ ++ hwe->ipv4.l2.common.etype = AIROHA_PPE_SOE_MAGIC_IPSEC; ++ hwe->ipv4.data &= ~AIROHA_FOE_CHANNEL; ++ hwe->ipv4.data |= FIELD_PREP(AIROHA_FOE_CHANNEL, ++ AIROHA_PPE_SOE_CHANNEL); ++ hwe->ipv4.ib2 &= ~AIROHA_FOE_IB2_PORT_AG; ++ hwe->ipv4.ib2 |= FIELD_PREP(AIROHA_FOE_IB2_PORT_AG, ++ AIROHA_PPE_SOE_PORT_AG); ++ ++ return 0; ++} ++ + static u32 airoha_ppe_foe_get_entry_hash(struct airoha_ppe *ppe, + struct airoha_foe_entry *hwe) + { +@@ -668,6 +884,9 @@ + meter = &hwe->ipv4.l2.meter; + } + ++ if (*data & AIROHA_FOE_TUNNEL) ++ return; ++ + pse_port = FIELD_GET(AIROHA_FOE_IB2_PSE_PORT, *ib2); + if (pse_port == FE_PSE_PORT_CDM4) + return; +@@ -794,6 +1013,98 @@ + return err; + } + ++static bool airoha_ppe_foe_commit_slot_free(struct airoha_ppe *ppe, ++ struct airoha_foe_entry *e, ++ u32 hash) ++{ ++ struct airoha_flow_table_entry tmp = { .data = *e }; ++ struct airoha_foe_entry *hwe; ++ u32 state; ++ ++ hwe = airoha_ppe_foe_get_entry_locked(ppe, hash); ++ if (!hwe) ++ return false; ++ ++ state = FIELD_GET(AIROHA_FOE_IB1_BIND_STATE, hwe->ib1); ++ if (state == AIROHA_FOE_STATE_INVALID || state == AIROHA_FOE_STATE_FIN) ++ return true; ++ ++ return airoha_ppe_foe_compare_entry(&tmp, hwe); ++} ++ ++static bool airoha_ppe_foe_commit_slot_match(struct airoha_ppe *ppe, ++ struct airoha_foe_entry *e, ++ u32 hash) ++{ ++ struct airoha_flow_table_entry tmp = { .data = *e }; ++ struct airoha_foe_entry *hwe; ++ u32 state; ++ ++ hwe = airoha_ppe_foe_get_entry_locked(ppe, hash); ++ if (!hwe) ++ return false; ++ ++ state = FIELD_GET(AIROHA_FOE_IB1_BIND_STATE, hwe->ib1); ++ if (state == AIROHA_FOE_STATE_INVALID) ++ return false; ++ ++ return airoha_ppe_foe_compare_entry(&tmp, hwe); ++} ++ ++static u32 airoha_ppe_foe_commit_hash(struct airoha_ppe *ppe, ++ struct airoha_foe_entry *e, u32 hash) ++{ ++ u32 num_entries = airoha_ppe_get_total_num_entries(ppe); ++ u32 mask = num_entries - 1; ++ u32 window, alt_hash, i; ++ ++ lockdep_assert_held(&ppe_lock); ++ ++ /* SOE XFRM rules are installed by flowtable before a matching PPE ++ * unbound entry is guaranteed to exist. Commit them immediately, but ++ * probe the nearest hash slots first so an unrelated live entry is not ++ * overwritten when the base hash collides. ++ */ ++ window = min_t(u32, ++ READ_ONCE(airoha_ppe_soe_inline_force_commit_probe_window), ++ mask); ++ for (i = 0; i <= window; i++) { ++ u32 candidates[2] = { (hash + i) & mask, (hash - i) & mask }; ++ unsigned int count = i ? ARRAY_SIZE(candidates) : 1; ++ unsigned int j; ++ ++ for (j = 0; j < count; j++) { ++ u32 candidate = candidates[j]; ++ ++ if (airoha_ppe_foe_commit_slot_match(ppe, e, ++ candidate)) ++ return candidate; ++ } ++ } ++ ++ if (airoha_ppe_foe_commit_slot_free(ppe, e, hash)) ++ return hash; ++ ++ alt_hash = hash ^ 1; ++ if (alt_hash < num_entries && ++ airoha_ppe_foe_commit_slot_free(ppe, e, alt_hash)) ++ return alt_hash; ++ ++ for (i = 1; i <= window; i++) { ++ u32 candidates[2] = { (hash + i) & mask, (hash - i) & mask }; ++ unsigned int j; ++ ++ for (j = 0; j < ARRAY_SIZE(candidates); j++) { ++ u32 candidate = candidates[j]; ++ ++ if (airoha_ppe_foe_commit_slot_free(ppe, e, candidate)) ++ return candidate; ++ } ++ } ++ ++ return hash; ++} ++ + static void airoha_ppe_foe_remove_flow(struct airoha_ppe *ppe, + struct airoha_flow_table_entry *e) + { +@@ -981,6 +1292,255 @@ + return false; + } + ++static void airoha_ppe_soe_meta_store(struct airoha_ppe_soe_meta *meta, ++ u32 key_hash, u16 foe_hash, u8 sa_index, ++ u8 hop) ++{ ++ u8 seen = 1; ++ ++ if (READ_ONCE(meta->valid) && ++ READ_ONCE(meta->key_hash) == key_hash && ++ READ_ONCE(meta->foe_hash) == foe_hash && ++ READ_ONCE(meta->sa_index) == sa_index && ++ READ_ONCE(meta->hop) == hop && ++ time_before_eq(jiffies, READ_ONCE(meta->expires))) ++ seen = min_t(u8, READ_ONCE(meta->seen) + 1, U8_MAX); ++ ++ WRITE_ONCE(meta->key_hash, key_hash); ++ WRITE_ONCE(meta->foe_hash, foe_hash); ++ WRITE_ONCE(meta->sa_index, sa_index); ++ WRITE_ONCE(meta->hop, hop); ++ WRITE_ONCE(meta->seen, seen); ++ WRITE_ONCE(meta->expires, ++ jiffies + msecs_to_jiffies(AIROHA_PPE_SOE_META_TIMEOUT_MS)); ++ WRITE_ONCE(meta->valid, 1); ++} ++ ++void airoha_ppe_soe_mark_skb(struct airoha_ppe_dev *dev, struct sk_buff *skb, ++ u16 hash, u8 sa_index, u8 hop) ++{ ++ struct airoha_ppe *ppe; ++ u32 ppe_hash_mask; ++ ++ if (!dev || !skb) ++ return; ++ ++ ppe = dev->priv; ++ if (!ppe || !ppe->soe_meta) ++ return; ++ ++ ppe_hash_mask = airoha_ppe_get_total_num_entries(ppe) - 1; ++ if (hash > ppe_hash_mask) ++ return; ++ ++ /* SOE decrypt completion is CPU-visible before normal routing has ++ * selected the plaintext egress netdev. Keep the original encrypted FOE ++ * hash and SA hop briefly on the skb so airoha_dev_xmit() can finish ++ * the PPE entry once the final egress descriptor is known. ++ */ ++ airoha_ppe_soe_meta_store(&ppe->soe_meta[hash], hash, hash, sa_index, ++ hop); ++ ppe->foe_check_time[hash] = 0; ++ ++ skb->mark &= ~(AIROHA_PPE_SOE_MARK_MAGIC_MASK | ++ AIROHA_PPE_SOE_MARK_HASH_MASK); ++ skb->mark |= AIROHA_PPE_SOE_MARK_MAGIC | ++ FIELD_PREP(AIROHA_PPE_SOE_MARK_HASH_MASK, hash); ++} ++ ++bool airoha_ppe_soe_skb_marked(struct sk_buff *skb) ++{ ++ return skb && ((skb->mark & AIROHA_PPE_SOE_MARK_MAGIC_MASK) == ++ AIROHA_PPE_SOE_MARK_MAGIC); ++} ++ ++void airoha_ppe_soe_xmit_skb(struct airoha_ppe_dev *dev, struct sk_buff *skb, ++ struct net_device *netdev) ++{ ++ struct airoha_foe_entry entry, tmpl, *hwe; ++ struct airoha_flow_data data = {}; ++ struct airoha_ppe_soe_meta *meta; ++ u32 ppe_hash_mask, key_hash; ++ bool soe_return_path; ++ struct airoha_ppe *ppe; ++ unsigned long expires; ++ u16 hash; ++ int err, l4proto, type; ++ u8 sa_index, hop; ++ u8 seen; ++ ++ if (!dev || !skb || !netdev) ++ return; ++ ++ if ((skb->mark & AIROHA_PPE_SOE_MARK_MAGIC_MASK) != ++ AIROHA_PPE_SOE_MARK_MAGIC) ++ return; ++ ++ ppe = dev->priv; ++ if (!ppe || !ppe->soe_meta) ++ goto clear_mark; ++ ++ ppe_hash_mask = airoha_ppe_get_total_num_entries(ppe) - 1; ++ key_hash = FIELD_GET(AIROHA_PPE_SOE_MARK_HASH_MASK, skb->mark); ++ if (key_hash > ppe_hash_mask) ++ goto clear_mark; ++ ++ meta = &ppe->soe_meta[key_hash]; ++ if (!READ_ONCE(meta->valid)) ++ goto clear_mark; ++ ++ if (READ_ONCE(meta->key_hash) != key_hash) ++ goto clear_mark; ++ ++ expires = READ_ONCE(meta->expires); ++ if (time_after(jiffies, expires)) { ++ WRITE_ONCE(meta->valid, 0); ++ goto clear_mark; ++ } ++ ++ hash = READ_ONCE(meta->foe_hash); ++ if (hash > ppe_hash_mask) { ++ WRITE_ONCE(meta->valid, 0); ++ goto clear_mark; ++ } ++ ++ seen = READ_ONCE(meta->seen); ++ /* Wait for two CPU-visible packets so the normal egress path is proven ++ * before the decrypt FOE entry is bound to hardware. ++ */ ++ if (seen <= 2) ++ goto clear_mark; ++ ++ err = airoha_ppe_soe_fill_inner_ipv4_data(skb, &data, &type, &l4proto); ++ if (err) ++ goto clear_mark; ++ ++ sa_index = READ_ONCE(meta->sa_index); ++ hop = READ_ONCE(meta->hop); ++ WRITE_ONCE(meta->valid, 0); ++ ++ spin_lock_bh(&ppe_lock); ++ ++ hwe = airoha_ppe_foe_get_entry_locked(ppe, hash); ++ if (!hwe) ++ goto unlock; ++ ++ switch (FIELD_GET(AIROHA_FOE_IB1_BIND_PACKET_TYPE, hwe->ib1)) { ++ case PPE_PKT_TYPE_IPV4_HNAPT: ++ case PPE_PKT_TYPE_IPV4_ROUTE: ++ break; ++ default: ++ goto unlock; ++ } ++ ++ err = airoha_ppe_foe_entry_prepare(ppe->eth, &tmpl, netdev, type, ++ &data, l4proto, 0); ++ if (err) ++ goto unlock; ++ ++ memcpy(&entry, hwe, sizeof(entry)); ++ entry.ib1 &= ~(AIROHA_FOE_IB1_BIND_STATE | ++ AIROHA_FOE_IB1_BIND_KEEPALIVE | ++ AIROHA_FOE_IB1_BIND_TIMESTAMP); ++ entry.ib1 |= FIELD_PREP(AIROHA_FOE_IB1_BIND_STATE, ++ AIROHA_FOE_STATE_BIND) | ++ AIROHA_FOE_IB1_BIND_TTL; ++ entry.ib1 = (entry.ib1 & (AIROHA_FOE_IB1_BIND_PACKET_TYPE | ++ AIROHA_FOE_IB1_BIND_UDP)) | ++ (tmpl.ib1 & ~(AIROHA_FOE_IB1_BIND_PACKET_TYPE | ++ AIROHA_FOE_IB1_BIND_UDP)); ++ entry.ipv4.ib2 = tmpl.ipv4.ib2; ++ entry.ipv4.data = tmpl.ipv4.data; ++ memcpy(&entry.ipv4.l2, &tmpl.ipv4.l2, sizeof(entry.ipv4.l2)); ++ ++ /* The delayed decrypt commit runs from the normal xmit path, so a ++ * bond can pass the currently selected lower device here. The SOE ++ * return rule still has to describe the GDM4-facing recirculation ++ * descriptor; keeping a GDM2 slave descriptor creates a BND entry that ++ * forwards plaintext normally instead of re-entering the SOE/PPE path. ++ */ ++ soe_return_path = netif_is_lag_port(netdev); ++ if (!soe_return_path && !netif_is_lag_master(netdev)) { ++ struct airoha_gdm_dev *gdm = netdev_priv(netdev); ++ ++ soe_return_path = airoha_is_valid_gdm_dev(ppe->eth, gdm) && ++ gdm->port && gdm->port->id == AIROHA_GDM4_IDX; ++ } ++ if (soe_return_path) { ++ entry.ipv4.l2.common.etype = AIROHA_PPE_SOE_MAGIC_GDM4; ++ entry.ipv4.data &= ~AIROHA_FOE_CHANNEL; ++ entry.ipv4.ib2 &= ~AIROHA_FOE_IB2_PSE_PORT; ++ entry.ipv4.ib2 |= FIELD_PREP(AIROHA_FOE_IB2_PSE_PORT, ++ FE_PSE_PORT_GDM4) | ++ AIROHA_FOE_IB2_FAST_PATH; ++ } ++ ++ if (FIELD_GET(AIROHA_FOE_IB1_BIND_PACKET_TYPE, entry.ib1) == ++ PPE_PKT_TYPE_IPV4_HNAPT) ++ memcpy(&entry.ipv4.new_tuple, &entry.ipv4.orig_tuple, ++ sizeof(entry.ipv4.new_tuple)); ++ ++ /* Commit the original decrypt entry only after the normal transmit path ++ * has provided the final plaintext egress descriptor. Binding it at SOE ++ * RX completion would miss this device-specific L2/PSE state. ++ */ ++ err = airoha_ppe_foe_entry_set_soe_fields(&entry, sa_index, hop, ++ AIROHA_PPE_SOE_DEFAULT_TUNNEL_MTU); ++ if (!err) ++ airoha_ppe_foe_commit_entry(ppe, &entry, hash, false); ++ ++unlock: ++ spin_unlock_bh(&ppe_lock); ++clear_mark: ++ skb->mark &= ~(AIROHA_PPE_SOE_MARK_MAGIC_MASK | ++ AIROHA_PPE_SOE_MARK_HASH_MASK); ++} ++ ++void airoha_ppe_soe_flush_sa(struct airoha_ppe *ppe, u8 sa_index) ++{ ++ u32 num_entries, hash; ++ ++ if (!ppe) ++ return; ++ ++ num_entries = airoha_ppe_get_total_num_entries(ppe); ++ ++ spin_lock_bh(&ppe_lock); ++ for (hash = 0; hash < num_entries; hash++) { ++ struct airoha_foe_entry *hwe; ++ u32 state, type; ++ ++ hwe = airoha_ppe_foe_get_entry_locked(ppe, hash); ++ if (!hwe) ++ continue; ++ ++ state = FIELD_GET(AIROHA_FOE_IB1_BIND_STATE, hwe->ib1); ++ if (state != AIROHA_FOE_STATE_BIND) ++ continue; ++ ++ type = FIELD_GET(AIROHA_FOE_IB1_BIND_PACKET_TYPE, hwe->ib1); ++ if (type != PPE_PKT_TYPE_IPV4_HNAPT && ++ type != PPE_PKT_TYPE_IPV4_ROUTE) ++ continue; ++ ++ if (!(hwe->ipv4.data & AIROHA_FOE_TUNNEL)) ++ continue; ++ ++ if (FIELD_GET(AIROHA_FOE_ACTDP, hwe->ipv4.data) != sa_index) ++ continue; ++ ++ /* NAT-T data and IKE control both use UDP/4500. A stale SOE ++ * bound entry can otherwise keep sending later IKE_AUTH packets ++ * to the SOE path after the SA has been deleted. ++ */ ++ hwe->ib1 &= ~AIROHA_FOE_IB1_BIND_STATE; ++ hwe->ib1 |= FIELD_PREP(AIROHA_FOE_IB1_BIND_STATE, ++ AIROHA_FOE_STATE_INVALID); ++ airoha_ppe_foe_commit_entry(ppe, hwe, hash, false); ++ } ++ spin_unlock_bh(&ppe_lock); ++} ++ + static void airoha_ppe_foe_insert_entry(struct airoha_ppe *ppe, + struct sk_buff *skb, + u32 hash, bool rx_wlan) +@@ -1064,7 +1624,9 @@ + struct airoha_flow_table_entry *e) + { + int type = FIELD_GET(AIROHA_FOE_IB1_BIND_PACKET_TYPE, e->data.ib1); ++ bool soe_tunnel; + u32 hash; ++ int err; + + if (type == PPE_PKT_TYPE_BRIDGE) + return airoha_ppe_foe_l2_flow_commit_entry(ppe, e); +@@ -1073,7 +1635,25 @@ + e->type = FLOW_TYPE_L4; + e->hash = 0xffff; + ++ soe_tunnel = type <= PPE_PKT_TYPE_IPV4_DSLITE && ++ (e->data.ipv4.data & AIROHA_FOE_TUNNEL) && ++ e->data.ipv4.l2.common.etype == ++ AIROHA_PPE_SOE_MAGIC_IPSEC; ++ + spin_lock_bh(&ppe_lock); ++ if (soe_tunnel) { ++ u32 commit_hash = airoha_ppe_foe_commit_hash(ppe, &e->data, ++ hash); ++ ++ err = airoha_ppe_foe_commit_entry(ppe, &e->data, commit_hash, ++ false); ++ if (err) { ++ spin_unlock_bh(&ppe_lock); ++ return err; ++ } ++ ++ e->hash = commit_hash; ++ } + hlist_add_head(&e->list, &ppe->foe_flow[hash]); + spin_unlock_bh(&ppe_lock); + +@@ -1177,6 +1757,7 @@ + static int airoha_ppe_flow_offload_replace(struct airoha_eth *eth, + struct flow_cls_offload *f) + { ++ const struct flow_offload_tuple *tuple = (const void *)f->cookie; + struct flow_rule *rule = flow_cls_offload_flow_rule(f); + struct airoha_flow_table_entry *e; + struct airoha_flow_data data = {}; +@@ -1310,7 +1891,9 @@ + flow_rule_match_ipv4_addrs(rule, &addrs); + data.v4.src_addr = addrs.key->src; + data.v4.dst_addr = addrs.key->dst; +- airoha_ppe_foe_entry_set_ipv4_tuple(&hwe, &data, false); ++ err = airoha_ppe_foe_entry_set_ipv4_tuple(&hwe, &data, false); ++ if (err) ++ return err; + } + + if (addr_type == FLOW_DISSECTOR_KEY_IPV6_ADDRS) { +@@ -1355,6 +1938,10 @@ + return err; + } + ++ err = airoha_ppe_foe_entry_set_soe_info(&hwe, tuple, odev); ++ if (err) ++ return err; ++ + e = kzalloc(sizeof(*e), GFP_KERNEL); + if (!e) + return -ENOMEM; +@@ -1480,17 +2067,55 @@ + static int airoha_ppe_flush_sram_entries(struct airoha_ppe *ppe, + struct airoha_npu *npu) + { ++ u32 ppe_num_entries = airoha_ppe_get_total_num_entries(ppe); + u32 sram_num_entries = airoha_ppe_get_total_sram_num_entries(ppe); + struct airoha_foe_entry *hwe = ppe->foe; +- int i; + +- for (i = 0; i < PPE_SRAM_NUM_ENTRIES; i++) +- memset(&hwe[i], 0, sizeof(*hwe)); ++ memset(hwe, 0, ppe_num_entries * sizeof(*hwe)); ++ if (ppe->foe_stats) { ++ u32 ppe_num_stats_entries = ++ airoha_ppe_get_total_num_stats_entries(ppe); ++ ++ memset(ppe->foe_stats, 0, ++ ppe_num_stats_entries * sizeof(*ppe->foe_stats)); ++ } ++ dma_wmb(); + + return npu->ops.ppe_flush_sram_entries(npu, ppe->foe_dma, + sram_num_entries); + } + ++static int airoha_ppe_set_bind_rate(const char *val, ++ const struct kernel_param *kp) ++{ ++ struct airoha_ppe *ppe; ++ unsigned long rate; ++ int err, i; ++ ++ err = kstrtoul(val, 0, &rate); ++ if (err) ++ return err; ++ if (rate > FIELD_MAX(PPE_BIND_RATE_BIND_MASK)) ++ return -ERANGE; ++ ++ WRITE_ONCE(airoha_ppe_bind_rate, (unsigned int)rate); ++ ++ mutex_lock(&flow_offload_mutex); ++ ppe = READ_ONCE(airoha_ppe_active); ++ if (ppe) { ++ for (i = 0; i < ppe->eth->soc->num_ppe; i++) ++ airoha_ppe_apply_bind_rate(ppe->eth, i); ++ } ++ mutex_unlock(&flow_offload_mutex); ++ ++ return 0; ++} ++ ++static int airoha_ppe_get_bind_rate(char *buf, const struct kernel_param *kp) ++{ ++ return sysfs_emit(buf, "%u\n", READ_ONCE(airoha_ppe_bind_rate)); ++} ++ + static struct airoha_npu *airoha_ppe_npu_get(struct airoha_eth *eth) + { + struct airoha_npu *npu = airoha_npu_get(eth->dev); +@@ -1734,6 +2359,12 @@ + if (!ppe->foe_check_time) + return -ENOMEM; + ++ ppe->soe_meta = devm_kzalloc(eth->dev, ++ ppe_num_entries * sizeof(*ppe->soe_meta), ++ GFP_KERNEL); ++ if (!ppe->soe_meta) ++ return -ENOMEM; ++ + err = rhashtable_init(ð->flow_table, &airoha_flow_table_params); + if (err) + return err; +@@ -1746,6 +2377,8 @@ + if (err) + goto error_l2_flow_table_destroy; + ++ WRITE_ONCE(airoha_ppe_active, ppe); ++ + return 0; + + error_l2_flow_table_destroy: +@@ -1760,6 +2393,8 @@ + { + struct airoha_npu *npu; + ++ WRITE_ONCE(airoha_ppe_active, NULL); ++ + mutex_lock(&flow_offload_mutex); + + npu = rcu_replace_pointer(eth->npu, NULL, diff --git a/target/linux/airoha/patches-6.18/9999-09-net-airoha-build-soe-provider.patch b/target/linux/airoha/patches-6.18/9999-09-net-airoha-build-soe-provider.patch new file mode 100644 index 00000000000000..c8c49d1d52d131 --- /dev/null +++ b/target/linux/airoha/patches-6.18/9999-09-net-airoha-build-soe-provider.patch @@ -0,0 +1,22 @@ +From: Jihong Min +Date: Mon, 1 Jun 2026 13:30:00 +0900 +Subject: [PATCH] net: airoha: build SOE provider + +Link the SOE provider into the existing airoha Ethernet module when +NET_AIROHA_SOE is enabled. Keeping this as the last airoha-local step +avoids wiring the build before the provider source exists in the patch +series. + +Assisted-by: Codex:gpt-5.5 +Signed-off-by: Jihong Min +--- +diff --git a/drivers/net/ethernet/airoha/Makefile b/drivers/net/ethernet/airoha/Makefile +index 9446805..b68b8f6 100644 +--- a/drivers/net/ethernet/airoha/Makefile ++++ b/drivers/net/ethernet/airoha/Makefile +@@ -6,4 +6,5 @@ + obj-$(CONFIG_NET_AIROHA) += airoha-eth.o + airoha-eth-y := airoha_eth.o airoha_ppe.o + airoha-eth-$(CONFIG_DEBUG_FS) += airoha_ppe_debugfs.o ++airoha-eth-$(CONFIG_NET_AIROHA_SOE) += airoha_soe.o + obj-$(CONFIG_NET_AIROHA_NPU) += airoha_npu.o diff --git a/target/linux/airoha/patches-6.18/9999-10-netfilter-flowtable-support-xfrm-forward-path.patch b/target/linux/airoha/patches-6.18/9999-10-netfilter-flowtable-support-xfrm-forward-path.patch new file mode 100644 index 00000000000000..c04e38acdd4b58 --- /dev/null +++ b/target/linux/airoha/patches-6.18/9999-10-netfilter-flowtable-support-xfrm-forward-path.patch @@ -0,0 +1,141 @@ +From: Jihong Min +Date: Mon, 1 Jun 2026 13:30:00 +0900 +Subject: [PATCH] netfilter: flowtable: support XFRM forward paths + +Allow flowtable offload setup to build hardware rules whose transmit +path is an XFRM packet-offload state rather than a plain neighbor +transmit. This provides the rule allocation and redirect support used by +SOE-backed ESP flows. + +Assisted-by: Codex:gpt-5.5 +Signed-off-by: Jihong Min +--- +diff --git a/net/netfilter/nf_flow_table_offload.c b/net/netfilter/nf_flow_table_offload.c +index d4d49ae..a9f0c9d 100644 +--- a/net/netfilter/nf_flow_table_offload.c ++++ b/net/netfilter/nf_flow_table_offload.c +@@ -12,6 +12,9 @@ + #include + #include + #include ++#include ++#include ++#include + + #define NF_FLOW_RULE_ACTION_MAX 24 + +@@ -254,6 +257,7 @@ static int flow_offload_eth_src(struct net *net, + case FLOW_OFFLOAD_XMIT_DIRECT: + addr = this_tuple->out.h_source; + break; ++ case FLOW_OFFLOAD_XMIT_XFRM: + case FLOW_OFFLOAD_XMIT_NEIGH: + other_tuple = &flow->tuplehash[!dir].tuple; + dev = dev_get_by_index(net, other_tuple->iifidx); +@@ -307,6 +311,42 @@ static int flow_offload_eth_dst(struct net *net, + case FLOW_OFFLOAD_XMIT_DIRECT: + ether_addr_copy(ha, this_tuple->out.h_dest); + break; ++ case FLOW_OFFLOAD_XMIT_XFRM: { ++ struct xfrm_state *x = dst_xfrm(this_tuple->dst_cache); ++ __be32 nexthop; ++ ++ if (!x) ++ return -EOPNOTSUPP; ++ ++ dst_cache = xfrm_dst_path(this_tuple->dst_cache); ++ switch (x->props.family) { ++ case AF_INET: ++ nexthop = rt_nexthop(dst_rtable(dst_cache), ++ x->id.daddr.a4); ++ daddr = &nexthop; ++ break; ++ case AF_INET6: ++ daddr = rt6_nexthop(dst_rt6_info(dst_cache), ++ &x->id.daddr.in6); ++ break; ++ default: ++ return -EOPNOTSUPP; ++ } ++ ++ n = dst_neigh_lookup(dst_cache, daddr); ++ if (!n) ++ return -ENOENT; ++ ++ read_lock_bh(&n->lock); ++ nud_state = n->nud_state; ++ ether_addr_copy(ha, n->ha); ++ read_unlock_bh(&n->lock); ++ neigh_release(n); ++ ++ if (!(nud_state & NUD_VALID)) ++ return -ENOENT; ++ break; ++ } + case FLOW_OFFLOAD_XMIT_NEIGH: + other_tuple = &flow->tuplehash[!dir].tuple; + daddr = &other_tuple->src_v4; +@@ -601,6 +641,7 @@ static int flow_offload_redirect(struct net *net, + this_tuple = &flow->tuplehash[dir].tuple; + ifindex = this_tuple->out.hw_ifidx; + break; ++ case FLOW_OFFLOAD_XMIT_XFRM: + case FLOW_OFFLOAD_XMIT_NEIGH: + other_tuple = &flow->tuplehash[!dir].tuple; + ifindex = other_tuple->iifidx; +@@ -849,6 +890,9 @@ static void __nf_flow_offload_destroy(struct nf_flow_rule *flow_rule) + struct flow_action_entry *entry; + int i; + ++ if (!flow_rule) ++ return; ++ + for (i = 0; i < flow_rule->rule->action.num_entries; i++) { + entry = &flow_rule->rule->action.entries[i]; + if (entry->id != FLOW_ACTION_REDIRECT) +@@ -872,18 +916,19 @@ static int nf_flow_offload_alloc(const struct flow_offload_work *offload, + struct nf_flow_rule *flow_rule[]) + { + struct net *net = read_pnet(&offload->flowtable->net); ++ int ok_count = 0; ++ int dir; + +- flow_rule[0] = nf_flow_offload_rule_alloc(net, offload, +- FLOW_OFFLOAD_DIR_ORIGINAL); +- if (!flow_rule[0]) +- return -ENOMEM; ++ for (dir = 0; dir < FLOW_OFFLOAD_DIR_MAX; dir++) { ++ flow_rule[dir] = nf_flow_offload_rule_alloc(net, offload, dir); ++ if (flow_rule[dir]) { ++ ok_count++; ++ continue; ++ } ++ } + +- flow_rule[1] = nf_flow_offload_rule_alloc(net, offload, +- FLOW_OFFLOAD_DIR_REPLY); +- if (!flow_rule[1]) { +- __nf_flow_offload_destroy(flow_rule[0]); ++ if (!ok_count) + return -ENOMEM; +- } + + return 0; + } +@@ -962,10 +1007,14 @@ static int flow_offload_rule_add(struct flow_offload_work *offload, + { + int ok_count = 0; + +- ok_count += flow_offload_tuple_add(offload, flow_rule[0], ++ if (flow_rule[FLOW_OFFLOAD_DIR_ORIGINAL]) ++ ok_count += flow_offload_tuple_add(offload, ++ flow_rule[FLOW_OFFLOAD_DIR_ORIGINAL], + FLOW_OFFLOAD_DIR_ORIGINAL); + if (test_bit(NF_FLOW_HW_BIDIRECTIONAL, &offload->flow->flags)) +- ok_count += flow_offload_tuple_add(offload, flow_rule[1], ++ if (flow_rule[FLOW_OFFLOAD_DIR_REPLY]) ++ ok_count += flow_offload_tuple_add(offload, ++ flow_rule[FLOW_OFFLOAD_DIR_REPLY], + FLOW_OFFLOAD_DIR_REPLY); + if (ok_count == 0) + return -ENOENT; diff --git a/target/linux/airoha/patches-6.18/9999-11-netfilter-flowtable-bind-xfrm-soe-flows.patch b/target/linux/airoha/patches-6.18/9999-11-netfilter-flowtable-bind-xfrm-soe-flows.patch new file mode 100644 index 00000000000000..ab758d59d794bf --- /dev/null +++ b/target/linux/airoha/patches-6.18/9999-11-netfilter-flowtable-bind-xfrm-soe-flows.patch @@ -0,0 +1,285 @@ +From: Jihong Min +Date: Mon, 1 Jun 2026 13:30:00 +0900 +Subject: [PATCH] netfilter: flowtable: bind XFRM SOE flows + +Teach nft flow offload routing to preserve XFRM packet-offload +destinations and to reclassify SOE-decrypted packets whose secpath +remains attached. This lets the Airoha PPE bind flows that pass through +SOE ESP encapsulation or decapsulation. + +Assisted-by: Codex:gpt-5.5 +Signed-off-by: Jihong Min +--- +diff --git a/net/netfilter/nf_flow_table_ip.c b/net/netfilter/nf_flow_table_ip.c +index 0b7bdfd..731545a 100644 +--- a/net/netfilter/nf_flow_table_ip.c ++++ b/net/netfilter/nf_flow_table_ip.c +@@ -19,8 +19,9 @@ + #include + #include + +-static int nf_flow_state_check(struct flow_offload *flow, int proto, +- struct sk_buff *skb, unsigned int thoff) ++static int nf_flow_state_check(struct nf_flowtable *flow_table, ++ struct flow_offload *flow, int proto, ++ struct sk_buff *skb, unsigned int thoff) + { + struct tcphdr *tcph; + +@@ -34,9 +35,18 @@ static int nf_flow_state_check(struct flow_offload *flow, int proto, + } + + if ((tcph->fin || tcph->rst) && +- !test_bit(NF_FLOW_CLOSING, &flow->flags)) ++ !test_bit(NF_FLOW_CLOSING, &flow->flags)) { + set_bit(NF_FLOW_CLOSING, &flow->flags); ++ flow_offload_teardown(flow); + ++ /* Hardware XFRM/SOE flows can otherwise keep forwarding the ++ * tail of short-lived TCP sessions until the next GC pass. ++ */ ++ if (test_bit(NF_FLOW_HW, &flow->flags) && ++ !test_bit(NF_FLOW_HW_DYING, &flow->flags)) ++ nf_flow_offload_del(flow_table, flow); ++ } ++ + return 0; + } + +@@ -387,7 +397,7 @@ static int nf_flow_offload_forward(struct nf_flowtable_ctx *ctx, + + iph = (struct iphdr *)(skb_network_header(skb) + ctx->offset); + thoff = (iph->ihl * 4) + ctx->offset; +- if (nf_flow_state_check(flow, iph->protocol, skb, thoff)) ++ if (nf_flow_state_check(flow_table, flow, iph->protocol, skb, thoff)) + return 0; + + if (!nf_flow_dst_check(&tuplehash->tuple)) { +@@ -673,7 +683,7 @@ static int nf_flow_offload_ipv6_forward(struct nf_flowtable_ctx *ctx, + + ip6h = (struct ipv6hdr *)(skb_network_header(skb) + ctx->offset); + thoff = sizeof(*ip6h) + ctx->offset; +- if (nf_flow_state_check(flow, ip6h->nexthdr, skb, thoff)) ++ if (nf_flow_state_check(flow_table, flow, ip6h->nexthdr, skb, thoff)) + return 0; + + if (!nf_flow_dst_check(&tuplehash->tuple)) { +diff --git a/net/netfilter/nft_flow_offload.c b/net/netfilter/nft_flow_offload.c +--- a/net/netfilter/nft_flow_offload.c ++++ b/net/netfilter/nft_flow_offload.c +@@ -12,6 +12,7 @@ + #include + #include + #include ++#include + #include + #include + #include +@@ -31,6 +32,21 @@ + return FLOW_OFFLOAD_XMIT_NEIGH; + } + ++static bool nft_flow_offload_xfrm_done(struct sk_buff *skb) ++{ ++ struct xfrm_offload *xo; ++ struct sec_path *sp; ++ ++ /* SOE returns decrypted packets with secpath still attached. */ ++ sp = skb_sec_path(skb); ++ if (!sp) ++ return false; ++ ++ xo = xfrm_offload(skb); ++ ++ return xo && (xo->flags & CRYPTO_DONE) && (xo->status & CRYPTO_SUCCESS); ++} ++ + static void nft_default_forward_path(struct nf_flow_route *route, + struct dst_entry *dst_cache, + enum ip_conntrack_dir dir) +@@ -382,7 +398,8 @@ + memcpy(route->tuple[dir].out.h_dest, info.h_dest, ETH_ALEN); + route->tuple[dir].out.ifindex = info.outdev->ifindex; + route->tuple[dir].out.hw_ifindex = info.hw_outdev->ifindex; +- route->tuple[dir].xmit_type = info.xmit_type; ++ if (route->tuple[dir].xmit_type == FLOW_OFFLOAD_XMIT_NEIGH) ++ route->tuple[dir].xmit_type = info.xmit_type; + } + + return 0; +@@ -396,6 +413,9 @@ + { + struct dst_entry *this_dst = skb_dst(pkt->skb); + struct dst_entry *other_dst = NULL; ++ struct dst_entry *relookup_dst = NULL; ++ bool this_dst_held = false; ++ bool this_dst_relooked = false; + struct flowi fl; + + memset(&fl, 0, sizeof(fl)); +@@ -420,7 +440,57 @@ + break; + } + +- if (!dst_hold_safe(this_dst)) ++ if (this_dst && dst_xfrm(this_dst)) { ++ switch (nft_pf(pkt)) { ++ case NFPROTO_IPV4: ++ fl.u.ip4.flowi4_oif = 0; ++ break; ++ case NFPROTO_IPV6: ++ fl.u.ip6.flowi6_oif = 0; ++ break; ++ } ++ } ++ ++ if (nft_flow_offload_xfrm_done(pkt->skb)) { ++ struct flowi cur_fl; ++ ++ memset(&cur_fl, 0, sizeof(cur_fl)); ++ switch (nft_pf(pkt)) { ++ case NFPROTO_IPV4: ++ cur_fl.u.ip4.daddr = ct->tuplehash[dir].tuple.dst.u3.ip; ++ cur_fl.u.ip4.saddr = ct->tuplehash[dir].tuple.src.u3.ip; ++ cur_fl.u.ip4.flowi4_iif = nft_in(pkt)->ifindex; ++ cur_fl.u.ip4.flowi4_dscp = ip4h_dscp(ip_hdr(pkt->skb)); ++ cur_fl.u.ip4.flowi4_mark = pkt->skb->mark; ++ cur_fl.u.ip4.flowi4_flags = FLOWI_FLAG_ANYSRC; ++ break; ++ case NFPROTO_IPV6: ++ cur_fl.u.ip6.daddr = ++ ct->tuplehash[dir].tuple.dst.u3.in6; ++ cur_fl.u.ip6.saddr = ++ ct->tuplehash[dir].tuple.src.u3.in6; ++ cur_fl.u.ip6.flowi6_iif = nft_in(pkt)->ifindex; ++ cur_fl.u.ip6.flowlabel = ++ ip6_flowinfo(ipv6_hdr(pkt->skb)); ++ cur_fl.u.ip6.flowi6_mark = pkt->skb->mark; ++ cur_fl.u.ip6.flowi6_flags = FLOWI_FLAG_ANYSRC; ++ break; ++ } ++ ++ nf_route(nft_net(pkt), &relookup_dst, &cur_fl, false, ++ nft_pf(pkt)); ++ if (relookup_dst) { ++ this_dst = relookup_dst; ++ this_dst_relooked = true; ++ } ++ } ++ ++ if (this_dst_relooked) ++ this_dst_held = true; ++ else if (dst_hold_safe(this_dst)) ++ this_dst_held = true; ++ ++ if (!this_dst_held) + return -ENOENT; + + nf_route(nft_net(pkt), &other_dst, &fl, false, nft_pf(pkt)); +@@ -428,18 +498,32 @@ + dst_release(this_dst); + return -ENOENT; + } ++ other_dst = xfrm_lookup(nft_net(pkt), other_dst, &fl, NULL, 0); ++ if (IS_ERR(other_dst)) { ++ dst_release(this_dst); ++ return -ENOENT; ++ } + + nft_default_forward_path(route, this_dst, dir); + nft_default_forward_path(route, other_dst, !dir); + +- if (route->tuple[dir].xmit_type == FLOW_OFFLOAD_XMIT_NEIGH && +- route->tuple[!dir].xmit_type == FLOW_OFFLOAD_XMIT_NEIGH) { +- if (nft_dev_forward_path(pkt, route, ct, dir, ft) || +- nft_dev_forward_path(pkt, route, ct, !dir, ft)) +- return -ENOENT; ++ if (route->tuple[dir].xmit_type == FLOW_OFFLOAD_XMIT_NEIGH || ++ route->tuple[dir].xmit_type == FLOW_OFFLOAD_XMIT_XFRM) { ++ if (nft_dev_forward_path(pkt, route, ct, dir, ft)) ++ goto err_release; ++ } ++ if (route->tuple[!dir].xmit_type == FLOW_OFFLOAD_XMIT_NEIGH || ++ route->tuple[!dir].xmit_type == FLOW_OFFLOAD_XMIT_XFRM) { ++ if (nft_dev_forward_path(pkt, route, ct, !dir, ft)) ++ goto err_release; + } + + return 0; ++ ++err_release: ++ dst_release(route->tuple[dir].dst); ++ dst_release(route->tuple[!dir].dst); ++ return -ENOENT; + } + + static int +@@ -540,9 +624,24 @@ + + static bool nft_flow_offload_skip(struct sk_buff *skb, int family) + { +- if (skb_sec_path(skb)) +- return true; ++ struct xfrm_offload *xo; ++ struct sec_path *sp; + ++ sp = skb_sec_path(skb); ++ if (sp) { ++ struct xfrm_state *x = sp->len ? sp->xvec[sp->len - 1] : NULL; ++ ++ xo = xfrm_offload(skb); ++ if (xo && (xo->flags & CRYPTO_DONE) && ++ (xo->status & CRYPTO_SUCCESS)) ++ goto out; ++ ++ if (!x || x->xso.type != XFRM_DEV_OFFLOAD_PACKET || ++ !x->xso.dev || !(x->xso.dev->features & NETIF_F_HW_ESP)) ++ return true; ++ } ++ ++out: + if (family == NFPROTO_IPV4) { + const struct ip_options *opt; + +@@ -574,6 +673,7 @@ + struct nf_flow_route route = {}; + enum ip_conntrack_info ctinfo; + struct flow_offload *flow; ++ bool tcp_not_established = false; + enum ip_conntrack_dir dir; + struct nf_conn *ct; + int ret; +@@ -589,9 +689,14 @@ + case IPPROTO_TCP: + tcph = skb_header_pointer(pkt->skb, nft_thoff(pkt), + sizeof(_tcph), &_tcph); +- if (unlikely(!tcph || tcph->fin || tcph->rst || +- !nf_conntrack_tcp_established(ct))) ++ if (unlikely(!tcph)) ++ goto out; ++ if (unlikely(tcph->fin || tcph->rst)) + goto out; ++ if (unlikely(!nf_conntrack_tcp_established(ct))) { ++ tcp_not_established = true; ++ break; ++ } + break; + case IPPROTO_UDP: + break; +@@ -631,6 +736,16 @@ + goto err_flow_route; + } + ++ /* XFRM packet offload can hide part of the plaintext TCP handshake from ++ * conntrack. Once routing proves that either direction is an XFRM ++ * packet-offload path, let the hardware rule be programmed; non-XFRM ++ * traffic stays on the normal established-only path. ++ */ ++ if (tcp_not_established && ++ route.tuple[IP_CT_DIR_ORIGINAL].xmit_type != FLOW_OFFLOAD_XMIT_XFRM && ++ route.tuple[IP_CT_DIR_REPLY].xmit_type != FLOW_OFFLOAD_XMIT_XFRM) ++ goto err_flow_route; ++ + flow = flow_offload_alloc(ct); + if (!flow) + goto err_flow_alloc; diff --git a/target/linux/apm821xx/config-6.18 b/target/linux/apm821xx/config-6.18 index f9f3467f20c0b7..2ba17951567831 100644 --- a/target/linux/apm821xx/config-6.18 +++ b/target/linux/apm821xx/config-6.18 @@ -131,7 +131,9 @@ CONFIG_IRQ_WORK=y CONFIG_ISA_DMA_API=y # CONFIG_JFFS2_FS is not set # CONFIG_KATMAI is not set +CONFIG_KERNEL_GZIP=y CONFIG_KERNEL_START=0xc0000000 +# CONFIG_KERNEL_XZ is not set CONFIG_LEDS_TRIGGER_MTD=y CONFIG_LEDS_TRIGGER_PATTERN=y CONFIG_LIBFDT=y diff --git a/target/linux/apm821xx/patches-6.18/900-powerpc-bootwrapper-force-gzip-as-mkimage-s-compress.patch b/target/linux/apm821xx/patches-6.18/900-powerpc-bootwrapper-force-gzip-as-mkimage-s-compress.patch deleted file mode 100644 index 29c5c2db4626d4..00000000000000 --- a/target/linux/apm821xx/patches-6.18/900-powerpc-bootwrapper-force-gzip-as-mkimage-s-compress.patch +++ /dev/null @@ -1,29 +0,0 @@ -From c9395ad54e2cabb87d408becc37566f3d8248933 Mon Sep 17 00:00:00 2001 -From: Christian Lamparter -Date: Sun, 1 Dec 2019 02:08:23 +0100 -Subject: [PATCH] powerpc: bootwrapper: force gzip as mkimage's compression - method - -Due to CONFIG_KERNEL_XZ symbol, the bootwrapper code tries to -instruct the mkimage to use the xz compression, which isn't -supported. This patch forces the gzip compression, which is -supported and doesn't matter because the generated uImage for -the apm821xx target gets ignored as the OpenWrt toolchain will -do separate U-Boot kernel images for each device individually. - -Signed-off-by: Christian Lamparter ---- - arch/powerpc/boot/Makefile | 2 +- - 1 file changed, 1 insertion(+), 1 deletion(-) - ---- a/arch/powerpc/boot/Makefile -+++ b/arch/powerpc/boot/Makefile -@@ -270,7 +270,7 @@ compressor-$(CONFIG_KERNEL_LZO) := lzo - - # args (to if_changed): 1 = (this rule), 2 = platform, 3 = dts 4=dtb 5=initrd - quiet_cmd_wrap = WRAP $@ -- cmd_wrap =$(CONFIG_SHELL) $(wrapper) -Z $(compressor-y) -c -o $@ -p $2 \ -+ cmd_wrap =$(CONFIG_SHELL) $(wrapper) -Z gzip -c -o $@ -p $2 \ - $(CROSSWRAP) $(if $3, -s $3)$(if $4, -d $4)$(if $5, -i $5) \ - vmlinux - diff --git a/target/linux/generic/backport-6.18/300-v7.2-MIPS-smp-report-dying-CPU-to-RCU-in-stop_this_cpu.patch b/target/linux/generic/backport-6.18/300-v7.2-MIPS-smp-report-dying-CPU-to-RCU-in-stop_this_cpu.patch deleted file mode 100644 index 8f419e2fe46d0c..00000000000000 --- a/target/linux/generic/backport-6.18/300-v7.2-MIPS-smp-report-dying-CPU-to-RCU-in-stop_this_cpu.patch +++ /dev/null @@ -1,66 +0,0 @@ -From 9f3f3bdc6d9dac1a5a8262ee7ad0f2ff1527a7e7 Mon Sep 17 00:00:00 2001 -From: Jonas Jelonek -Date: Mon, 8 Jun 2026 09:37:29 +0000 -Subject: MIPS: smp: report dying CPU to RCU in stop_this_cpu() - -smp_send_stop() parks all secondary CPUs in stop_this_cpu(). The function -marks the CPU offline for the scheduler via set_cpu_online(false) but -never informs RCU, so RCU keeps expecting a quiescent state from CPUs -that are now spinning forever with interrupts disabled. - -As long as nothing waits for an RCU grace period after smp_send_stop() -this is harmless, which is why it went unnoticed. Since commit -91840be8f710 ("irq_work: Fix use-after-free in irq_work_single() on PREEMPT_RT") -however, irq_work_sync() calls synchronize_rcu() on architectures without -an irq_work self-IPI, i.e. where arch_irq_work_has_interrupt() returns -false. That is the asm-generic default used by MIPS. Any irq_work_sync() -issued in the reboot/shutdown path after smp_send_stop() then blocks on -a grace period that can never complete, hanging the reboot: - - WARNING: CPU: 0 PID: 15 at kernel/irq_work.c:144 irq_work_queue_on - ... - rcu: INFO: rcu_sched detected stalls on CPUs/tasks: - rcu: Offline CPU 1 blocking current GP. - rcu: Offline CPU 2 blocking current GP. - rcu: Offline CPU 3 blocking current GP. - -This issue was noticed on several Realtek MIPS switch SoCs (MIPS -interAptiv) and came up during kernel bump downstream in OpenWrt from -6.18.33 to 6.18.34, after the backport of the patch to the 6.18 stable -branch. The patch also has been backported all the way back to 6.1. - -Call rcutree_report_cpu_dead() once interrupts are disabled, mirroring the -generic CPU-hotplug offline path, so RCU stops waiting on the parked CPUs -and grace periods can still complete. MIPS shuts down all CPUs here -without going through the CPU-hotplug mechanism, so this report is not -otherwise issued. Reporting a dying CPU to RCU outside the regular hotplug -offline path is not unprecedented: arm64 does the same in cpu_die_early(). -There it is an exception for a CPU that was coming online and is aborting -bringup, rather than the default shutdown action as on MIPS. - -Fixes: 91840be8f710 ("irq_work: Fix use-after-free in irq_work_single() on PREEMPT_RT") -CC: stable@vger.kernel.org -Signed-off-by: Jonas Jelonek -Signed-off-by: Thomas Bogendoerfer ---- - arch/mips/kernel/smp.c | 2 ++ - 1 file changed, 2 insertions(+) - ---- a/arch/mips/kernel/smp.c -+++ b/arch/mips/kernel/smp.c -@@ -20,6 +20,7 @@ - #include - #include - #include -+#include - #include - #include - #include -@@ -422,6 +423,7 @@ static void stop_this_cpu(void *dummy) - set_cpu_online(smp_processor_id(), false); - calculate_cpu_foreign_map(); - local_irq_disable(); -+ rcutree_report_cpu_dead(); - while (1); - } - diff --git a/target/linux/generic/backport-6.18/9910-v7.0-crypto-inside-secure-eip93-add-missing-address-terminator-character.patch b/target/linux/generic/backport-6.18/9910-v7.0-crypto-inside-secure-eip93-add-missing-address-terminator-character.patch new file mode 100644 index 00000000000000..6ea26483120863 --- /dev/null +++ b/target/linux/generic/backport-6.18/9910-v7.0-crypto-inside-secure-eip93-add-missing-address-terminator-character.patch @@ -0,0 +1,195 @@ +From fdacdc8cf897703a5a3e8b521448befbb6620034 Mon Sep 17 00:00:00 2001 +From: Mieczyslaw Nalewaj +Date: Sat, 21 Mar 2026 11:23:06 +0100 +Subject: [PATCH] crypto: inside-secure/eip93 - add missing address terminator + character + +Add the missing > characters to the end of the email address + +Signed-off-by: Mieczyslaw Nalewaj +Signed-off-by: Herbert Xu +--- + drivers/crypto/inside-secure/eip93/eip93-aead.c | 2 +- + drivers/crypto/inside-secure/eip93/eip93-aead.h | 2 +- + drivers/crypto/inside-secure/eip93/eip93-aes.h | 2 +- + drivers/crypto/inside-secure/eip93/eip93-cipher.c | 2 +- + drivers/crypto/inside-secure/eip93/eip93-cipher.h | 2 +- + drivers/crypto/inside-secure/eip93/eip93-common.c | 2 +- + drivers/crypto/inside-secure/eip93/eip93-common.h | 2 +- + drivers/crypto/inside-secure/eip93/eip93-des.h | 2 +- + drivers/crypto/inside-secure/eip93/eip93-hash.c | 2 +- + drivers/crypto/inside-secure/eip93/eip93-hash.h | 2 +- + drivers/crypto/inside-secure/eip93/eip93-main.c | 2 +- + drivers/crypto/inside-secure/eip93/eip93-main.h | 2 +- + drivers/crypto/inside-secure/eip93/eip93-regs.h | 2 +- + 13 files changed, 13 insertions(+), 13 deletions(-) + +diff --git a/drivers/crypto/inside-secure/eip93/eip93-aead.c b/drivers/crypto/inside-secure/eip93/eip93-aead.c +index 1a08aed5de13..2bbd0af7b0e0 100644 +--- a/drivers/crypto/inside-secure/eip93/eip93-aead.c ++++ b/drivers/crypto/inside-secure/eip93/eip93-aead.c +@@ -3,7 +3,7 @@ + * Copyright (C) 2019 - 2021 + * + * Richard van Schagen +- * Christian Marangi + */ + + #include +diff --git a/drivers/crypto/inside-secure/eip93/eip93-aead.h b/drivers/crypto/inside-secure/eip93/eip93-aead.h +index e2fa8fd39c50..d933a8fbdf04 100644 +--- a/drivers/crypto/inside-secure/eip93/eip93-aead.h ++++ b/drivers/crypto/inside-secure/eip93/eip93-aead.h +@@ -3,7 +3,7 @@ + * Copyright (C) 2019 - 2021 + * + * Richard van Schagen +- * Christian Marangi + */ + #ifndef _EIP93_AEAD_H_ + #define _EIP93_AEAD_H_ +diff --git a/drivers/crypto/inside-secure/eip93/eip93-aes.h b/drivers/crypto/inside-secure/eip93/eip93-aes.h +index 1d83d39cab2a..82064cc8f5c7 100644 +--- a/drivers/crypto/inside-secure/eip93/eip93-aes.h ++++ b/drivers/crypto/inside-secure/eip93/eip93-aes.h +@@ -3,7 +3,7 @@ + * Copyright (C) 2019 - 2021 + * + * Richard van Schagen +- * Christian Marangi + */ + #ifndef _EIP93_AES_H_ + #define _EIP93_AES_H_ +diff --git a/drivers/crypto/inside-secure/eip93/eip93-cipher.c b/drivers/crypto/inside-secure/eip93/eip93-cipher.c +index b91586853c27..4dd7ab7503e8 100644 +--- a/drivers/crypto/inside-secure/eip93/eip93-cipher.c ++++ b/drivers/crypto/inside-secure/eip93/eip93-cipher.c +@@ -3,7 +3,7 @@ + * Copyright (C) 2019 - 2021 + * + * Richard van Schagen +- * Christian Marangi + */ + + #include +diff --git a/drivers/crypto/inside-secure/eip93/eip93-cipher.h b/drivers/crypto/inside-secure/eip93/eip93-cipher.h +index 6e2545ebd879..47e4e84ff14e 100644 +--- a/drivers/crypto/inside-secure/eip93/eip93-cipher.h ++++ b/drivers/crypto/inside-secure/eip93/eip93-cipher.h +@@ -3,7 +3,7 @@ + * Copyright (C) 2019 - 2021 + * + * Richard van Schagen +- * Christian Marangi + */ + #ifndef _EIP93_CIPHER_H_ + #define _EIP93_CIPHER_H_ +diff --git a/drivers/crypto/inside-secure/eip93/eip93-common.c b/drivers/crypto/inside-secure/eip93/eip93-common.c +index f4ad6beff15e..6f147014f996 100644 +--- a/drivers/crypto/inside-secure/eip93/eip93-common.c ++++ b/drivers/crypto/inside-secure/eip93/eip93-common.c +@@ -3,7 +3,7 @@ + * Copyright (C) 2019 - 2021 + * + * Richard van Schagen +- * Christian Marangi + */ + + #include +diff --git a/drivers/crypto/inside-secure/eip93/eip93-common.h b/drivers/crypto/inside-secure/eip93/eip93-common.h +index 80964cfa34df..41c43782eb5c 100644 +--- a/drivers/crypto/inside-secure/eip93/eip93-common.h ++++ b/drivers/crypto/inside-secure/eip93/eip93-common.h +@@ -3,7 +3,7 @@ + * Copyright (C) 2019 - 2021 + * + * Richard van Schagen +- * Christian Marangi + */ + + #ifndef _EIP93_COMMON_H_ +diff --git a/drivers/crypto/inside-secure/eip93/eip93-des.h b/drivers/crypto/inside-secure/eip93/eip93-des.h +index 74748df04acf..53ffe0f341b8 100644 +--- a/drivers/crypto/inside-secure/eip93/eip93-des.h ++++ b/drivers/crypto/inside-secure/eip93/eip93-des.h +@@ -3,7 +3,7 @@ + * Copyright (C) 2019 - 2021 + * + * Richard van Schagen +- * Christian Marangi + */ + #ifndef _EIP93_DES_H_ + #define _EIP93_DES_H_ +diff --git a/drivers/crypto/inside-secure/eip93/eip93-hash.c b/drivers/crypto/inside-secure/eip93/eip93-hash.c +index 2705855475b2..84d3ff2d3836 100644 +--- a/drivers/crypto/inside-secure/eip93/eip93-hash.c ++++ b/drivers/crypto/inside-secure/eip93/eip93-hash.c +@@ -2,7 +2,7 @@ + /* + * Copyright (C) 2024 + * +- * Christian Marangi + */ + + #include +diff --git a/drivers/crypto/inside-secure/eip93/eip93-hash.h b/drivers/crypto/inside-secure/eip93/eip93-hash.h +index 556f22fc1dd0..29da18d78894 100644 +--- a/drivers/crypto/inside-secure/eip93/eip93-hash.h ++++ b/drivers/crypto/inside-secure/eip93/eip93-hash.h +@@ -3,7 +3,7 @@ + * Copyright (C) 2019 - 2021 + * + * Richard van Schagen +- * Christian Marangi + */ + #ifndef _EIP93_HASH_H_ + #define _EIP93_HASH_H_ +diff --git a/drivers/crypto/inside-secure/eip93/eip93-main.c b/drivers/crypto/inside-secure/eip93/eip93-main.c +index 76858bb4fcc2..7dccfdeb7b11 100644 +--- a/drivers/crypto/inside-secure/eip93/eip93-main.c ++++ b/drivers/crypto/inside-secure/eip93/eip93-main.c +@@ -3,7 +3,7 @@ + * Copyright (C) 2019 - 2021 + * + * Richard van Schagen +- * Christian Marangi + */ + + #include +diff --git a/drivers/crypto/inside-secure/eip93/eip93-main.h b/drivers/crypto/inside-secure/eip93/eip93-main.h +index 79b078f0e5da..990c2401b7ce 100644 +--- a/drivers/crypto/inside-secure/eip93/eip93-main.h ++++ b/drivers/crypto/inside-secure/eip93/eip93-main.h +@@ -3,7 +3,7 @@ + * Copyright (C) 2019 - 2021 + * + * Richard van Schagen +- * Christian Marangi + */ + #ifndef _EIP93_MAIN_H_ + #define _EIP93_MAIN_H_ +diff --git a/drivers/crypto/inside-secure/eip93/eip93-regs.h b/drivers/crypto/inside-secure/eip93/eip93-regs.h +index 116b3fbb6ad7..96285ca6fbbe 100644 +--- a/drivers/crypto/inside-secure/eip93/eip93-regs.h ++++ b/drivers/crypto/inside-secure/eip93/eip93-regs.h +@@ -3,7 +3,7 @@ + * Copyright (C) 2019 - 2021 + * + * Richard van Schagen +- * Christian Marangi + */ + #ifndef REG_EIP93_H + #define REG_EIP93_H diff --git a/target/linux/generic/backport-6.18/9911-v7.0-crypto-inside-secure-eip93-correct-ecb-des-eip93-typo.patch b/target/linux/generic/backport-6.18/9911-v7.0-crypto-inside-secure-eip93-correct-ecb-des-eip93-typo.patch new file mode 100644 index 00000000000000..2be9fbc0b68e2d --- /dev/null +++ b/target/linux/generic/backport-6.18/9911-v7.0-crypto-inside-secure-eip93-correct-ecb-des-eip93-typo.patch @@ -0,0 +1,26 @@ +From 9503ab5a1d0ef4cad2731d88699d7e6bb1a8a85d Mon Sep 17 00:00:00 2001 +From: Mieczyslaw Nalewaj +Date: Sat, 21 Mar 2026 10:59:37 +0100 +Subject: [PATCH] crypto: inside-secure/eip93 - correct ecb(des-eip93) typo + +Correct the typo in the name "ecb(des-eip93)". + +Signed-off-by: Mieczyslaw Nalewaj +Signed-off-by: Herbert Xu +--- + drivers/crypto/inside-secure/eip93/eip93-cipher.c | 2 +- + 1 file changed, 1 insertion(+), 1 deletion(-) + +diff --git a/drivers/crypto/inside-secure/eip93/eip93-cipher.c b/drivers/crypto/inside-secure/eip93/eip93-cipher.c +index 0713c71ab458..b91586853c27 100644 +--- a/drivers/crypto/inside-secure/eip93/eip93-cipher.c ++++ b/drivers/crypto/inside-secure/eip93/eip93-cipher.c +@@ -320,7 +320,7 @@ struct eip93_alg_template eip93_alg_ecb_des = { + .ivsize = 0, + .base = { + .cra_name = "ecb(des)", +- .cra_driver_name = "ebc(des-eip93)", ++ .cra_driver_name = "ecb(des-eip93)", + .cra_priority = EIP93_CRA_PRIORITY, + .cra_flags = CRYPTO_ALG_ASYNC | + CRYPTO_ALG_KERN_DRIVER_ONLY, diff --git a/target/linux/generic/backport-6.18/9912-v7.0-crypto-inside-secure-eip93-make-it-selectable-for-ECONET.patch b/target/linux/generic/backport-6.18/9912-v7.0-crypto-inside-secure-eip93-make-it-selectable-for-ECONET.patch new file mode 100644 index 00000000000000..188a45d12f8551 --- /dev/null +++ b/target/linux/generic/backport-6.18/9912-v7.0-crypto-inside-secure-eip93-make-it-selectable-for-ECONET.patch @@ -0,0 +1,32 @@ +From 5c8009f3c1885d5d996acdcd7e884aff25ac26a4 Mon Sep 17 00:00:00 2001 +From: Aleksander Jan Bajkowski +Date: Fri, 20 Mar 2026 22:19:23 +0100 +Subject: [PATCH] crypto: inside-secure/eip93 - make it selectable for ECONET + +Econet SoCs feature an integrated EIP93 in revision 3.0p1. It is identical +to the one used by the Airoha AN7581 and the MediaTek MT7621. Ahmed reports +that the EN7528 passes testmgr's self-tests. This driver should also work +on other little endian Econet SoCs. + +CC: Ahmed Naseef +Signed-off-by: Aleksander Jan Bajkowski +Reviewed-by: Antoine Tenart +Tested-by: Ahmed Naseef +Signed-off-by: Herbert Xu +--- + drivers/crypto/inside-secure/eip93/Kconfig | 2 +- + 1 file changed, 1 insertion(+), 1 deletion(-) + +diff --git a/drivers/crypto/inside-secure/eip93/Kconfig b/drivers/crypto/inside-secure/eip93/Kconfig +index 8353d3d7ec9b..29523f6927dd 100644 +--- a/drivers/crypto/inside-secure/eip93/Kconfig ++++ b/drivers/crypto/inside-secure/eip93/Kconfig +@@ -1,7 +1,7 @@ + # SPDX-License-Identifier: GPL-2.0 + config CRYPTO_DEV_EIP93 + tristate "Support for EIP93 crypto HW accelerators" +- depends on SOC_MT7621 || ARCH_AIROHA ||COMPILE_TEST ++ depends on SOC_MT7621 || ARCH_AIROHA || ECONET || COMPILE_TEST + select CRYPTO_LIB_AES + select CRYPTO_LIB_DES + select CRYPTO_SKCIPHER diff --git a/target/linux/generic/backport-6.18/9913-v7.1-crypto-eip93-fix-reset-ring-register-definition.patch b/target/linux/generic/backport-6.18/9913-v7.1-crypto-eip93-fix-reset-ring-register-definition.patch new file mode 100644 index 00000000000000..93ed8a4e623b5f --- /dev/null +++ b/target/linux/generic/backport-6.18/9913-v7.1-crypto-eip93-fix-reset-ring-register-definition.patch @@ -0,0 +1,29 @@ +From 09e6b79b8ce388993aec9ac91b1cb2c181c27bd9 Mon Sep 17 00:00:00 2001 +From: Aleksander Jan Bajkowski +Date: Sat, 16 May 2026 14:26:51 +0200 +Subject: [PATCH] crypto: eip93 - fix reset ring register definition + +This patch fixes a descriptor ring reset. This causes a hang in the +driver's unload/load sequence. + +Fixes: 9739f5f93b78 ("crypto: eip93 - Add Inside Secure SafeXcel EIP-93 crypto engine support") +Suggested-by: Benjamin Larsson +Signed-off-by: Aleksander Jan Bajkowski +Signed-off-by: Herbert Xu +--- + drivers/crypto/inside-secure/eip93/eip93-regs.h | 2 +- + 1 file changed, 1 insertion(+), 1 deletion(-) + +diff --git a/drivers/crypto/inside-secure/eip93/eip93-regs.h b/drivers/crypto/inside-secure/eip93/eip93-regs.h +index 96285ca6fbbe..96d28c6651bd 100644 +--- a/drivers/crypto/inside-secure/eip93/eip93-regs.h ++++ b/drivers/crypto/inside-secure/eip93/eip93-regs.h +@@ -103,7 +103,7 @@ + #define EIP93_PE_TARGET_COMMAND_NO_RDR_MODE FIELD_PREP(EIP93_PE_CONFIG_PE_MODE, 0x2) + #define EIP93_PE_TARGET_COMMAND_WITH_RDR_MODE FIELD_PREP(EIP93_PE_CONFIG_PE_MODE, 0x1) + #define EIP93_PE_DIRECT_HOST_MODE FIELD_PREP(EIP93_PE_CONFIG_PE_MODE, 0x0) +-#define EIP93_PE_CONFIG_RST_RING BIT(2) ++#define EIP93_PE_CONFIG_RST_RING BIT(1) + #define EIP93_PE_CONFIG_RST_PE BIT(0) + #define EIP93_REG_PE_STATUS 0x104 + #define EIP93_REG_PE_BUF_THRESH 0x10c diff --git a/target/linux/generic/backport-6.18/9914-crypto-eip93-return-IRQ-request-errors-from-probe.patch b/target/linux/generic/backport-6.18/9914-crypto-eip93-return-IRQ-request-errors-from-probe.patch new file mode 100644 index 00000000000000..72082b1cac9119 --- /dev/null +++ b/target/linux/generic/backport-6.18/9914-crypto-eip93-return-IRQ-request-errors-from-probe.patch @@ -0,0 +1,31 @@ +From 040c5833580ba315b8cda268ef5daf2da9856607 Mon Sep 17 00:00:00 2001 +From: Jihong Min +Date: Mon, 25 May 2026 04:06:15 +0900 +Subject: [PATCH] crypto: eip93: return IRQ request errors from probe + +devm_request_threaded_irq() can fail, but eip93_crypto_probe() +continues as if the interrupt handler was installed. Return the error +immediately so the driver does not register algorithms for a device that +cannot signal completions. + +Fixes: 9739f5f93b78 ("crypto: eip93 - Add Inside Secure SafeXcel EIP-93 crypto engine support") +Originally-by: Mieczyslaw Nalewaj +Assisted-by: Codex:gpt-5.5 +Signed-off-by: Jihong Min +--- + drivers/crypto/inside-secure/eip93/eip93-main.c | 2 ++ + 1 file changed, 2 insertions(+) + +diff --git a/drivers/crypto/inside-secure/eip93/eip93-main.c b/drivers/crypto/inside-secure/eip93/eip93-main.c +index 7dccfdeb7b11..276839e1a515 100644 +--- a/drivers/crypto/inside-secure/eip93/eip93-main.c ++++ b/drivers/crypto/inside-secure/eip93/eip93-main.c +@@ -433,6 +433,8 @@ static int eip93_crypto_probe(struct platform_device *pdev) + ret = devm_request_threaded_irq(eip93->dev, eip93->irq, eip93_irq_handler, + NULL, IRQF_ONESHOT, + dev_name(eip93->dev), eip93); ++ if (ret) ++ return ret; + + eip93->ring = devm_kcalloc(eip93->dev, 1, sizeof(*eip93->ring), GFP_KERNEL); + if (!eip93->ring) diff --git a/target/linux/generic/backport-6.18/9915-crypto-eip93-guard-DMA-cleanup-on-uninitialized-mappings.patch b/target/linux/generic/backport-6.18/9915-crypto-eip93-guard-DMA-cleanup-on-uninitialized-mappings.patch new file mode 100644 index 00000000000000..f56d49c1621af0 --- /dev/null +++ b/target/linux/generic/backport-6.18/9915-crypto-eip93-guard-DMA-cleanup-on-uninitialized-mappings.patch @@ -0,0 +1,172 @@ +From 89c0aa7945ed592a1ba8a4f3f14df4e39ef296df Mon Sep 17 00:00:00 2001 +From: Jihong Min +Date: Mon, 25 May 2026 04:10:21 +0900 +Subject: [PATCH] crypto: eip93: guard DMA cleanup on uninitialized mappings + +Several error paths can reach cleanup before all DMA addresses have been +initialized or mapped. Initialize request DMA handles and check them before +cleanup so the driver does not unmap zero or stale addresses. + +If mapping the temporary HMAC SA record fails, also release the block data +DMA mapping that was already active. + +Fixes: 9739f5f93b78 ("crypto: eip93 - Add Inside Secure SafeXcel EIP-93 crypto engine support") +Reported-by: Benjamin Larsson +Originally-by: Mieczyslaw Nalewaj +Suggested-by: Benjamin Larsson +Assisted-by: Codex:gpt-5.5 +Signed-off-by: Jihong Min +--- + .../crypto/inside-secure/eip93/eip93-common.c | 8 ++- + .../crypto/inside-secure/eip93/eip93-hash.c | 54 ++++++++++++------- + 2 files changed, 41 insertions(+), 21 deletions(-) + +diff --git a/drivers/crypto/inside-secure/eip93/eip93-common.c b/drivers/crypto/inside-secure/eip93/eip93-common.c +index 4c163d7281b3..ed46730c36bc 100644 +--- a/drivers/crypto/inside-secure/eip93/eip93-common.c ++++ b/drivers/crypto/inside-secure/eip93/eip93-common.c +@@ -527,6 +527,8 @@ int eip93_send_req(struct crypto_async_request *async, + + rctx->sa_state_ctr = NULL; + rctx->sa_state = NULL; ++ rctx->sa_state_ctr_base = 0; ++ rctx->sa_state_base = 0; + + if (IS_ECB(flags)) + goto skip_iv; +@@ -534,8 +536,10 @@ int eip93_send_req(struct crypto_async_request *async, + memcpy(iv, reqiv, rctx->ivsize); + + rctx->sa_state = kzalloc(sizeof(*rctx->sa_state), GFP_KERNEL); +- if (!rctx->sa_state) +- return -ENOMEM; ++ if (!rctx->sa_state) { ++ err = -ENOMEM; ++ goto free_sa_state; ++ } + + sa_state = rctx->sa_state; + +diff --git a/drivers/crypto/inside-secure/eip93/eip93-hash.c b/drivers/crypto/inside-secure/eip93/eip93-hash.c +index 84d3ff2d3836..63bb6c4670cb 100644 +--- a/drivers/crypto/inside-secure/eip93/eip93-hash.c ++++ b/drivers/crypto/inside-secure/eip93/eip93-hash.c +@@ -34,7 +34,7 @@ static void eip93_hash_free_data_blocks(struct ahash_request *req) + if (!list_empty(&rctx->blocks)) + INIT_LIST_HEAD(&rctx->blocks); + +- if (rctx->finalize) ++ if (rctx->finalize && rctx->data_dma) + dma_unmap_single(eip93->dev, rctx->data_dma, + rctx->data_used, + DMA_TO_DEVICE); +@@ -47,12 +47,13 @@ static void eip93_hash_free_sa_record(struct ahash_request *req) + struct eip93_hash_ctx *ctx = crypto_ahash_ctx(ahash); + struct eip93_device *eip93 = ctx->eip93; + +- if (IS_HMAC(ctx->flags)) ++ if (IS_HMAC(ctx->flags) && rctx->sa_record_hmac_base) + dma_unmap_single(eip93->dev, rctx->sa_record_hmac_base, + sizeof(rctx->sa_record_hmac), DMA_TO_DEVICE); + +- dma_unmap_single(eip93->dev, rctx->sa_record_base, +- sizeof(rctx->sa_record), DMA_TO_DEVICE); ++ if (rctx->sa_record_base) ++ dma_unmap_single(eip93->dev, rctx->sa_record_base, ++ sizeof(rctx->sa_record), DMA_TO_DEVICE); + } + + void eip93_hash_handle_result(struct crypto_async_request *async, int err) +@@ -66,8 +67,9 @@ void eip93_hash_handle_result(struct crypto_async_request *async, int err) + struct eip93_device *eip93 = ctx->eip93; + int i; + +- dma_unmap_single(eip93->dev, rctx->sa_state_base, +- sizeof(*sa_state), DMA_FROM_DEVICE); ++ if (rctx->sa_state_base) ++ dma_unmap_single(eip93->dev, rctx->sa_state_base, ++ sizeof(*sa_state), DMA_FROM_DEVICE); + + /* + * With partial_hash assume SHA256_DIGEST_SIZE buffer is passed. +@@ -200,6 +202,10 @@ static void __eip93_hash_init(struct ahash_request *req) + + rctx->len = 0; + rctx->data_used = 0; ++ rctx->sa_record_base = 0; ++ rctx->sa_state_base = 0; ++ rctx->sa_record_hmac_base = 0; ++ rctx->data_dma = 0; + rctx->partial_hash = false; + rctx->finalize = false; + INIT_LIST_HEAD(&rctx->blocks); +@@ -250,8 +256,12 @@ static int eip93_send_hash_req(struct crypto_async_request *async, u8 *data, + sizeof(*sa_record_hmac), + DMA_TO_DEVICE); + ret = dma_mapping_error(eip93->dev, rctx->sa_record_hmac_base); +- if (ret) ++ if (ret) { ++ rctx->sa_record_hmac_base = 0; ++ dma_unmap_single(eip93->dev, src_addr, len, ++ DMA_TO_DEVICE); + return ret; ++ } + + cdesc.sa_addr = rctx->sa_record_hmac_base; + } +@@ -420,12 +430,14 @@ static int eip93_hash_update(struct ahash_request *req) + return ret; + + free_sa_record: +- dma_unmap_single(eip93->dev, rctx->sa_record_base, +- sizeof(*sa_record), DMA_TO_DEVICE); ++ if (rctx->sa_record_base) ++ dma_unmap_single(eip93->dev, rctx->sa_record_base, ++ sizeof(*sa_record), DMA_TO_DEVICE); + + free_sa_state: +- dma_unmap_single(eip93->dev, rctx->sa_state_base, +- sizeof(*sa_state), DMA_TO_DEVICE); ++ if (rctx->sa_state_base) ++ dma_unmap_single(eip93->dev, rctx->sa_state_base, ++ sizeof(*sa_state), DMA_TO_DEVICE); + + return ret; + } +@@ -501,12 +513,14 @@ static int __eip93_hash_final(struct ahash_request *req, bool map_dma) + free_blocks: + eip93_hash_free_data_blocks(req); + +- dma_unmap_single(eip93->dev, rctx->sa_record_base, +- sizeof(*sa_record), DMA_TO_DEVICE); ++ if (rctx->sa_record_base) ++ dma_unmap_single(eip93->dev, rctx->sa_record_base, ++ sizeof(*sa_record), DMA_TO_DEVICE); + + free_sa_state: +- dma_unmap_single(eip93->dev, rctx->sa_state_base, +- sizeof(*sa_state), DMA_TO_DEVICE); ++ if (rctx->sa_state_base) ++ dma_unmap_single(eip93->dev, rctx->sa_state_base, ++ sizeof(*sa_state), DMA_TO_DEVICE); + + return ret; + } +@@ -549,11 +563,13 @@ static int eip93_hash_finup(struct ahash_request *req) + return __eip93_hash_final(req, false); + + free_sa_record: +- dma_unmap_single(eip93->dev, rctx->sa_record_base, +- sizeof(*sa_record), DMA_TO_DEVICE); ++ if (rctx->sa_record_base) ++ dma_unmap_single(eip93->dev, rctx->sa_record_base, ++ sizeof(*sa_record), DMA_TO_DEVICE); + free_sa_state: +- dma_unmap_single(eip93->dev, rctx->sa_state_base, +- sizeof(*sa_state), DMA_TO_DEVICE); ++ if (rctx->sa_state_base) ++ dma_unmap_single(eip93->dev, rctx->sa_state_base, ++ sizeof(*sa_state), DMA_TO_DEVICE); + + return ret; + } diff --git a/target/linux/generic/backport-6.18/9916-crypto-eip93-reject-HMAC-requests-before-setkey.patch b/target/linux/generic/backport-6.18/9916-crypto-eip93-reject-HMAC-requests-before-setkey.patch new file mode 100644 index 00000000000000..0ff50315c22c30 --- /dev/null +++ b/target/linux/generic/backport-6.18/9916-crypto-eip93-reject-HMAC-requests-before-setkey.patch @@ -0,0 +1,33 @@ +From 706e4ff7848cc1bec97fbe0f77e5997358362228 Mon Sep 17 00:00:00 2001 +From: Jihong Min +Date: Mon, 25 May 2026 04:10:44 +0900 +Subject: [PATCH] crypto: eip93: reject HMAC requests before setkey + +HMAC requests need the precomputed ipad/opad state installed by setkey(). +Using an HMAC tfm before setkey() initializes the request with an all-zero +ipad and produces invalid hardware input. + +Reject those requests during hash init so the failure is explicit. + +Fixes: 9739f5f93b78 ("crypto: eip93 - Add Inside Secure SafeXcel EIP-93 crypto engine support") +Originally-by: Mieczyslaw Nalewaj +Assisted-by: Codex:gpt-5.5 +Signed-off-by: Jihong Min +--- + drivers/crypto/inside-secure/eip93/eip93-hash.c | 3 +++ + 1 file changed, 3 insertions(+) + +diff --git a/drivers/crypto/inside-secure/eip93/eip93-hash.c b/drivers/crypto/inside-secure/eip93/eip93-hash.c +index 63bb6c4670cb..060e90c5eaa7 100644 +--- a/drivers/crypto/inside-secure/eip93/eip93-hash.c ++++ b/drivers/crypto/inside-secure/eip93/eip93-hash.c +@@ -300,6 +300,9 @@ static int eip93_hash_init(struct ahash_request *req) + struct eip93_hash_ctx *ctx = crypto_ahash_ctx(ahash); + struct sa_state *sa_state = &rctx->sa_state; + ++ if (IS_HMAC(ctx->flags) && !memchr_inv(ctx->ipad, 0, SHA256_BLOCK_SIZE)) ++ return -EINVAL; ++ + memset(sa_state->state_byte_cnt, 0, sizeof(u32) * 2); + eip93_hash_init_sa_state_digest(ctx->flags & EIP93_HASH_MASK, + sa_state->state_i_digest); diff --git a/target/linux/generic/backport-6.18/9917-crypto-eip93-use-request-local-SA-records-for-cipher-requests.patch b/target/linux/generic/backport-6.18/9917-crypto-eip93-use-request-local-SA-records-for-cipher-requests.patch new file mode 100644 index 00000000000000..3edcb916bf39ab --- /dev/null +++ b/target/linux/generic/backport-6.18/9917-crypto-eip93-use-request-local-SA-records-for-cipher-requests.patch @@ -0,0 +1,260 @@ +From 518e19c26b7aac06f8fdfb608e9b9b1a4c6efc7a Mon Sep 17 00:00:00 2001 +From: Jihong Min +Date: Mon, 25 May 2026 04:11:31 +0900 +Subject: [PATCH] crypto: eip93: use request-local SA records for cipher + requests + +Cipher and AEAD requests keep mutable direction and copy flags in the SA +record. Updating the tfm-level SA record for decrypt requests can leak +those settings into concurrent requests using the same transform. + +Copy the prepared SA record into the request context and apply the +request-specific flags there. Map that request-local record for DMA, then +unmap it on normal completion and validation failures. + +Fixes: 9739f5f93b78 ("crypto: eip93 - Add Inside Secure SafeXcel EIP-93 crypto engine support") +Reported-by: Benjamin Larsson +Suggested-by: Benjamin Larsson +Assisted-by: Codex:gpt-5.5 +Signed-off-by: Jihong Min +--- + .../crypto/inside-secure/eip93/eip93-aead.c | 34 +++++++++++++------ + .../crypto/inside-secure/eip93/eip93-cipher.c | 34 ++++++++++++------- + .../crypto/inside-secure/eip93/eip93-cipher.h | 3 +- + .../crypto/inside-secure/eip93/eip93-common.c | 9 +++++ + 4 files changed, 55 insertions(+), 25 deletions(-) + +diff --git a/drivers/crypto/inside-secure/eip93/eip93-aead.c b/drivers/crypto/inside-secure/eip93/eip93-aead.c +index 2bbd0af7b0e0..3b2edb012048 100644 +--- a/drivers/crypto/inside-secure/eip93/eip93-aead.c ++++ b/drivers/crypto/inside-secure/eip93/eip93-aead.c +@@ -42,12 +42,18 @@ void eip93_aead_handle_result(struct crypto_async_request *async, int err) + + static int eip93_aead_send_req(struct crypto_async_request *async) + { ++ struct eip93_crypto_ctx *ctx = crypto_tfm_ctx(async->tfm); + struct aead_request *req = aead_request_cast(async); + struct eip93_cipher_reqctx *rctx = aead_request_ctx(req); + int err; + + err = check_valid_request(rctx); + if (err) { ++ if (rctx->sa_record_base) { ++ dma_unmap_single(ctx->eip93->dev, rctx->sa_record_base, ++ sizeof(rctx->sa_record), DMA_TO_DEVICE); ++ rctx->sa_record_base = 0; ++ } + aead_request_complete(req, err); + return err; + } +@@ -81,8 +87,6 @@ static void eip93_aead_cra_exit(struct crypto_tfm *tfm) + { + struct eip93_crypto_ctx *ctx = crypto_tfm_ctx(tfm); + +- dma_unmap_single(ctx->eip93->dev, ctx->sa_record_base, +- sizeof(*ctx->sa_record), DMA_TO_DEVICE); + kfree(ctx->sa_record); + } + +@@ -191,11 +195,24 @@ static int eip93_aead_crypt(struct aead_request *req) + struct crypto_aead *aead = crypto_aead_reqtfm(req); + int ret; + +- ctx->sa_record_base = dma_map_single(ctx->eip93->dev, ctx->sa_record, +- sizeof(*ctx->sa_record), DMA_TO_DEVICE); +- ret = dma_mapping_error(ctx->eip93->dev, ctx->sa_record_base); +- if (ret) ++ memcpy(&rctx->sa_record, ctx->sa_record, sizeof(rctx->sa_record)); ++ if (IS_DECRYPT(rctx->flags)) { ++ rctx->sa_record.sa_cmd0_word |= EIP93_SA_CMD_DIRECTION_IN; ++ rctx->sa_record.sa_cmd1_word &= ~(EIP93_SA_CMD_COPY_PAD | ++ EIP93_SA_CMD_COPY_DIGEST); ++ } else { ++ rctx->sa_record.sa_cmd0_word &= ~EIP93_SA_CMD_DIRECTION_IN; ++ rctx->sa_record.sa_cmd1_word |= EIP93_SA_CMD_COPY_PAD | ++ EIP93_SA_CMD_COPY_DIGEST; ++ } ++ ++ rctx->sa_record_base = dma_map_single(ctx->eip93->dev, &rctx->sa_record, ++ sizeof(rctx->sa_record), DMA_TO_DEVICE); ++ ret = dma_mapping_error(ctx->eip93->dev, rctx->sa_record_base); ++ if (ret) { ++ rctx->sa_record_base = 0; + return ret; ++ } + + rctx->textsize = req->cryptlen; + rctx->blksize = ctx->blksize; +@@ -205,7 +222,6 @@ static int eip93_aead_crypt(struct aead_request *req) + rctx->sg_dst = req->dst; + rctx->ivsize = crypto_aead_ivsize(aead); + rctx->desc_flags = EIP93_DESC_AEAD; +- rctx->sa_record_base = ctx->sa_record_base; + + if (IS_DECRYPT(rctx->flags)) + rctx->textsize -= rctx->authsize; +@@ -238,10 +254,6 @@ static int eip93_aead_decrypt(struct aead_request *req) + struct eip93_crypto_ctx *ctx = crypto_tfm_ctx(req->base.tfm); + struct eip93_cipher_reqctx *rctx = aead_request_ctx(req); + +- ctx->sa_record->sa_cmd0_word |= EIP93_SA_CMD_DIRECTION_IN; +- ctx->sa_record->sa_cmd1_word &= ~(EIP93_SA_CMD_COPY_PAD | +- EIP93_SA_CMD_COPY_DIGEST); +- + rctx->flags = ctx->flags; + rctx->flags |= EIP93_DECRYPT; + if (ctx->set_assoc) { +diff --git a/drivers/crypto/inside-secure/eip93/eip93-cipher.c b/drivers/crypto/inside-secure/eip93/eip93-cipher.c +index 4dd7ab7503e8..66b85781ef93 100644 +--- a/drivers/crypto/inside-secure/eip93/eip93-cipher.c ++++ b/drivers/crypto/inside-secure/eip93/eip93-cipher.c +@@ -32,6 +32,7 @@ void eip93_skcipher_handle_result(struct crypto_async_request *async, int err) + + static int eip93_skcipher_send_req(struct crypto_async_request *async) + { ++ struct eip93_crypto_ctx *ctx = crypto_tfm_ctx(async->tfm); + struct skcipher_request *req = skcipher_request_cast(async); + struct eip93_cipher_reqctx *rctx = skcipher_request_ctx(req); + int err; +@@ -39,6 +40,11 @@ static int eip93_skcipher_send_req(struct crypto_async_request *async) + err = check_valid_request(rctx); + + if (err) { ++ if (rctx->sa_record_base) { ++ dma_unmap_single(ctx->eip93->dev, rctx->sa_record_base, ++ sizeof(rctx->sa_record), DMA_TO_DEVICE); ++ rctx->sa_record_base = 0; ++ } + skcipher_request_complete(req, err); + return err; + } +@@ -72,8 +78,6 @@ static void eip93_skcipher_cra_exit(struct crypto_tfm *tfm) + { + struct eip93_crypto_ctx *ctx = crypto_tfm_ctx(tfm); + +- dma_unmap_single(ctx->eip93->dev, ctx->sa_record_base, +- sizeof(*ctx->sa_record), DMA_TO_DEVICE); + kfree(ctx->sa_record); + } + +@@ -133,7 +137,7 @@ static int eip93_skcipher_setkey(struct crypto_skcipher *ctfm, const u8 *key, + return 0; + } + +-static int eip93_skcipher_crypt(struct skcipher_request *req) ++static int eip93_skcipher_crypt(struct skcipher_request *req, bool encrypt) + { + struct eip93_cipher_reqctx *rctx = skcipher_request_ctx(req); + struct crypto_async_request *async = &req->base; +@@ -153,11 +157,19 @@ static int eip93_skcipher_crypt(struct skcipher_request *req) + crypto_skcipher_blocksize(skcipher))) + return -EINVAL; + +- ctx->sa_record_base = dma_map_single(ctx->eip93->dev, ctx->sa_record, +- sizeof(*ctx->sa_record), DMA_TO_DEVICE); +- ret = dma_mapping_error(ctx->eip93->dev, ctx->sa_record_base); +- if (ret) ++ memcpy(&rctx->sa_record, ctx->sa_record, sizeof(rctx->sa_record)); ++ if (encrypt) ++ rctx->sa_record.sa_cmd0_word &= ~EIP93_SA_CMD_DIRECTION_IN; ++ else ++ rctx->sa_record.sa_cmd0_word |= EIP93_SA_CMD_DIRECTION_IN; ++ ++ rctx->sa_record_base = dma_map_single(ctx->eip93->dev, &rctx->sa_record, ++ sizeof(rctx->sa_record), DMA_TO_DEVICE); ++ ret = dma_mapping_error(ctx->eip93->dev, rctx->sa_record_base); ++ if (ret) { ++ rctx->sa_record_base = 0; + return ret; ++ } + + rctx->assoclen = 0; + rctx->textsize = req->cryptlen; +@@ -167,7 +179,6 @@ static int eip93_skcipher_crypt(struct skcipher_request *req) + rctx->ivsize = crypto_skcipher_ivsize(skcipher); + rctx->blksize = ctx->blksize; + rctx->desc_flags = EIP93_DESC_SKCIPHER; +- rctx->sa_record_base = ctx->sa_record_base; + + return eip93_skcipher_send_req(async); + } +@@ -181,22 +192,19 @@ static int eip93_skcipher_encrypt(struct skcipher_request *req) + rctx->flags = tmpl->flags; + rctx->flags |= EIP93_ENCRYPT; + +- return eip93_skcipher_crypt(req); ++ return eip93_skcipher_crypt(req, true); + } + + static int eip93_skcipher_decrypt(struct skcipher_request *req) + { +- struct eip93_crypto_ctx *ctx = crypto_tfm_ctx(req->base.tfm); + struct eip93_cipher_reqctx *rctx = skcipher_request_ctx(req); + struct eip93_alg_template *tmpl = container_of(req->base.tfm->__crt_alg, + struct eip93_alg_template, alg.skcipher.base); + +- ctx->sa_record->sa_cmd0_word |= EIP93_SA_CMD_DIRECTION_IN; +- + rctx->flags = tmpl->flags; + rctx->flags |= EIP93_DECRYPT; + +- return eip93_skcipher_crypt(req); ++ return eip93_skcipher_crypt(req, false); + } + + /* Available algorithms in this module */ +diff --git a/drivers/crypto/inside-secure/eip93/eip93-cipher.h b/drivers/crypto/inside-secure/eip93/eip93-cipher.h +index 47e4e84ff14e..e9612696c388 100644 +--- a/drivers/crypto/inside-secure/eip93/eip93-cipher.h ++++ b/drivers/crypto/inside-secure/eip93/eip93-cipher.h +@@ -9,6 +9,7 @@ + #define _EIP93_CIPHER_H_ + + #include "eip93-main.h" ++#include "eip93-regs.h" + + struct eip93_crypto_ctx { + struct eip93_device *eip93; +@@ -16,7 +17,6 @@ struct eip93_crypto_ctx { + struct sa_record *sa_record; + u32 sa_nonce; + int blksize; +- dma_addr_t sa_record_base; + /* AEAD specific */ + unsigned int authsize; + unsigned int assoclen; +@@ -32,6 +32,7 @@ struct eip93_cipher_reqctx { + unsigned int textsize; + unsigned int assoclen; + unsigned int authsize; ++ struct sa_record sa_record __aligned(CRYPTO_DMA_ALIGN); + dma_addr_t sa_record_base; + struct sa_state *sa_state; + dma_addr_t sa_state_base; +diff --git a/drivers/crypto/inside-secure/eip93/eip93-common.c b/drivers/crypto/inside-secure/eip93/eip93-common.c +index ed46730c36bc..f422c93748c9 100644 +--- a/drivers/crypto/inside-secure/eip93/eip93-common.c ++++ b/drivers/crypto/inside-secure/eip93/eip93-common.c +@@ -637,6 +637,10 @@ int eip93_send_req(struct crypto_async_request *async, + DMA_TO_DEVICE); + free_sa_state: + kfree(rctx->sa_state); ++ if (rctx->sa_record_base) ++ dma_unmap_single(eip93->dev, rctx->sa_record_base, ++ sizeof(rctx->sa_record), DMA_TO_DEVICE); ++ rctx->sa_record_base = 0; + + return err; + } +@@ -693,6 +697,11 @@ void eip93_handle_result(struct eip93_device *eip93, struct eip93_cipher_reqctx + sizeof(*rctx->sa_state_ctr), + DMA_FROM_DEVICE); + ++ if (rctx->sa_record_base) ++ dma_unmap_single(eip93->dev, rctx->sa_record_base, ++ sizeof(rctx->sa_record), DMA_TO_DEVICE); ++ rctx->sa_record_base = 0; ++ + if (rctx->sa_state) + dma_unmap_single(eip93->dev, rctx->sa_state_base, + sizeof(*rctx->sa_state), diff --git a/target/linux/generic/backport-6.18/9918-crypto-eip93-order-result-descriptor-reads-after-PE_READY.patch b/target/linux/generic/backport-6.18/9918-crypto-eip93-order-result-descriptor-reads-after-PE_READY.patch new file mode 100644 index 00000000000000..f02fce7506158d --- /dev/null +++ b/target/linux/generic/backport-6.18/9918-crypto-eip93-order-result-descriptor-reads-after-PE_READY.patch @@ -0,0 +1,44 @@ +From 94efa7fb58b96ed72676004784c87ae40bc7767a Mon Sep 17 00:00:00 2001 +From: Jihong Min +Date: Mon, 25 May 2026 04:12:01 +0900 +Subject: [PATCH] crypto: eip93: order result descriptor reads after PE_READY + +The result handler polls ownership bits until the packet engine reports the +descriptor as ready. Ensure later descriptor reads observe the DMA writes +that completed before PE_READY became visible. + +Use the value already read from the descriptor for error parsing. + +Fixes: 9739f5f93b78 ("crypto: eip93 - Add Inside Secure SafeXcel EIP-93 crypto engine support") +Reported-by: Benjamin Larsson +Suggested-by: Benjamin Larsson +Assisted-by: Codex:gpt-5.5 +Signed-off-by: Jihong Min +--- + drivers/crypto/inside-secure/eip93/eip93-main.c | 13 ++++++++----- + 1 file changed, 8 insertions(+), 5 deletions(-) + +diff --git a/drivers/crypto/inside-secure/eip93/eip93-main.c b/drivers/crypto/inside-secure/eip93/eip93-main.c +index 276839e1a515..e3bd28cc0c67 100644 +--- a/drivers/crypto/inside-secure/eip93/eip93-main.c ++++ b/drivers/crypto/inside-secure/eip93/eip93-main.c +@@ -224,11 +224,14 @@ static void eip93_handle_result_descriptor(struct eip93_device *eip93) + FIELD_GET(EIP93_PE_LENGTH_HOST_PE_READY, pe_length) != + EIP93_PE_LENGTH_PE_READY); + +- err = rdesc->pe_ctrl_stat_word & (EIP93_PE_CTRL_PE_EXT_ERR_CODE | +- EIP93_PE_CTRL_PE_EXT_ERR | +- EIP93_PE_CTRL_PE_SEQNUM_ERR | +- EIP93_PE_CTRL_PE_PAD_ERR | +- EIP93_PE_CTRL_PE_AUTH_ERR); ++ /* Order descriptor reads after device ownership is returned. */ ++ dma_rmb(); ++ ++ err = pe_ctrl_stat & (EIP93_PE_CTRL_PE_EXT_ERR_CODE | ++ EIP93_PE_CTRL_PE_EXT_ERR | ++ EIP93_PE_CTRL_PE_SEQNUM_ERR | ++ EIP93_PE_CTRL_PE_PAD_ERR | ++ EIP93_PE_CTRL_PE_AUTH_ERR); + + desc_flags = FIELD_GET(EIP93_PE_USER_ID_DESC_FLAGS, rdesc->user_id); + crypto_idr = FIELD_GET(EIP93_PE_USER_ID_CRYPTO_IDR, rdesc->user_id); diff --git a/target/linux/generic/backport-6.18/9919-crypto-eip93-handle-request-ID-exhaustion.patch b/target/linux/generic/backport-6.18/9919-crypto-eip93-handle-request-ID-exhaustion.patch new file mode 100644 index 00000000000000..2a464a89fc9c5d --- /dev/null +++ b/target/linux/generic/backport-6.18/9919-crypto-eip93-handle-request-ID-exhaustion.patch @@ -0,0 +1,214 @@ +From 55bb5d8b5d55a26941fb08be2701931ce8affd43 Mon Sep 17 00:00:00 2001 +From: Jihong Min +Date: Mon, 25 May 2026 04:13:48 +0900 +Subject: [PATCH] crypto: eip93: handle request ID exhaustion + +The driver stores the async request pointer in an IDR and places the ID in +the hardware descriptor. The old allocation used the ring depth as the IDR +limit. It also did not check allocation failure, so request pressure could +encode a negative error value as a descriptor user ID. + +Allocate request IDs from the full user ID field range and wait while the +IDR is full. Publish the descriptor only after DMA mappings and ID +allocation have succeeded. Add unwind paths for mappings that are active +when ID allocation fails, and tolerate stale or missing result IDs in the +interrupt handler. + +Fixes: 9739f5f93b78 ("crypto: eip93 - Add Inside Secure SafeXcel EIP-93 crypto engine support") +Reported-by: Benjamin Larsson +Suggested-by: Benjamin Larsson +Tested-by: Aleksander Jan Bajkowski +Assisted-by: Codex:gpt-5.5 +Signed-off-by: Jihong Min +--- + .../crypto/inside-secure/eip93/eip93-common.c | 48 +++++++++++++++---- + .../crypto/inside-secure/eip93/eip93-common.h | 3 ++ + .../crypto/inside-secure/eip93/eip93-hash.c | 26 +++++++--- + .../crypto/inside-secure/eip93/eip93-main.c | 6 +++ + .../crypto/inside-secure/eip93/eip93-main.h | 2 + + 5 files changed, 69 insertions(+), 16 deletions(-) + +diff --git a/drivers/crypto/inside-secure/eip93/eip93-common.c b/drivers/crypto/inside-secure/eip93/eip93-common.c +index f422c93748c9..88b89d05d510 100644 +--- a/drivers/crypto/inside-secure/eip93/eip93-common.c ++++ b/drivers/crypto/inside-secure/eip93/eip93-common.c +@@ -65,6 +65,31 @@ int eip93_parse_ctrl_stat_err(struct eip93_device *eip93, int err) + } + } + ++int eip93_alloc_request_id(struct eip93_device *eip93, void *request) ++{ ++ int id; ++ ++ scoped_guard(spinlock_bh, &eip93->ring->idr_lock) ++ id = idr_alloc(&eip93->ring->crypto_async_idr, request, 0, ++ EIP93_REQUEST_IDR_LIMIT, GFP_ATOMIC); ++ ++ return id; ++} ++ ++int eip93_alloc_request_id_wait(struct eip93_device *eip93, void *request) ++{ ++ int id; ++ ++ for (;;) { ++ id = eip93_alloc_request_id(eip93, request); ++ if (id != -ENOSPC) ++ return id; ++ ++ usleep_range(EIP93_RING_BUSY_DELAY, ++ EIP93_RING_BUSY_DELAY * 2); ++ } ++} ++ + static void *eip93_ring_next_wptr(struct eip93_device *eip93, + struct eip93_desc_ring *ring) + { +@@ -597,15 +622,6 @@ int eip93_send_req(struct crypto_async_request *async, + cdesc.sa_addr = rctx->sa_record_base; + cdesc.arc4_addr = 0; + +- scoped_guard(spinlock_bh, &eip93->ring->idr_lock) +- crypto_async_idr = idr_alloc(&eip93->ring->crypto_async_idr, async, 0, +- EIP93_RING_NUM - 1, GFP_ATOMIC); +- +- cdesc.user_id = FIELD_PREP(EIP93_PE_USER_ID_CRYPTO_IDR, (u16)crypto_async_idr) | +- FIELD_PREP(EIP93_PE_USER_ID_DESC_FLAGS, rctx->desc_flags); +- +- rctx->cdesc = &cdesc; +- + /* map DMA_BIDIRECTIONAL to invalidate cache on destination + * implies __dma_cache_wback_inv + */ +@@ -620,8 +636,22 @@ int eip93_send_req(struct crypto_async_request *async, + goto free_sg_dma; + } + ++ crypto_async_idr = eip93_alloc_request_id_wait(eip93, async); ++ if (crypto_async_idr < 0) { ++ err = crypto_async_idr; ++ goto free_src_sg_dma; ++ } ++ ++ cdesc.user_id = FIELD_PREP(EIP93_PE_USER_ID_CRYPTO_IDR, crypto_async_idr) | ++ FIELD_PREP(EIP93_PE_USER_ID_DESC_FLAGS, rctx->desc_flags); ++ ++ rctx->cdesc = &cdesc; ++ + return eip93_scatter_combine(eip93, rctx, datalen, split, offsetin); + ++free_src_sg_dma: ++ if (src != dst) ++ dma_unmap_sg(eip93->dev, src, rctx->src_nents, DMA_TO_DEVICE); + free_sg_dma: + dma_unmap_sg(eip93->dev, dst, rctx->dst_nents, DMA_BIDIRECTIONAL); + free_sa_state_ctr_dma: +diff --git a/drivers/crypto/inside-secure/eip93/eip93-common.h b/drivers/crypto/inside-secure/eip93/eip93-common.h +index 41c43782eb5c..3898962d0abf 100644 +--- a/drivers/crypto/inside-secure/eip93/eip93-common.h ++++ b/drivers/crypto/inside-secure/eip93/eip93-common.h +@@ -17,6 +17,9 @@ void eip93_set_sa_record(struct sa_record *sa_record, const unsigned int keylen, + + int eip93_parse_ctrl_stat_err(struct eip93_device *eip93, int err); + ++int eip93_alloc_request_id(struct eip93_device *eip93, void *request); ++int eip93_alloc_request_id_wait(struct eip93_device *eip93, void *request); ++ + int eip93_hmac_setkey(u32 ctx_flags, const u8 *key, unsigned int keylen, + unsigned int hashlen, u8 *ipad, u8 *opad, + bool skip_ipad); +diff --git a/drivers/crypto/inside-secure/eip93/eip93-hash.c b/drivers/crypto/inside-secure/eip93/eip93-hash.c +index 060e90c5eaa7..512e0e2ce25e 100644 +--- a/drivers/crypto/inside-secure/eip93/eip93-hash.c ++++ b/drivers/crypto/inside-secure/eip93/eip93-hash.c +@@ -221,6 +221,7 @@ static int eip93_send_hash_req(struct crypto_async_request *async, u8 *data, + struct eip93_device *eip93 = ctx->eip93; + struct eip93_descriptor cdesc = { }; + dma_addr_t src_addr; ++ bool hmac_sa_mapped = false; + int ret; + + /* Map block data to DMA */ +@@ -258,22 +259,23 @@ static int eip93_send_hash_req(struct crypto_async_request *async, u8 *data, + ret = dma_mapping_error(eip93->dev, rctx->sa_record_hmac_base); + if (ret) { + rctx->sa_record_hmac_base = 0; +- dma_unmap_single(eip93->dev, src_addr, len, +- DMA_TO_DEVICE); +- return ret; ++ goto unmap_src; + } + + cdesc.sa_addr = rctx->sa_record_hmac_base; ++ hmac_sa_mapped = true; + } + + cdesc.pe_ctrl_stat_word |= EIP93_PE_CTRL_PE_HASH_FINAL; + } + +- scoped_guard(spinlock_bh, &eip93->ring->idr_lock) +- crypto_async_idr = idr_alloc(&eip93->ring->crypto_async_idr, async, 0, +- EIP93_RING_NUM - 1, GFP_ATOMIC); ++ crypto_async_idr = eip93_alloc_request_id_wait(eip93, async); ++ if (crypto_async_idr < 0) { ++ ret = crypto_async_idr; ++ goto unmap_hmac_sa; ++ } + +- cdesc.user_id |= FIELD_PREP(EIP93_PE_USER_ID_CRYPTO_IDR, (u16)crypto_async_idr) | ++ cdesc.user_id |= FIELD_PREP(EIP93_PE_USER_ID_CRYPTO_IDR, crypto_async_idr) | + FIELD_PREP(EIP93_PE_USER_ID_DESC_FLAGS, EIP93_DESC_LAST); + } + +@@ -291,6 +293,16 @@ static int eip93_send_hash_req(struct crypto_async_request *async, u8 *data, + + *data_dma = src_addr; + return 0; ++ ++unmap_hmac_sa: ++ if (hmac_sa_mapped) { ++ dma_unmap_single(eip93->dev, rctx->sa_record_hmac_base, ++ sizeof(rctx->sa_record_hmac), DMA_TO_DEVICE); ++ rctx->sa_record_hmac_base = 0; ++ } ++unmap_src: ++ dma_unmap_single(eip93->dev, src_addr, len, DMA_TO_DEVICE); ++ return ret; + } + + static int eip93_hash_init(struct ahash_request *req) +diff --git a/drivers/crypto/inside-secure/eip93/eip93-main.c b/drivers/crypto/inside-secure/eip93/eip93-main.c +index e3bd28cc0c67..0de18a0cbe33 100644 +--- a/drivers/crypto/inside-secure/eip93/eip93-main.c ++++ b/drivers/crypto/inside-secure/eip93/eip93-main.c +@@ -257,6 +257,12 @@ static void eip93_handle_result_descriptor(struct eip93_device *eip93) + idr_remove(&eip93->ring->crypto_async_idr, crypto_idr); + } + ++ if (!async) { ++ dev_warn_ratelimited(eip93->dev, "missing request id %u\n", ++ crypto_idr); ++ goto get_more; ++ } ++ + /* Parse error in ctrl stat word */ + err = eip93_parse_ctrl_stat_err(eip93, err); + +diff --git a/drivers/crypto/inside-secure/eip93/eip93-main.h b/drivers/crypto/inside-secure/eip93/eip93-main.h +index 990c2401b7ce..5237b75bba62 100644 +--- a/drivers/crypto/inside-secure/eip93/eip93-main.h ++++ b/drivers/crypto/inside-secure/eip93/eip93-main.h +@@ -13,11 +13,13 @@ + #include + #include + #include ++#include + + #define EIP93_RING_BUSY_DELAY 500 + + #define EIP93_RING_NUM 512 + #define EIP93_RING_BUSY 32 ++#define EIP93_REQUEST_IDR_LIMIT (U16_MAX + 1) + #define EIP93_CRA_PRIORITY 1500 + + #define EIP93_RING_SA_STATE_ADDR(base, idx) ((base) + (idx)) diff --git a/target/linux/generic/hack-6.18/926-crypto-eip93-use-AES-fallback-for-small-requests.patch b/target/linux/generic/hack-6.18/926-crypto-eip93-use-AES-fallback-for-small-requests.patch index 09f6cbfbccdb29..696237d0bfea0c 100644 --- a/target/linux/generic/hack-6.18/926-crypto-eip93-use-AES-fallback-for-small-requests.patch +++ b/target/linux/generic/hack-6.18/926-crypto-eip93-use-AES-fallback-for-small-requests.patch @@ -95,14 +95,7 @@ Signed-off-by: Aviana Cruz ctx->blksize = AES_BLOCK_SIZE; ret = aes_expandkey(&aes, key, keylen); -@@ -133,12 +159,13 @@ static int eip93_skcipher_setkey(struct - return 0; - } - --static int eip93_skcipher_crypt(struct skcipher_request *req) -+static int eip93_skcipher_crypt(struct skcipher_request *req, bool encrypt) - { - struct eip93_cipher_reqctx *rctx = skcipher_request_ctx(req); +@@ -162,6 +188,7 @@ static int eip93_skcipher_crypt(struct skcipher_request *req, bool encrypt) struct crypto_async_request *async = &req->base; struct eip93_crypto_ctx *ctx = crypto_tfm_ctx(req->base.tfm); struct crypto_skcipher *skcipher = crypto_skcipher_reqtfm(req); @@ -110,7 +103,7 @@ Signed-off-by: Aviana Cruz int ret; if (!req->cryptlen) -@@ -153,6 +180,21 @@ static int eip93_skcipher_crypt(struct s +@@ -176,6 +203,21 @@ static int eip93_skcipher_crypt(struct skcipher_request *req, bool encrypt) crypto_skcipher_blocksize(skcipher))) return -EINVAL; @@ -129,27 +122,9 @@ Signed-off-by: Aviana Cruz + crypto_skcipher_decrypt(&rctx->fallback_req); + } + - ctx->sa_record_base = dma_map_single(ctx->eip93->dev, ctx->sa_record, - sizeof(*ctx->sa_record), DMA_TO_DEVICE); - ret = dma_mapping_error(ctx->eip93->dev, ctx->sa_record_base); -@@ -181,7 +223,7 @@ static int eip93_skcipher_encrypt(struct - rctx->flags = tmpl->flags; - rctx->flags |= EIP93_ENCRYPT; - -- return eip93_skcipher_crypt(req); -+ return eip93_skcipher_crypt(req, true); - } - - static int eip93_skcipher_decrypt(struct skcipher_request *req) -@@ -196,7 +238,7 @@ static int eip93_skcipher_decrypt(struct - rctx->flags = tmpl->flags; - rctx->flags |= EIP93_DECRYPT; - -- return eip93_skcipher_crypt(req); -+ return eip93_skcipher_crypt(req, false); - } - - /* Available algorithms in this module */ + memcpy(&rctx->sa_record, ctx->sa_record, sizeof(rctx->sa_record)); + if (encrypt) + rctx->sa_record.sa_cmd0_word &= ~EIP93_SA_CMD_DIRECTION_IN; --- a/drivers/crypto/inside-secure/eip93/eip93-cipher.h +++ b/drivers/crypto/inside-secure/eip93/eip93-cipher.h @@ -22,6 +22,7 @@ struct eip93_crypto_ctx { diff --git a/target/linux/generic/hack-6.18/9990-crypto-eip93-select-aes-fallback-threshold-by-key-length.patch b/target/linux/generic/hack-6.18/9990-crypto-eip93-select-aes-fallback-threshold-by-key-length.patch new file mode 100644 index 00000000000000..4e3ed4c7bf98a5 --- /dev/null +++ b/target/linux/generic/hack-6.18/9990-crypto-eip93-select-aes-fallback-threshold-by-key-length.patch @@ -0,0 +1,48 @@ +From: Mieczyslaw Nalewaj +Date: Tue, 14 Apr 2026 08:47:00 +0200 +Subject: [PATCH] crypto: eip93 - select AES fallback threshold by key length + +The local EIP93 small-request fallback patch has separate software +fallback thresholds for AES-128 and generic requests, but the current +condition checks the constant AES_KEYSIZE_128 directly. Since that +constant is non-zero, the AES-128 threshold is always selected. + +Remember the selected skcipher key length and use it to choose the +fallback threshold. + +Signed-off-by: Mieczyslaw Nalewaj +--- + drivers/crypto/inside-secure/eip93/eip93-cipher.c | 4 +++- + drivers/crypto/inside-secure/eip93/eip93-cipher.h | 1 + + 2 files changed, 4 insertions(+), 1 deletion(-) + +--- a/drivers/crypto/inside-secure/eip93/eip93-cipher.c ++++ b/drivers/crypto/inside-secure/eip93/eip93-cipher.c +@@ -133,6 +133,8 @@ static int eip93_skcipher_setkey(struct + return ret; + } + ++ ctx->keylen = keylen; ++ + if (flags & EIP93_ALG_AES) { + struct crypto_aes_ctx aes; + bool fallback = eip93_skcipher_is_fallback(tfm, flags); +@@ -182,7 +184,7 @@ static int eip93_skcipher_crypt(struct + return -EINVAL; + + if (fallback && +- req->cryptlen <= (AES_KEYSIZE_128 ? ++ req->cryptlen <= ((ctx->keylen == AES_KEYSIZE_128) ? + CONFIG_CRYPTO_DEV_EIP93_AES_128_SW_MAX_LEN : + CONFIG_CRYPTO_DEV_EIP93_GENERIC_SW_MAX_LEN)) { + skcipher_request_set_tfm(&rctx->fallback_req, ctx->fallback); +--- a/drivers/crypto/inside-secure/eip93/eip93-cipher.h ++++ b/drivers/crypto/inside-secure/eip93/eip93-cipher.h +@@ -23,6 +23,7 @@ struct eip93_crypto_ctx { + bool set_assoc; + enum eip93_alg_type type; + struct crypto_skcipher *fallback; ++ u32 keylen; + }; + + struct eip93_cipher_reqctx { diff --git a/target/linux/generic/kernel-6.18 b/target/linux/generic/kernel-6.18 index efbfe514334d0e..4c51e5d02ca540 100644 --- a/target/linux/generic/kernel-6.18 +++ b/target/linux/generic/kernel-6.18 @@ -1,2 +1,2 @@ -LINUX_VERSION-6.18 = .37 -LINUX_KERNEL_HASH-6.18.37 = a83cd200e6646db52866b8309e9137b9e9048b613cbda10ced2b811aae125255 +LINUX_VERSION-6.18 = .38 +LINUX_KERNEL_HASH-6.18.38 = ac26e508abd56e9f8b89872b6e10c49fc823bcc70d8068a5d8504c1a7c4ff045 diff --git a/target/linux/generic/pending-6.12/675-01-net-export-__dev_fill_forward_path-and-br_fdb_find_rcu.patch b/target/linux/generic/pending-6.12/675-01-net-export-__dev_fill_forward_path-and-br_fdb_find_rcu.patch new file mode 100644 index 00000000000000..9bfca875deafa5 --- /dev/null +++ b/target/linux/generic/pending-6.12/675-01-net-export-__dev_fill_forward_path-and-br_fdb_find_rcu.patch @@ -0,0 +1,107 @@ +From: Ryan Chen +Date: Sun, 16 Mar 2026 12:00:00 -0500 +Subject: [PATCH 1/2] net: export __dev_fill_forward_path and br_fdb_find_rcu + +Export __dev_fill_forward_path() which accepts an external +net_device_path_ctx, allowing callers to pre-populate VLAN info +for bridge offload. Also export br_fdb_find_rcu() so netfilter +flow offload can check if a destination MAC is known in the +bridge FDB. + +These are prerequisites for nft_flow_offload bridging support. + +Signed-off-by: Ryan Chen +--- + include/linux/netdevice.h | 2 ++ + net/bridge/br_fdb.c | 1 + + net/core/dev.c | 32 ++++++++++++++++++-------------- + 3 files changed, 21 insertions(+), 14 deletions(-) + +--- a/include/linux/netdevice.h ++++ b/include/linux/netdevice.h +@@ -3089,6 +3089,8 @@ + int dev_fill_metadata_dst(struct net_device *dev, struct sk_buff *skb); + int dev_fill_forward_path(const struct net_device *dev, const u8 *daddr, + struct net_device_path_stack *stack); ++int __dev_fill_forward_path(struct net_device_path_ctx *ctx, const u8 *daddr, ++ struct net_device_path_stack *stack); + struct net_device *__dev_get_by_flags(struct net *net, unsigned short flags, + unsigned short mask); + struct net_device *dev_get_by_name(struct net *net, const char *name); +--- a/net/bridge/br_fdb.c ++++ b/net/bridge/br_fdb.c +@@ -269,6 +269,7 @@ + { + return fdb_find_rcu(&br->fdb_hash_tbl, addr, vid); + } ++EXPORT_SYMBOL_GPL(br_fdb_find_rcu); + + /* When a static FDB entry is added, the mac address from the entry is + * added to the bridge private HW address list and all required ports +--- a/net/core/dev.c ++++ b/net/core/dev.c +@@ -726,44 +726,52 @@ + return &stack->path[k]; + } + +-int dev_fill_forward_path(const struct net_device *dev, const u8 *daddr, +- struct net_device_path_stack *stack) ++int __dev_fill_forward_path(struct net_device_path_ctx *ctx, const u8 *daddr, ++ struct net_device_path_stack *stack) + { + const struct net_device *last_dev; +- struct net_device_path_ctx ctx = { +- .dev = dev, +- }; + struct net_device_path *path; + int ret = 0; + +- memcpy(ctx.daddr, daddr, sizeof(ctx.daddr)); ++ memcpy(ctx->daddr, daddr, sizeof(ctx->daddr)); + stack->num_paths = 0; +- while (ctx.dev && ctx.dev->netdev_ops->ndo_fill_forward_path) { +- last_dev = ctx.dev; ++ while (ctx->dev && ctx->dev->netdev_ops->ndo_fill_forward_path) { ++ last_dev = ctx->dev; + path = dev_fwd_path(stack); + if (!path) + return -1; + + memset(path, 0, sizeof(struct net_device_path)); +- ret = ctx.dev->netdev_ops->ndo_fill_forward_path(&ctx, path); ++ ret = ctx->dev->netdev_ops->ndo_fill_forward_path(ctx, path); + if (ret < 0) + return -1; + +- if (WARN_ON_ONCE(last_dev == ctx.dev)) ++ if (WARN_ON_ONCE(last_dev == ctx->dev)) + return -1; + } + +- if (!ctx.dev) ++ if (!ctx->dev) + return ret; + + path = dev_fwd_path(stack); + if (!path) + return -1; + path->type = DEV_PATH_ETHERNET; +- path->dev = ctx.dev; ++ path->dev = ctx->dev; + + return ret; + } ++EXPORT_SYMBOL_GPL(__dev_fill_forward_path); ++ ++int dev_fill_forward_path(const struct net_device *dev, const u8 *daddr, ++ struct net_device_path_stack *stack) ++{ ++ struct net_device_path_ctx ctx = { ++ .dev = dev, ++ }; ++ ++ return __dev_fill_forward_path(&ctx, daddr, stack); ++} + EXPORT_SYMBOL_GPL(dev_fill_forward_path); + + /* must be called under rcu_read_lock(), as we dont take a reference */ diff --git a/target/linux/generic/pending-6.12/675-02-nft_flow_offload-add-bridging-support.patch b/target/linux/generic/pending-6.12/675-02-nft_flow_offload-add-bridging-support.patch new file mode 100644 index 00000000000000..b2fc1285beff0a --- /dev/null +++ b/target/linux/generic/pending-6.12/675-02-nft_flow_offload-add-bridging-support.patch @@ -0,0 +1,339 @@ +From: Ryan Chen +Date: Sun, 16 Mar 2026 12:00:00 -0500 +Subject: [PATCH 2/2] netfilter: nft_flow_offload: add bridging support + +Without this patch, bridging traffic is unable to BIND using +nft_flow_offload. When two devices are bridged (e.g. WiFi STA to +LAN port via br-lan), the flow offload engine only knows how to +handle L3 routed traffic. + +This patch adds: +- Bridge detection via FDB lookup (nft_flow_offload_is_bridging) +- Separate route path for bridged traffic that uses in/out ifindex + directly instead of L3 route lookup +- MAC address extraction from ethernet header for bridged flows +- VLAN-aware bridge support via bridge port VLAN context +- Fix for VLAN untag underflow in nft_dev_path_info + +Based on MediaTek SDK patch by Bo-Cun Chen . +Adapted for Airoha AN7581 PPE offload. + +Signed-off-by: Ryan Chen +--- + net/netfilter/nft_flow_offload.c | 220 +++++++++++++++++++++++++++---- + 1 file changed, 196 insertions(+), 24 deletions(-) + +--- a/net/netfilter/nft_flow_offload.c ++++ b/net/netfilter/nft_flow_offload.c +@@ -15,6 +15,7 @@ + #include + #include + #include ++#include "../bridge/br_private.h" + + struct nft_flow_offload { + struct nft_flowtable *flowtable; +@@ -46,7 +47,48 @@ + return true; + } + +-static int nft_dev_fill_forward_path(const struct nf_flow_route *route, ++static u16 nft_flow_offload_get_vlan_id(struct net_bridge_port *port, ++ struct sk_buff *skb) ++{ ++ u16 vlan_id = 0; ++ ++ if (!port || !br_opt_get(port->br, BROPT_VLAN_ENABLED)) ++ return 0; ++ ++ if (skb_vlan_tag_present(skb)) ++ vlan_id = skb_vlan_tag_get_id(skb); ++ else ++ br_vlan_get_pvid_rcu(skb->dev, &vlan_id); ++ ++ return vlan_id; ++} ++ ++static bool nft_flow_offload_is_bridging(struct sk_buff *skb) ++{ ++ struct net_bridge_port *port; ++ unsigned char *dmac = eth_hdr(skb)->h_dest; ++ bool bridging = false; ++ u16 vlan_id; ++ ++ if (!netif_is_bridge_port(skb->dev)) ++ return false; ++ ++ rcu_read_lock(); ++ port = br_port_get_rcu(skb->dev); ++ if (port) { ++ vlan_id = nft_flow_offload_get_vlan_id(port, skb); ++ ++ /* lookup fdb entry */ ++ if (br_fdb_find_rcu(port->br, dmac, vlan_id)) ++ bridging = true; ++ } ++ rcu_read_unlock(); ++ ++ return bridging; ++} ++ ++static int nft_dev_fill_forward_path(struct net_device_path_ctx *ctx, ++ const struct nf_flow_route *route, + const struct dst_entry *dst_cache, + const struct nf_conn *ct, + enum ip_conntrack_dir dir, u8 *ha, +@@ -60,6 +102,9 @@ + if (!nft_is_valid_ether_device(dev)) + goto out; + ++ if (!is_zero_ether_addr(ha)) ++ goto out; ++ + n = dst_neigh_lookup(dst_cache, daddr); + if (!n) + return -1; +@@ -74,7 +119,26 @@ + return -1; + + out: +- return dev_fill_forward_path(dev, ha, stack); ++ return __dev_fill_forward_path(ctx, ha, stack); ++} ++ ++static void nft_br_vlan_dev_fill_forward_path(const struct nft_pktinfo *pkt, ++ struct net_device_path_ctx *ctx) ++{ ++ struct net_bridge_port *port; ++ u16 vlan_id; ++ ++ rcu_read_lock(); ++ port = br_port_get_rcu(pkt->skb->dev); ++ if (port) { ++ vlan_id = nft_flow_offload_get_vlan_id(port, pkt->skb); ++ if (vlan_id) { ++ ctx->num_vlans = 1; ++ ctx->vlan[0].id = vlan_id; ++ ctx->vlan[0].proto = port->br->vlan_proto; ++ } ++ } ++ rcu_read_unlock(); + } + + struct nft_forward_info { +@@ -103,12 +167,12 @@ + + for (i = 0; i < stack->num_paths; i++) { + path = &stack->path[i]; ++ info->indev = path->dev; + switch (path->type) { + case DEV_PATH_ETHERNET: + case DEV_PATH_DSA: + case DEV_PATH_VLAN: + case DEV_PATH_PPPOE: +- info->indev = path->dev; + if (is_zero_ether_addr(info->h_source)) + memcpy(info->h_source, path->dev->dev_addr, ETH_ALEN); + +@@ -150,10 +214,8 @@ + info->num_encaps++; + break; + case DEV_PATH_BR_VLAN_UNTAG: +- if (WARN_ON_ONCE(info->num_encaps-- == 0)) { +- info->indev = NULL; +- break; +- } ++ if (info->num_encaps > 0) ++ info->num_encaps--; + break; + case DEV_PATH_BR_VLAN_KEEP: + break; +@@ -161,7 +223,6 @@ + info->xmit_type = FLOW_OFFLOAD_XMIT_DIRECT; + break; + default: +- info->indev = NULL; + break; + } + } +@@ -192,22 +253,44 @@ + return found; + } + +-static void nft_dev_forward_path(struct nf_flow_route *route, +- const struct nf_conn *ct, +- enum ip_conntrack_dir dir, +- struct nft_flowtable *ft) ++static int nft_dev_forward_path(const struct nft_pktinfo *pkt, ++ struct nf_flow_route *route, ++ const struct nf_conn *ct, ++ enum ip_conntrack_dir dir, ++ struct nft_flowtable *ft) + { + const struct dst_entry *dst = route->tuple[dir].dst; + struct net_device_path_stack stack; ++ struct net_device_path_ctx ctx = { ++ .dev = dst->dev, ++ }; + struct nft_forward_info info = {}; ++ struct ethhdr *eth; ++ enum ip_conntrack_dir skb_dir; + unsigned char ha[ETH_ALEN]; + int i; + +- if (nft_dev_fill_forward_path(route, dst, ct, dir, ha, &stack) >= 0) ++ memset(ha, 0, sizeof(ha)); ++ ++ if (nft_flow_offload_is_bridging(pkt->skb) && skb_mac_header_was_set(pkt->skb)) { ++ eth = eth_hdr(pkt->skb); ++ skb_dir = CTINFO2DIR(skb_get_nfct(pkt->skb) & NFCT_INFOMASK); ++ if (skb_dir != dir) { ++ memcpy(ha, eth->h_source, ETH_ALEN); ++ memcpy(info.h_source, eth->h_dest, ETH_ALEN); ++ } else { ++ memcpy(ha, eth->h_dest, ETH_ALEN); ++ memcpy(info.h_source, eth->h_source, ETH_ALEN); ++ } ++ ++ nft_br_vlan_dev_fill_forward_path(pkt, &ctx); ++ } ++ ++ if (nft_dev_fill_forward_path(&ctx, route, dst, ct, dir, ha, &stack) >= 0) + nft_dev_path_info(&stack, &info, ha, &ft->data); + + if (!info.indev || !nft_flowtable_find_dev(info.indev, ft)) +- return; ++ return -ENOENT; + + route->tuple[!dir].in.ifindex = info.indev->ifindex; + for (i = 0; i < info.num_encaps; i++) { +@@ -224,13 +307,15 @@ + route->tuple[dir].out.hw_ifindex = info.hw_outdev->ifindex; + route->tuple[dir].xmit_type = info.xmit_type; + } ++ ++ return 0; + } + +-static int nft_flow_route(const struct nft_pktinfo *pkt, +- const struct nf_conn *ct, +- struct nf_flow_route *route, +- enum ip_conntrack_dir dir, +- struct nft_flowtable *ft) ++static int nft_flow_route_routing(const struct nft_pktinfo *pkt, ++ const struct nf_conn *ct, ++ struct nf_flow_route *route, ++ enum ip_conntrack_dir dir, ++ struct nft_flowtable *ft) + { + struct dst_entry *this_dst = skb_dst(pkt->skb); + struct dst_entry *other_dst = NULL; +@@ -270,13 +355,89 @@ + nft_default_forward_path(route, this_dst, dir); + nft_default_forward_path(route, other_dst, !dir); + +- if (route->tuple[dir].xmit_type == FLOW_OFFLOAD_XMIT_NEIGH && ++ if (route->tuple[dir].xmit_type == FLOW_OFFLOAD_XMIT_NEIGH && ++ route->tuple[!dir].xmit_type == FLOW_OFFLOAD_XMIT_NEIGH) { ++ if (nft_dev_forward_path(pkt, route, ct, dir, ft) || ++ nft_dev_forward_path(pkt, route, ct, !dir, ft)) ++ return -ENOENT; ++ } ++ ++ return 0; ++} ++ ++static int ++nft_flow_route_dir(const struct nft_pktinfo *pkt, ++ const struct nf_conn *ct, ++ struct nf_flow_route *route, ++ enum ip_conntrack_dir dir, ++ int ifindex) ++{ ++ struct dst_entry *other_dst = NULL; ++ struct flowi fl; ++ ++ memset(&fl, 0, sizeof(fl)); ++ switch (nft_pf(pkt)) { ++ case NFPROTO_IPV4: ++ fl.u.ip4.daddr = ct->tuplehash[!dir].tuple.src.u3.ip; ++ fl.u.ip4.flowi4_oif = ifindex; ++ fl.u.ip4.flowi4_tos = RT_TOS(ip_hdr(pkt->skb)->tos); ++ fl.u.ip4.flowi4_mark = pkt->skb->mark; ++ fl.u.ip4.flowi4_flags = FLOWI_FLAG_ANYSRC; ++ break; ++ case NFPROTO_IPV6: ++ fl.u.ip6.saddr = ct->tuplehash[!dir].tuple.dst.u3.in6; ++ fl.u.ip6.daddr = ct->tuplehash[!dir].tuple.src.u3.in6; ++ fl.u.ip6.flowi6_oif = ifindex; ++ fl.u.ip6.flowlabel = ip6_flowinfo(ipv6_hdr(pkt->skb)); ++ fl.u.ip6.flowi6_mark = pkt->skb->mark; ++ fl.u.ip6.flowi6_flags = FLOWI_FLAG_ANYSRC; ++ break; ++ } ++ ++ nf_route(nft_net(pkt), &other_dst, &fl, false, nft_pf(pkt)); ++ if (!other_dst) ++ return -ENOENT; ++ ++ nft_default_forward_path(route, other_dst, dir); ++ ++ return 0; ++} ++ ++static int ++nft_flow_route_bridging(const struct nft_pktinfo *pkt, ++ const struct nf_conn *ct, ++ struct nf_flow_route *route, ++ enum ip_conntrack_dir dir, ++ struct nft_flowtable *ft) ++{ ++ int ret; ++ ++ ret = nft_flow_route_dir(pkt, ct, route, dir, ++ nft_out(pkt)->ifindex); ++ if (ret) ++ return ret; ++ ++ ret = nft_flow_route_dir(pkt, ct, route, !dir, ++ nft_in(pkt)->ifindex); ++ if (ret) ++ goto err_route_dir1; ++ ++ if (route->tuple[dir].xmit_type == FLOW_OFFLOAD_XMIT_NEIGH && + route->tuple[!dir].xmit_type == FLOW_OFFLOAD_XMIT_NEIGH) { +- nft_dev_forward_path(route, ct, dir, ft); +- nft_dev_forward_path(route, ct, !dir, ft); ++ if (nft_dev_forward_path(pkt, route, ct, dir, ft) || ++ nft_dev_forward_path(pkt, route, ct, !dir, ft)) { ++ ret = -ENOENT; ++ goto err_route_dir2; ++ } + } + + return 0; ++ ++err_route_dir2: ++ dst_release(route->tuple[!dir].dst); ++err_route_dir1: ++ dst_release(route->tuple[dir].dst); ++ return ret; + } + + static bool nft_flow_offload_skip(struct sk_buff *skb, int family) +@@ -364,8 +525,13 @@ + goto out; + + dir = CTINFO2DIR(ctinfo); +- if (nft_flow_route(pkt, ct, &route, dir, priv->flowtable) < 0) +- goto err_flow_route; ++ if (nft_flow_offload_is_bridging(pkt->skb)) { ++ if (nft_flow_route_bridging(pkt, ct, &route, dir, priv->flowtable) < 0) ++ goto err_flow_route; ++ } else { ++ if (nft_flow_route_routing(pkt, ct, &route, dir, priv->flowtable) < 0) ++ goto err_flow_route; ++ } + + flow = flow_offload_alloc(ct); + if (!flow) diff --git a/target/linux/generic/pending-6.12/675-03-nft_flow_offload-add-DEV_PATH_MTK_WDMA-path.patch b/target/linux/generic/pending-6.12/675-03-nft_flow_offload-add-DEV_PATH_MTK_WDMA-path.patch new file mode 100644 index 00000000000000..7eb3c6b43a32ce --- /dev/null +++ b/target/linux/generic/pending-6.12/675-03-nft_flow_offload-add-DEV_PATH_MTK_WDMA-path.patch @@ -0,0 +1,31 @@ +From: Ryan Chen +Date: Sun, 16 Mar 2026 13:00:00 -0500 +Subject: [PATCH] netfilter: add DEV_PATH_MTK_WDMA path to nft_flow_offload + +Without this patch, nft_dev_path_info will populate an incorrect source +MAC address for WiFi bridge offload via WDMA. When a bridged flow goes +through the MT7996 WiFi device, the DEV_PATH_MTK_WDMA step in the +device path doesn't set h_source, causing the PPE entry to get a zero +source MAC and packets stall in both software fastpath and hardware path. + +Based on MediaTek SDK patch by Bo-Cun Chen . + +Signed-off-by: Ryan Chen +--- + net/netfilter/nft_flow_offload.c | 4 ++++ + 1 file changed, 4 insertions(+) + +diff --git a/net/netfilter/nft_flow_offload.c b/net/netfilter/nft_flow_offload.c +--- a/net/netfilter/nft_flow_offload.c ++++ b/net/netfilter/nft_flow_offload.c +@@ -222,6 +222,10 @@ + } + info->xmit_type = FLOW_OFFLOAD_XMIT_DIRECT; + break; ++ case DEV_PATH_MTK_WDMA: ++ if (is_zero_ether_addr(info->h_source)) ++ memcpy(info->h_source, path->dev->dev_addr, ETH_ALEN); ++ break; + default: + break; + } diff --git a/target/linux/generic/pending-6.12/675-04-nft_flow_offload-add-vlan-passthrough-support.patch b/target/linux/generic/pending-6.12/675-04-nft_flow_offload-add-vlan-passthrough-support.patch new file mode 100644 index 00000000000000..e340ec23830851 --- /dev/null +++ b/target/linux/generic/pending-6.12/675-04-nft_flow_offload-add-vlan-passthrough-support.patch @@ -0,0 +1,68 @@ +From: Ryan Chen +Date: Thu, 20 Mar 2026 10:00:00 -0500 +Subject: [PATCH 4/4] netfilter: nft_flow_offload: add VLAN passthrough support + +VLAN passthrough packets can be offloaded when bridge-nf-filter-vlan-tagged +is enabled. When a packet has a VLAN tag and the bridge does not have VLAN +filtering enabled (passthrough mode), record the VLAN encap info so the +hardware flow offload entry includes the correct VLAN tag. + +Without this patch, VLAN-tagged bridged traffic cannot be offloaded by PPE +because the VLAN encap information is missing from the flow entry. + +Enable with: echo 1 > /proc/sys/net/bridge/bridge-nf-filter-vlan-tagged + +Based on MediaTek SDK patch by chak-kei.lam . + +Signed-off-by: Ryan Chen +--- + net/netfilter/nft_flow_offload.c | 31 +++++++++++++++++++++++++++++-- + 1 file changed, 29 insertions(+), 2 deletions(-) + +--- a/net/netfilter/nft_flow_offload.c ++++ b/net/netfilter/nft_flow_offload.c +@@ -156,6 +156,30 @@ + enum flow_offload_xmit_type xmit_type; + }; + ++static void nft_fill_vlan_passthrough_info(const struct nft_pktinfo *pkt, ++ struct nft_forward_info *info) ++{ ++ struct net_bridge_port *port; ++ ++ if (!skb_vlan_tag_present(pkt->skb)) ++ return; ++ ++ rcu_read_lock(); ++ port = br_port_get_rcu(pkt->skb->dev); ++ /* when bridge vlan enabled, the vlan tag would be handled by the bridge */ ++ if (port && !br_opt_get(port->br, BROPT_VLAN_ENABLED)) { ++ if (info->num_encaps >= NF_FLOW_TABLE_ENCAP_MAX) { ++ info->indev = NULL; ++ goto out; ++ } ++ info->encap[info->num_encaps].id = skb_vlan_tag_get_id(pkt->skb); ++ info->encap[info->num_encaps].proto = pkt->skb->vlan_proto; ++ info->num_encaps++; ++ } ++out: ++ rcu_read_unlock(); ++} ++ + static void nft_dev_path_info(const struct net_device_path_stack *stack, + struct nft_forward_info *info, + unsigned char *ha, struct nf_flowtable *flowtable) +@@ -290,8 +314,12 @@ + nft_br_vlan_dev_fill_forward_path(pkt, &ctx); + } + +- if (nft_dev_fill_forward_path(&ctx, route, dst, ct, dir, ha, &stack) >= 0) ++ if (nft_dev_fill_forward_path(&ctx, route, dst, ct, dir, ha, &stack) >= 0) { ++ /* Add VLAN pass through info if needed */ ++ nft_fill_vlan_passthrough_info(pkt, &info); ++ + nft_dev_path_info(&stack, &info, ha, &ft->data); ++ } + + if (!info.indev || !nft_flowtable_find_dev(info.indev, ft)) + return -ENOENT; diff --git a/target/linux/generic/pending-6.18/454-block-add-support-for-notifications.patch b/target/linux/generic/pending-6.18/454-block-add-support-for-notifications.patch index b6ee4b53bd111e..b537bff35993b9 100644 --- a/target/linux/generic/pending-6.18/454-block-add-support-for-notifications.patch +++ b/target/linux/generic/pending-6.18/454-block-add-support-for-notifications.patch @@ -149,7 +149,7 @@ Signed-off-by: Daniel Golle +device_initcall(blk_notifications_init); --- a/include/linux/blkdev.h +++ b/include/linux/blkdev.h -@@ -1875,4 +1875,12 @@ static inline int bio_split_rw_at(struct +@@ -1871,4 +1871,12 @@ static inline int bio_split_rw_at(struct #define DEFINE_IO_COMP_BATCH(name) struct io_comp_batch name = { } diff --git a/target/linux/generic/pending-6.18/675-01-net-export-__dev_fill_forward_path-and-br_fdb_find_rcu.patch b/target/linux/generic/pending-6.18/675-01-net-export-__dev_fill_forward_path-and-br_fdb_find_rcu.patch new file mode 100644 index 00000000000000..f9344fdb2048a1 --- /dev/null +++ b/target/linux/generic/pending-6.18/675-01-net-export-__dev_fill_forward_path-and-br_fdb_find_rcu.patch @@ -0,0 +1,108 @@ +From d4f7c34d056805f03336bb971966a4176e79d09f Mon Sep 17 00:00:00 2001 +From: Yanghan Ye +Date: Mon, 13 Apr 2026 03:48:10 +0800 +Subject: [PATCH 1/4] net: export __dev_fill_forward_path and br_fdb_find_rcu + +Signed-off-by: Yanghan Ye +--- + include/linux/netdevice.h | 2 ++ + net/bridge/br_fdb.c | 1 + + net/core/dev.c | 32 ++++++++++++++++++++------------ + 3 files changed, 23 insertions(+), 12 deletions(-) + +diff --git a/include/linux/netdevice.h b/include/linux/netdevice.h +index 6065569..a20f91c 100644 +--- a/include/linux/netdevice.h ++++ b/include/linux/netdevice.h +@@ -3351,6 +3351,8 @@ void dev_remove_offload(struct packet_offload *po); + + int dev_get_iflink(const struct net_device *dev); + int dev_fill_metadata_dst(struct net_device *dev, struct sk_buff *skb); ++int __dev_fill_forward_path(struct net_device_path_ctx *ctx, const u8 *daddr, ++ struct net_device_path_stack *stack); + int dev_fill_forward_path(const struct net_device *dev, const u8 *daddr, + struct net_device_path_stack *stack); + struct net_device *dev_get_by_name(struct net *net, const char *name); +diff --git a/net/bridge/br_fdb.c b/net/bridge/br_fdb.c +index 0501ffc..3b87413 100644 +--- a/net/bridge/br_fdb.c ++++ b/net/bridge/br_fdb.c +@@ -262,6 +262,7 @@ struct net_bridge_fdb_entry *br_fdb_find_rcu(struct net_bridge *br, + { + return fdb_find_rcu(&br->fdb_hash_tbl, addr, vid); + } ++EXPORT_SYMBOL_GPL(br_fdb_find_rcu); + + /* When a static FDB entry is added, the mac address from the entry is + * added to the bridge private HW address list and all required ports +diff --git a/net/core/dev.c b/net/core/dev.c +index 6775134..df6cab2 100644 +--- a/net/core/dev.c ++++ b/net/core/dev.c +@@ -744,44 +744,52 @@ static struct net_device_path *dev_fwd_path(struct net_device_path_stack *stack) + return &stack->path[k]; + } + +-int dev_fill_forward_path(const struct net_device *dev, const u8 *daddr, +- struct net_device_path_stack *stack) ++int __dev_fill_forward_path(struct net_device_path_ctx *ctx, const u8 *daddr, ++ struct net_device_path_stack *stack) + { + const struct net_device *last_dev; +- struct net_device_path_ctx ctx = { +- .dev = dev, +- }; + struct net_device_path *path; + int ret = 0; + +- memcpy(ctx.daddr, daddr, sizeof(ctx.daddr)); ++ memcpy(ctx->daddr, daddr, sizeof(ctx->daddr)); + stack->num_paths = 0; +- while (ctx.dev && ctx.dev->netdev_ops->ndo_fill_forward_path) { +- last_dev = ctx.dev; ++ while (ctx->dev && ctx->dev->netdev_ops->ndo_fill_forward_path) { ++ last_dev = ctx->dev; + path = dev_fwd_path(stack); + if (!path) + return -1; + + memset(path, 0, sizeof(struct net_device_path)); +- ret = ctx.dev->netdev_ops->ndo_fill_forward_path(&ctx, path); ++ ret = ctx->dev->netdev_ops->ndo_fill_forward_path(ctx, path); + if (ret < 0) + return -1; + +- if (WARN_ON_ONCE(last_dev == ctx.dev)) ++ if (WARN_ON_ONCE(last_dev == ctx->dev)) + return -1; + } + +- if (!ctx.dev) ++ if (!ctx->dev) + return ret; + + path = dev_fwd_path(stack); + if (!path) + return -1; + path->type = DEV_PATH_ETHERNET; +- path->dev = ctx.dev; ++ path->dev = ctx->dev; + + return ret; + } ++EXPORT_SYMBOL_GPL(__dev_fill_forward_path); ++ ++int dev_fill_forward_path(const struct net_device *dev, const u8 *daddr, ++ struct net_device_path_stack *stack) ++{ ++ struct net_device_path_ctx ctx = { ++ .dev = dev, ++ }; ++ ++ return __dev_fill_forward_path(&ctx, daddr, stack); ++} + EXPORT_SYMBOL_GPL(dev_fill_forward_path); + + /* must be called under rcu_read_lock(), as we dont take a reference */ +-- +2.43.0 diff --git a/target/linux/generic/pending-6.18/675-02-nft_flow_offload-add-bridging-support.patch b/target/linux/generic/pending-6.18/675-02-nft_flow_offload-add-bridging-support.patch new file mode 100644 index 00000000000000..b0d4dee46ed616 --- /dev/null +++ b/target/linux/generic/pending-6.18/675-02-nft_flow_offload-add-bridging-support.patch @@ -0,0 +1,328 @@ +From 1933e4896f9d4494c6c7bc2127fba6723783f651 Mon Sep 17 00:00:00 2001 +From: Yanghan Ye +Date: Mon, 13 Apr 2026 03:48:41 +0800 +Subject: [PATCH 2/4] netfilter: nft_flow_offload: add bridging support + +Signed-off-by: Yanghan Ye +--- + net/netfilter/nft_flow_offload.c | 214 +++++++++++++++++++++++++++---- + 1 file changed, 190 insertions(+), 24 deletions(-) + +diff --git a/net/netfilter/nft_flow_offload.c b/net/netfilter/nft_flow_offload.c +index 7df7b70..988751a 100644 +--- a/net/netfilter/nft_flow_offload.c ++++ b/net/netfilter/nft_flow_offload.c +@@ -15,6 +15,7 @@ + #include + #include + #include ++#include "../bridge/br_private.h" + + struct nft_flow_offload { + struct nft_flowtable *flowtable; +@@ -46,7 +47,48 @@ static bool nft_is_valid_ether_device(const struct net_device *dev) + return true; + } + +-static int nft_dev_fill_forward_path(const struct nf_flow_route *route, ++static u16 nft_flow_offload_get_vlan_id(struct net_bridge_port *port, ++ struct sk_buff *skb) ++{ ++ u16 vlan_id = 0; ++ ++ if (!port || !br_opt_get(port->br, BROPT_VLAN_ENABLED)) ++ return 0; ++ ++ if (skb_vlan_tag_present(skb)) ++ vlan_id = skb_vlan_tag_get_id(skb); ++ else ++ br_vlan_get_pvid_rcu(skb->dev, &vlan_id); ++ ++ return vlan_id; ++} ++ ++static bool nft_flow_offload_is_bridging(struct sk_buff *skb) ++{ ++ struct net_bridge_port *port; ++ unsigned char *dmac = eth_hdr(skb)->h_dest; ++ bool bridging = false; ++ u16 vlan_id; ++ ++ if (!netif_is_bridge_port(skb->dev)) ++ return false; ++ ++ rcu_read_lock(); ++ port = br_port_get_rcu(skb->dev); ++ if (port) { ++ vlan_id = nft_flow_offload_get_vlan_id(port, skb); ++ ++ /* lookup fdb entry */ ++ if (br_fdb_find_rcu(port->br, dmac, vlan_id)) ++ bridging = true; ++ } ++ rcu_read_unlock(); ++ ++ return bridging; ++} ++ ++static int nft_dev_fill_forward_path(struct net_device_path_ctx *ctx, ++ const struct nf_flow_route *route, + const struct dst_entry *dst_cache, + const struct nf_conn *ct, + enum ip_conntrack_dir dir, u8 *ha, +@@ -60,6 +102,9 @@ static int nft_dev_fill_forward_path(const struct nf_flow_route *route, + if (!nft_is_valid_ether_device(dev)) + goto out; + ++ if (!is_zero_ether_addr(ha)) ++ goto out; ++ + n = dst_neigh_lookup(dst_cache, daddr); + if (!n) + return -1; +@@ -74,7 +119,26 @@ static int nft_dev_fill_forward_path(const struct nf_flow_route *route, + return -1; + + out: +- return dev_fill_forward_path(dev, ha, stack); ++ return __dev_fill_forward_path(ctx, ha, stack); ++} ++ ++static void nft_br_vlan_dev_fill_forward_path(const struct nft_pktinfo *pkt, ++ struct net_device_path_ctx *ctx) ++{ ++ struct net_bridge_port *port; ++ u16 vlan_id; ++ ++ rcu_read_lock(); ++ port = br_port_get_rcu(pkt->skb->dev); ++ if (port) { ++ vlan_id = nft_flow_offload_get_vlan_id(port, pkt->skb); ++ if (vlan_id) { ++ ctx->num_vlans = 1; ++ ctx->vlan[0].id = vlan_id; ++ ctx->vlan[0].proto = port->br->vlan_proto; ++ } ++ } ++ rcu_read_unlock(); + } + + struct nft_forward_info { +@@ -103,12 +167,12 @@ static void nft_dev_path_info(const struct net_device_path_stack *stack, + + for (i = 0; i < stack->num_paths; i++) { + path = &stack->path[i]; ++ info->indev = path->dev; + switch (path->type) { + case DEV_PATH_ETHERNET: + case DEV_PATH_DSA: + case DEV_PATH_VLAN: + case DEV_PATH_PPPOE: +- info->indev = path->dev; + if (is_zero_ether_addr(info->h_source)) + memcpy(info->h_source, path->dev->dev_addr, ETH_ALEN); + +@@ -150,10 +214,8 @@ static void nft_dev_path_info(const struct net_device_path_stack *stack, + info->num_encaps++; + break; + case DEV_PATH_BR_VLAN_UNTAG: +- if (WARN_ON_ONCE(info->num_encaps-- == 0)) { +- info->indev = NULL; +- break; +- } ++ if (info->num_encaps > 0) ++ info->num_encaps--; + break; + case DEV_PATH_BR_VLAN_KEEP: + break; +@@ -161,7 +223,6 @@ static void nft_dev_path_info(const struct net_device_path_stack *stack, + info->xmit_type = FLOW_OFFLOAD_XMIT_DIRECT; + break; + default: +- info->indev = NULL; + break; + } + } +@@ -192,22 +253,44 @@ static bool nft_flowtable_find_dev(const struct net_device *dev, + return found; + } + +-static void nft_dev_forward_path(struct nf_flow_route *route, +- const struct nf_conn *ct, +- enum ip_conntrack_dir dir, +- struct nft_flowtable *ft) ++static int nft_dev_forward_path(const struct nft_pktinfo *pkt, ++ struct nf_flow_route *route, ++ const struct nf_conn *ct, ++ enum ip_conntrack_dir dir, ++ struct nft_flowtable *ft) + { + const struct dst_entry *dst = route->tuple[dir].dst; + struct net_device_path_stack stack; ++ struct net_device_path_ctx ctx = { ++ .dev = dst->dev, ++ }; + struct nft_forward_info info = {}; ++ struct ethhdr *eth; ++ enum ip_conntrack_dir skb_dir; + unsigned char ha[ETH_ALEN]; + int i; + +- if (nft_dev_fill_forward_path(route, dst, ct, dir, ha, &stack) >= 0) ++ memset(ha, 0, sizeof(ha)); ++ ++ if (nft_flow_offload_is_bridging(pkt->skb) && skb_mac_header_was_set(pkt->skb)) { ++ eth = eth_hdr(pkt->skb); ++ skb_dir = CTINFO2DIR(skb_get_nfct(pkt->skb) & NFCT_INFOMASK); ++ if (skb_dir != dir) { ++ memcpy(ha, eth->h_source, ETH_ALEN); ++ memcpy(info.h_source, eth->h_dest, ETH_ALEN); ++ } else { ++ memcpy(ha, eth->h_dest, ETH_ALEN); ++ memcpy(info.h_source, eth->h_source, ETH_ALEN); ++ } ++ ++ nft_br_vlan_dev_fill_forward_path(pkt, &ctx); ++ } ++ ++ if (nft_dev_fill_forward_path(&ctx, route, dst, ct, dir, ha, &stack) >= 0) + nft_dev_path_info(&stack, &info, ha, &ft->data); + + if (!info.indev || !nft_flowtable_find_dev(info.indev, ft)) +- return; ++ return -ENOENT; + + route->tuple[!dir].in.ifindex = info.indev->ifindex; + for (i = 0; i < info.num_encaps; i++) { +@@ -224,13 +307,15 @@ static void nft_dev_forward_path(struct nf_flow_route *route, + route->tuple[dir].out.hw_ifindex = info.hw_outdev->ifindex; + route->tuple[dir].xmit_type = info.xmit_type; + } ++ ++ return 0; + } + +-static int nft_flow_route(const struct nft_pktinfo *pkt, +- const struct nf_conn *ct, +- struct nf_flow_route *route, +- enum ip_conntrack_dir dir, +- struct nft_flowtable *ft) ++static int nft_flow_route_routing(const struct nft_pktinfo *pkt, ++ const struct nf_conn *ct, ++ struct nf_flow_route *route, ++ enum ip_conntrack_dir dir, ++ struct nft_flowtable *ft) + { + struct dst_entry *this_dst = skb_dst(pkt->skb); + struct dst_entry *other_dst = NULL; +@@ -270,13 +355,89 @@ static int nft_flow_route(const struct nft_pktinfo *pkt, + nft_default_forward_path(route, this_dst, dir); + nft_default_forward_path(route, other_dst, !dir); + +- if (route->tuple[dir].xmit_type == FLOW_OFFLOAD_XMIT_NEIGH && ++ if (route->tuple[dir].xmit_type == FLOW_OFFLOAD_XMIT_NEIGH && ++ route->tuple[!dir].xmit_type == FLOW_OFFLOAD_XMIT_NEIGH) { ++ if (nft_dev_forward_path(pkt, route, ct, dir, ft) || ++ nft_dev_forward_path(pkt, route, ct, !dir, ft)) ++ return -ENOENT; ++ } ++ ++ return 0; ++} ++ ++static int ++nft_flow_route_dir(const struct nft_pktinfo *pkt, ++ const struct nf_conn *ct, ++ struct nf_flow_route *route, ++ enum ip_conntrack_dir dir, ++ int ifindex) ++{ ++ struct dst_entry *other_dst = NULL; ++ struct flowi fl; ++ ++ memset(&fl, 0, sizeof(fl)); ++ switch (nft_pf(pkt)) { ++ case NFPROTO_IPV4: ++ fl.u.ip4.daddr = ct->tuplehash[!dir].tuple.src.u3.ip; ++ fl.u.ip4.flowi4_oif = ifindex; ++ fl.u.ip4.flowi4_dscp = ip4h_dscp(ip_hdr(pkt->skb)); ++ fl.u.ip4.flowi4_mark = pkt->skb->mark; ++ fl.u.ip4.flowi4_flags = FLOWI_FLAG_ANYSRC; ++ break; ++ case NFPROTO_IPV6: ++ fl.u.ip6.saddr = ct->tuplehash[!dir].tuple.dst.u3.in6; ++ fl.u.ip6.daddr = ct->tuplehash[!dir].tuple.src.u3.in6; ++ fl.u.ip6.flowi6_oif = ifindex; ++ fl.u.ip6.flowlabel = ip6_flowinfo(ipv6_hdr(pkt->skb)); ++ fl.u.ip6.flowi6_mark = pkt->skb->mark; ++ fl.u.ip6.flowi6_flags = FLOWI_FLAG_ANYSRC; ++ break; ++ } ++ ++ nf_route(nft_net(pkt), &other_dst, &fl, false, nft_pf(pkt)); ++ if (!other_dst) ++ return -ENOENT; ++ ++ nft_default_forward_path(route, other_dst, dir); ++ ++ return 0; ++} ++ ++static int ++nft_flow_route_bridging(const struct nft_pktinfo *pkt, ++ const struct nf_conn *ct, ++ struct nf_flow_route *route, ++ enum ip_conntrack_dir dir, ++ struct nft_flowtable *ft) ++{ ++ int ret; ++ ++ ret = nft_flow_route_dir(pkt, ct, route, dir, ++ nft_out(pkt)->ifindex); ++ if (ret) ++ return ret; ++ ++ ret = nft_flow_route_dir(pkt, ct, route, !dir, ++ nft_in(pkt)->ifindex); ++ if (ret) ++ goto err_route_dir1; ++ ++ if (route->tuple[dir].xmit_type == FLOW_OFFLOAD_XMIT_NEIGH && + route->tuple[!dir].xmit_type == FLOW_OFFLOAD_XMIT_NEIGH) { +- nft_dev_forward_path(route, ct, dir, ft); +- nft_dev_forward_path(route, ct, !dir, ft); ++ if (nft_dev_forward_path(pkt, route, ct, dir, ft) || ++ nft_dev_forward_path(pkt, route, ct, !dir, ft)) { ++ ret = -ENOENT; ++ goto err_route_dir2; ++ } + } + + return 0; ++ ++err_route_dir2: ++ dst_release(route->tuple[!dir].dst); ++err_route_dir1: ++ dst_release(route->tuple[dir].dst); ++ return ret; + } + + static bool nft_flow_offload_skip(struct sk_buff *skb, int family) +@@ -364,8 +525,13 @@ static void nft_flow_offload_eval(const struct nft_expr *expr, + goto out; + + dir = CTINFO2DIR(ctinfo); +- if (nft_flow_route(pkt, ct, &route, dir, priv->flowtable) < 0) +- goto err_flow_route; ++ if (nft_flow_offload_is_bridging(pkt->skb)) { ++ if (nft_flow_route_bridging(pkt, ct, &route, dir, priv->flowtable) < 0) ++ goto err_flow_route; ++ } else { ++ if (nft_flow_route_routing(pkt, ct, &route, dir, priv->flowtable) < 0) ++ goto err_flow_route; ++ } + + flow = flow_offload_alloc(ct); + if (!flow) +-- +2.43.0 diff --git a/target/linux/generic/pending-6.18/675-03-nft_flow_offload-add-DEV_PATH_MTK_WDMA-path.patch b/target/linux/generic/pending-6.18/675-03-nft_flow_offload-add-DEV_PATH_MTK_WDMA-path.patch new file mode 100644 index 00000000000000..03e7be94fdc015 --- /dev/null +++ b/target/linux/generic/pending-6.18/675-03-nft_flow_offload-add-DEV_PATH_MTK_WDMA-path.patch @@ -0,0 +1,27 @@ +From 72ed8cc4e261041e60765deba813d3b1fb9a2455 Mon Sep 17 00:00:00 2001 +From: Yanghan Ye +Date: Mon, 13 Apr 2026 03:49:08 +0800 +Subject: [PATCH 3/4] netfilter: nft_flow_offload: add DEV_PATH_MTK_WDMA path + +Signed-off-by: Yanghan Ye +--- + net/netfilter/nft_flow_offload.c | 4 ++++ + 1 file changed, 4 insertions(+) + +diff --git a/net/netfilter/nft_flow_offload.c b/net/netfilter/nft_flow_offload.c +index 988751a..f526335 100644 +--- a/net/netfilter/nft_flow_offload.c ++++ b/net/netfilter/nft_flow_offload.c +@@ -222,6 +222,10 @@ static void nft_dev_path_info(const struct net_device_path_stack *stack, + } + info->xmit_type = FLOW_OFFLOAD_XMIT_DIRECT; + break; ++ case DEV_PATH_MTK_WDMA: ++ if (is_zero_ether_addr(info->h_source)) ++ memcpy(info->h_source, path->dev->dev_addr, ETH_ALEN); ++ break; + default: + break; + } +-- +2.43.0 diff --git a/target/linux/generic/pending-6.18/675-04-nft_flow_offload-add-vlan-passthrough-support.patch b/target/linux/generic/pending-6.18/675-04-nft_flow_offload-add-vlan-passthrough-support.patch new file mode 100644 index 00000000000000..27f4ebe91cfaf7 --- /dev/null +++ b/target/linux/generic/pending-6.18/675-04-nft_flow_offload-add-vlan-passthrough-support.patch @@ -0,0 +1,61 @@ +From ba6a1fa7bc09b319662572c7532323701ec2c9de Mon Sep 17 00:00:00 2001 +From: Yanghan Ye +Date: Mon, 13 Apr 2026 03:49:35 +0800 +Subject: [PATCH 4/4] netfilter: nft_flow_offload: add VLAN passthrough support + +Signed-off-by: Yanghan Ye +--- + net/netfilter/nft_flow_offload.c | 30 +++++++++++++++++++++++++++++- + 1 file changed, 29 insertions(+), 1 deletion(-) + +diff --git a/net/netfilter/nft_flow_offload.c b/net/netfilter/nft_flow_offload.c +index f526335..38b33be 100644 +--- a/net/netfilter/nft_flow_offload.c ++++ b/net/netfilter/nft_flow_offload.c +@@ -156,6 +156,30 @@ struct nft_forward_info { + enum flow_offload_xmit_type xmit_type; + }; + ++static void nft_fill_vlan_passthrough_info(const struct nft_pktinfo *pkt, ++ struct nft_forward_info *info) ++{ ++ struct net_bridge_port *port; ++ ++ if (!skb_vlan_tag_present(pkt->skb)) ++ return; ++ ++ rcu_read_lock(); ++ port = br_port_get_rcu(pkt->skb->dev); ++ /* when bridge vlan enabled, the vlan tag would be handled by the bridge */ ++ if (port && !br_opt_get(port->br, BROPT_VLAN_ENABLED)) { ++ if (info->num_encaps >= NF_FLOW_TABLE_ENCAP_MAX) { ++ info->indev = NULL; ++ goto out; ++ } ++ info->encap[info->num_encaps].id = skb_vlan_tag_get_id(pkt->skb); ++ info->encap[info->num_encaps].proto = pkt->skb->vlan_proto; ++ info->num_encaps++; ++ } ++out: ++ rcu_read_unlock(); ++} ++ + static void nft_dev_path_info(const struct net_device_path_stack *stack, + struct nft_forward_info *info, + unsigned char *ha, struct nf_flowtable *flowtable) +@@ -290,8 +314,12 @@ static int nft_dev_forward_path(const struct nft_pktinfo *pkt, + nft_br_vlan_dev_fill_forward_path(pkt, &ctx); + } + +- if (nft_dev_fill_forward_path(&ctx, route, dst, ct, dir, ha, &stack) >= 0) ++ if (nft_dev_fill_forward_path(&ctx, route, dst, ct, dir, ha, &stack) >= 0) { ++ /* Add VLAN pass through info if needed */ ++ nft_fill_vlan_passthrough_info(pkt, &info); ++ + nft_dev_path_info(&stack, &info, ha, &ft->data); ++ } + + if (!info.indev || !nft_flowtable_find_dev(info.indev, ft)) + return -ENOENT; +-- +2.43.0 diff --git a/target/linux/generic/pending-6.18/710-bridge-add-knob-for-filtering-rx-tx-BPDU-pack.patch b/target/linux/generic/pending-6.18/710-bridge-add-knob-for-filtering-rx-tx-BPDU-pack.patch index 034675f9511867..48cf504d5a6af0 100644 --- a/target/linux/generic/pending-6.18/710-bridge-add-knob-for-filtering-rx-tx-BPDU-pack.patch +++ b/target/linux/generic/pending-6.18/710-bridge-add-knob-for-filtering-rx-tx-BPDU-pack.patch @@ -161,7 +161,7 @@ Signed-off-by: Felix Fietkau struct rtnl_link { rtnl_doit_func doit; -@@ -5332,7 +5332,9 @@ int ndo_dflt_bridge_getlink(struct sk_bu +@@ -5340,7 +5340,9 @@ int ndo_dflt_bridge_getlink(struct sk_bu brport_nla_put_flag(skb, flags, mask, IFLA_BRPORT_MCAST_FLOOD, BR_MCAST_FLOOD) || brport_nla_put_flag(skb, flags, mask, diff --git a/target/linux/generic/pending-6.18/9990-net-add-flow-aware-forward-path-discovery.patch b/target/linux/generic/pending-6.18/9990-net-add-flow-aware-forward-path-discovery.patch new file mode 100644 index 00000000000000..5e330b1af4b804 --- /dev/null +++ b/target/linux/generic/pending-6.18/9990-net-add-flow-aware-forward-path-discovery.patch @@ -0,0 +1,74 @@ +From: Jihong Min +Date: Tue, 26 May 2026 04:53:48 +0900 +Subject: [PATCH] net: add flow-aware forward path discovery + +Some upper devices select their lower egress device from packet flow +information, so a destination address alone is not enough to describe +the same path that the transmit path will use. + +Add an optional flow key pointer to net_device_path_ctx and provide +dev_fill_forward_path_flow() for callers that can describe the packet +flow without passing a mutable skb into path discovery. Keep +dev_fill_forward_path() as a wrapper so existing callers keep the same +behaviour. + +Assisted-by: Codex:gpt-5.5 +Signed-off-by: Jihong Min +--- +--- a/include/linux/netdevice.h ++++ b/include/linux/netdevice.h +@@ -226,6 +226,7 @@ struct net_device_core_stats { + + struct neighbour; + struct neigh_parms; ++struct flow_keys; + struct sk_buff; + + struct netdev_hw_addr { +@@ -922,6 +923,7 @@ struct net_device_path_stack { + + struct net_device_path_ctx { + const struct net_device *dev; ++ const struct flow_keys *flow; + u8 daddr[ETH_ALEN]; + + int num_vlans; +@@ -3359,6 +3361,9 @@ int dev_get_iflink(const struct net_devi + int dev_fill_metadata_dst(struct net_device *dev, struct sk_buff *skb); + int __dev_fill_forward_path(struct net_device_path_ctx *ctx, const u8 *daddr, + struct net_device_path_stack *stack); ++int dev_fill_forward_path_flow(const struct net_device *dev, const u8 *daddr, ++ const struct flow_keys *flow, ++ struct net_device_path_stack *stack); + int dev_fill_forward_path(const struct net_device *dev, const u8 *daddr, + struct net_device_path_stack *stack); + struct net_device *dev_get_by_name(struct net *net, const char *name); +--- a/net/core/dev.c ++++ b/net/core/dev.c +@@ -782,15 +782,24 @@ int __dev_fill_forward_path(struct net_d + } + EXPORT_SYMBOL_GPL(__dev_fill_forward_path); + +-int dev_fill_forward_path(const struct net_device *dev, const u8 *daddr, +- struct net_device_path_stack *stack) ++int dev_fill_forward_path_flow(const struct net_device *dev, const u8 *daddr, ++ const struct flow_keys *flow, ++ struct net_device_path_stack *stack) + { + struct net_device_path_ctx ctx = { + .dev = dev, ++ .flow = flow, + }; + + return __dev_fill_forward_path(&ctx, daddr, stack); + } ++EXPORT_SYMBOL_GPL(dev_fill_forward_path_flow); ++ ++int dev_fill_forward_path(const struct net_device *dev, const u8 *daddr, ++ struct net_device_path_stack *stack) ++{ ++ return dev_fill_forward_path_flow(dev, daddr, NULL, stack); ++} + EXPORT_SYMBOL_GPL(dev_fill_forward_path); + + /* must be called under rcu_read_lock(), as we dont take a reference */ diff --git a/target/linux/generic/pending-6.18/9991-bonding-expose-selected-slave-through-forward-path.patch b/target/linux/generic/pending-6.18/9991-bonding-expose-selected-slave-through-forward-path.patch new file mode 100644 index 00000000000000..15a036df1e0045 --- /dev/null +++ b/target/linux/generic/pending-6.18/9991-bonding-expose-selected-slave-through-forward-path.patch @@ -0,0 +1,179 @@ +From: Jihong Min +Date: Tue, 26 May 2026 04:53:59 +0900 +Subject: [PATCH] bonding: expose selected slave through forward path + +Bonding can choose a physical egress slave from the transmit flow, while +forward path discovery only reports the upper device unless the bonding +driver participates in the path walk. + +Add DEV_PATH_LAG and a bonding ndo_fill_forward_path implementation. +Active-backup reports the active slave, and 802.3ad/XOR compute the same +supported layer2, layer2+3 or layer3+4 hash from immutable flow keys. +Modes that do not have a stable single slave, or policies that need +packet parsing not represented by the flow keys, keep path discovery +unsupported. + +Assisted-by: Codex:gpt-5.5 +Signed-off-by: Jihong Min +--- +--- a/drivers/net/bonding/bond_main.c ++++ b/drivers/net/bonding/bond_main.c +@@ -4195,7 +4195,7 @@ static bool bond_flow_dissect(struct bon + return true; + } + +-static u32 bond_ip_hash(u32 hash, struct flow_keys *flow, int xmit_policy) ++static u32 bond_ip_hash(u32 hash, const struct flow_keys *flow, int xmit_policy) + { + hash ^= (__force u32)flow_get_u32_dst(flow) ^ + (__force u32)flow_get_u32_src(flow); +@@ -4279,6 +4279,41 @@ static u32 bond_xmit_hash_xdp(struct bon + sizeof(struct ethhdr), xdp->data_end - xdp->data); + } + ++static bool bond_flow_hash(const struct bonding *bond, ++ const struct net_device_path_ctx *ctx, u32 *hash) ++{ ++ const struct flow_keys *flow = ctx->flow; ++ int xmit_policy = bond->params.xmit_policy; ++ u32 l2_hash; ++ ++ if (!flow) ++ return false; ++ if (flow->control.addr_type != FLOW_DISSECTOR_KEY_IPV4_ADDRS && ++ flow->control.addr_type != FLOW_DISSECTOR_KEY_IPV6_ADDRS) ++ return false; ++ ++ l2_hash = ctx->daddr[5] ^ ctx->dev->dev_addr[5] ^ ++ be16_to_cpu(flow->basic.n_proto); ++ ++ switch (xmit_policy) { ++ case BOND_XMIT_POLICY_LAYER2: ++ *hash = l2_hash; ++ return true; ++ case BOND_XMIT_POLICY_LAYER23: ++ *hash = bond_ip_hash(l2_hash, flow, xmit_policy); ++ return true; ++ case BOND_XMIT_POLICY_LAYER34: ++ if (flow->icmp.id) ++ memcpy(hash, &flow->icmp, sizeof(*hash)); ++ else ++ memcpy(hash, &flow->ports.ports, sizeof(*hash)); ++ *hash = bond_ip_hash(*hash, flow, xmit_policy); ++ return true; ++ } ++ ++ return false; ++} ++ + /*-------------------------- Device entry points ----------------------------*/ + + void bond_work_init_all(struct bonding *bond) +@@ -5197,37 +5232,34 @@ int bond_update_slave_arr(struct bonding + return ret; + } + +-static struct slave *bond_xmit_3ad_xor_slave_get(struct bonding *bond, +- struct sk_buff *skb, +- struct bond_up_slave *slaves) ++static struct slave *bond_xmit_hash_slave_get(struct bond_up_slave *slaves, ++ u32 hash) + { +- struct slave *slave; + unsigned int count; +- u32 hash; + +- hash = bond_xmit_hash(bond, skb); + count = slaves ? READ_ONCE(slaves->count) : 0; + if (unlikely(!count)) + return NULL; + +- slave = slaves->arr[hash % count]; +- return slave; ++ return slaves->arr[hash % count]; ++} ++ ++static struct slave *bond_xmit_3ad_xor_slave_get(struct bonding *bond, ++ struct sk_buff *skb, ++ struct bond_up_slave *slaves) ++{ ++ return bond_xmit_hash_slave_get(slaves, bond_xmit_hash(bond, skb)); + } + + static struct slave *bond_xdp_xmit_3ad_xor_slave_get(struct bonding *bond, + struct xdp_buff *xdp) + { + struct bond_up_slave *slaves; +- unsigned int count; + u32 hash; + + hash = bond_xmit_hash_xdp(bond, xdp); + slaves = rcu_dereference(bond->usable_slaves); +- count = slaves ? READ_ONCE(slaves->count) : 0; +- if (unlikely(!count)) +- return NULL; +- +- return slaves->arr[hash % count]; ++ return bond_xmit_hash_slave_get(slaves, hash); + } + + static bool bond_should_broadcast_neighbor(struct sk_buff *skb, +@@ -5424,6 +5456,40 @@ static struct net_device *bond_xmit_get_ + return NULL; + } + ++static int bond_fill_forward_path(struct net_device_path_ctx *ctx, ++ struct net_device_path *path) ++{ ++ struct bonding *bond = netdev_priv(ctx->dev); ++ struct slave *slave = NULL; ++ struct bond_up_slave *slaves; ++ u32 hash; ++ ++ switch (BOND_MODE(bond)) { ++ case BOND_MODE_ACTIVEBACKUP: ++ slave = bond_xmit_activebackup_slave_get(bond); ++ break; ++ case BOND_MODE_8023AD: ++ case BOND_MODE_XOR: ++ if (!bond_flow_hash(bond, ctx, &hash)) ++ return -EOPNOTSUPP; ++ ++ slaves = rcu_dereference(bond->usable_slaves); ++ slave = bond_xmit_hash_slave_get(slaves, hash); ++ break; ++ default: ++ return -EOPNOTSUPP; ++ } ++ ++ if (!slave) ++ return -ENODEV; ++ ++ path->type = DEV_PATH_LAG; ++ path->dev = ctx->dev; ++ ctx->dev = slave->dev; ++ ++ return 0; ++} ++ + static void bond_sk_to_flow(struct sock *sk, struct flow_keys *flow) + { + switch (sk->sk_family) { +@@ -5893,6 +5959,7 @@ static const struct net_device_ops bond_ + .ndo_features_check = passthru_features_check, + .ndo_get_xmit_slave = bond_xmit_get_slave, + .ndo_sk_get_lower_dev = bond_sk_get_lower_dev, ++ .ndo_fill_forward_path = bond_fill_forward_path, + .ndo_bpf = bond_xdp, + .ndo_xdp_xmit = bond_xdp_xmit, + .ndo_xdp_get_xmit_slave = bond_xdp_get_xmit_slave, +--- a/include/linux/netdevice.h ++++ b/include/linux/netdevice.h +@@ -869,6 +869,7 @@ enum net_device_path_type { + DEV_PATH_ETHERNET = 0, + DEV_PATH_VLAN, + DEV_PATH_BRIDGE, ++ DEV_PATH_LAG, + DEV_PATH_PPPOE, + DEV_PATH_DSA, + DEV_PATH_MTK_WDMA, diff --git a/target/linux/generic/pending-6.18/9992-netfilter-flowtable-use-hardware-path-device-for-direct-redirect.patch b/target/linux/generic/pending-6.18/9992-netfilter-flowtable-use-hardware-path-device-for-direct-redirect.patch new file mode 100644 index 00000000000000..9a7867b29b2b7e --- /dev/null +++ b/target/linux/generic/pending-6.18/9992-netfilter-flowtable-use-hardware-path-device-for-direct-redirect.patch @@ -0,0 +1,108 @@ +From: Jihong Min +Date: Tue, 26 May 2026 04:54:11 +0900 +Subject: [PATCH] netfilter: flowtable: use hardware path device for direct redirect + +Forward path discovery can cross an upper device whose software transmit +device differs from the lower device that hardware should redirect to. +This happens for bonding once the bonding driver exposes the selected +slave through fill_forward_path. + +Keep the direct software output ifindex in the existing out.ifidx field +and add a separate hardware output ifindex for the flow action redirect. +When the path includes DEV_PATH_LAG, preserve the LAG master as the +software output device and use the final lower device as the hardware +redirect device. + +Build immutable flow keys from the post-NAT flowtable tuple for each +direction, so bonding can select the same slave without touching the +current skb or reusing it for the opposite direction. + +Assisted-by: Codex:gpt-5.5 +Signed-off-by: Jihong Min +--- +--- a/net/netfilter/nft_flow_offload.c ++++ b/net/netfilter/nft_flow_offload.c +@@ -8,6 +8,7 @@ + #include + #include + #include ++#include + #include + #include + #include +@@ -137,6 +138,41 @@ + return __dev_fill_forward_path(ctx, ha, stack); + } + ++static void nft_flow_keys_init(const struct nf_conn *ct, ++ enum ip_conntrack_dir dir, ++ struct flow_keys *flow) ++{ ++ const struct nf_conntrack_tuple *tuple = &ct->tuplehash[dir].tuple; ++ const struct nf_conntrack_tuple *other = &ct->tuplehash[!dir].tuple; ++ ++ memset(flow, 0, sizeof(*flow)); ++ ++ /* Match the packet tuple after flowtable NAT mangling. */ ++ switch (tuple->src.l3num) { ++ case NFPROTO_IPV4: ++ flow->control.addr_type = FLOW_DISSECTOR_KEY_IPV4_ADDRS; ++ flow->basic.n_proto = htons(ETH_P_IP); ++ flow->addrs.v4addrs.src = other->dst.u3.ip; ++ flow->addrs.v4addrs.dst = other->src.u3.ip; ++ break; ++ case NFPROTO_IPV6: ++ flow->control.addr_type = FLOW_DISSECTOR_KEY_IPV6_ADDRS; ++ flow->basic.n_proto = htons(ETH_P_IPV6); ++ flow->addrs.v6addrs.src = other->dst.u3.in6; ++ flow->addrs.v6addrs.dst = other->src.u3.in6; ++ break; ++ } ++ ++ flow->basic.ip_proto = tuple->dst.protonum; ++ switch (tuple->dst.protonum) { ++ case IPPROTO_TCP: ++ case IPPROTO_UDP: ++ flow->ports.src = other->dst.u.all; ++ flow->ports.dst = other->src.u.all; ++ break; ++ } ++} ++ + static void nft_br_vlan_dev_fill_forward_path(const struct nft_pktinfo *pkt, + struct net_device_path_ctx *ctx) + { +@@ -211,12 +247,17 @@ + case DEV_PATH_ETHERNET: + case DEV_PATH_DSA: + case DEV_PATH_VLAN: ++ case DEV_PATH_LAG: + case DEV_PATH_PPPOE: + if (is_zero_ether_addr(info->h_source)) + memcpy(info->h_source, path->dev->dev_addr, ETH_ALEN); + + if (path->type == DEV_PATH_ETHERNET) + break; ++ if (path->type == DEV_PATH_LAG) { ++ info->outdev = path->dev; ++ break; ++ } + if (path->type == DEV_PATH_DSA) { + i = stack->num_paths; + break; +@@ -311,12 +352,15 @@ + .dev = dst->dev, + }; + struct nft_forward_info info = {}; ++ struct flow_keys flow; + struct ethhdr *eth; + enum ip_conntrack_dir skb_dir; + unsigned char ha[ETH_ALEN]; + int i; + + memset(ha, 0, sizeof(ha)); ++ nft_flow_keys_init(ct, dir, &flow); ++ ctx.flow = &flow; + + if (nft_flow_offload_is_bridging(pkt->skb)) { + enum ip_conntrack_info pkt_ctinfo; diff --git a/target/linux/generic/pending-6.18/9993-xfrm-allow-packet-offload-drivers-to-own-transmit.patch b/target/linux/generic/pending-6.18/9993-xfrm-allow-packet-offload-drivers-to-own-transmit.patch new file mode 100644 index 00000000000000..aa05d59c0924bb --- /dev/null +++ b/target/linux/generic/pending-6.18/9993-xfrm-allow-packet-offload-drivers-to-own-transmit.patch @@ -0,0 +1,68 @@ +From: Jihong Min +Date: Sat, 23 May 2026 20:05:10 +0900 +Subject: [PATCH] xfrm: allow packet offload drivers to own transmit + +Some packet-mode ESP engines are reachable through a device-private +descriptor path rather than through the normal netdev hard-start path. The +generic packet offload path can already hand plaintext packets directly to +the offload netdev, but that assumes the device transmit path itself is the +hardware IPsec path. + +Add an optional xfrmdev_ops transmit callback so a driver can consume the +packet offload skb and submit it through its private packet engine path. If +the callback is absent, keep the existing packet offload direct-output +behavior unchanged. + +The Airoha SOE path uses this hook to submit ESP packets through its QDMA hop +descriptor path instead of relying on the regular netdev hard-start transmit +entry point. + +Assisted-by: Codex:gpt-5.5 +Signed-off-by: Jihong Min +--- + include/linux/netdevice.h | 3 +++ + net/xfrm/xfrm_output.c | 11 +++++++++++ + 2 files changed, 14 insertions(+) + +diff --git a/include/linux/netdevice.h b/include/linux/netdevice.h +index d593dda..db59844 100644 +--- a/include/linux/netdevice.h ++++ b/include/linux/netdevice.h +@@ -1035,6 +1035,9 @@ struct xfrmdev_ops { + int (*xdo_dev_policy_add) (struct xfrm_policy *x, struct netlink_ext_ack *extack); + void (*xdo_dev_policy_delete) (struct xfrm_policy *x); + void (*xdo_dev_policy_free) (struct xfrm_policy *x); ++ /* Optional packet-offload TX path. The driver consumes skb. */ ++ int (*xdo_dev_packet_xmit)(struct sk_buff *skb, ++ struct xfrm_state *x); + }; + #endif + +diff --git a/net/xfrm/xfrm_output.c b/net/xfrm/xfrm_output.c +index 54222fc..e1cca58 100644 +--- a/net/xfrm/xfrm_output.c ++++ b/net/xfrm/xfrm_output.c +@@ -763,12 +763,23 @@ int xfrm_output(struct sock *sk, struct sk_buff *skb) + } + + if (x->xso.type == XFRM_DEV_OFFLOAD_PACKET) { ++#ifdef CONFIG_XFRM_OFFLOAD ++ const struct xfrmdev_ops *ops; ++#endif ++ ++#ifdef CONFIG_XFRM_OFFLOAD ++ ops = x->xso.dev->xfrmdev_ops; ++ /* Callback validates, consumes skb and returns final TX status. */ ++ if (ops && ops->xdo_dev_packet_xmit) ++ return ops->xdo_dev_packet_xmit(skb, x); ++#endif ++ + if (!xfrm_dev_offload_ok(skb, x)) { + XFRM_INC_STATS(net, LINUX_MIB_XFRMOUTERROR); + kfree_skb(skb); + return -EHOSTUNREACH; + } + + /* Exclusive direct xmit for tunnel mode, as + * some filtering or matching rules may apply + * in transport mode. diff --git a/target/linux/generic/pending-6.18/9999-01-xfrm-add-a-lower-device-offload-handle-resolver.patch b/target/linux/generic/pending-6.18/9999-01-xfrm-add-a-lower-device-offload-handle-resolver.patch new file mode 100644 index 00000000000000..b50c90194f8108 --- /dev/null +++ b/target/linux/generic/pending-6.18/9999-01-xfrm-add-a-lower-device-offload-handle-resolver.patch @@ -0,0 +1,176 @@ +From ddf38b0a103b3adb6afd0461dc453bf9d10b3234 Mon Sep 17 00:00:00 2001 +From: Jihong Min +Date: Wed, 20 May 2026 16:47:32 +0900 +Subject: [PATCH] xfrm: add a lower-device offload handle resolver + +An upper device can own an XFRM offload state while the selected datapath +device is one of its lower devices. A single xso.offload_handle is not +enough for that case because each lower device may return a different +hardware handle for the same state. + +Add an optional xfrmdev_ops resolver and a lower-driver opt-in flag so +helper-aware lower drivers can resolve the handle for the lower device +they are transmitting or receiving on. Keep the direct-device path as +the fast path and clear upper private state when device offload state is +freed. + +Assisted-by: Codex:gpt-5.5 +Signed-off-by: Jihong Min +--- + include/linux/netdevice.h | 27 +++++++++++++++++++++ + include/net/xfrm.h | 50 +++++++++++++++++++++++++++++++++++++-- + net/xfrm/xfrm_state.c | 1 + + 3 files changed, 76 insertions(+), 2 deletions(-) + +diff --git a/include/linux/netdevice.h b/include/linux/netdevice.h +index 0e1e581efc5a..b4e844e90db8 100644 +--- a/include/linux/netdevice.h ++++ b/include/linux/netdevice.h +@@ -1033,6 +1033,16 @@ struct netdev_bpf { + #define XDP_WAKEUP_TX (1 << 1) + + #ifdef CONFIG_XFRM_OFFLOAD ++/* ++ * xfrmdev_ops.flags values. ++ * ++ * XFRMDEV_OPS_F_LOWER_HANDLE marks a lower driver whose datapath gets XFRM ++ * hardware handles with xfrm_dev_state_lower_handle(). This is required when ++ * the XFRM state is owned by an upper device because xso.offload_handle may ++ * not contain the handle for the current lower device. ++ */ ++#define XFRMDEV_OPS_F_LOWER_HANDLE BIT(0) ++ + struct xfrmdev_ops { + int (*xdo_dev_state_add)(struct net_device *dev, + struct xfrm_state *x, +@@ -1035,9 +1045,26 @@ struct xfrmdev_ops { + int (*xdo_dev_policy_add) (struct xfrm_policy *x, struct netlink_ext_ack *extack); + void (*xdo_dev_policy_delete) (struct xfrm_policy *x); + void (*xdo_dev_policy_free) (struct xfrm_policy *x); ++ /* ++ * Resolve the offload handle for lower_dev when this upper device ++ * owns the XFRM state. This belongs in xfrmdev_ops because the ++ * resolver is an XFRM offload operation of the device that owns the ++ * state. Keeping the dispatch here avoids a bonding-specific dependency ++ * in the XFRM helper. ++ * ++ * Upper devices like bonding may implement this callback when they ++ * keep the lower-device handle mapping. Lower devices must leave it ++ * NULL because they do not own that map. Lower drivers advertise ++ * that their datapath calls the resolver with ++ * XFRMDEV_OPS_F_LOWER_HANDLE instead. ++ */ ++ unsigned long (*xdo_dev_state_lower_handle)(struct net_device *dev, ++ struct xfrm_state *x, ++ struct net_device *lower_dev); ++ u32 flags; + /* Optional packet-offload TX path. The driver consumes skb. */ + int (*xdo_dev_packet_xmit)(struct sk_buff *skb, + struct xfrm_state *x); + }; + #endif + +diff --git a/include/net/xfrm.h b/include/net/xfrm.h +index 874409127e29..69a33da10a6a 100644 +--- a/include/net/xfrm.h ++++ b/include/net/xfrm.h +@@ -162,6 +162,10 @@ struct xfrm_dev_offload { + */ + struct net_device *real_dev; + unsigned long offload_handle; ++ /* Private state owned by dev in this structure when that device is an ++ * upper device. Lower drivers must not use this directly. ++ */ ++ void __rcu *upper_priv; + u8 dir : 2; + u8 type : 2; + u8 flags : 2; +@@ -1701,6 +1705,40 @@ struct xfrm_state *xfrm_state_lookup_byspi(struct net *net, __be32 spi, + int xfrm_state_check_expire(struct xfrm_state *x); + void xfrm_state_update_stats(struct net *net); + #ifdef CONFIG_XFRM_OFFLOAD ++/* ++ * Return the hardware offload handle lower_dev should use for x. States ++ * installed directly on lower_dev use a nonzero xso.offload_handle. During ++ * upper-device callbacks, xso.real_dev may identify the lower device whose ++ * callback is running; use xso.offload_handle directly in that case. Other ++ * states owned by an upper device are resolved through the owner's ++ * xdo_dev_state_lower_handle(). Bonding uses that callback for replicated XFRM ++ * states because it installs the state on each slave and keeps the per-slave ++ * hardware handles internally. ++ */ ++static inline unsigned long ++xfrm_dev_state_lower_handle(struct xfrm_state *x, struct net_device *lower_dev) ++{ ++ struct xfrm_dev_offload *xdo = &x->xso; ++ struct net_device *real_dev = READ_ONCE(xdo->real_dev); ++ struct net_device *dev = READ_ONCE(xdo->dev); ++ unsigned long offload_handle = READ_ONCE(xdo->offload_handle); ++ ++ if (!dev || !lower_dev) ++ return 0; ++ ++ if (dev == lower_dev && offload_handle) ++ return offload_handle; ++ ++ if (real_dev == lower_dev && offload_handle) ++ return offload_handle; ++ ++ if (dev->xfrmdev_ops && dev->xfrmdev_ops->xdo_dev_state_lower_handle) ++ return dev->xfrmdev_ops->xdo_dev_state_lower_handle(dev, x, ++ lower_dev); ++ ++ return 0; ++} ++ + static inline void xfrm_dev_state_update_stats(struct xfrm_state *x) + { + struct xfrm_dev_offload *xdo = &x->xso; +@@ -1712,6 +1750,12 @@ static inline void xfrm_dev_state_update_stats(struct xfrm_state *x) + + } + #else ++static inline unsigned long ++xfrm_dev_state_lower_handle(struct xfrm_state *x, struct net_device *lower_dev) ++{ ++ return 0; ++} ++ + static inline void xfrm_dev_state_update_stats(struct xfrm_state *x) {} + #endif + void xfrm_state_insert(struct xfrm_state *x); +@@ -2090,15 +2134,18 @@ static inline void xfrm_dev_state_advance_esn(struct xfrm_state *x) + static inline bool xfrm_dst_offload_ok(struct dst_entry *dst) + { + struct xfrm_state *x = dst->xfrm; ++ bool has_offload_state; + struct xfrm_dst *xdst; + + if (!x || !x->type_offload) + return false; + + xdst = (struct xfrm_dst *) dst; +- if (!x->xso.offload_handle && !xdst->child->xfrm) ++ has_offload_state = x->xso.offload_handle || ++ rcu_access_pointer(x->xso.upper_priv); ++ if (!has_offload_state && !xdst->child->xfrm) + return true; +- if (x->xso.offload_handle && (x->xso.dev == xfrm_dst_path(dst)->dev) && ++ if (has_offload_state && (x->xso.dev == xfrm_dst_path(dst)->dev) && + !xdst->child->xfrm) + return true; + +diff --git a/net/xfrm/xfrm_state.c b/net/xfrm/xfrm_state.c +index 589c3b6e4679..90785e9ba159 100644 +--- a/net/xfrm/xfrm_state.c ++++ b/net/xfrm/xfrm_state.c +@@ -791,6 +791,7 @@ void xfrm_dev_state_free(struct xfrm_state *x) + if (dev->xfrmdev_ops->xdo_dev_state_free) + dev->xfrmdev_ops->xdo_dev_state_free(dev, x); + WRITE_ONCE(xso->dev, NULL); ++ RCU_INIT_POINTER(xso->upper_priv, NULL); + xso->type = XFRM_DEV_OFFLOAD_UNSPECIFIED; + netdev_put(dev, &xso->dev_tracker); + } +-- +2.53.0 diff --git a/target/linux/generic/pending-6.18/9999-02-bonding-replicate-XFRM-offload-state-across-LAG-slaves.patch b/target/linux/generic/pending-6.18/9999-02-bonding-replicate-XFRM-offload-state-across-LAG-slaves.patch new file mode 100644 index 00000000000000..a1c43005f4a2df --- /dev/null +++ b/target/linux/generic/pending-6.18/9999-02-bonding-replicate-XFRM-offload-state-across-LAG-slaves.patch @@ -0,0 +1,1116 @@ +From e95c85030e2503da42eacd27d86eb32af9f77c37 Mon Sep 17 00:00:00 2001 +From: Jihong Min +Date: Wed, 20 May 2026 16:50:16 +0900 +Subject: [PATCH] bonding: replicate XFRM offload state across LAG slaves + +LAG bonds need to install the same IPsec/XFRM state on every eligible +lower device, but each lower device may return a different hardware +handle. Add a replicated bonding-private XFRM state object that stores +per-lower-device instances and handles. + +Use the replicated model for 802.3ad and balance-xor with layer3+4 +hashing. Install the state on every eligible running slave, capture each +lower handle, and roll back in reverse order on failure. Keep +active-backup on the existing single-lower path, expose a bonding +resolver for lower drivers that call xfrm_dev_state_lower_handle(), and +forward packet-offload transmit callbacks to the selected slave. Expose +the selected slave ifindex so hardware flow redirects use the same lower +path as packet offload transmit. + +Assisted-by: Codex:gpt-5.5 +Signed-off-by: Jihong Min +--- + drivers/net/bonding/bond_main.c | 761 ++++++++++++++++++++++++++++++++++++++- + include/linux/netdevice.h | 8 + + include/net/bonding.h | 29 +- + include/net/xfrm.h | 21 ++ + net/xfrm/xfrm_state.c | 26 +- + 5 files changed, 831 insertions(+), 14 deletions(-) + +diff --git a/drivers/net/bonding/bond_main.c b/drivers/net/bonding/bond_main.c +index 82e779f7916b..fcfe7a65f41d 100644 +--- a/drivers/net/bonding/bond_main.c ++++ b/drivers/net/bonding/bond_main.c +@@ -455,6 +455,438 @@ static struct net_device *bond_ipsec_dev(struct xfrm_state *xs) + return slave->dev; + } + ++static void bond_ipsec_inst_rcu_free(struct rcu_head *rcu) ++{ ++ struct bond_ipsec_inst *inst; ++ ++ inst = container_of(rcu, struct bond_ipsec_inst, rcu); ++ netdev_put(inst->real_dev, &inst->dev_tracker); ++ kfree(inst); ++} ++ ++static void bond_ipsec_rcu_free(struct rcu_head *rcu) ++{ ++ struct bond_ipsec *ipsec; ++ ++ ipsec = container_of(rcu, struct bond_ipsec, rcu); ++ kfree(ipsec); ++} ++ ++static bool bond_ipsec_slave_has_xfrm_ops(struct net_device *real_dev) ++{ ++ const struct xfrmdev_ops *ops; ++ ++ if (!real_dev || netif_is_bond_master(real_dev)) ++ return false; ++ ++ ops = real_dev->xfrmdev_ops; ++ if (!ops) ++ return false; ++ ++ return ops->xdo_dev_state_add && ops->xdo_dev_state_delete; ++} ++ ++static bool bond_ipsec_lag_slave_has_ops(struct net_device *real_dev) ++{ ++ return bond_ipsec_slave_has_xfrm_ops(real_dev) && ++ real_dev->xfrmdev_ops->flags & XFRMDEV_OPS_F_LOWER_HANDLE; ++} ++ ++static bool bond_ipsec_lag_slave_ok(struct net_device *real_dev) ++{ ++ return (real_dev->features & NETIF_F_HW_ESP) && ++ bond_ipsec_lag_slave_has_ops(real_dev); ++} ++ ++static void bond_ipsec_lag_free_instances(struct bond_ipsec *ipsec) ++{ ++ struct bond_ipsec_inst *inst, *tmp; ++ ++ list_for_each_entry_safe(inst, tmp, &ipsec->inst_list, list) { ++ list_del_rcu(&inst->list); ++ call_rcu(&inst->rcu, bond_ipsec_inst_rcu_free); ++ } ++} ++ ++static void bond_ipsec_lag_call_inst(struct xfrm_state *xs, ++ struct bond_ipsec_inst *inst, ++ bool delete_state, ++ bool free_state) ++{ ++ unsigned long bond_handle = READ_ONCE(xs->xso.offload_handle); ++ unsigned long lower_handle = READ_ONCE(inst->lower_handle); ++ struct net_device *bond_real_dev = READ_ONCE(xs->xso.real_dev); ++ const struct xfrmdev_ops *ops = inst->real_dev->xfrmdev_ops; ++ ++ if (!lower_handle) ++ return; ++ ++ if (!ops) ++ return; ++ ++ WRITE_ONCE(xs->xso.real_dev, inst->real_dev); ++ WRITE_ONCE(xs->xso.offload_handle, lower_handle); ++ if (delete_state) { ++ if (!inst->deleted && ops->xdo_dev_state_delete) { ++ ops->xdo_dev_state_delete(inst->real_dev, xs); ++ WRITE_ONCE(xs->xso.offload_handle, lower_handle); ++ inst->deleted = true; ++ } ++ WRITE_ONCE(inst->added, false); ++ } ++ if (free_state && ops->xdo_dev_state_free) ++ ops->xdo_dev_state_free(inst->real_dev, xs); ++ if (free_state) ++ WRITE_ONCE(inst->lower_handle, 0); ++ ++ WRITE_ONCE(xs->xso.real_dev, bond_real_dev); ++ WRITE_ONCE(xs->xso.offload_handle, bond_handle); ++} ++ ++static void bond_ipsec_lag_call_state(struct xfrm_state *xs, ++ struct bond_ipsec *ipsec, ++ bool delete_state, ++ bool free_state) ++{ ++ struct bond_ipsec_inst *inst; ++ ++ list_for_each_entry_reverse(inst, &ipsec->inst_list, list) { ++ bond_ipsec_lag_call_inst(xs, inst, delete_state, free_state); ++ } ++} ++ ++static int bond_ipsec_lag_add_inst(struct xfrm_state *xs, ++ struct bond_ipsec_inst *inst, ++ struct netlink_ext_ack *extack) ++{ ++ unsigned long bond_handle = xs->xso.offload_handle; ++ struct net_device *bond_real_dev = xs->xso.real_dev; ++ unsigned long lower_handle; ++ const struct xfrmdev_ops *ops; ++ int err; ++ ++ if (!bond_ipsec_lag_slave_ok(inst->real_dev)) ++ return -EOPNOTSUPP; ++ ++ ops = inst->real_dev->xfrmdev_ops; ++ xs->xso.real_dev = inst->real_dev; ++ xs->xso.offload_handle = 0; ++ err = ops->xdo_dev_state_add(inst->real_dev, xs, extack); ++ if (err) ++ goto out; ++ ++ lower_handle = xs->xso.offload_handle; ++ if (!lower_handle) { ++ err = -EINVAL; ++ NL_SET_ERR_MSG_MOD(extack, "Slave did not return an IPsec offload handle"); ++ if (ops->xdo_dev_state_delete) ++ ops->xdo_dev_state_delete(inst->real_dev, xs); ++ if (ops->xdo_dev_state_free) ++ ops->xdo_dev_state_free(inst->real_dev, xs); ++ goto out; ++ } ++ ++ WRITE_ONCE(inst->lower_handle, lower_handle); ++ inst->deleted = false; ++ WRITE_ONCE(inst->added, true); ++ ++out: ++ xs->xso.real_dev = bond_real_dev; ++ xs->xso.offload_handle = bond_handle; ++ return err; ++} ++ ++static int bond_ipsec_lag_add_sa(struct net_device *bond_dev, ++ struct xfrm_state *xs, ++ struct netlink_ext_ack *extack) ++{ ++ struct bonding *bond = netdev_priv(bond_dev); ++ struct bond_ipsec_inst *inst; ++ struct bond_ipsec *ipsec; ++ struct list_head *iter; ++ struct slave *slave; ++ int err = 0; ++ int count = 0; ++ ++ if (xs->xso.type != XFRM_DEV_OFFLOAD_CRYPTO && ++ xs->xso.type != XFRM_DEV_OFFLOAD_PACKET) { ++ NL_SET_ERR_MSG_MOD(extack, ++ "LAG supports only XFRM crypto or packet offload"); ++ return -EOPNOTSUPP; ++ } ++ ++ if (xs->props.flags & XFRM_STATE_ESN) { ++ NL_SET_ERR_MSG_MOD(extack, "LAG does not support XFRM ESN offload"); ++ return -EOPNOTSUPP; ++ } ++ ++ ipsec = kmalloc_obj(*ipsec, GFP_KERNEL); ++ if (!ipsec) ++ return -ENOMEM; ++ ++ ipsec->xs = xs; ++ ipsec->replicated = true; ++ INIT_LIST_HEAD(&ipsec->list); ++ INIT_LIST_HEAD(&ipsec->inst_list); ++ ++ /* Serialize with slave down/remove and LAG eligibility changes so they ++ * cannot miss lower SAs installed before this state is published. ++ */ ++ mutex_lock(&bond->ipsec_lock); ++ if (bond->ipsec_lag_blocked) { ++ err = -EAGAIN; ++ NL_SET_ERR_MSG_MOD(extack, "Bond LAG XFRM state add is blocked"); ++ goto err_free_unlock; ++ } ++ if (!(bond_dev->features & NETIF_F_HW_ESP)) { ++ err = -EOPNOTSUPP; ++ NL_SET_ERR_MSG_MOD(extack, "Bond IPsec offload is disabled"); ++ goto err_free_unlock; ++ } ++ if (!bond_mode_can_use_lag_xfrm(bond)) { ++ err = -EAGAIN; ++ NL_SET_ERR_MSG_MOD(extack, "Bond LAG XFRM eligibility changed"); ++ goto err_free_unlock; ++ } ++ rcu_read_lock(); ++ bond_for_each_slave_rcu(bond, slave, iter) { ++ struct net_device *real_dev = slave->dev; ++ ++ if (!netif_running(real_dev)) ++ continue; ++ ++ if (!bond_ipsec_lag_slave_ok(real_dev)) { ++ err = -EOPNOTSUPP; ++ break; ++ } ++ ++ inst = kzalloc_obj(*inst, GFP_ATOMIC); ++ if (!inst) { ++ err = -ENOMEM; ++ break; ++ } ++ ++ inst->real_dev = real_dev; ++ netdev_hold(real_dev, &inst->dev_tracker, GFP_ATOMIC); ++ list_add_tail(&inst->list, &ipsec->inst_list); ++ count++; ++ } ++ rcu_read_unlock(); ++ ++ if (!err && !count) ++ err = -ENODEV; ++ if (err) { ++ if (err == -EOPNOTSUPP) ++ NL_SET_ERR_MSG_MOD(extack, "Not all slaves support IPsec offload"); ++ goto err_free_unlock; ++ } ++ ++ list_for_each_entry(inst, &ipsec->inst_list, list) { ++ err = bond_ipsec_lag_add_inst(xs, inst, extack); ++ if (err) ++ goto err_delete; ++ } ++ ++ xs->xso.real_dev = NULL; ++ xs->xso.offload_handle = 0; ++ if (!bond_mode_can_use_lag_xfrm(bond)) { ++ err = -EAGAIN; ++ NL_SET_ERR_MSG_MOD(extack, "Bond LAG XFRM eligibility changed"); ++ goto err_delete; ++ } ++ rcu_assign_pointer(xs->xso.upper_priv, ipsec); ++ list_add(&ipsec->list, &bond->ipsec_list); ++ mutex_unlock(&bond->ipsec_lock); ++ ++ return 0; ++ ++err_delete: ++ bond_ipsec_lag_call_state(xs, ipsec, true, true); ++ xs->xso.real_dev = NULL; ++ xs->xso.offload_handle = 0; ++ RCU_INIT_POINTER(xs->xso.upper_priv, NULL); ++err_free_unlock: ++ mutex_unlock(&bond->ipsec_lock); ++ bond_ipsec_lag_free_instances(ipsec); ++ kfree(ipsec); ++ return err; ++} ++ ++static void bond_ipsec_lag_flush_pending(struct bonding *bond) ++{ ++ struct bond_ipsec *ipsec, *tmp; ++ ++ /* Caller must hold ipsec_lock to serialize with LAG SA add. */ ++ list_for_each_entry_safe(ipsec, tmp, &bond->ipsec_list, list) { ++ struct xfrm_dev_offload *xso; ++ struct xfrm_state *xs; ++ struct net *net; ++ bool pending; ++ ++ if (!ipsec->replicated) ++ continue; ++ ++ xs = ipsec->xs; ++ net = xs_net(xs); ++ spin_lock_bh(&net->xfrm.xfrm_state_lock); ++ pending = hlist_unhashed(&xs->bydst) && ++ xs->km.state != XFRM_STATE_DEAD; ++ if (pending) ++ xs->km.state = XFRM_STATE_DEAD; ++ spin_unlock_bh(&net->xfrm.xfrm_state_lock); ++ if (!pending) ++ continue; ++ ++ /* The state is between successful offload add and XFRM hash ++ * insertion. Marking it DEAD under xfrm_state_lock makes ++ * xfrm_state_add()/xfrm_state_insert() fail instead of ++ * publishing a state whose lower hardware handles are being ++ * synchronously torn down here. ++ */ ++ xso = &xs->xso; ++ list_del(&ipsec->list); ++ bond_ipsec_lag_call_state(xs, ipsec, true, true); ++ RCU_INIT_POINTER(xso->upper_priv, NULL); ++ bond_ipsec_lag_free_instances(ipsec); ++ call_rcu(&ipsec->rcu, bond_ipsec_rcu_free); ++ ++ WRITE_ONCE(xso->real_dev, NULL); ++ WRITE_ONCE(xso->offload_handle, 0); ++ } ++} ++ ++void bond_ipsec_lag_begin_flush(struct bonding *bond) ++{ ++ mutex_lock(&bond->ipsec_lock); ++ bond->ipsec_lag_blocked = true; ++ bond_ipsec_lag_flush_pending(bond); ++ mutex_unlock(&bond->ipsec_lock); ++} ++ ++void bond_ipsec_lag_end_flush(struct bonding *bond) ++{ ++ mutex_lock(&bond->ipsec_lock); ++ bond->ipsec_lag_blocked = false; ++ mutex_unlock(&bond->ipsec_lock); ++} ++ ++static void bond_ipsec_lag_remove_slave(struct bonding *bond, ++ struct net_device *real_dev) ++{ ++ struct bond_ipsec_inst *inst, *tmp; ++ struct bond_ipsec *ipsec; ++ bool removed = false; ++ ++ if (!bond_mode_can_use_lag_xfrm(bond)) ++ return; ++ ++ mutex_lock(&bond->ipsec_lock); ++ list_for_each_entry(ipsec, &bond->ipsec_list, list) { ++ if (!ipsec->replicated) ++ continue; ++ ++ list_for_each_entry(inst, &ipsec->inst_list, list) { ++ if (inst->real_dev != real_dev) ++ continue; ++ ++ WRITE_ONCE(inst->added, false); ++ removed = true; ++ } ++ } ++ if (!removed) ++ goto out; ++ ++ synchronize_net(); ++ ++ list_for_each_entry(ipsec, &bond->ipsec_list, list) { ++ if (!ipsec->replicated) ++ continue; ++ ++ list_for_each_entry_safe(inst, tmp, &ipsec->inst_list, list) { ++ if (inst->real_dev != real_dev) ++ continue; ++ ++ bond_ipsec_lag_call_inst(ipsec->xs, inst, true, true); ++ list_del_rcu(&inst->list); ++ call_rcu(&inst->rcu, bond_ipsec_inst_rcu_free); ++ } ++ } ++out: ++ mutex_unlock(&bond->ipsec_lock); ++} ++ ++static int bond_ipsec_lag_add_slave(struct bonding *bond, ++ struct slave *slave, ++ struct netlink_ext_ack *extack) ++{ ++ struct net_device *real_dev = slave->dev; ++ struct bond_ipsec_inst *inst; ++ struct bond_ipsec *ipsec; ++ bool have_states = false; ++ bool slave_ok; ++ int err = 0; ++ ++ if (!bond_mode_can_use_lag_xfrm(bond) || !netif_running(real_dev)) ++ return 0; ++ ++ slave_ok = bond_ipsec_lag_slave_ok(real_dev); ++ ++ mutex_lock(&bond->ipsec_lock); ++ list_for_each_entry(ipsec, &bond->ipsec_list, list) { ++ bool found = false; ++ ++ if (!ipsec->replicated) ++ continue; ++ have_states = true; ++ ++ if (ipsec->xs->km.state == XFRM_STATE_DEAD) ++ continue; ++ ++ if (!slave_ok) { ++ err = -EOPNOTSUPP; ++ break; ++ } ++ ++ list_for_each_entry(inst, &ipsec->inst_list, list) { ++ if (inst->real_dev == real_dev) { ++ found = true; ++ break; ++ } ++ } ++ if (found) ++ continue; ++ ++ inst = kzalloc_obj(*inst, GFP_KERNEL); ++ if (!inst) { ++ err = -ENOMEM; ++ break; ++ } ++ ++ inst->real_dev = real_dev; ++ netdev_hold(real_dev, &inst->dev_tracker, GFP_KERNEL); ++ ++ err = bond_ipsec_lag_add_inst(ipsec->xs, inst, extack); ++ if (err) { ++ netdev_put(real_dev, &inst->dev_tracker); ++ kfree(inst); ++ break; ++ } ++ ++ list_add_tail_rcu(&inst->list, &ipsec->inst_list); ++ } ++ mutex_unlock(&bond->ipsec_lock); ++ ++ if (err && have_states) { ++ slave_warn(bond->dev, real_dev, ++ "failed to replicate IPsec SA, flushing bond states\n"); ++ bond_ipsec_lag_begin_flush(bond); ++ xfrm_dev_state_flush(dev_net(bond->dev), bond->dev, true); ++ bond_ipsec_lag_end_flush(bond); ++ } ++ ++ return err; ++} ++ + /** + * bond_ipsec_add_sa - program device with a security association + * @bond_dev: pointer to the bond net device +@@ -475,8 +907,15 @@ static int bond_ipsec_add_sa(struct net_device *bond_dev, + if (!bond_dev) + return -EINVAL; + +- rcu_read_lock(); + bond = netdev_priv(bond_dev); ++ if (bond_mode_can_use_lag_xfrm(bond)) ++ return bond_ipsec_lag_add_sa(bond_dev, xs, extack); ++ if (BOND_MODE(bond) != BOND_MODE_ACTIVEBACKUP) { ++ NL_SET_ERR_MSG_MOD(extack, "Bond mode does not support IPsec offload"); ++ return -EOPNOTSUPP; ++ } ++ ++ rcu_read_lock(); + slave = rcu_dereference(bond->curr_active_slave); + real_dev = slave ? slave->dev : NULL; + netdev_hold(real_dev, &tracker, GFP_ATOMIC); +@@ -504,7 +943,9 @@ static int bond_ipsec_add_sa(struct net_device *bond_dev, + if (!err) { + xs->xso.real_dev = real_dev; + ipsec->xs = xs; ++ ipsec->replicated = false; + INIT_LIST_HEAD(&ipsec->list); ++ INIT_LIST_HEAD(&ipsec->inst_list); + mutex_lock(&bond->ipsec_lock); + list_add(&ipsec->list, &bond->ipsec_list); + mutex_unlock(&bond->ipsec_lock); +@@ -523,6 +964,9 @@ static void bond_ipsec_add_sa_all(struct bonding *bond) + struct bond_ipsec *ipsec; + struct slave *slave; + ++ if (BOND_MODE(bond) != BOND_MODE_ACTIVEBACKUP) ++ return; ++ + slave = rtnl_dereference(bond->curr_active_slave); + real_dev = slave ? slave->dev : NULL; + if (!real_dev) +@@ -540,6 +984,9 @@ static void bond_ipsec_add_sa_all(struct bonding *bond) + } + + list_for_each_entry(ipsec, &bond->ipsec_list, list) { ++ if (ipsec->replicated) ++ continue; ++ + /* If new state is added before ipsec_lock acquired */ + if (ipsec->xs->xso.real_dev == real_dev) + continue; +@@ -568,6 +1015,19 @@ static void bond_ipsec_add_sa_all(struct bonding *bond) + mutex_unlock(&bond->ipsec_lock); + } + ++static struct bond_ipsec *bond_ipsec_find(struct bonding *bond, ++ struct xfrm_state *xs) ++{ ++ struct bond_ipsec *ipsec; ++ ++ list_for_each_entry(ipsec, &bond->ipsec_list, list) { ++ if (ipsec->xs == xs) ++ return ipsec; ++ } ++ ++ return NULL; ++} ++ + /** + * bond_ipsec_del_sa - clear out this specific SA + * @bond_dev: pointer to the bond net device +@@ -577,8 +1037,24 @@ static void bond_ipsec_del_sa(struct net_device *bond_dev, + struct xfrm_state *xs) + { + struct net_device *real_dev; ++ struct bond_ipsec *ipsec; ++ struct bonding *bond; + +- if (!bond_dev || !xs->xso.real_dev) ++ if (!bond_dev) ++ return; ++ ++ bond = netdev_priv(bond_dev); ++ ++ mutex_lock(&bond->ipsec_lock); ++ ipsec = bond_ipsec_find(bond, xs); ++ if (ipsec && ipsec->replicated) { ++ bond_ipsec_lag_call_state(xs, ipsec, true, false); ++ mutex_unlock(&bond->ipsec_lock); ++ return; ++ } ++ mutex_unlock(&bond->ipsec_lock); ++ ++ if (!xs->xso.real_dev) + return; + + real_dev = xs->xso.real_dev; +@@ -600,6 +1076,9 @@ static void bond_ipsec_del_sa_all(struct bonding *bond) + struct bond_ipsec *ipsec; + struct slave *slave; + ++ if (BOND_MODE(bond) != BOND_MODE_ACTIVEBACKUP) ++ return; ++ + slave = rtnl_dereference(bond->curr_active_slave); + real_dev = slave ? slave->dev : NULL; + if (!real_dev) +@@ -607,6 +1086,9 @@ static void bond_ipsec_del_sa_all(struct bonding *bond) + + mutex_lock(&bond->ipsec_lock); + list_for_each_entry(ipsec, &bond->ipsec_list, list) { ++ if (ipsec->replicated) ++ continue; ++ + if (!ipsec->xs->xso.real_dev) + continue; + +@@ -647,23 +1129,33 @@ static void bond_ipsec_free_sa(struct net_device *bond_dev, + bond = netdev_priv(bond_dev); + + mutex_lock(&bond->ipsec_lock); +- if (!xs->xso.real_dev) ++ ipsec = bond_ipsec_find(bond, xs); ++ if (ipsec && ipsec->replicated) { ++ list_del(&ipsec->list); ++ bond_ipsec_lag_call_state(xs, ipsec, false, true); ++ RCU_INIT_POINTER(xs->xso.upper_priv, NULL); ++ bond_ipsec_lag_free_instances(ipsec); ++ call_rcu(&ipsec->rcu, bond_ipsec_rcu_free); ++ WRITE_ONCE(xs->xso.real_dev, NULL); ++ WRITE_ONCE(xs->xso.offload_handle, 0); + goto out; ++ } + + real_dev = xs->xso.real_dev; ++ if (!real_dev) ++ goto free_ipsec; + + xs->xso.real_dev = NULL; + if (real_dev->xfrmdev_ops && + real_dev->xfrmdev_ops->xdo_dev_state_free) + real_dev->xfrmdev_ops->xdo_dev_state_free(real_dev, xs); +-out: +- list_for_each_entry(ipsec, &bond->ipsec_list, list) { +- if (ipsec->xs == xs) { +- list_del(&ipsec->list); +- kfree(ipsec); +- break; +- } ++ ++free_ipsec: ++ if (ipsec) { ++ list_del(&ipsec->list); ++ kfree(ipsec); + } ++out: + mutex_unlock(&bond->ipsec_lock); + } + +@@ -674,7 +1166,18 @@ static void bond_ipsec_free_sa(struct net_device *bond_dev, + **/ + static bool bond_ipsec_offload_ok(struct sk_buff *skb, struct xfrm_state *xs) + { ++ struct net_device *bond_dev = xs->xso.dev; + struct net_device *real_dev; ++ struct bonding *bond; ++ ++ if (!bond_dev) ++ return false; ++ ++ bond = netdev_priv(bond_dev); ++ if (bond_mode_can_use_lag_xfrm(bond)) ++ return (xs->xso.type == XFRM_DEV_OFFLOAD_CRYPTO || ++ xs->xso.type == XFRM_DEV_OFFLOAD_PACKET) && ++ rcu_access_pointer(xs->xso.upper_priv); + + rcu_read_lock(); + real_dev = bond_ipsec_dev(xs); +@@ -735,6 +1238,234 @@ static void bond_xfrm_update_stats(struct xfrm_state *xs) + rcu_read_unlock(); + } + ++/* xdo_dev_state_lower_handle implementation for bond-owned XFRM states. ++ * lower_dev is usually the slave selected by the lower driver datapath. It can ++ * also be the bond master when the RX path has already promoted skb->dev before ++ * resolving the replicated packet-offload handle. Single-lower active-backup ++ * state is resolved from xso.real_dev/offload_handle here because ++ * xfrm_dev_state_lower_handle() delegates bond-owned lookups to bonding. ++ */ ++static unsigned long bond_ipsec_lower_handle(struct net_device *bond_dev, ++ struct xfrm_state *xs, ++ struct net_device *lower_dev) ++{ ++ struct bonding *bond = netdev_priv(bond_dev); ++ struct bond_ipsec_inst *inst; ++ struct bond_ipsec *ipsec; ++ unsigned long handle = 0; ++ ++ if (BOND_MODE(bond) == BOND_MODE_ACTIVEBACKUP) { ++ struct net_device *real_dev = READ_ONCE(xs->xso.real_dev); ++ ++ return real_dev == lower_dev ? READ_ONCE(xs->xso.offload_handle) : 0; ++ } ++ if (!bond_mode_can_use_lag_xfrm(bond)) ++ return 0; ++ ++ rcu_read_lock(); ++ ipsec = rcu_dereference(xs->xso.upper_priv); ++ if (!ipsec || !ipsec->replicated || ipsec->xs != xs) ++ goto out; ++ ++ list_for_each_entry_rcu(inst, &ipsec->inst_list, list) { ++ if (READ_ONCE(inst->added) && inst->real_dev == lower_dev) { ++ handle = READ_ONCE(inst->lower_handle); ++ break; ++ } ++ } ++ if (!handle && lower_dev == bond_dev) { ++ /* Packet-offload users may resolve a bond-owned SA from an ++ * RX path that has already promoted skb->dev to the master. ++ * The replicated lower SAs are equivalent for devices whose ++ * packet engine owns SA state globally, so return an installed ++ * lower handle instead of making the upper-device lookup fail. ++ */ ++ list_for_each_entry_rcu(inst, &ipsec->inst_list, list) { ++ if (READ_ONCE(inst->added)) { ++ handle = READ_ONCE(inst->lower_handle); ++ break; ++ } ++ } ++ } ++ ++out: ++ rcu_read_unlock(); ++ return handle; ++} ++ ++static int bond_ipsec_policy_add(struct xfrm_policy *xp, ++ struct netlink_ext_ack *extack) ++{ ++ struct net_device *bond_dev = xp->xdo.dev; ++ struct bonding *bond; ++ ++ if (!bond_dev) ++ return -ENODEV; ++ ++ if (xp->xdo.type != XFRM_DEV_OFFLOAD_PACKET) { ++ NL_SET_ERR_MSG_MOD(extack, ++ "Bond supports only XFRM packet policies"); ++ return -EOPNOTSUPP; ++ } ++ ++ if (xfrm_policy_id2dir(xp->index) >= XFRM_POLICY_MAX) { ++ NL_SET_ERR_MSG_MOD(extack, ++ "Bond does not offload socket policies"); ++ return -EOPNOTSUPP; ++ } ++ ++ if (xp->xfrm_nr != 1 || ++ xp->xfrm_vec[0].id.proto != IPPROTO_ESP) { ++ NL_SET_ERR_MSG_MOD(extack, ++ "Bond offloads ESP policies only"); ++ return -EOPNOTSUPP; ++ } ++ ++ if (!(bond_dev->features & NETIF_F_HW_ESP)) { ++ NL_SET_ERR_MSG_MOD(extack, "Bond IPsec offload is disabled"); ++ return -EOPNOTSUPP; ++ } ++ ++ bond = netdev_priv(bond_dev); ++ if (BOND_MODE(bond) != BOND_MODE_ACTIVEBACKUP && ++ !bond_mode_can_use_lag_xfrm(bond)) { ++ NL_SET_ERR_MSG_MOD(extack, ++ "Bond mode cannot use XFRM packet offload"); ++ return -EOPNOTSUPP; ++ } ++ ++ switch (xp->xdo.dir) { ++ case XFRM_DEV_OFFLOAD_IN: ++ case XFRM_DEV_OFFLOAD_OUT: ++ case XFRM_DEV_OFFLOAD_FWD: ++ return 0; ++ default: ++ NL_SET_ERR_MSG_MOD(extack, ++ "Bond does not support this policy direction"); ++ return -EOPNOTSUPP; ++ } ++} ++ ++static struct net_device *bond_xmit_get_slave(struct net_device *master_dev, ++ struct sk_buff *skb, ++ bool all_slaves); ++ ++static u32 bond_ipsec_xfrm_hash(const struct xfrm_state *xs) ++{ ++ u32 hash = (__force u32)xs->id.spi ^ xs->id.proto ^ xs->props.family; ++ ++ if (xs->props.family == AF_INET) { ++ hash ^= (__force u32)xs->id.daddr.a4; ++ hash ^= (__force u32)xs->props.saddr.a4; ++ } else if (xs->props.family == AF_INET6) { ++ const __be32 *daddr = xs->id.daddr.a6; ++ const __be32 *saddr = xs->props.saddr.a6; ++ int i; ++ ++ for (i = 0; i < 4; i++) ++ hash ^= (__force u32)daddr[i] ^ (__force u32)saddr[i]; ++ } ++ ++ if (xs->encap) { ++ hash ^= (__force u16)xs->encap->encap_sport << 16; ++ hash ^= (__force u16)xs->encap->encap_dport; ++ } ++ ++ hash ^= hash >> 16; ++ hash ^= hash >> 8; ++ ++ return hash; ++} ++ ++static struct net_device *bond_ipsec_lag_xmit_dev(struct bonding *bond, ++ struct xfrm_state *xs) ++{ ++ struct bond_up_slave *slaves; ++ unsigned int count; ++ unsigned int start; ++ unsigned int i; ++ ++ slaves = rcu_dereference(bond->usable_slaves); ++ count = slaves ? READ_ONCE(slaves->count) : 0; ++ if (!count) ++ return NULL; ++ ++ /* Hardware packet engines keep sequence/replay state per lower SA. ++ * Keep each logical XFRM SA pinned to one usable slave instead of ++ * hashing every plaintext skb independently. ++ */ ++ start = bond_ipsec_xfrm_hash(xs) % count; ++ for (i = 0; i < count; i++) { ++ struct net_device *real_dev = slaves->arr[(start + i) % count]->dev; ++ ++ if (xfrm_dev_state_lower_handle(xs, real_dev)) ++ return real_dev; ++ } ++ ++ return NULL; ++} ++ ++static int bond_ipsec_xmit_ifindex(struct net_device *bond_dev, ++ struct xfrm_state *xs) ++{ ++ struct bonding *bond = netdev_priv(bond_dev); ++ struct net_device *real_dev; ++ int ifindex = 0; ++ ++ rcu_read_lock(); ++ if (bond_mode_can_use_lag_xfrm(bond)) ++ real_dev = bond_ipsec_lag_xmit_dev(bond, xs); ++ else ++ real_dev = bond_ipsec_dev(xs); ++ if (real_dev) ++ ifindex = real_dev->ifindex; ++ rcu_read_unlock(); ++ ++ return ifindex; ++} ++ ++static int bond_ipsec_packet_xmit(struct sk_buff *skb, struct xfrm_state *xs) ++{ ++ struct net_device *bond_dev = xs->xso.dev; ++ const struct xfrmdev_ops *ops; ++ struct net_device *real_dev; ++ struct bonding *bond; ++ int err = -EHOSTUNREACH; ++ ++ if (!bond_dev) ++ goto drop; ++ ++ bond = netdev_priv(bond_dev); ++ rcu_read_lock(); ++ if (bond_mode_can_use_lag_xfrm(bond)) ++ real_dev = bond_ipsec_lag_xmit_dev(bond, xs); ++ else ++ real_dev = bond_xmit_get_slave(bond_dev, skb, false); ++ if (real_dev) ++ dev_hold(real_dev); ++ rcu_read_unlock(); ++ if (!real_dev) ++ goto drop; ++ ++ ops = real_dev->xfrmdev_ops; ++ if (!ops || !ops->xdo_dev_packet_xmit || ++ !xfrm_dev_state_lower_handle(xs, real_dev)) ++ goto put_drop; ++ ++ skb->dev = real_dev; ++ err = ops->xdo_dev_packet_xmit(skb, xs); ++ dev_put(real_dev); ++ ++ return err; ++ ++put_drop: ++ dev_put(real_dev); ++drop: ++ XFRM_INC_STATS(xs_net(xs), LINUX_MIB_XFRMOUTERROR); ++ kfree_skb(skb); ++ return err; ++} ++ + static const struct xfrmdev_ops bond_xfrmdev_ops = { + .xdo_dev_state_add = bond_ipsec_add_sa, + .xdo_dev_state_delete = bond_ipsec_del_sa, +@@ -742,7 +1473,28 @@ static const struct xfrmdev_ops bond_xfrmdev_ops = { + .xdo_dev_offload_ok = bond_ipsec_offload_ok, + .xdo_dev_state_advance_esn = bond_advance_esn_state, + .xdo_dev_state_update_stats = bond_xfrm_update_stats, ++ .xdo_dev_state_lower_handle = bond_ipsec_lower_handle, ++ .xdo_dev_state_xmit_ifindex = bond_ipsec_xmit_ifindex, ++ .xdo_dev_policy_add = bond_ipsec_policy_add, ++ .xdo_dev_packet_xmit = bond_ipsec_packet_xmit, + }; ++#else ++static void bond_ipsec_lag_remove_slave(struct bonding *bond, ++ struct net_device *real_dev) ++{ ++} ++ ++static int bond_ipsec_lag_add_slave(struct bonding *bond, ++ struct slave *slave, ++ struct netlink_ext_ack *extack) ++{ ++ return 0; ++} ++ ++static void bond_sync_slave_xfrm_features(struct bonding *bond, ++ struct slave *slave) ++{ ++} + #endif /* CONFIG_XFRM_OFFLOAD */ + + /*------------------------------- Link status -------------------------------*/ +@@ -6012,10 +6764,11 @@ void bond_setup(struct net_device *bond_dev) + bond_dev->priv_flags &= ~(IFF_XMIT_DST_RELEASE | IFF_TX_SKB_SHARING); + + #ifdef CONFIG_XFRM_OFFLOAD +- /* set up xfrm device ops (only supported in active-backup right now) */ ++ /* set up xfrm device ops */ + bond_dev->xfrmdev_ops = &bond_xfrmdev_ops; + INIT_LIST_HEAD(&bond->ipsec_list); + mutex_init(&bond->ipsec_lock); ++ bond->ipsec_lag_blocked = false; + #endif /* CONFIG_XFRM_OFFLOAD */ + + /* don't acquire bond device's netif_tx_lock when transmitting */ +diff --git a/include/linux/netdevice.h b/include/linux/netdevice.h +index b4e844e90db8..6d12f71c5067 100644 +--- a/include/linux/netdevice.h ++++ b/include/linux/netdevice.h +@@ -1061,6 +1061,14 @@ struct xfrmdev_ops { + unsigned long (*xdo_dev_state_lower_handle)(struct net_device *dev, + struct xfrm_state *x, + struct net_device *lower_dev); ++ /* ++ * Return the lower device ifindex that packet-offload transmit should ++ * use for x. Upper devices that pin an SA to one lower device need this ++ * so flowtable hardware redirects use the same lower path as ++ * xdo_dev_packet_xmit(). ++ */ ++ int (*xdo_dev_state_xmit_ifindex)(struct net_device *dev, ++ struct xfrm_state *x); + u32 flags; + /* Optional packet-offload TX path. The driver consumes skb. */ + int (*xdo_dev_packet_xmit)(struct sk_buff *skb, +diff --git a/include/net/bonding.h b/include/net/bonding.h +index edd1942dcd73..a581252b5b06 100644 +--- a/include/net/bonding.h ++++ b/include/net/bonding.h +@@ -203,9 +203,24 @@ struct bond_up_slave { + */ + #define BOND_LINK_NOCHANGE -1 + ++/* XFRM offload state tracked by bonding for one xfrm_state. */ + struct bond_ipsec { + struct list_head list; + struct xfrm_state *xs; ++ struct list_head inst_list; ++ struct rcu_head rcu; ++ bool replicated; ++}; ++ ++/* Per-lower-device instance of a replicated LAG XFRM state. */ ++struct bond_ipsec_inst { ++ struct list_head list; ++ struct net_device *real_dev; ++ netdevice_tracker dev_tracker; ++ unsigned long lower_handle; ++ struct rcu_head rcu; ++ bool added; ++ bool deleted; + }; + + /* +@@ -259,8 +274,9 @@ struct bonding { + struct rtnl_link_stats64 bond_stats; + #ifdef CONFIG_XFRM_OFFLOAD + struct list_head ipsec_list; +- /* protecting ipsec_list */ ++ /* protecting ipsec_list and ipsec_lag_blocked */ + struct mutex ipsec_lock; ++ bool ipsec_lag_blocked; + #endif /* CONFIG_XFRM_OFFLOAD */ + struct bpf_prog *xdp_prog; + }; +@@ -325,6 +341,13 @@ static inline bool bond_mode_can_use_xmit_hash(const struct bonding *bond) + BOND_MODE(bond) == BOND_MODE_ALB); + } + ++static inline bool bond_mode_can_use_lag_xfrm(const struct bonding *bond) ++{ ++ return (BOND_MODE(bond) == BOND_MODE_8023AD || ++ BOND_MODE(bond) == BOND_MODE_XOR) && ++ bond->params.xmit_policy == BOND_XMIT_POLICY_LAYER34; ++} ++ + static inline bool bond_mode_uses_xmit_hash(const struct bonding *bond) + { + return (BOND_MODE(bond) == BOND_MODE_8023AD || +@@ -712,6 +735,10 @@ void bond_slave_arr_work_rearm(struct bonding *bond, unsigned long delay); + void bond_peer_notify_work_rearm(struct bonding *bond, unsigned long delay); + void bond_work_init_all(struct bonding *bond); + void bond_work_cancel_all(struct bonding *bond); ++#if IS_ENABLED(CONFIG_XFRM_OFFLOAD) ++void bond_ipsec_lag_begin_flush(struct bonding *bond); ++void bond_ipsec_lag_end_flush(struct bonding *bond); ++#endif + + #ifdef CONFIG_PROC_FS + void bond_create_proc_entry(struct bonding *bond); +diff --git a/include/net/xfrm.h b/include/net/xfrm.h +index 69a33da10a6a..708c9f67e221 100644 +--- a/include/net/xfrm.h ++++ b/include/net/xfrm.h +@@ -1737,6 +1737,22 @@ xfrm_dev_state_lower_handle(struct xfrm_state *x, struct net_device *lower_dev) + return 0; + } + ++static inline int xfrm_dev_state_xmit_ifindex(struct xfrm_state *x) ++{ ++ struct xfrm_dev_offload *xdo = &x->xso; ++ struct net_device *real_dev = READ_ONCE(xdo->real_dev); ++ struct net_device *dev = READ_ONCE(xdo->dev); ++ ++ if (real_dev) ++ return real_dev->ifindex; ++ ++ if (dev && dev->xfrmdev_ops && ++ dev->xfrmdev_ops->xdo_dev_state_xmit_ifindex) ++ return dev->xfrmdev_ops->xdo_dev_state_xmit_ifindex(dev, x); ++ ++ return dev ? dev->ifindex : 0; ++} ++ + static inline void xfrm_dev_state_update_stats(struct xfrm_state *x) + { + struct xfrm_dev_offload *xdo = &x->xso; +@@ -1753,6 +1769,11 @@ xfrm_dev_state_lower_handle(struct xfrm_state *x, struct net_device *lower_dev) + { + return 0; + } ++ ++static inline int xfrm_dev_state_xmit_ifindex(struct xfrm_state *x) ++{ ++ return 0; ++} + + static inline void xfrm_dev_state_update_stats(struct xfrm_state *x) {} + #endif +diff --git a/net/xfrm/xfrm_state.c b/net/xfrm/xfrm_state.c +index 90785e9ba159..2aa07b6614a7 100644 +--- a/net/xfrm/xfrm_state.c ++++ b/net/xfrm/xfrm_state.c +@@ -1786,14 +1786,25 @@ static void __xfrm_state_bump_genids(struct xfrm_state *xnew) + } + } + +-void xfrm_state_insert(struct xfrm_state *x) ++static int xfrm_state_insert_check(struct xfrm_state *x) + { + struct net *net = xs_net(x); ++ int err = -EINVAL; + + spin_lock_bh(&net->xfrm.xfrm_state_lock); ++ if (x->km.state == XFRM_STATE_DEAD) ++ goto out; + __xfrm_state_bump_genids(x); + __xfrm_state_insert(x); ++ err = 0; ++out: + spin_unlock_bh(&net->xfrm.xfrm_state_lock); ++ return err; ++} ++ ++void xfrm_state_insert(struct xfrm_state *x) ++{ ++ xfrm_state_insert_check(x); + } + EXPORT_SYMBOL(xfrm_state_insert); + +@@ -1896,9 +1907,14 @@ int xfrm_state_add(struct xfrm_state *x) + + family = x->props.family; + ++ x1 = NULL; + to_put = NULL; + + spin_lock_bh(&net->xfrm.xfrm_state_lock); ++ if (x->km.state == XFRM_STATE_DEAD) { ++ err = -EINVAL; ++ goto out; ++ } + + x1 = __xfrm_state_locate(x, use_spi, family); + if (x1) { +@@ -2157,7 +2173,8 @@ struct xfrm_state *xfrm_state_migrate(struct xfrm_state *x, + if (xfrm_addr_equal(&x->id.daddr, &m->new_daddr, m->new_family)) { + /* a care is needed when the destination address of the + state is to be updated as it is a part of triplet */ +- xfrm_state_insert(xc); ++ if (xfrm_state_insert_check(xc) < 0) ++ goto error_add; + } else { + if (xfrm_state_add(xc) < 0) + goto error_add; +@@ -2185,6 +2202,11 @@ int xfrm_state_update(struct xfrm_state *x) + to_put = NULL; + + spin_lock_bh(&net->xfrm.xfrm_state_lock); ++ if (x->km.state == XFRM_STATE_DEAD) { ++ err = -EINVAL; ++ goto out; ++ } ++ + x1 = __xfrm_state_locate(x, use_spi, x->props.family); + + err = -ESRCH; +-- +2.53.0 diff --git a/target/linux/generic/pending-6.18/9999-03-bonding-expose-user-controlled-IPsec-features-for-LAG.patch b/target/linux/generic/pending-6.18/9999-03-bonding-expose-user-controlled-IPsec-features-for-LAG.patch new file mode 100644 index 00000000000000..6251cf3b5513a9 --- /dev/null +++ b/target/linux/generic/pending-6.18/9999-03-bonding-expose-user-controlled-IPsec-features-for-LAG.patch @@ -0,0 +1,288 @@ +From 8cf9483d336e38556d33ab100efe339443bb4688 Mon Sep 17 00:00:00 2001 +From: Jihong Min +Date: Wed, 20 May 2026 16:52:15 +0900 +Subject: [PATCH] bonding: expose user-controlled IPsec features for LAG + +Expose LAG IPsec offload as user-controlled bonding features instead of +enabling it by default. Keep the existing active-backup default behavior, +but make newly eligible LAG bonds start with ESP/XFRM features explicitly +disabled so users opt in with ethtool. + +Let 802.3ad and balance-xor with layer3+4 advertise the intersection of +XFRM features across running eligible slaves, with supported features +shown as mutable off features rather than fixed-off capabilities. + +Propagate mutable XFRM feature requests to running lower devices on a +best-effort basis. Keep the master feature request intact if a lower +device is not ready yet; actual lower-handle eligibility is still +validated when XFRM state is installed. Disable dependent ESP checksum +and segmentation features when HW ESP is not available. + +Assisted-by: Codex:gpt-5.5 +Signed-off-by: Jihong Min +--- + drivers/net/bonding/bond_main.c | 211 +++++++++++++++++++++++++++++ + drivers/net/bonding/bond_options.c | 10 +- + 2 files changed, 219 insertions(+), 2 deletions(-) + +diff --git a/drivers/net/bonding/bond_main.c b/drivers/net/bonding/bond_main.c +index fcfe7a65f41d..09ee7dfdc2cd 100644 +--- a/drivers/net/bonding/bond_main.c ++++ b/drivers/net/bonding/bond_main.c +@@ -2052,6 +2052,13 @@ static netdev_features_t bond_fix_features(struct net_device *dev, + struct list_head *iter; + netdev_features_t mask; + struct slave *slave; ++#ifdef CONFIG_XFRM_OFFLOAD ++ netdev_features_t lag_xfrm_features = NETIF_F_HW_ESP; ++ bool ab_xfrm = BOND_MODE(bond) == BOND_MODE_ACTIVEBACKUP; ++ bool lag_xfrm_ok = true; ++ bool lag_xfrm = bond_mode_can_use_lag_xfrm(bond); ++ int lag_xfrm_slaves = 0; ++#endif /* CONFIG_XFRM_OFFLOAD */ + + mask = features; + features = netdev_base_features(features); +@@ -2060,12 +2067,203 @@ static netdev_features_t bond_fix_features(struct net_device *dev, + features = netdev_increment_features(features, + slave->dev->features, + mask); ++#ifdef CONFIG_XFRM_OFFLOAD ++ if (lag_xfrm && (mask & NETIF_F_HW_ESP) && ++ netif_running(slave->dev)) { ++ netdev_features_t slave_xfrm_features; ++ netdev_features_t slave_xfrm_enableable; ++ netdev_features_t missing; ++ ++ slave_xfrm_features = slave->dev->features & ++ NETIF_F_HW_ESP; ++ slave_xfrm_enableable = slave->dev->hw_features & ++ mask & NETIF_F_HW_ESP; ++ slave_xfrm_features |= slave_xfrm_enableable; ++ missing = (NETIF_F_HW_ESP & mask) & ++ ~slave_xfrm_features; ++ if (missing) ++ slave_dbg(dev, slave->dev, ++ "missing LAG XFRM feature(s) %pNF\n", ++ &missing); ++ lag_xfrm_features &= slave_xfrm_features; ++ ++ if (!(slave_xfrm_features & NETIF_F_HW_ESP)) { ++ slave_dbg(dev, slave->dev, ++ "missing LAG XFRM HW ESP feature\n"); ++ lag_xfrm_ok = false; ++ } ++ lag_xfrm_slaves++; ++ } ++#endif /* CONFIG_XFRM_OFFLOAD */ + } + features = netdev_add_tso_features(features, mask); + ++#ifdef CONFIG_XFRM_OFFLOAD ++ if (!ab_xfrm && !lag_xfrm) ++ features &= ~BOND_XFRM_FEATURES; ++ else if (lag_xfrm && (!lag_xfrm_ok || !lag_xfrm_slaves)) ++ features &= ~BOND_XFRM_FEATURES; ++ else if (lag_xfrm) ++ features = (features & ~BOND_XFRM_FEATURES) | ++ (lag_xfrm_features & mask); ++ if (!(features & NETIF_F_HW_ESP)) ++ features &= ~(NETIF_F_HW_ESP_TX_CSUM | NETIF_F_GSO_ESP); ++#endif /* CONFIG_XFRM_OFFLOAD */ ++ + return features; + } + ++#ifdef CONFIG_XFRM_OFFLOAD ++static int bond_set_slave_xfrm_features(struct bonding *bond, ++ struct slave *slave, ++ netdev_features_t features) ++{ ++ struct net_device *real_dev = slave->dev; ++ netdev_features_t wanted_features; ++ netdev_features_t xfrm_features; ++ netdev_features_t mutable; ++ bool notifier_ctx; ++ int err = 0; ++ ++ mutable = real_dev->hw_features & NETIF_F_HW_ESP; ++ if (!mutable) ++ return 0; ++ ++ xfrm_features = features & NETIF_F_HW_ESP; ++ ++ notifier_ctx = bond->notifier_ctx; ++ bond->notifier_ctx = true; ++ netdev_lock_ops(real_dev); ++ wanted_features = real_dev->wanted_features; ++ real_dev->wanted_features = real_dev->features; ++ real_dev->wanted_features &= ~mutable; ++ real_dev->wanted_features |= xfrm_features & mutable; ++ netdev_update_features(real_dev); ++ real_dev->wanted_features = wanted_features; ++ real_dev->wanted_features &= ~mutable; ++ real_dev->wanted_features |= xfrm_features & mutable; ++ if ((real_dev->features & mutable) != (xfrm_features & mutable)) ++ err = -EOPNOTSUPP; ++ netdev_unlock_ops(real_dev); ++ bond->notifier_ctx = notifier_ctx; ++ ++ return err; ++} ++ ++static void bond_sync_slave_xfrm_features(struct bonding *bond, ++ struct slave *slave) ++{ ++ netdev_features_t requested = bond->dev->wanted_features; ++ netdev_features_t available; ++ netdev_features_t missing; ++ int err; ++ ++ if (!bond_mode_can_use_lag_xfrm(bond)) ++ return; ++ ++ if (!netif_running(slave->dev)) ++ return; ++ ++ requested &= NETIF_F_HW_ESP; ++ if (!requested) ++ return; ++ ++ available = slave->dev->features | slave->dev->hw_features; ++ missing = requested & ~available; ++ if (missing) ++ goto disable_missing; ++ ++ err = bond_set_slave_xfrm_features(bond, slave, requested); ++ missing = requested & ~slave->dev->features; ++ if (err && !missing) ++ missing = requested; ++ if (!missing) ++ return; ++ ++disable_missing: ++ slave_warn(bond->dev, slave->dev, ++ "could not sync XFRM feature(s) %pNF\n", ++ &missing); ++} ++ ++static int bond_set_features(struct net_device *dev, netdev_features_t features) ++{ ++ struct bonding *bond = netdev_priv(dev); ++ netdev_features_t changed; ++ netdev_features_t enabled; ++ struct list_head *iter; ++ struct slave *slave; ++ bool flush; ++ int err = 0; ++ ++ if (!bond_mode_can_use_lag_xfrm(bond)) ++ return 0; ++ ++ changed = (dev->features ^ features) & NETIF_F_HW_ESP; ++ if (!changed) ++ return 0; ++ ++ enabled = features & NETIF_F_HW_ESP; ++ flush = (dev->features & NETIF_F_HW_ESP) && ++ !(features & NETIF_F_HW_ESP); ++ if (enabled) { ++ int targets = 0; ++ ++ bond_for_each_slave(bond, slave, iter) { ++ netdev_features_t available; ++ netdev_features_t missing; ++ ++ if (!netif_running(slave->dev)) ++ continue; ++ ++ available = slave->dev->features | slave->dev->hw_features; ++ missing = enabled & ~available; ++ if (missing) { ++ slave_warn(dev, slave->dev, ++ "missing XFRM feature(s) %pNF\n", ++ &missing); ++ } ++ targets++; ++ } ++ if (!targets) ++ return -EOPNOTSUPP; ++ } ++ ++ if (flush) { ++ bond_ipsec_lag_begin_flush(bond); ++ xfrm_dev_state_flush(dev_net(dev), dev, true); ++ } ++ ++ bond_for_each_slave(bond, slave, iter) { ++ if (!netif_running(slave->dev)) ++ continue; ++ ++ err = bond_set_slave_xfrm_features(bond, slave, features); ++ if (err) ++ slave_warn(dev, slave->dev, ++ "failed to apply XFRM feature request\n"); ++ } ++ ++ bond_for_each_slave(bond, slave, iter) { ++ netdev_features_t missing = enabled & ~slave->dev->features; ++ ++ if (!netif_running(slave->dev)) ++ continue; ++ ++ if (missing) { ++ slave_warn(dev, slave->dev, ++ "failed to enable XFRM feature(s) %pNF\n", ++ &missing); ++ } ++ } ++ ++ if (flush) ++ bond_ipsec_lag_end_flush(bond); ++ ++ return 0; ++} ++#endif /* CONFIG_XFRM_OFFLOAD */ ++ + static int bond_header_create(struct sk_buff *skb, struct net_device *bond_dev, + unsigned short type, const void *daddr, + const void *saddr, unsigned int len) +@@ -6520,6 +6718,9 @@ static const struct net_device_ops bond_netdev_ops = { + .ndo_add_slave = bond_enslave, + .ndo_del_slave = bond_release, + .ndo_fix_features = bond_fix_features, ++#ifdef CONFIG_XFRM_OFFLOAD ++ .ndo_set_features = bond_set_features, ++#endif /* CONFIG_XFRM_OFFLOAD */ + .ndo_features_check = passthru_features_check, + .ndo_get_xmit_slave = bond_xmit_get_slave, + .ndo_sk_get_lower_dev = bond_sk_get_lower_dev, +diff --git a/drivers/net/bonding/bond_options.c b/drivers/net/bonding/bond_options.c +index 7380cc4ee75a..946621b20359 100644 +--- a/drivers/net/bonding/bond_options.c ++++ b/drivers/net/bonding/bond_options.c +@@ -880,15 +880,21 @@ const struct bond_option *bond_opt_get(unsigned int option) + + static bool bond_set_xfrm_features(struct bonding *bond) + { ++ netdev_features_t changed; ++ netdev_features_t wanted = bond->dev->wanted_features; ++ + if (!IS_ENABLED(CONFIG_XFRM_OFFLOAD)) + return false; + + if (BOND_MODE(bond) == BOND_MODE_ACTIVEBACKUP) + bond->dev->wanted_features |= BOND_XFRM_FEATURES; + else + bond->dev->wanted_features &= ~BOND_XFRM_FEATURES; + +- return true; ++ changed = wanted ^ bond->dev->wanted_features; ++ changed |= bond->dev->features ^ bond->dev->wanted_features; ++ ++ return changed & BOND_XFRM_FEATURES; + } + + static int bond_option_mode_set(struct bonding *bond, +-- +2.53.0 diff --git a/target/linux/generic/pending-6.18/9999-04-bonding-handle-replicated-IPsec-SAs-across-LAG-changes.patch b/target/linux/generic/pending-6.18/9999-04-bonding-handle-replicated-IPsec-SAs-across-LAG-changes.patch new file mode 100644 index 00000000000000..6ac5cea1b9166d --- /dev/null +++ b/target/linux/generic/pending-6.18/9999-04-bonding-handle-replicated-IPsec-SAs-across-LAG-changes.patch @@ -0,0 +1,228 @@ +From f523d3eaf4787cdf84affd46eec53ca77d3f273a Mon Sep 17 00:00:00 2001 +From: Jihong Min +Date: Wed, 20 May 2026 16:52:30 +0900 +Subject: [PATCH] bonding: handle replicated IPsec SAs across LAG changes + +Keep replicated bonding IPsec state consistent as the LAG changes. Add +newly usable slaves to existing replicated states, remove only the +departing lower instance on down/remove, and update the usable slave +array before hiding lower handles. + +Flush bond-owned XFRM offload state when mode or hash policy leaves the +LAG XFRM eligible configuration. Block new LAG offload adds while +pending replicated states are cleaned up and the XFRM table is flushed. + +Assisted-by: Codex:gpt-5.5 +Signed-off-by: Jihong Min +--- + drivers/net/bonding/bond_main.c | 51 +++++++++++++++++++++++--- + drivers/net/bonding/bond_options.c | 57 ++++++++++++++++++++++++++++++ + 2 files changed, 104 insertions(+), 4 deletions(-) + +diff --git a/drivers/net/bonding/bond_main.c b/drivers/net/bonding/bond_main.c +index 09ee7dfdc2cd..d1b149cc9011 100644 +--- a/drivers/net/bonding/bond_main.c ++++ b/drivers/net/bonding/bond_main.c +@@ -787,6 +787,12 @@ static void bond_ipsec_lag_remove_slave(struct bonding *bond, + if (inst->real_dev != real_dev) + continue; + ++ /* Hide the handle from new datapath lookups before ++ * waiting for existing RCU readers. The lower driver's ++ * delete/free callbacks still resolve the handle through ++ * the temporary xso.real_dev/offload_handle set by ++ * bond_ipsec_lag_call_inst(). ++ */ + WRITE_ONCE(inst->added, false); + removed = true; + } +@@ -3114,6 +3120,18 @@ int bond_enslave(struct net_device *bond_dev, struct net_device *slave_dev, + bpf_prog_inc(bond->xdp_prog); + } + ++#ifdef CONFIG_XFRM_OFFLOAD ++ if ((bond_dev->wanted_features & BOND_XFRM_FEATURES) && ++ bond_mode_can_use_lag_xfrm(bond)) { ++ bond_sync_slave_xfrm_features(bond, new_slave); ++ bond->notifier_ctx = true; ++ netdev_compute_master_upper_features(bond->dev, true); ++ bond->notifier_ctx = false; ++ } ++#endif /* CONFIG_XFRM_OFFLOAD */ ++ ++ bond_ipsec_lag_add_slave(bond, new_slave, extack); ++ + /* broadcast mode uses the all_slaves to loop through slaves. */ + if (bond_mode_can_use_xmit_hash(bond) || + BOND_MODE(bond) == BOND_MODE_BROADCAST) +@@ -3232,6 +3250,9 @@ static int __bond_release_one(struct net_device *bond_dev, + } + + bond_set_slave_inactive_flags(slave, BOND_SLAVE_NOTIFY_NOW); ++ if (bond_mode_can_use_xmit_hash(bond) || ++ BOND_MODE(bond) == BOND_MODE_BROADCAST) ++ bond_update_slave_arr(bond, slave); + + bond_sysfs_slave_del(slave); + +@@ -3249,8 +3270,10 @@ static int __bond_release_one(struct net_device *bond_dev, + slave_warn(bond_dev, slave_dev, "failed to unload XDP program\n"); + } + +- /* unregister rx_handler early so bond_handle_frame wouldn't be called +- * for this slave anymore. ++ bond_ipsec_lag_remove_slave(bond, slave_dev); ++ ++ /* unregister rx_handler after lower IPsec state is gone so RX cannot ++ * bypass the bond while a bond-owned SA is still installed. + */ + netdev_rx_handler_unregister(slave_dev); + +@@ -4768,8 +4791,13 @@ static int bond_slave_netdev_event(unsigned long event, + + if (BOND_MODE(bond) == BOND_MODE_8023AD) + bond_3ad_adapter_speed_duplex_changed(slave); +- fallthrough; +- case NETDEV_DOWN: ++ bond_sync_slave_xfrm_features(bond, slave); ++ if (bond_mode_can_use_lag_xfrm(bond)) { ++ bond->notifier_ctx = true; ++ netdev_compute_master_upper_features(bond->dev, true); ++ bond->notifier_ctx = false; ++ } ++ bond_ipsec_lag_add_slave(bond, slave, NULL); + /* Refresh slave-array if applicable! + * If the setup does not use miimon or arpmon (mode-specific!), + * then these events will not cause the slave-array to be +@@ -4781,6 +4809,19 @@ static int bond_slave_netdev_event(unsigned long event, + if (bond_mode_can_use_xmit_hash(bond)) + bond_update_slave_arr(bond, NULL); + break; ++ case NETDEV_DOWN: ++ /* Refresh slave-array before deleting IPsec state so no new ++ * TX path picks this slave after its offload handle is hidden. ++ */ ++ if (bond_mode_can_use_xmit_hash(bond)) ++ bond_update_slave_arr(bond, slave); ++ bond_ipsec_lag_remove_slave(bond, slave_dev); ++ if (bond_mode_can_use_lag_xfrm(bond)) { ++ bond->notifier_ctx = true; ++ netdev_compute_master_upper_features(bond->dev, true); ++ bond->notifier_ctx = false; ++ } ++ break; + case NETDEV_CHANGEMTU: + /* TODO: Should slaves be allowed to + * independently alter their MTU? For +@@ -4819,10 +4860,12 @@ static int bond_slave_netdev_event(unsigned long event, + break; + case NETDEV_FEAT_CHANGE: + if (!bond->notifier_ctx) { ++ bond_sync_slave_xfrm_features(bond, slave); + bond->notifier_ctx = true; + netdev_compute_master_upper_features(bond->dev, true); + bond->notifier_ctx = false; + } ++ bond_ipsec_lag_add_slave(bond, slave, NULL); + break; + case NETDEV_RESEND_IGMP: + /* Propagate to master device */ +diff --git a/drivers/net/bonding/bond_options.c b/drivers/net/bonding/bond_options.c +index 946621b20359..9d977e220ca2 100644 +--- a/drivers/net/bonding/bond_options.c ++++ b/drivers/net/bonding/bond_options.c +@@ -17,6 +17,7 @@ + + #include + #include ++#include + + static int bond_option_active_slave_set(struct bonding *bond, + const struct bond_opt_value *newval); +@@ -900,6 +901,13 @@ static bool bond_set_xfrm_features(struct bonding *bond) + static int bond_option_mode_set(struct bonding *bond, + const struct bond_opt_value *newval) + { ++#if IS_ENABLED(CONFIG_XFRM_OFFLOAD) ++ bool old_ab_xfrm = BOND_MODE(bond) == BOND_MODE_ACTIVEBACKUP; ++ bool old_lag_xfrm = bond_mode_can_use_lag_xfrm(bond); ++ bool new_lag_xfrm; ++ bool flush_lag_xfrm = false; ++#endif ++ + if (bond->xdp_prog && !bond_xdp_check(bond, newval->value)) + return -EOPNOTSUPP; + +@@ -924,8 +932,26 @@ static int bond_option_mode_set(struct bonding *bond, + + /* don't cache arp_validate between modes */ + bond->params.arp_validate = BOND_ARP_VALIDATE_NONE; ++ + bond->params.mode = newval->value; + ++#if IS_ENABLED(CONFIG_XFRM_OFFLOAD) ++ new_lag_xfrm = bond_mode_can_use_lag_xfrm(bond); ++ if (old_ab_xfrm && new_lag_xfrm) ++ bond->dev->wanted_features &= ~BOND_XFRM_FEATURES; ++ if (old_lag_xfrm && !new_lag_xfrm) { ++ bond_ipsec_lag_begin_flush(bond); ++ flush_lag_xfrm = true; ++ } ++ ++ if (flush_lag_xfrm) { ++ if (bond->dev->reg_state == NETREG_REGISTERED) ++ xfrm_dev_state_flush(dev_net(bond->dev), bond->dev, ++ true); ++ bond_ipsec_lag_end_flush(bond); ++ } ++#endif ++ + /* When changing mode, the bond device is down, we may reduce + * the bond_bcast_neigh_enabled in bond_close() if broadcast_neighbor + * enabled in 8023ad mode. Therefore, only clear broadcast_neighbor +@@ -1581,12 +1607,43 @@ static int bond_option_fail_over_mac_set(struct bonding *bond, + static int bond_option_xmit_hash_policy_set(struct bonding *bond, + const struct bond_opt_value *newval) + { ++#if IS_ENABLED(CONFIG_XFRM_OFFLOAD) ++ bool old_lag_xfrm = bond_mode_can_use_lag_xfrm(bond); ++ bool new_lag_xfrm; ++ bool flush_lag_xfrm = false; ++#endif ++ + if (bond->xdp_prog && !__bond_xdp_check(BOND_MODE(bond), newval->value)) + return -EOPNOTSUPP; + netdev_dbg(bond->dev, "Setting xmit hash policy to %s (%llu)\n", + newval->string, newval->value); ++ + bond->params.xmit_policy = newval->value; + ++#if IS_ENABLED(CONFIG_XFRM_OFFLOAD) ++ new_lag_xfrm = bond_mode_can_use_lag_xfrm(bond); ++ if (old_lag_xfrm && !new_lag_xfrm) { ++ bond_ipsec_lag_begin_flush(bond); ++ flush_lag_xfrm = true; ++ } ++ ++ if (flush_lag_xfrm) { ++ if (bond->dev->reg_state == NETREG_REGISTERED) ++ xfrm_dev_state_flush(dev_net(bond->dev), bond->dev, ++ true); ++ bond_ipsec_lag_end_flush(bond); ++ } ++#endif ++ ++ if (bond->dev->reg_state == NETREG_REGISTERED) { ++ bool update = false; ++ ++ update |= bond_set_xfrm_features(bond); ++ ++ if (update) ++ netdev_update_features(bond->dev); ++ } ++ + return 0; + } + +-- +2.53.0 diff --git a/target/linux/ipq40xx/patches-6.18/900-PCI-qcom-add-hack-compatible-for-ipq4019-Lantiq-DSL.patch b/target/linux/ipq40xx/patches-6.18/900-PCI-qcom-add-hack-compatible-for-ipq4019-Lantiq-DSL.patch index bfd9e4459ed940..42a010b8839102 100644 --- a/target/linux/ipq40xx/patches-6.18/900-PCI-qcom-add-hack-compatible-for-ipq4019-Lantiq-DSL.patch +++ b/target/linux/ipq40xx/patches-6.18/900-PCI-qcom-add-hack-compatible-for-ipq4019-Lantiq-DSL.patch @@ -43,7 +43,7 @@ Signed-off-by: Florian Maurer }; #define QCOM_PCIE_2_7_0_MAX_SUPPLIES 2 -@@ -732,12 +745,65 @@ static int qcom_pcie_post_init_2_3_2(str +@@ -727,12 +740,65 @@ static int qcom_pcie_post_init_2_3_2(str return 0; } @@ -110,7 +110,7 @@ Signed-off-by: Florian Maurer int ret; res->num_clks = devm_clk_bulk_get_all(dev, &res->clks); -@@ -776,6 +842,17 @@ static void qcom_pcie_deinit_2_4_0(struc +@@ -771,6 +837,17 @@ static void qcom_pcie_deinit_2_4_0(struc clk_bulk_disable_unprepare(res->num_clks, res->clks); } @@ -128,7 +128,7 @@ Signed-off-by: Florian Maurer static int qcom_pcie_init_2_4_0(struct qcom_pcie *pcie) { struct qcom_pcie_resources_2_4_0 *res = &pcie->res.v2_4_0; -@@ -1384,6 +1461,16 @@ static const struct qcom_pcie_ops ops_2_ +@@ -1379,6 +1456,16 @@ static const struct qcom_pcie_ops ops_2_ .ltssm_enable = qcom_pcie_2_3_2_ltssm_enable, }; @@ -145,7 +145,7 @@ Signed-off-by: Florian Maurer /* Qcom IP rev.: 2.3.3 Synopsys IP rev.: 4.30a */ static const struct qcom_pcie_ops ops_2_3_3 = { .get_resources = qcom_pcie_get_resources_2_3_3, -@@ -1462,6 +1549,10 @@ static const struct qcom_pcie_cfg cfg_2_ +@@ -1457,6 +1544,10 @@ static const struct qcom_pcie_cfg cfg_2_ .ops = &ops_2_4_0, }; @@ -156,7 +156,7 @@ Signed-off-by: Florian Maurer static const struct qcom_pcie_cfg cfg_2_7_0 = { .ops = &ops_2_7_0, }; -@@ -2021,6 +2112,7 @@ static const struct of_device_id qcom_pc +@@ -2016,6 +2107,7 @@ static const struct of_device_id qcom_pc { .compatible = "qcom,pcie-apq8064", .data = &cfg_2_1_0 }, { .compatible = "qcom,pcie-apq8084", .data = &cfg_1_0_0 }, { .compatible = "qcom,pcie-ipq4019", .data = &cfg_2_4_0 }, diff --git a/target/linux/ramips/files/drivers/gpio/gpio-ralink.c b/target/linux/ramips/files/drivers/gpio/gpio-ralink.c new file mode 100644 index 00000000000000..6d6428b7d7c648 --- /dev/null +++ b/target/linux/ramips/files/drivers/gpio/gpio-ralink.c @@ -0,0 +1,311 @@ +/* + * This program is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 as published + * by the Free Software Foundation. + * + * Copyright (C) 2009-2011 Gabor Juhos + * Copyright (C) 2013 John Crispin + */ + +#include +#include +#include +#include +#include +#include +#include + +#define MAX_NGPIOS 95 + +enum ralink_gpio_reg { + GPIO_REG_INT = 0, + GPIO_REG_EDGE, + GPIO_REG_RENA, + GPIO_REG_FENA, + GPIO_REG_DATA, + GPIO_REG_DIR, + GPIO_REG_POL, + GPIO_REG_SET, + GPIO_REG_RESET, + GPIO_REG_TOGGLE, + GPIO_REG_MAX +}; + +struct ralink_gpio_chip { + struct gpio_chip chip; + u8 regs[GPIO_REG_MAX]; + + spinlock_t lock; + void __iomem *membase; + int gpio_irq; + + u32 rising; + u32 falling; +}; + +static inline void rt_gpio_w32(struct ralink_gpio_chip *rg, u8 reg, u32 val) +{ + iowrite32(val, rg->membase + rg->regs[reg]); +} + +static inline u32 rt_gpio_r32(struct ralink_gpio_chip *rg, u8 reg) +{ + return ioread32(rg->membase + rg->regs[reg]); +} + +static irqreturn_t ralink_gpio_irq_handler(int irq, void *data) +{ + struct gpio_chip *gc = data; + struct ralink_gpio_chip *rg = gpiochip_get_data(gc); + irqreturn_t ret = IRQ_NONE; + unsigned long pending; + int bit; + + pending = rt_gpio_r32(rg, GPIO_REG_INT); + for_each_set_bit(bit, &pending, rg->chip.ngpio) { + generic_handle_domain_irq(gc->irq.domain, bit); + rt_gpio_w32(rg, GPIO_REG_INT, BIT(bit)); + ret |= IRQ_HANDLED; + } + + return ret; +} + +static void ralink_gpio_irq_unmask(struct irq_data *d) +{ + struct gpio_chip *gc = irq_data_get_irq_chip_data(d); + struct ralink_gpio_chip *rg = gpiochip_get_data(gc); + unsigned long flags; + u32 rise, fall; + + rise = rt_gpio_r32(rg, GPIO_REG_RENA); + fall = rt_gpio_r32(rg, GPIO_REG_FENA); + + spin_lock_irqsave(&rg->lock, flags); + rt_gpio_w32(rg, GPIO_REG_RENA, rise | (BIT(d->hwirq) & rg->rising)); + rt_gpio_w32(rg, GPIO_REG_FENA, fall | (BIT(d->hwirq) & rg->falling)); + spin_unlock_irqrestore(&rg->lock, flags); +} + +static void ralink_gpio_irq_mask(struct irq_data *d) +{ + struct gpio_chip *gc = irq_data_get_irq_chip_data(d); + struct ralink_gpio_chip *rg = gpiochip_get_data(gc); + unsigned long flags; + u32 rise, fall; + + rise = rt_gpio_r32(rg, GPIO_REG_RENA); + fall = rt_gpio_r32(rg, GPIO_REG_FENA); + + spin_lock_irqsave(&rg->lock, flags); + rt_gpio_w32(rg, GPIO_REG_FENA, fall & ~BIT(d->hwirq)); + rt_gpio_w32(rg, GPIO_REG_RENA, rise & ~BIT(d->hwirq)); + spin_unlock_irqrestore(&rg->lock, flags); +} + +static int ralink_gpio_irq_type(struct irq_data *d, unsigned int type) +{ + struct gpio_chip *gc = irq_data_get_irq_chip_data(d); + struct ralink_gpio_chip *rg = gpiochip_get_data(gc); + u32 mask = BIT(d->hwirq); + + if (type == IRQ_TYPE_PROBE) { + if ((rg->rising | rg->falling) & mask) + return 0; + + type = IRQ_TYPE_EDGE_RISING | IRQ_TYPE_EDGE_FALLING; + } + + if (type & IRQ_TYPE_EDGE_RISING) + rg->rising |= mask; + else + rg->rising &= ~mask; + + if (type & IRQ_TYPE_EDGE_FALLING) + rg->falling |= mask; + else + rg->falling &= ~mask; + + return 0; +} + +static struct irq_chip ralink_gpio_irq_chip = { + .name = "gpio-ralink", + .irq_unmask = ralink_gpio_irq_unmask, + .irq_mask = ralink_gpio_irq_mask, + .irq_mask_ack = ralink_gpio_irq_mask, + .irq_set_type = ralink_gpio_irq_type, + .flags = IRQCHIP_IMMUTABLE, + GPIOCHIP_IRQ_RESOURCE_HELPERS, +}; + +static int ralink_get_direction(struct gpio_chip *gc, unsigned offset) +{ + struct ralink_gpio_chip *rg = gpiochip_get_data(gc); + + return !(rt_gpio_r32(rg, GPIO_REG_DIR) & BIT(offset)); +} + +static int ralink_direction_input(struct gpio_chip *gc, unsigned offset) +{ + struct ralink_gpio_chip *rg = gpiochip_get_data(gc); + u32 val = rt_gpio_r32(rg, GPIO_REG_DIR); + rt_gpio_w32(rg, GPIO_REG_DIR, val & ~BIT(offset)); + + return 0; +} + +static int ralink_direction_output(struct gpio_chip *gc, unsigned offset, int val) +{ + struct ralink_gpio_chip *rg = gpiochip_get_data(gc); + rt_gpio_w32(rg, val ? GPIO_REG_SET : GPIO_REG_RESET, BIT(offset)); + rt_gpio_w32(rg, GPIO_REG_DIR, rt_gpio_r32(rg, GPIO_REG_DIR) | BIT(offset)); + + return 0; +} + +static int ralink_get(struct gpio_chip *gc, unsigned offset) +{ + struct ralink_gpio_chip *rg = gpiochip_get_data(gc); + + return !!(rt_gpio_r32(rg, GPIO_REG_DATA) & BIT(offset)); +} + +static int ralink_set(struct gpio_chip *gc, unsigned offset, int val) +{ + struct ralink_gpio_chip *rg = gpiochip_get_data(gc); + rt_gpio_w32(rg, val ? GPIO_REG_SET : GPIO_REG_RESET, BIT(offset)); + + return 0; +} + +static int ralink_set_multiple(struct gpio_chip *gc, unsigned long *mask, + unsigned long *bits) +{ + struct ralink_gpio_chip *rg = gpiochip_get_data(gc); + unsigned long set = *mask & *bits; + unsigned long reset = *mask & ~*bits; + + if (set) + rt_gpio_w32(rg, GPIO_REG_SET, set); + if (reset) + rt_gpio_w32(rg, GPIO_REG_RESET, reset); + + return 0; +} + +static int ralink_get_multiple(struct gpio_chip *gc, unsigned long *mask, + unsigned long *bits) +{ + struct ralink_gpio_chip *rg = gpiochip_get_data(gc); + *bits = rt_gpio_r32(rg, GPIO_REG_DATA) & *mask; + + return 0; +} + +static int ralink_gpio_probe(struct platform_device *pdev) +{ + struct device *dev = &pdev->dev; + struct fwnode_handle *fw = dev_fwnode(dev); + struct ralink_gpio_chip *rg; + struct fwnode_reference_args args; + u32 ngpios = 0; + int ret; + + rg = devm_kzalloc(dev, sizeof(struct ralink_gpio_chip), GFP_KERNEL); + if (!rg) + return -ENOMEM; + + rg->membase = devm_platform_ioremap_resource(pdev, 0); + if (IS_ERR(rg->membase)) + return PTR_ERR(rg->membase); + + if (device_property_read_u8_array(dev, "ralink,register-map", + rg->regs, GPIO_REG_MAX)) { + dev_err(dev, "failed to read register definition\n"); + return -EINVAL; + } + + if (device_property_read_u32(dev, "ngpios", &ngpios) < 0) + ngpios = 0; + + if (ngpios == 0) { + if (!fwnode_property_get_reference_args(fw, "gpio-ranges", "#gpio-cells", 0, 0, &args)) { + if (args.nargs > 0) + ngpios = args.args[args.nargs - 1]; + fwnode_handle_put(args.fwnode); + } + } + + if (ngpios == 0 || ngpios > MAX_NGPIOS) + ngpios = MAX_NGPIOS; + + spin_lock_init(&rg->lock); + + rg->chip.label = dev_name(dev); + rg->chip.parent = dev; + rg->chip.owner = THIS_MODULE; + rg->chip.base = -1; + rg->chip.ngpio = ngpios; + rg->chip.get_direction = ralink_get_direction; + rg->chip.direction_input = ralink_direction_input; + rg->chip.direction_output= ralink_direction_output; + rg->chip.get = ralink_get; + rg->chip.set = ralink_set; + rg->chip.get_multiple = ralink_get_multiple; + rg->chip.set_multiple = ralink_set_multiple; + + /* set polarity to low for all lines */ + rt_gpio_w32(rg, GPIO_REG_POL, 0); + + rg->gpio_irq = platform_get_irq(pdev, 0); + if (rg->gpio_irq < 0) + return rg->gpio_irq; + + if (rg->gpio_irq) { + struct gpio_irq_chip *girq; + + /* + * Directly request the irq here instead of passing + * a flow-handler because the irq is shared. + */ + ret = devm_request_irq(dev, rg->gpio_irq, + ralink_gpio_irq_handler, IRQF_SHARED, + NULL, &rg->chip); + if (ret) { + dev_err(dev, "Error requesting IRQ %d: %d\n", + rg->gpio_irq, ret); + return ret; + } + + girq = &rg->chip.irq; + gpio_irq_chip_set_chip(girq, &ralink_gpio_irq_chip); + /* This will let us handle the parent IRQ in the driver */ + girq->parent_handler = NULL; + girq->num_parents = 0; + girq->parents = NULL; + girq->default_type = IRQ_TYPE_NONE; + girq->handler = handle_simple_irq; + + rt_gpio_w32(rg, GPIO_REG_RENA, 0); + rt_gpio_w32(rg, GPIO_REG_FENA, 0); + } + + return devm_gpiochip_add_data(dev, &rg->chip, rg); +} + +static const struct of_device_id ralink_gpio_match[] = { + { .compatible = "ralink,rt2880-gpio" }, + {}, +}; +MODULE_DEVICE_TABLE(of, ralink_gpio_match); + +static struct platform_driver ralink_gpio_driver = { + .probe = ralink_gpio_probe, + .driver = { + .name = "ralink_gpio", + .of_match_table = ralink_gpio_match, + }, +}; + +module_platform_driver(ralink_gpio_driver); diff --git a/target/linux/ramips/patches-6.18/802-GPIO-MIPS-ralink-add-gpio-driver-for-ralink-SoC.patch b/target/linux/ramips/patches-6.18/802-GPIO-MIPS-ralink-add-gpio-driver-for-ralink-SoC.patch index 75892d4c8cd3b5..3a7e8d3bbfb23c 100644 --- a/target/linux/ramips/patches-6.18/802-GPIO-MIPS-ralink-add-gpio-driver-for-ralink-SoC.patch +++ b/target/linux/ramips/patches-6.18/802-GPIO-MIPS-ralink-add-gpio-driver-for-ralink-SoC.patch @@ -9,9 +9,7 @@ Signed-off-by: John Crispin --- drivers/gpio/Kconfig | 8 ++ drivers/gpio/Makefile | 1 + - drivers/gpio/gpio-ralink.c | 230 +++++++++++++++++++++++++++++++++++++ - 3 files changed, 239 insertions(+) - create mode 100644 drivers/gpio/gpio-ralink.c + 2 files changed, 9 insertions(+) --- a/drivers/gpio/Kconfig +++ b/drivers/gpio/Kconfig @@ -40,322 +38,3 @@ Signed-off-by: John Crispin obj-$(CONFIG_GPIO_RASPBERRYPI_EXP) += gpio-raspberrypi-exp.o obj-$(CONFIG_GPIO_RC5T583) += gpio-rc5t583.o obj-$(CONFIG_GPIO_RCAR) += gpio-rcar.o ---- /dev/null -+++ b/drivers/gpio/gpio-ralink.c -@@ -0,0 +1,316 @@ -+/* -+ * This program is free software; you can redistribute it and/or modify it -+ * under the terms of the GNU General Public License version 2 as published -+ * by the Free Software Foundation. -+ * -+ * Copyright (C) 2009-2011 Gabor Juhos -+ * Copyright (C) 2013 John Crispin -+ */ -+ -+#include -+#include -+#include -+#include -+#include -+#include -+#include -+ -+#define MAX_NGPIOS 95 -+ -+enum ralink_gpio_reg { -+ GPIO_REG_INT = 0, -+ GPIO_REG_EDGE, -+ GPIO_REG_RENA, -+ GPIO_REG_FENA, -+ GPIO_REG_DATA, -+ GPIO_REG_DIR, -+ GPIO_REG_POL, -+ GPIO_REG_SET, -+ GPIO_REG_RESET, -+ GPIO_REG_TOGGLE, -+ GPIO_REG_MAX -+}; -+ -+struct ralink_gpio_chip { -+ struct gpio_chip chip; -+ u8 regs[GPIO_REG_MAX]; -+ -+ spinlock_t lock; -+ void __iomem *membase; -+ int gpio_irq; -+ -+ u32 rising; -+ u32 falling; -+}; -+ -+static inline void rt_gpio_w32(struct ralink_gpio_chip *rg, u8 reg, u32 val) -+{ -+ iowrite32(val, rg->membase + rg->regs[reg]); -+} -+ -+static inline u32 rt_gpio_r32(struct ralink_gpio_chip *rg, u8 reg) -+{ -+ return ioread32(rg->membase + rg->regs[reg]); -+} -+ -+static irqreturn_t ralink_gpio_irq_handler(int irq, void *data) -+{ -+ struct gpio_chip *gc = data; -+ struct ralink_gpio_chip *rg = gpiochip_get_data(gc); -+ irqreturn_t ret = IRQ_NONE; -+ unsigned long pending; -+ int bit; -+ -+ pending = rt_gpio_r32(rg, GPIO_REG_INT); -+ for_each_set_bit(bit, &pending, rg->chip.ngpio) { -+ generic_handle_domain_irq(gc->irq.domain, bit); -+ rt_gpio_w32(rg, GPIO_REG_INT, BIT(bit)); -+ ret |= IRQ_HANDLED; -+ } -+ -+ return ret; -+} -+ -+static void ralink_gpio_irq_unmask(struct irq_data *d) -+{ -+ struct gpio_chip *gc = irq_data_get_irq_chip_data(d); -+ struct ralink_gpio_chip *rg = gpiochip_get_data(gc); -+ unsigned long flags; -+ u32 rise, fall; -+ -+ rise = rt_gpio_r32(rg, GPIO_REG_RENA); -+ fall = rt_gpio_r32(rg, GPIO_REG_FENA); -+ -+ spin_lock_irqsave(&rg->lock, flags); -+ rt_gpio_w32(rg, GPIO_REG_RENA, rise | (BIT(d->hwirq) & rg->rising)); -+ rt_gpio_w32(rg, GPIO_REG_FENA, fall | (BIT(d->hwirq) & rg->falling)); -+ spin_unlock_irqrestore(&rg->lock, flags); -+} -+ -+static void ralink_gpio_irq_mask(struct irq_data *d) -+{ -+ struct gpio_chip *gc = irq_data_get_irq_chip_data(d); -+ struct ralink_gpio_chip *rg = gpiochip_get_data(gc); -+ unsigned long flags; -+ u32 rise, fall; -+ -+ rise = rt_gpio_r32(rg, GPIO_REG_RENA); -+ fall = rt_gpio_r32(rg, GPIO_REG_FENA); -+ -+ spin_lock_irqsave(&rg->lock, flags); -+ rt_gpio_w32(rg, GPIO_REG_FENA, fall & ~BIT(d->hwirq)); -+ rt_gpio_w32(rg, GPIO_REG_RENA, rise & ~BIT(d->hwirq)); -+ spin_unlock_irqrestore(&rg->lock, flags); -+} -+ -+static int ralink_gpio_irq_type(struct irq_data *d, unsigned int type) -+{ -+ struct gpio_chip *gc = irq_data_get_irq_chip_data(d); -+ struct ralink_gpio_chip *rg = gpiochip_get_data(gc); -+ u32 mask = BIT(d->hwirq); -+ -+ if (type == IRQ_TYPE_PROBE) { -+ if ((rg->rising | rg->falling) & mask) -+ return 0; -+ -+ type = IRQ_TYPE_EDGE_RISING | IRQ_TYPE_EDGE_FALLING; -+ } -+ -+ if (type & IRQ_TYPE_EDGE_RISING) -+ rg->rising |= mask; -+ else -+ rg->rising &= ~mask; -+ -+ if (type & IRQ_TYPE_EDGE_FALLING) -+ rg->falling |= mask; -+ else -+ rg->falling &= ~mask; -+ -+ return 0; -+} -+ -+static struct irq_chip ralink_gpio_irq_chip = { -+ .name = "gpio-ralink", -+ .irq_unmask = ralink_gpio_irq_unmask, -+ .irq_mask = ralink_gpio_irq_mask, -+ .irq_mask_ack = ralink_gpio_irq_mask, -+ .irq_set_type = ralink_gpio_irq_type, -+ .flags = IRQCHIP_IMMUTABLE, -+ GPIOCHIP_IRQ_RESOURCE_HELPERS, -+}; -+ -+static int ralink_get_direction(struct gpio_chip *gc, unsigned offset) -+{ -+ struct ralink_gpio_chip *rg = gpiochip_get_data(gc); -+ -+ return !(rt_gpio_r32(rg, GPIO_REG_DIR) & BIT(offset)); -+} -+ -+static int ralink_direction_input(struct gpio_chip *gc, unsigned offset) -+{ -+ struct ralink_gpio_chip *rg = gpiochip_get_data(gc); -+ u32 val = rt_gpio_r32(rg, GPIO_REG_DIR); -+ rt_gpio_w32(rg, GPIO_REG_DIR, val & ~BIT(offset)); -+ -+ return 0; -+} -+ -+static int ralink_direction_output(struct gpio_chip *gc, unsigned offset, int val) -+{ -+ struct ralink_gpio_chip *rg = gpiochip_get_data(gc); -+ rt_gpio_w32(rg, val ? GPIO_REG_SET : GPIO_REG_RESET, BIT(offset)); -+ rt_gpio_w32(rg, GPIO_REG_DIR, rt_gpio_r32(rg, GPIO_REG_DIR) | BIT(offset)); -+ -+ return 0; -+} -+ -+static int ralink_get(struct gpio_chip *gc, unsigned offset) -+{ -+ struct ralink_gpio_chip *rg = gpiochip_get_data(gc); -+ -+ return !!(rt_gpio_r32(rg, GPIO_REG_DATA) & BIT(offset)); -+} -+ -+static int ralink_set(struct gpio_chip *gc, unsigned offset, int val) -+{ -+ struct ralink_gpio_chip *rg = gpiochip_get_data(gc); -+ rt_gpio_w32(rg, val ? GPIO_REG_SET : GPIO_REG_RESET, BIT(offset)); -+ -+ return 0; -+} -+ -+static int ralink_set_multiple(struct gpio_chip *gc, unsigned long *mask, -+ unsigned long *bits) -+{ -+ struct ralink_gpio_chip *rg = gpiochip_get_data(gc); -+ unsigned long set = *mask & *bits; -+ unsigned long reset = *mask & ~*bits; -+ -+ if (set) -+ rt_gpio_w32(rg, GPIO_REG_SET, set); -+ if (reset) -+ rt_gpio_w32(rg, GPIO_REG_RESET, reset); -+ -+ return 0; -+} -+ -+static int ralink_get_multiple(struct gpio_chip *gc, unsigned long *mask, -+ unsigned long *bits) -+{ -+ struct ralink_gpio_chip *rg = gpiochip_get_data(gc); -+ *bits = rt_gpio_r32(rg, GPIO_REG_DATA) & *mask; -+ -+ return 0; -+} -+ -+static int ralink_gpio_probe(struct platform_device *pdev) -+{ -+ struct device *dev = &pdev->dev; -+ struct device_node *np = dev->of_node; -+ struct ralink_gpio_chip *rg; -+ struct of_phandle_args args; -+ u32 ngpios = 0; -+ int ret; -+ -+ rg = devm_kzalloc(dev, sizeof(struct ralink_gpio_chip), GFP_KERNEL); -+ if (!rg) -+ return -ENOMEM; -+ -+ rg->membase = devm_platform_ioremap_resource(pdev, 0); -+ if (IS_ERR(rg->membase)) -+ return PTR_ERR(rg->membase); -+ -+ if (of_property_read_u8_array(np, "ralink,register-map", -+ rg->regs, GPIO_REG_MAX)) { -+ dev_err(dev, "failed to read register definition\n"); -+ return -EINVAL; -+ } -+ -+ if (of_property_read_u32(np, "ngpios", &ngpios) < 0) -+ ngpios = 0; -+ -+ if (ngpios == 0) { -+ if (of_parse_phandle_with_args(np, "gpio-ranges", "#gpio-cells", 0, &args) == 0) { -+ if (args.args_count > 0) -+ ngpios = args.args[args.args_count - 1]; -+ of_node_put(args.np); -+ } -+ } -+ -+ if (ngpios == 0 || ngpios > MAX_NGPIOS) -+ ngpios = MAX_NGPIOS; -+ -+ spin_lock_init(&rg->lock); -+ -+ rg->chip.label = dev_name(dev); -+ rg->chip.parent = dev; -+ rg->chip.owner = THIS_MODULE; -+ rg->chip.base = -1; -+ rg->chip.ngpio = ngpios; -+ rg->chip.get_direction = ralink_get_direction; -+ rg->chip.direction_input = ralink_direction_input; -+ rg->chip.direction_output= ralink_direction_output; -+ rg->chip.get = ralink_get; -+ rg->chip.set = ralink_set; -+ rg->chip.get_multiple = ralink_get_multiple; -+ rg->chip.set_multiple = ralink_set_multiple; -+ -+ /* set polarity to low for all lines */ -+ rt_gpio_w32(rg, GPIO_REG_POL, 0); -+ -+ rg->gpio_irq = platform_get_irq(pdev, 0); -+ if (rg->gpio_irq < 0) -+ return rg->gpio_irq; -+ -+ if (rg->gpio_irq) { -+ struct gpio_irq_chip *girq; -+ -+ /* -+ * Directly request the irq here instead of passing -+ * a flow-handler because the irq is shared. -+ */ -+ ret = devm_request_irq(dev, rg->gpio_irq, -+ ralink_gpio_irq_handler, IRQF_SHARED, -+ NULL, &rg->chip); -+ if (ret) { -+ dev_err(dev, "Error requesting IRQ %d: %d\n", -+ rg->gpio_irq, ret); -+ return ret; -+ } -+ -+ girq = &rg->chip.irq; -+ gpio_irq_chip_set_chip(girq, &ralink_gpio_irq_chip); -+ /* This will let us handle the parent IRQ in the driver */ -+ girq->parent_handler = NULL; -+ girq->num_parents = 0; -+ girq->parents = NULL; -+ girq->default_type = IRQ_TYPE_NONE; -+ girq->handler = handle_simple_irq; -+ -+ rt_gpio_w32(rg, GPIO_REG_RENA, 0); -+ rt_gpio_w32(rg, GPIO_REG_FENA, 0); -+ } -+ -+ return devm_gpiochip_add_data(dev, &rg->chip, rg); -+} -+ -+static const struct of_device_id ralink_gpio_match[] = { -+ { .compatible = "ralink,rt2880-gpio" }, -+ {}, -+}; -+MODULE_DEVICE_TABLE(of, ralink_gpio_match); -+ -+static struct platform_driver ralink_gpio_driver = { -+ .probe = ralink_gpio_probe, -+ .driver = { -+ .name = "ralink_gpio", -+ .of_match_table = ralink_gpio_match, -+ }, -+}; -+ -+static int __init ralink_gpio_init(void) -+{ -+ return platform_driver_register(&ralink_gpio_driver); -+} -+ -+subsys_initcall(ralink_gpio_init); diff --git a/target/linux/realtek/base-files/etc/board.d/02_network b/target/linux/realtek/base-files/etc/board.d/02_network index 95ea56a268e1fb..899f3f117d6e15 100644 --- a/target/linux/realtek/base-files/etc/board.d/02_network +++ b/target/linux/realtek/base-files/etc/board.d/02_network @@ -95,12 +95,14 @@ realtek_setup_macs() plasmacloud,psx10|\ plasmacloud,psx28|\ sirivision,sr-st3408f|\ + ubnt,usw-pro-xg-8-poe|\ zyxel,gs1920-24hp-v2) lan_mac="$(get_mac_label)" ;; hasivo,f1100w-4sx-4xgt|\ hasivo,f1100w-4sx-4xgt-512mb|\ hasivo,f5800w-12s-plus|\ + hasivo,s1300wp-8xgt-4s-plus|\ tplink,tl-st1008f-v2|\ zyxel,xgs1010-12-a1|\ zyxel,xgs1010-12-b1) @@ -249,9 +251,21 @@ realtek_setup_poe() esac } +realtek_lan_list() +{ + local labels + + labels="$(find /sys/firmware/devicetree/base -path '*/ethernet-ports/port@*/label' 2>/dev/null | \ + xargs cat 2>/dev/null | tr '\0' '\n' | grep '^lan' | sort -V | uniq | xargs)" + + [ -n "$labels" ] || labels="$(ls -1 -v -d /sys/class/net/lan* 2>/dev/null | xargs -n1 basename | xargs)" + + echo "$labels" +} + board=$(board_name) board_config_update -lan_list=$(ls -1 -v -d /sys/class/net/lan* | xargs -n1 basename | xargs) +lan_list=$(realtek_lan_list) realtek_setup_interfaces "$board" "$lan_list" realtek_setup_macs "$board" "$lan_list" realtek_setup_poe "$board" "$lan_list" diff --git a/target/linux/realtek/base-files/etc/uci-defaults/99_fwenv-store-ethaddr b/target/linux/realtek/base-files/etc/uci-defaults/99_fwenv-store-ethaddr index 5a8e3a8ae1bd28..53fb5607d10f84 100644 --- a/target/linux/realtek/base-files/etc/uci-defaults/99_fwenv-store-ethaddr +++ b/target/linux/realtek/base-files/etc/uci-defaults/99_fwenv-store-ethaddr @@ -44,6 +44,7 @@ case "$(board_name)" in hasivo,f1100w-4sx-4xgt|\ hasivo,f1100w-4sx-4xgt-512mb|\ hasivo,f5800w-12s-plus|\ +hasivo,s1300wp-8xgt-4s-plus|\ tplink,tl-st1008f-v2) env_ethaddr=$(macaddr_canonicalize "$(fw_printenv -n ethaddr 2>/dev/null)") ethaddr_prefix=$(echo "$env_ethaddr" | cut -d: -f1-5) diff --git a/target/linux/realtek/dts/rtl9313_hasivo_s1300wp-8xgt-4s-plus.dts b/target/linux/realtek/dts/rtl9313_hasivo_s1300wp-8xgt-4s-plus.dts new file mode 100644 index 00000000000000..8f6cdf4627c9ff --- /dev/null +++ b/target/linux/realtek/dts/rtl9313_hasivo_s1300wp-8xgt-4s-plus.dts @@ -0,0 +1,320 @@ +// SPDX-License-Identifier: GPL-2.0-or-later + +#include "rtl931x.dtsi" + +#include +#include +#include +#include + +/ { + compatible = "hasivo,s1300wp-8xgt-4s-plus", "realtek,rtl9313-soc"; + model = "Hasivo S1300WP-8XGT-4S+"; + + memory@0 { + device_type = "memory"; + reg = <0x00000000 0x10000000>, + <0x90000000 0x10000000>; + }; + + aliases { + label-mac-device = ðernet0; + led-boot = &led_sys; + led-failsafe = &led_sys; + led-running = &led_sys; + led-upgrade = &led_sys; + }; + + chosen { + stdout-path = "serial0:38400n8"; + }; + + keys { + compatible = "gpio-keys"; + + button-reset { + label = "reset"; + gpios = <&gpio0 31 GPIO_ACTIVE_LOW>; + linux,code = ; + }; + }; + + sfp9: sfp-p9 { + compatible = "sff,sfp"; + i2c-bus = <&i2c1_sda8>; + los-gpios = <&gpio0 9 GPIO_ACTIVE_HIGH>; + #thermal-sensor-cells = <0>; + }; + + sfp10: sfp-p10 { + compatible = "sff,sfp"; + i2c-bus = <&i2c1_sda6>; + los-gpios = <&gpio0 10 GPIO_ACTIVE_HIGH>; + #thermal-sensor-cells = <0>; + }; + + sfp11: sfp-p11 { + compatible = "sff,sfp"; + i2c-bus = <&i2c1_sda0>; + los-gpios = <&gpio0 8 GPIO_ACTIVE_HIGH>; + #thermal-sensor-cells = <0>; + }; + + sfp12: sfp-p12 { + compatible = "sff,sfp"; + i2c-bus = <&i2c1_sda1>; + los-gpios = <&gpio0 11 GPIO_ACTIVE_HIGH>; + #thermal-sensor-cells = <0>; + }; + + i2c_sys: i2c-system { + compatible = "i2c-gpio"; + #address-cells = <1>; + #size-cells = <0>; + + pinctrl-names = "default"; + pinctrl-0 = <&pinmux_disable_jtag>; + + scl-gpios = <&gpio0 4 (GPIO_ACTIVE_HIGH | GPIO_OPEN_DRAIN)>; + sda-gpios = <&gpio0 3 (GPIO_ACTIVE_HIGH | GPIO_OPEN_DRAIN)>; + i2c-gpio,delay-us = <28>; + + rtc@51 { + compatible = "nxp,pcf8563"; + reg = <0x51>; + }; + + mcu_mgmt: mcu@6f { + compatible = "hasivo,stc8-mfd", "syscon"; + reg = <0x6f>; + + mcu_wdt: watchdog { + compatible = "hasivo,mcu-wdt"; + }; + + sensor { + compatible = "hasivo,mcu-sensor"; + }; + }; + }; + + leds { + compatible = "gpio-leds"; + + led_sys: led-0 { + gpios = <&gpio0 30 GPIO_ACTIVE_HIGH>; + color = ; + function = LED_FUNCTION_STATUS; + }; + }; + + led_set { + compatible = "realtek,rtl9300-leds"; + active-low; + + led_set0 = <(RTL93XX_LED_SET_10G | RTL93XX_LED_SET_LINK | + RTL93XX_LED_SET_ACT) + (RTL93XX_LED_SET_5G | RTL93XX_LED_SET_2P5G | + RTL93XX_LED_SET_LINK | RTL93XX_LED_SET_ACT) + (RTL93XX_LED_SET_1G | RTL93XX_LED_SET_100M | + RTL93XX_LED_SET_10M | RTL93XX_LED_SET_LINK | + RTL93XX_LED_SET_ACT)>; + }; +}; + +&i2c_mst1 { + status = "okay"; + + i2c1_sda0: i2c@0 { reg = <0>; }; + i2c1_sda1: i2c@1 { reg = <1>; }; + i2c1_sda6: i2c@6 { reg = <6>; }; + i2c1_sda8: i2c@8 { reg = <8>; }; +}; + +&spi0 { + status = "okay"; + + flash@0 { + compatible = "jedec,spi-nor"; + reg = <0>; + spi-max-frequency = <10000000>; + + partitions { + compatible = "fixed-partitions"; + #address-cells = <1>; + #size-cells = <1>; + + partition@0 { + label = "u-boot"; + reg = <0x000000 0x0e0000>; + read-only; + }; + + /* stock layout calls this BDINFO; it holds the U-Boot environment */ + partition@e0000 { + label = "u-boot-env"; + reg = <0x0e0000 0x010000>; + read-only; + }; + + /* stock layout calls this SYSINFO */ + partition@f0000 { + label = "u-boot-env2"; + reg = <0x0f0000 0x010000>; + read-only; + }; + + partition@100000 { + label = "cfg"; + reg = <0x100000 0x100000>; + }; + + partition@200000 { + label = "log"; + reg = <0x200000 0x100000>; + }; + + partition@300000 { + compatible = "openwrt,uimage", "denx,uimage"; + label = "firmware"; + reg = <0x300000 0x1d00000>; + }; + }; + }; +}; + +&mdio_ctrl { + pinctrl-names = "default"; + pinctrl-0 = <&pinmux_enable_mdc_mdio_0>, + <&pinmux_enable_mdc_mdio_1>; +}; + +&mdio_bus0 { + /* RTL8264 quad-PHY package */ + ethernet-phy-package@0 { + #address-cells = <1>; + #size-cells = <0>; + reg = <0>; + + PHY_C45(0, 0) + PHY_C45(8, 1) + PHY_C45(16, 2) + PHY_C45(24, 3) + }; +}; + +&mdio_bus1 { + /* RTL8264 quad-PHY package */ + ethernet-phy-package@4 { + #address-cells = <1>; + #size-cells = <0>; + reg = <4>; + + PHY_C45(32, 4) + PHY_C45(40, 5) + PHY_C45(48, 6) + PHY_C45(50, 7) + }; +}; + +&phy0 { + tx-polarity = ; + realtek,enable-pma-low-power; +}; + +&phy8 { + tx-polarity = ; + rx-polarity = ; + realtek,enable-pma-low-power; +}; + +&phy16 { + tx-polarity = ; + realtek,enable-pma-low-power; +}; + +&phy24 { + tx-polarity = ; + rx-polarity = ; + realtek,enable-pma-low-power; +}; + +&phy32 { + tx-polarity = ; + realtek,enable-pma-low-power; +}; + +&phy40 { + tx-polarity = ; + rx-polarity = ; + realtek,enable-pma-low-power; +}; + +&phy48 { + tx-polarity = ; + realtek,enable-pma-low-power; +}; + +&phy50 { + tx-polarity = ; + rx-polarity = ; + realtek,enable-pma-low-power; +}; + +&switch0 { + ethernet-ports { + #address-cells = <1>; + #size-cells = <0>; + + SWITCH_PORT_LED(0, 1, 2, 0, 0, usxgmii) + SWITCH_PORT_LED(8, 2, 3, 0, 0, usxgmii) + SWITCH_PORT_LED(16, 3, 4, 0, 0, usxgmii) + SWITCH_PORT_LED(24, 4, 5, 0, 0, usxgmii) + SWITCH_PORT_LED(32, 5, 6, 0, 0, usxgmii) + SWITCH_PORT_LED(40, 6, 7, 0, 0, usxgmii) + SWITCH_PORT_LED(48, 7, 8, 0, 0, usxgmii) + SWITCH_PORT_LED(50, 8, 9, 0, 0, usxgmii) + + SWITCH_PORT_SFP(52, 9, 10, 0, 9) + SWITCH_PORT_SFP(53, 10, 11, 0, 10) + SWITCH_PORT_SFP(54, 11, 12, 0, 11) + SWITCH_PORT_SFP(55, 12, 13, 0, 12) + + port@56 { + ethernet = <ðernet0>; + reg = <56>; + phy-mode = "internal"; + + fixed-link { + speed = <1000>; + full-duplex; + }; + }; + }; +}; + +&serdes2 { rx-polarity = ; }; +&serdes3 { + tx-polarity = ; + rx-polarity = ; +}; +&serdes4 { rx-polarity = ; }; +&serdes5 { + tx-polarity = ; + rx-polarity = ; +}; +&serdes6 { rx-polarity = ; }; +&serdes7 { + tx-polarity = ; + rx-polarity = ; +}; +&serdes8 { rx-polarity = ; }; +&serdes9 { + tx-polarity = ; + rx-polarity = ; +}; + +&serdes10 { tx-polarity = ; }; +&serdes11 { tx-polarity = ; }; +&serdes12 { tx-polarity = ; }; +&serdes13 { tx-polarity = ; }; diff --git a/target/linux/realtek/dts/rtl9313_ubnt_usw-pro-xg-8-poe.dts b/target/linux/realtek/dts/rtl9313_ubnt_usw-pro-xg-8-poe.dts new file mode 100644 index 00000000000000..b853ee1d172a64 --- /dev/null +++ b/target/linux/realtek/dts/rtl9313_ubnt_usw-pro-xg-8-poe.dts @@ -0,0 +1,291 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/dts-v1/; + +#include "rtl931x.dtsi" + +#include +#include +#include +#include +#include + +/ { + compatible = "ubnt,usw-pro-xg-8-poe", "realtek,rtl9313-soc"; + model = "UniFi USW Pro XG 8 PoE"; + + aliases { + label-mac-device = ðernet0; + led-boot = &led_sys_white; + led-failsafe = &led_sys_white; + led-running = &led_sys_blue; + led-upgrade = &led_sys_white; + }; + + chosen { + bootargs = "console=ttyS0,115200 earlycon"; + stdout-path = "serial0:115200n8"; + }; + + memory@0 { + device_type = "memory"; + reg = <0x00000000 0x10000000>, /* first 256 MiB */ + <0x90000000 0x10000000>; /* remaining 256 MiB */ + }; + + keys { + compatible = "gpio-keys"; + + key-reset { + label = "reset"; + gpios = <&gpio0 26 GPIO_ACTIVE_LOW>; + linux,code = ; + }; + }; + + leds: leds { + compatible = "gpio-leds"; + pinctrl-names = "default"; + pinctrl-0 = <&pinmux_disable_sys_led>; + + led_sys_blue: led-0 { + color = ; + function = LED_FUNCTION_INDICATOR; + gpios = <&gpio0 7 GPIO_ACTIVE_HIGH>; + }; + + led_sys_white: led-1 { + color = ; + function = LED_FUNCTION_INDICATOR; + gpios = <&gpio0 9 GPIO_ACTIVE_HIGH>; + }; + }; + + led_set: led_set@0 { + compatible = "realtek,rtl9300-leds"; + clock-frequency = <1250000>; + active-low; + + /* + * selects all speed modes to trigger a LED, two slots. This doesn't correspond + * fully to actual LED behavior. The serial stream is fed into the Etherlighting + * MCU which translates that into the LEDs, managing color, behavior etc. in + * addition. + */ + led_set0 = <(RTL93XX_LED_SET_10G | RTL93XX_LED_SET_5G | RTL93XX_LED_SET_2P5G | + RTL93XX_LED_SET_1G | RTL93XX_LED_SET_100M | RTL93XX_LED_SET_10M | + RTL93XX_LED_SET_LINK | RTL93XX_LED_SET_ACT) + (RTL93XX_LED_SET_10G | RTL93XX_LED_SET_5G | RTL93XX_LED_SET_2P5G | + RTL93XX_LED_SET_1G | RTL93XX_LED_SET_100M | RTL93XX_LED_SET_10M | + RTL93XX_LED_SET_LINK | RTL93XX_LED_SET_ACT)>; + }; + + sfp1: sfp-p1 { + compatible = "sff,sfp"; + i2c-bus = <&i2c_sfp1>; + los-gpio = <&gpio2 0 GPIO_ACTIVE_HIGH>; + mod-def0-gpio = <&gpio2 2 GPIO_ACTIVE_LOW>; + tx-disable-gpio = <&gpio2 3 GPIO_ACTIVE_HIGH>; + tx-fault-gpio = <&gpio2 1 GPIO_ACTIVE_HIGH>; + }; + + sfp2: sfp-p2 { + compatible = "sff,sfp"; + i2c-bus = <&i2c_sfp2>; + los-gpio = <&gpio2 4 GPIO_ACTIVE_HIGH>; + mod-def0-gpio = <&gpio2 6 GPIO_ACTIVE_LOW>; + tx-disable-gpio = <&gpio2 7 GPIO_ACTIVE_HIGH>; + tx-fault-gpio = <&gpio2 5 GPIO_ACTIVE_HIGH>; + }; +}; + +&spi0 { + status = "okay"; + + flash@0 { + compatible = "jedec,spi-nor"; + reg = <0>; + + partitions { + compatible = "fixed-partitions"; + #address-cells = <1>; + #size-cells = <1>; + + partition@0 { + label = "u-boot"; + reg = <0x0 0x140000>; + read-only; + }; + + partition@140000 { + label = "u-boot-env"; + reg = <0x140000 0x10000>; + }; + + /* + * Vendor layout has two kernel partitions: + * (1) <0x150000 0xec0000> = "kernel0" + * (2) <0x1010000 0xed0000> = "kernel1" + */ + partition@150000 { + label = "firmware"; + reg = <0x150000 0x1d90000>; + compatible = "openwrt,uimage", "denx,uimage"; + }; + + partition@1ee0000 { + label = "cdata"; + reg = <0x1ee0000 0x10000>; + }; + + partition@1ef0000 { + label = "cfg"; + reg = <0x1ef0000 0x100000>; + }; + + partition@1ff0000 { + label = "EEPROM"; + reg = <0x1ff0000 0x10000>; + + nvmem-layout { + compatible = "fixed-layout"; + #address-cells = <1>; + #size-cells = <1>; + + factory_macaddr: macaddr@0 { + compatible = "mac-base"; + reg = <0x0 0x6>; + #nvmem-cell-cells = <1>; + }; + }; + }; + }; + }; +}; + +ðernet0 { + nvmem-cells = <&factory_macaddr 0>; + nvmem-cell-names = "mac-address"; +}; + +&gpio0 { + pinctrl-names = "default"; + pinctrl-0 = <&pinmux_disable_jtag>; + + /* + * GPIO 1 is the global reset pin shared by all PHYs across all MDIO + * buses. It is intentionally not declared as reset-gpios on any bus: + * the MDIO driver / phylink only support a reset GPIO per bus, not on + * the parent controller. Attaching it to a single bus would still reset + * the PHYs on the other buses as a side effect, leaving their software + * state out of sync with the hardware and likely breaking them. + */ + phy_reset_hog { + gpio-hog; + gpios = <1 GPIO_ACTIVE_LOW>; + output-low; + line-name = "phy-reset"; + }; +}; + +&i2c_mst1 { + status = "okay"; + + i2c_sfp1: i2c@0 { reg = <0>; }; + i2c_sfp2: i2c@1 { reg = <1>; }; + i2c7: i2c@7 { reg = <7>; }; /* Etherlighting MCU at 0x66 */ + + i2c8: i2c@8 { + reg = <8>; + + adt7475@2e { + compatible = "adi,adt7475"; + reg = <0x2e>; + }; + }; +}; + +&i2c_mst2 { + status = "okay"; + + i2c5: i2c@5 { + reg = <5>; + + gpio1: gpio@22 { + compatible = "nxp,pca9555"; + reg = <0x22>; + gpio-controller; + #gpio-cells = <2>; + }; + + gpio2: gpio@24 { + compatible = "nxp,pca9555"; + reg = <0x24>; + gpio-controller; + #gpio-cells = <2>; + }; + }; + + i2c6: i2c@6 { reg = <6>; }; /* PoE MCU at 0x20 */ +}; + +&mdio_ctrl { + pinctrl-names = "default"; + pinctrl-0 = <&pinmux_enable_mdc_mdio_0>; +}; + +&mdio_bus0 { + /* RTL8264B */ + ethernet-phy-package@0 { + #address-cells = <1>; + #size-cells = <0>; + reg = <0>; + + PHY_C45(0, 0) + PHY_C45(8, 1) + PHY_C45(16, 2) + PHY_C45(24, 3) + }; + + /* RTL8264B */ + ethernet-phy-package@4 { + #address-cells = <1>; + #size-cells = <0>; + reg = <4>; + + PHY_C45(32, 4) + PHY_C45(40, 5) + PHY_C45(48, 6) + PHY_C45(50, 7) + }; +}; + +&switch0 { + ethernet-ports { + #address-cells = <1>; + #size-cells = <0>; + + SWITCH_PORT_LED(0, 1, 2, 0, 0, usxgmii) + SWITCH_PORT_LED(8, 2, 3, 0, 0, usxgmii) + SWITCH_PORT_LED(16, 3, 4, 0, 0, usxgmii) + SWITCH_PORT_LED(24, 4, 5, 0, 0, usxgmii) + SWITCH_PORT_LED(32, 5, 6, 0, 0, usxgmii) + SWITCH_PORT_LED(40, 6, 7, 0, 0, usxgmii) + SWITCH_PORT_LED(48, 7, 8, 0, 0, usxgmii) + SWITCH_PORT_LED(50, 8, 9, 0, 0, usxgmii) + + SWITCH_PORT_SFP(54, 9, 12, 0, 1) + SWITCH_PORT_SFP(55, 10, 13, 0, 2) + + /* CPU port */ + port@56 { + ethernet = <ðernet0>; + reg = <56>; + phy-mode = "internal"; + fixed-link { + speed = <1000>; + full-duplex; + }; + }; + }; +}; + diff --git a/target/linux/realtek/files-6.18/drivers/net/pcs/pcs-rtl-otto.c b/target/linux/realtek/files-6.18/drivers/net/pcs/pcs-rtl-otto.c index bb6b1c82191841..1e71c84e825232 100644 --- a/target/linux/realtek/files-6.18/drivers/net/pcs/pcs-rtl-otto.c +++ b/target/linux/realtek/files-6.18/drivers/net/pcs/pcs-rtl-otto.c @@ -1,5 +1,6 @@ // SPDX-License-Identifier: GPL-2.0-or-later +#include #include #include #include @@ -350,11 +351,25 @@ struct rtpcs_sds_tx_config { u8 post_amp; }; +/* Calculation helpers */ + static int rtpcs_sds_to_mmd(enum rtpcs_page sds_page, int sds_regnum) { return (sds_page << 8) + sds_regnum; } +static int rtpcs_sign_mag_decode(unsigned int val, unsigned int sign_bit) +{ + int mag = val & GENMASK(sign_bit - 1, 0); + + return (val & BIT(sign_bit)) ? -mag : mag; +} + +static unsigned int rtpcs_sign_mag_encode(int val, unsigned int sign_bit) +{ + return (val < 0 ? BIT(sign_bit) : 0) | (abs(val) & GENMASK(sign_bit - 1, 0)); +} + /* * Basic helpers * @@ -1702,7 +1717,7 @@ static void rtpcs_930x_sds_rx_reset(struct rtpcs_serdes *sds, page = PAGE_ANA_1G2; rtpcs_sds_write_bits(sds, page, 0x15, 4, 4, 0x1); - mdelay(5); + usleep_range(5000, 6000); rtpcs_sds_write_bits(sds, page, 0x15, 4, 4, 0x0); } @@ -2032,116 +2047,106 @@ static void rtpcs_930x_sds_tx_config(struct rtpcs_serdes *sds, rtpcs_sds_write_bits(sds, page, 0x18, 15, 12, impedance); } -__always_unused -static void rtpcs_930x_sds_rxcal_dcvs_manual(struct rtpcs_serdes *sds, - u32 dcvs_id, bool manual, u32 dcvs_list[]) +static int rtpcs_930x_sds_set_debug(struct rtpcs_serdes *sds, unsigned int debug_sel) +{ + struct rtpcs_serdes *even_sds = rtpcs_sds_get_even(sds); + int ret; + + ret = rtpcs_sds_write(even_sds, PAGE_WDIG, 0x2, (sds == even_sds) ? 0x2f : 0x31); + if (ret < 0) + return ret; + + ret = rtpcs_sds_write_bits(sds, PAGE_ANA_10G, 0x15, 9, 9, 0x1); /* RX_EN_TEST */ + if (ret < 0) + return ret; + + return rtpcs_sds_write_bits(sds, PAGE_ANA_COM, 0x06, 11, 6, debug_sel); /* RX_DEBUG_SEL */ +} + +static int rtpcs_930x_sds_rxcal_dcvs_set_adapt(struct rtpcs_serdes *sds, unsigned int dcvs_id, + bool enable) { u8 reg[6] = { 0x1e, 0x1e, 0x1e, 0x1e, 0x01, 0x02 }; u8 bit[6] = { 14, 13, 12, 11, 15, 11 }; if (dcvs_id > 5) - return; + return -EINVAL; - /* set DCVS auto/manual */ - rtpcs_sds_write_bits(sds, PAGE_ANA_10G, reg[dcvs_id], bit[dcvs_id], bit[dcvs_id], - manual ? 0x1 : 0x0); + return rtpcs_sds_write_bits(sds, PAGE_ANA_10G, reg[dcvs_id], bit[dcvs_id], + bit[dcvs_id], enable ? 0x0 : 0x1); +} - if (!manual) { - /* give auto mode some time */ - mdelay(1); - return; - } +static int rtpcs_930x_sds_rxcal_dcvs_set_coef(struct rtpcs_serdes *sds, unsigned int dcvs_id, + int dcvs_coef) +{ + u8 reg[6] = { 0x1c, 0x1d, 0x1d, 0x1d, 0x02, 0x11 }; + u8 lbit[6] = { 0, 11, 6, 1, 6, 0 }; + if (dcvs_id > 5) + return -EINVAL; - switch (dcvs_id) { - case 0: - rtpcs_sds_write_bits(sds, PAGE_ANA_10G_EXT, 0x03, 5, 5, dcvs_list[0]); - rtpcs_sds_write_bits(sds, PAGE_ANA_10G_EXT, 0x03, 4, 0, dcvs_list[1]); - break; - case 1: - rtpcs_sds_write_bits(sds, PAGE_ANA_10G, 0x1d, 15, 15, dcvs_list[0]); - rtpcs_sds_write_bits(sds, PAGE_ANA_10G, 0x1d, 14, 11, dcvs_list[1]); - break; - case 2: - rtpcs_sds_write_bits(sds, PAGE_ANA_10G, 0x1d, 10, 10, dcvs_list[0]); - rtpcs_sds_write_bits(sds, PAGE_ANA_10G, 0x1d, 9, 6, dcvs_list[1]); - break; - case 3: - rtpcs_sds_write_bits(sds, PAGE_ANA_10G, 0x1d, 5, 5, dcvs_list[0]); - rtpcs_sds_write_bits(sds, PAGE_ANA_10G, 0x1d, 4, 1, dcvs_list[1]); - break; - case 4: - rtpcs_sds_write_bits(sds, PAGE_ANA_10G, 0x11, 10, 10, dcvs_list[0]); - rtpcs_sds_write_bits(sds, PAGE_ANA_10G, 0x11, 9, 6, dcvs_list[1]); - break; - case 5: - rtpcs_sds_write_bits(sds, PAGE_ANA_10G, 0x11, 4, 4, dcvs_list[0]); - rtpcs_sds_write_bits(sds, PAGE_ANA_10G, 0x11, 3, 0, dcvs_list[1]); - break; - default: - break; - } + return rtpcs_sds_write_bits(sds, PAGE_ANA_10G, reg[dcvs_id], lbit[dcvs_id] + 4, + lbit[dcvs_id], rtpcs_sign_mag_encode(dcvs_coef, 4)); } -__always_unused -static void rtpcs_930x_sds_rxcal_dcvs_get(struct rtpcs_serdes *sds, - u32 dcvs_id, u32 dcvs_list[]) +__maybe_unused +static int rtpcs_930x_sds_rxcal_dcvs_get_coef(struct rtpcs_serdes *sds, + unsigned int dcvs_id, int *dcvs_coef) { u8 manual_reg[6] = { 0x1e, 0x1e, 0x1e, 0x1e, 0x01, 0x02 }; u8 coeff_sel[6] = { 0x22, 0x23, 0x24, 0x25, 0x2c, 0x2d }; u8 manual_bit[6] = { 14, 13, 12, 11, 15, 11 }; - u32 dcvs_sign_out = 0, dcvs_coef_bin = 0; - struct rtpcs_serdes *even_sds; - bool dcvs_manual; + int ret, val; if (dcvs_id > 5) - return; - - even_sds = rtpcs_sds_get_even(sds); - if (sds == even_sds) - rtpcs_sds_write(sds, PAGE_WDIG, 0x2, 0x2f); - else - rtpcs_sds_write(even_sds, PAGE_WDIG, 0x2, 0x31); + return -EINVAL; - rtpcs_sds_write_bits(sds, PAGE_ANA_10G, 0x15, 9, 9, 0x1); /* REG0_RX_EN_TEST */ - rtpcs_sds_write_bits(sds, PAGE_ANA_COM, 0x06, 11, 6, 0x20); /* REG0_RX_DEBUG_SEL */ + ret = rtpcs_930x_sds_set_debug(sds, 0x20); + if (ret < 0) + return ret; - rtpcs_sds_write_bits(sds, PAGE_ANA_10G_EXT, 0x0c, 5, 0, coeff_sel[dcvs_id]); - mdelay(1); + ret = rtpcs_sds_write_bits(sds, PAGE_ANA_10G_EXT, 0x0c, 5, 0, coeff_sel[dcvs_id]); + if (ret < 0) + return ret; + usleep_range(1000, 2000); /* ## DCVSX Read Out */ - dcvs_sign_out = rtpcs_sds_read_bits(sds, PAGE_WDIG, 0x14, 4, 4); - dcvs_coef_bin = rtpcs_sds_read_bits(sds, PAGE_WDIG, 0x14, 3, 0); - dcvs_manual = !!rtpcs_sds_read_bits(sds, PAGE_ANA_10G, manual_reg[dcvs_id], - manual_bit[dcvs_id], manual_bit[dcvs_id]); + val = rtpcs_sds_read_bits(sds, PAGE_WDIG, 0x14, 4, 0); + if (val < 0) + return val; - pr_info("%s: DCVS %u sign = %s, manual = %u, even coefficient = %u\n", __func__, - dcvs_id, dcvs_sign_out ? "-" : "+", dcvs_manual, dcvs_coef_bin); + *dcvs_coef = rtpcs_sign_mag_decode(val, 4); + val = rtpcs_sds_read_bits(sds, PAGE_ANA_10G, manual_reg[dcvs_id], manual_bit[dcvs_id], + manual_bit[dcvs_id]); + if (val < 0) + return val; - dcvs_list[0] = dcvs_sign_out; - dcvs_list[1] = dcvs_coef_bin; + pr_debug("%s: DCVS %u, manual = %u, coefficient = %d\n", __func__, + dcvs_id, val, *dcvs_coef); + + return 0; } -static void rtpcs_930x_sds_rxcal_leq_manual(struct rtpcs_serdes *sds, - bool manual, u32 leq_gray) +static int rtpcs_930x_sds_rxcal_leq_set_adapt(struct rtpcs_serdes *sds, bool enable) { - if (manual) { - rtpcs_sds_write_bits(sds, PAGE_ANA_10G, 0x18, 15, 15, 0x1); - rtpcs_sds_write_bits(sds, PAGE_ANA_10G, 0x16, 14, 10, leq_gray); - } else { - rtpcs_sds_write_bits(sds, PAGE_ANA_10G, 0x18, 15, 15, 0x0); - mdelay(100); - } + int ret; + + ret = rtpcs_sds_write_bits(sds, PAGE_ANA_10G, 0x18, 15, 15, enable ? 0x0 : 0x1); + if (!ret && enable) + msleep(100); + + return ret; } -static void rtpcs_930x_sds_rxcal_leq_offset_manual(struct rtpcs_serdes *sds, - bool manual, u32 offset) +static int rtpcs_930x_sds_rxcal_leq_set_coef(struct rtpcs_serdes *sds, unsigned int leq_gray, + unsigned int offset) { - if (manual) { - rtpcs_sds_write_bits(sds, PAGE_ANA_10G, 0x17, 6, 2, offset); - } else { - rtpcs_sds_write_bits(sds, PAGE_ANA_10G, 0x17, 6, 2, offset); - mdelay(1); - } + int ret; + + ret = rtpcs_sds_write_bits(sds, PAGE_ANA_10G, 0x17, 6, 2, offset); + if (ret < 0) + return ret; + + return rtpcs_sds_write_bits(sds, PAGE_ANA_10G, 0x16, 14, 10, leq_gray); } static u32 rtpcs_930x_sds_rxcal_gray_to_binary(u32 gray_code) @@ -2155,263 +2160,253 @@ static u32 rtpcs_930x_sds_rxcal_gray_to_binary(u32 gray_code) return binary; } -static u32 rtpcs_930x_sds_rxcal_leq_read(struct rtpcs_serdes *sds) +static int rtpcs_930x_sds_rxcal_leq_get_coef(struct rtpcs_serdes *sds) { - struct rtpcs_serdes *even_sds = rtpcs_sds_get_even(sds); - u32 leq_gray, leq_bin; - bool leq_manual; + int bin, gray, manual, ret; - rtpcs_sds_write(even_sds, PAGE_WDIG, 0x2, (sds == even_sds) ? 0x2f : 0x31); /* REG_DBGO_SEL */ - rtpcs_sds_write_bits(sds, PAGE_ANA_10G, 0x15, 9, 9, 0x1); /* REG0_RX_EN_TEST */ - rtpcs_sds_write_bits(sds, PAGE_ANA_COM, 0x06, 11, 6, 0x10); /* REG0_RX_DEBUG_SEL */ - mdelay(1); + ret = rtpcs_930x_sds_set_debug(sds, 0x10); + if (ret < 0) + return ret; + usleep_range(1000, 2000); /* ##LEQ Read Out */ - leq_gray = rtpcs_sds_read_bits(sds, PAGE_WDIG, 0x14, 7, 3); - leq_manual = !!rtpcs_sds_read_bits(sds, PAGE_ANA_10G, 0x18, 15, 15); - leq_bin = rtpcs_930x_sds_rxcal_gray_to_binary(leq_gray); + gray = rtpcs_sds_read_bits(sds, PAGE_WDIG, 0x14, 7, 3); + if (gray < 0) + return gray; + + bin = rtpcs_930x_sds_rxcal_gray_to_binary(gray); - pr_info("LEQ gray: %u, LEQ bin: %u, LEQ manual: %u\n", leq_gray, leq_bin, leq_manual); - return leq_bin; + manual = rtpcs_sds_read_bits(sds, PAGE_ANA_10G, 0x18, 15, 15); + if (manual < 0) + return manual; + + pr_debug("LEQ gray: %d, LEQ bin: %d, LEQ manual: %u\n", gray, bin, manual); + return bin; } -static void rtpcs_930x_sds_rxcal_vth_manual(struct rtpcs_serdes *sds, - bool manual, u32 vth_list[]) +static int rtpcs_930x_sds_rxcal_vth_set_adapt(struct rtpcs_serdes *sds, bool enable) { - /* REG0_LOAD_IN_INIT, [13:13] = VTH */ - rtpcs_sds_write_bits(sds, PAGE_ANA_10G, 0x0f, 13, 13, manual ? 0x1 : 0x0); + return rtpcs_sds_write_bits(sds, PAGE_ANA_10G, 0x0f, 13, 13, enable ? 0 : 1); +} - if (manual) { - rtpcs_sds_write_bits(sds, PAGE_ANA_10G, 0x13, 5, 3, vth_list[0]); - rtpcs_sds_write_bits(sds, PAGE_ANA_10G, 0x13, 2, 0, vth_list[1]); - } else - mdelay(10); +static int rtpcs_930x_sds_rxcal_vth_set_value(struct rtpcs_serdes *sds, unsigned int vth_p, + unsigned int vth_n) +{ + int ret; + + ret = rtpcs_sds_write_bits(sds, PAGE_ANA_10G, 0x13, 5, 3, vth_p); + if (ret < 0) + return ret; + + ret = rtpcs_sds_write_bits(sds, PAGE_ANA_10G, 0x13, 2, 0, vth_n); + if (ret < 0) + return ret; + + return 0; } -static void rtpcs_930x_sds_rxcal_vth_get(struct rtpcs_serdes *sds, - u32 vth_list[]) +static int rtpcs_930x_sds_rxcal_vth_get(struct rtpcs_serdes *sds, unsigned int *vth_p, + unsigned int *vth_n) { - struct rtpcs_serdes *even_sds = rtpcs_sds_get_even(sds); - int vth_manual; + int manual, ret, val; - rtpcs_sds_write(even_sds, PAGE_WDIG, 0x2, (sds == even_sds) ? 0x2f : 0x31); /* REG_DBGO_SEL */ - rtpcs_sds_write_bits(sds, PAGE_ANA_10G, 0x15, 9, 9, 0x1); /* REG0_RX_EN_TEST */ - rtpcs_sds_write_bits(sds, PAGE_ANA_COM, 0x06, 11, 6, 0x20); /* REG0_RX_DEBUG_SEL */ - rtpcs_sds_write_bits(sds, PAGE_ANA_10G_EXT, 0x0c, 5, 0, 0xc); /* REG0_COEF_SEL */ + ret = rtpcs_930x_sds_set_debug(sds, 0x20); + if (ret < 0) + return ret; - mdelay(1); + ret = rtpcs_sds_write_bits(sds, PAGE_ANA_10G_EXT, 0x0c, 5, 0, 0xc); /* COEF_SEL */ + if (ret < 0) + return ret; + usleep_range(1000, 2000); /* ##VthP & VthN Read Out */ - vth_list[0] = rtpcs_sds_read_bits(sds, PAGE_WDIG, 0x14, 2, 0); /* v_thp set bin */ - vth_list[1] = rtpcs_sds_read_bits(sds, PAGE_WDIG, 0x14, 5, 3); /* v_thn set bin */ - vth_manual = rtpcs_sds_read_bits(sds, PAGE_ANA_10G, 0x0f, 13, 13); + val = rtpcs_sds_read_bits(sds, PAGE_WDIG, 0x14, 5, 0); + if (val < 0) + return val; + + *vth_p = FIELD_GET(GENMASK(2, 0), val); + *vth_n = FIELD_GET(GENMASK(5, 3), val); + manual = rtpcs_sds_read_bits(sds, PAGE_ANA_10G, 0x0f, 13, 13); - pr_info("vthp_set_bin = %d, vthn_set_bin = %d, manual = %d\n", vth_list[0], vth_list[1], - vth_manual); + pr_debug("vth_p = %d, vth_n = %d, manual = %d\n", *vth_p, *vth_n, + manual); + return 0; } -static void rtpcs_930x_sds_rxcal_tap_manual(struct rtpcs_serdes *sds, - int tap_id, bool manual, u32 tap_list[]) +static int rtpcs_930x_sds_rxcal_tap_set_adapt(struct rtpcs_serdes *sds, unsigned int tap_id, + bool enable) { if (tap_id > 4) - return; + return -EINVAL; /* ##REG0_LOAD_IN_INIT[0], [11:7] = TAP0-TAP4 */ - rtpcs_sds_write_bits(sds, PAGE_ANA_10G, 0xf, tap_id + 7, tap_id + 7, - manual ? 0x1 : 0x0); + return rtpcs_sds_write_bits(sds, PAGE_ANA_10G, 0xf, tap_id + 7, tap_id + 7, + enable ? 0x0 : 0x1); +} - if (!manual) { - mdelay(10); - return; - } +static int rtpcs_930x_sds_rxcal_tap_set_value(struct rtpcs_serdes *sds, unsigned int tap_id, + int tap_even, int tap_odd) +{ + int ret = 0; + + if (tap_id > 4) + return -EINVAL; switch (tap_id) { case 0: - /* ##REG0_TAP0_INIT[5:0]=Tap0_Value */ - rtpcs_sds_write_bits(sds, PAGE_ANA_10G_EXT, 0x03, 5, 5, tap_list[0]); - rtpcs_sds_write_bits(sds, PAGE_ANA_10G_EXT, 0x03, 4, 0, tap_list[1]); + ret = rtpcs_sds_write_bits(sds, PAGE_ANA_10G_EXT, 0x03, 5, 0, + rtpcs_sign_mag_encode(tap_even, 5)); break; case 1: - rtpcs_sds_write_bits(sds, PAGE_ANA_COM, 0x07, 6, 6, tap_list[0]); - rtpcs_sds_write_bits(sds, PAGE_ANA_10G, 0x09, 11, 6, tap_list[1]); - rtpcs_sds_write_bits(sds, PAGE_ANA_COM, 0x07, 5, 5, tap_list[2]); - rtpcs_sds_write_bits(sds, PAGE_ANA_10G_EXT, 0x12, 5, 0, tap_list[3]); + ret = rtpcs_sds_write_bits(sds, PAGE_ANA_COM, 0x07, 6, 5, + (tap_even < 0) << 1 | (tap_odd < 0)); + if (!ret) + ret = rtpcs_sds_write_bits(sds, PAGE_ANA_10G, 0x09, 11, 6, + abs(tap_even) & GENMASK(4, 0)); + if (!ret) + ret = rtpcs_sds_write_bits(sds, PAGE_ANA_10G_EXT, 0x12, 5, 0, + abs(tap_odd) & GENMASK(4, 0)); break; case 2: - rtpcs_sds_write_bits(sds, PAGE_ANA_10G, 0x09, 5, 5, tap_list[0]); - rtpcs_sds_write_bits(sds, PAGE_ANA_10G, 0x09, 4, 0, tap_list[1]); - rtpcs_sds_write_bits(sds, PAGE_ANA_10G, 0x0a, 11, 11, tap_list[2]); - rtpcs_sds_write_bits(sds, PAGE_ANA_10G, 0x0a, 10, 6, tap_list[3]); + ret = rtpcs_sds_write_bits(sds, PAGE_ANA_10G, 0x09, 5, 0, + rtpcs_sign_mag_encode(tap_even, 5)); + if (!ret) + ret = rtpcs_sds_write_bits(sds, PAGE_ANA_10G, 0x0a, 11, 6, + rtpcs_sign_mag_encode(tap_odd, 5)); break; case 3: - rtpcs_sds_write_bits(sds, PAGE_ANA_10G, 0x0a, 5, 5, tap_list[0]); - rtpcs_sds_write_bits(sds, PAGE_ANA_10G, 0x0a, 4, 0, tap_list[1]); - rtpcs_sds_write_bits(sds, PAGE_ANA_10G, 0x06, 5, 5, tap_list[2]); - rtpcs_sds_write_bits(sds, PAGE_ANA_10G, 0x06, 4, 0, tap_list[3]); + ret = rtpcs_sds_write_bits(sds, PAGE_ANA_10G, 0x0a, 5, 0, + rtpcs_sign_mag_encode(tap_even, 5)); + if (!ret) + ret = rtpcs_sds_write_bits(sds, PAGE_ANA_10G, 0x06, 5, 0, + rtpcs_sign_mag_encode(tap_odd, 5)); break; case 4: - rtpcs_sds_write_bits(sds, PAGE_ANA_10G_EXT, 0x01, 5, 5, tap_list[0]); - rtpcs_sds_write_bits(sds, PAGE_ANA_10G_EXT, 0x01, 4, 0, tap_list[1]); - rtpcs_sds_write_bits(sds, PAGE_ANA_10G, 0x06, 11, 11, tap_list[2]); - rtpcs_sds_write_bits(sds, PAGE_ANA_10G, 0x06, 10, 6, tap_list[3]); + ret = rtpcs_sds_write_bits(sds, PAGE_ANA_10G_EXT, 0x01, 5, 0, + rtpcs_sign_mag_encode(tap_even, 5)); + if (!ret) + ret = rtpcs_sds_write_bits(sds, PAGE_ANA_10G, 0x06, 11, 6, + rtpcs_sign_mag_encode(tap_odd, 5)); break; default: break; } + + return ret; } -static void rtpcs_930x_sds_rxcal_tap_get(struct rtpcs_serdes *sds, - u32 tap_id, u32 tap_list[]) +static int rtpcs_930x_sds_rxcal_tap_get(struct rtpcs_serdes *sds, unsigned int tap_id, + int *tap_even, int *tap_odd) { - struct rtpcs_serdes *even_sds = rtpcs_sds_get_even(sds); - u32 tap0_sign_out; - u32 tap0_coef_bin; - u32 tap_sign_out_even; - u32 tap_coef_bin_even; - u32 tap_sign_out_odd; - u32 tap_coef_bin_odd; - bool tap_manual; - - rtpcs_sds_write(even_sds, PAGE_WDIG, 0x2, (sds == even_sds) ? 0x2f : 0x31); /* REG_DBGO_SEL */ - rtpcs_sds_write_bits(sds, PAGE_ANA_10G, 0x15, 9, 9, 0x1); /* REG0_RX_EN_TEST */ - rtpcs_sds_write_bits(sds, PAGE_ANA_COM, 0x06, 11, 6, 0x20); /* REG0_RX_DEBUG_SEL */ - - if (!tap_id) { - rtpcs_sds_write_bits(sds, PAGE_ANA_10G_EXT, 0x0c, 5, 0, 0); /* REG0_COEF_SEL */ - /* ##Tap1 Even Read Out */ - mdelay(1); - tap0_sign_out = rtpcs_sds_read_bits(sds, PAGE_WDIG, 0x14, 5, 5); - tap0_coef_bin = rtpcs_sds_read_bits(sds, PAGE_WDIG, 0x14, 4, 0); - - pr_info("tap0: coef_bin = %d, sign = %s\n", tap0_coef_bin, - tap0_sign_out ? "-" : "+"); - - tap_list[0] = tap0_sign_out; - tap_list[1] = tap0_coef_bin; - - tap_manual = !!rtpcs_sds_read_bits(sds, PAGE_ANA_10G, 0x0f, 7, 7); - pr_info("tap0: manual = %u\n", tap_manual); - } else { - rtpcs_sds_write_bits(sds, PAGE_ANA_10G_EXT, 0x0c, 5, 0, tap_id); /* REG0_COEF_SEL */ - mdelay(1); - /* ##Tap1 Even Read Out */ - tap_sign_out_even = rtpcs_sds_read_bits(sds, PAGE_WDIG, 0x14, 5, 5); - tap_coef_bin_even = rtpcs_sds_read_bits(sds, PAGE_WDIG, 0x14, 4, 0); - - rtpcs_sds_write_bits(sds, PAGE_ANA_10G_EXT, 0x0c, 5, 0, (tap_id + 5)); /* REG0_COEF_SEL */ - /* ##Tap1 Odd Read Out */ - tap_sign_out_odd = rtpcs_sds_read_bits(sds, PAGE_WDIG, 0x14, 5, 5); - tap_coef_bin_odd = rtpcs_sds_read_bits(sds, PAGE_WDIG, 0x14, 4, 0); - - pr_info("tap%u: even coefficient = %u, sign = %s\n", tap_id, tap_coef_bin_even, - tap_sign_out_even ? "-" : "+"); - - pr_info("tap%u: odd coefficient = %u, sign = %s\n", tap_id, tap_coef_bin_odd, - tap_sign_out_odd ? "-" : "+"); - - tap_list[0] = tap_sign_out_even; - tap_list[1] = tap_coef_bin_even; - tap_list[2] = tap_sign_out_odd; - tap_list[3] = tap_coef_bin_odd; - - tap_manual = rtpcs_sds_read_bits(sds, PAGE_ANA_10G, 0x0f, tap_id + 7, - tap_id + 7); - pr_info("tap%u: manual = %d\n", tap_id, tap_manual); + int ret, val; + + ret = rtpcs_930x_sds_set_debug(sds, 0x20); + if (ret < 0) + return ret; + + ret = rtpcs_sds_write_bits(sds, PAGE_ANA_10G_EXT, 0x0c, 5, 0, tap_id); /* COEF_SEL */ + if (ret < 0) + return ret; + usleep_range(1000, 2000); + + val = rtpcs_sds_read_bits(sds, PAGE_WDIG, 0x14, 5, 0); + if (val < 0) + return val; + + *tap_even = rtpcs_sign_mag_decode(val, 5); + pr_debug("tap%d: even coefficient = %d\n", tap_id, *tap_even); + + if (tap_id > 0) { + /* COEF_SEL */ + ret = rtpcs_sds_write_bits(sds, PAGE_ANA_10G_EXT, 0x0c, 5, 0, (tap_id + 5)); + if (ret < 0) + return ret; + + val = rtpcs_sds_read_bits(sds, PAGE_WDIG, 0x14, 5, 0); + if (val < 0) + return val; + + *tap_odd = rtpcs_sign_mag_decode(val, 5); + pr_debug("tap%u: odd coefficient = %d\n", tap_id, *tap_odd); } + + val = rtpcs_sds_read_bits(sds, PAGE_ANA_10G, 0x0f, tap_id + 7, tap_id + 7); + if (val < 0) + return val; + + pr_debug("tap%u: manual = %d\n", tap_id, val); + return 0; } -static void rtpcs_930x_sds_do_rx_calibration_1(struct rtpcs_serdes *sds, - enum rtpcs_sds_mode hw_mode) +static void rtpcs_930x_sds_rxcal_init(struct rtpcs_serdes *sds, enum rtpcs_sds_mode hw_mode) { - /* From both rtl9300_rxCaliConf_serdes_myParam and rtl9300_rxCaliConf_phy_myParam */ - int tap0_init_val = 0x1f; /* Initial Decision Fed Equalizer 0 tap */ + int tap0_init_val = 0x1f; /* initial DFE TAP0 */ int vth_min = 0x1; - /* 1.1.1 --- */ - rtpcs_sds_write(sds, PAGE_TGR_PRO_0, 0, 0); /* initial value */ + /* Clear some seeds and bits */ + rtpcs_sds_write(sds, PAGE_TGR_PRO_0, 0, 0); /* FGCAL */ rtpcs_sds_write_bits(sds, PAGE_ANA_10G, 0x01, 14, 14, 0x00); - rtpcs_sds_write_bits(sds, PAGE_ANA_10G, 0x1c, 10, 5, 0x20); - rtpcs_sds_write_bits(sds, PAGE_ANA_10G_EXT, 0x02, 0, 0, 0x01); + rtpcs_sds_write_bits(sds, PAGE_ANA_10G, 0x1c, 10, 5, 0x20); /* offset_ini */ + rtpcs_sds_write_bits(sds, PAGE_ANA_10G_EXT, 0x02, 0, 0, 0x01); /* z0_ok */ /* DCVS */ - rtpcs_sds_write_bits(sds, PAGE_ANA_10G, 0x1e, 14, 11, 0x00); - rtpcs_sds_write_bits(sds, PAGE_ANA_10G, 0x01, 15, 15, 0x00); - rtpcs_sds_write_bits(sds, PAGE_ANA_10G, 0x02, 11, 11, 0x00); - rtpcs_sds_write_bits(sds, PAGE_ANA_10G, 0x1c, 4, 0, 0x00); - rtpcs_sds_write_bits(sds, PAGE_ANA_10G, 0x1d, 15, 11, 0x00); - rtpcs_sds_write_bits(sds, PAGE_ANA_10G, 0x1d, 10, 6, 0x00); - rtpcs_sds_write_bits(sds, PAGE_ANA_10G, 0x1d, 5, 1, 0x00); - rtpcs_sds_write_bits(sds, PAGE_ANA_10G, 0x02, 10, 6, 0x00); - rtpcs_sds_write_bits(sds, PAGE_ANA_10G, 0x11, 4, 0, 0x00); - rtpcs_sds_write_bits(sds, PAGE_ANA_10G_EXT, 0x00, 3, 0, 0x0f); - rtpcs_sds_write_bits(sds, PAGE_ANA_10G, 0x04, 6, 6, 0x01); - rtpcs_sds_write_bits(sds, PAGE_ANA_10G, 0x04, 7, 7, 0x01); - - /* LEQ (Long Term Equivalent signal level) */ - rtpcs_sds_write_bits(sds, PAGE_ANA_10G, 0x16, 14, 8, 0x00); - - /* DFE (Decision Fed Equalizer) */ - rtpcs_sds_write_bits(sds, PAGE_ANA_10G_EXT, 0x03, 5, 0, tap0_init_val); - rtpcs_sds_write_bits(sds, PAGE_ANA_10G, 0x09, 11, 6, 0x00); - rtpcs_sds_write_bits(sds, PAGE_ANA_10G, 0x09, 5, 0, 0x00); - rtpcs_sds_write_bits(sds, PAGE_ANA_10G, 0x0a, 5, 0, 0x00); - rtpcs_sds_write_bits(sds, PAGE_ANA_10G_EXT, 0x01, 5, 0, 0x00); - rtpcs_sds_write_bits(sds, PAGE_ANA_10G_EXT, 0x12, 5, 0, 0x00); - rtpcs_sds_write_bits(sds, PAGE_ANA_10G, 0x0a, 11, 6, 0x00); - rtpcs_sds_write_bits(sds, PAGE_ANA_10G, 0x06, 5, 0, 0x00); - rtpcs_sds_write_bits(sds, PAGE_ANA_10G_EXT, 0x01, 5, 0, 0x00); - - /* Vth */ - rtpcs_sds_write_bits(sds, PAGE_ANA_10G, 0x13, 5, 3, 0x07); - rtpcs_sds_write_bits(sds, PAGE_ANA_10G, 0x13, 2, 0, 0x07); - rtpcs_sds_write_bits(sds, PAGE_ANA_10G_EXT, 0x0b, 5, 3, vth_min); - - /* --- 1.1.1 */ - - /* 1.1.2 Load DFE initial value --- */ - rtpcs_sds_write_bits(sds, PAGE_ANA_10G, 0x0f, 13, 7, 0x7f); - - /* --- 1.1.2 */ - - /* 1.1.3 disable LEQ training, enable DFE clock --- */ - rtpcs_sds_write_bits(sds, PAGE_ANA_10G, 0x17, 7, 7, 0x00); - rtpcs_sds_write_bits(sds, PAGE_ANA_10G, 0x17, 6, 2, 0x00); - rtpcs_sds_write_bits(sds, PAGE_ANA_10G, 0x0c, 8, 8, 0x00); - rtpcs_sds_write_bits(sds, PAGE_ANA_10G, 0x0b, 4, 4, 0x01); - rtpcs_sds_write_bits(sds, PAGE_ANA_10G, 0x12, 14, 14, 0x00); - rtpcs_sds_write_bits(sds, PAGE_ANA_10G_EXT, 0x02, 15, 15, 0x00); - - /* --- 1.1.3 */ - - /* 1.1.4 offset cali setting --- */ - rtpcs_sds_write_bits(sds, PAGE_ANA_10G, 0x0f, 15, 14, 0x03); - /* --- 1.1.4 */ - - /* 1.1.5 LEQ and DFE setting --- */ + for (int i = 0; i <= 5; i++) { + rtpcs_930x_sds_rxcal_dcvs_set_coef(sds, i, 0); + rtpcs_930x_sds_rxcal_dcvs_set_adapt(sds, i, true); + } + + rtpcs_sds_write_bits(sds, PAGE_ANA_10G_EXT, 0x00, 3, 0, 0x0f); /* z0_ok_X */ + rtpcs_sds_write_bits(sds, PAGE_ANA_10G, 0x04, 7, 6, 0x03); + + /* LEQ (Linear Equalization) */ + rtpcs_sds_write_bits(sds, PAGE_ANA_10G, 0x16, 14, 8, 0x00); /* FILTER_OUT */ + + /* DFE (Decision Feedback Equalizer) TAPs */ + rtpcs_930x_sds_rxcal_tap_set_value(sds, 0, tap0_init_val, 0); + rtpcs_930x_sds_rxcal_tap_set_value(sds, 1, 0, 0); + rtpcs_930x_sds_rxcal_tap_set_value(sds, 2, 0, 0); + rtpcs_930x_sds_rxcal_tap_set_value(sds, 3, 0, 0); + rtpcs_930x_sds_rxcal_tap_set_value(sds, 4, 0, 0); + + /* VTH (Voltage Threshold) */ + rtpcs_930x_sds_rxcal_vth_set_value(sds, 0x07, 0x07); + rtpcs_sds_write_bits(sds, PAGE_ANA_10G_EXT, 0x0b, 5, 3, vth_min); + + /* load DFE initial value */ + rtpcs_sds_write_bits(sds, PAGE_ANA_10G, 0x0f, 13, 7, 0x7f); /* load_in_init */ + + /* disable LEQ training, enable DFE clock */ + rtpcs_sds_write_bits(sds, PAGE_ANA_10G, 0x17, 7, 2, 0x00); /* [7] = EQHOLD, [6:2] = EQOUT */ + rtpcs_sds_write_bits(sds, PAGE_ANA_10G, 0x0c, 8, 8, 0x00); /* MAXHOLD_EN */ + rtpcs_sds_write_bits(sds, PAGE_ANA_10G, 0x0b, 4, 4, 0x01); /* dfe_adapt_eqen */ + rtpcs_sds_write_bits(sds, PAGE_ANA_10G, 0x12, 14, 14, 0x00); /* start_timer_en */ + rtpcs_sds_write_bits(sds, PAGE_ANA_10G_EXT, 0x02, 15, 15, 0x00); /* hold_timer_en */ + + /* offset cali setting */ + rtpcs_sds_write_bits(sds, PAGE_ANA_10G, 0x0f, 15, 14, 0x03); /* cali_en */ + + /* LEQ and DFE setting */ /* assume this is equivalent with (PHY_TYPE == SERDES && MEDIA == FIBER_10G) for now */ if (hw_mode == RTPCS_SDS_MODE_10GBASER) { rtpcs_sds_write_bits(sds, PAGE_ANA_10G, 0x03, 13, 8, 0x1f); rtpcs_sds_write_bits(sds, PAGE_ANA_10G, 0x00, 13, 13, 0x01); - rtpcs_sds_write_bits(sds, PAGE_ANA_10G, 0x16, 14, 8, 0x00); /* REG0_FILTER_OUT */ + rtpcs_sds_write_bits(sds, PAGE_ANA_10G, 0x16, 14, 8, 0x00); /* FILTER_OUT */ } /* REG0_LEQ_DC_GAIN, 0x01 for short DACs */ - rtpcs_sds_write_bits(sds, PAGE_ANA_10G, 0x16, 3, 2, 0x02); - rtpcs_sds_write_bits(sds, PAGE_ANA_10G, 0x0f, 6, 0, 0x5f); - rtpcs_sds_write_bits(sds, PAGE_ANA_10G_EXT, 0x05, 7, 2, 0x1f); - rtpcs_sds_write_bits(sds, PAGE_ANA_10G, 0x19, 9, 5, 0x1f); - rtpcs_sds_write_bits(sds, PAGE_ANA_10G_EXT, 0x0b, 15, 9, 0x3c); - rtpcs_sds_write_bits(sds, PAGE_ANA_10G, 0x0b, 1, 0, 0x03); - - /* --- 1.1.5 */ + rtpcs_sds_write_bits(sds, PAGE_ANA_10G, 0x16, 3, 2, 0x02); /* LEQ_DC_GAIN */ + rtpcs_sds_write_bits(sds, PAGE_ANA_10G, 0x0f, 6, 0, 0x5f); /* dfe_adapt_en */ + rtpcs_sds_write_bits(sds, PAGE_ANA_10G_EXT, 0x05, 7, 2, 0x1f); /* dfe_adapt_en2 */ + rtpcs_sds_write_bits(sds, PAGE_ANA_10G, 0x19, 9, 5, 0x1f); /* leq_min */ + rtpcs_sds_write_bits(sds, PAGE_ANA_10G_EXT, 0x0b, 15, 9, 0x3c); /* gray_en */ + rtpcs_sds_write_bits(sds, PAGE_ANA_10G, 0x0b, 1, 0, 0x03); /* dfe_adapt_mode */ } static void rtpcs_930x_sds_rxcal_fgcal(struct rtpcs_serdes *sds) { - struct rtpcs_serdes *even_sds = rtpcs_sds_get_even(sds); u32 fgcal_binary, fgcal_gray; u32 offset_range; @@ -2432,15 +2427,11 @@ static void rtpcs_930x_sds_rxcal_fgcal(struct rtpcs_serdes *sds) /* Foreground Calibration --- */ for (int run = 0; run < 10; run++) { - /* REG_DBGO_SEL */ - rtpcs_sds_write(even_sds, PAGE_WDIG, 0x2, (sds == even_sds) ? 0x2f : 0x31); - rtpcs_sds_write_bits(sds, PAGE_ANA_10G, 0x15, 9, 9, 0x1); /* REG0_RX_EN_TEST */ - rtpcs_sds_write_bits(sds, PAGE_ANA_COM, 0x06, 11, 6, 0x20); /* REG0_RX_DEBUG_SEL */ - - rtpcs_sds_write_bits(sds, PAGE_ANA_10G_EXT, 0x0c, 5, 0, 0xf); /* REG0_COEF_SEL */ + rtpcs_930x_sds_set_debug(sds, 0x20); + rtpcs_sds_write_bits(sds, PAGE_ANA_10G_EXT, 0x0c, 5, 0, 0xf); /* COEF_SEL */ /* ##FGCAL read gray */ fgcal_gray = rtpcs_sds_read_bits(sds, PAGE_WDIG, 0x14, 5, 0); - rtpcs_sds_write_bits(sds, PAGE_ANA_10G_EXT, 0x0c, 5, 0, 0xe); /* REG0_COEF_SEL */ + rtpcs_sds_write_bits(sds, PAGE_ANA_10G_EXT, 0x0c, 5, 0, 0xe); /* COEF_SEL */ /* ##FGCAL read binary */ fgcal_binary = rtpcs_sds_read_bits(sds, PAGE_WDIG, 0x14, 5, 0); @@ -2479,18 +2470,22 @@ static void rtpcs_930x_sds_rxcal_leq_adapt_lock(struct rtpcs_serdes *sds) sds->media == RTPCS_SDS_MEDIA_DAC_SHORT || sds->media == RTPCS_SDS_MEDIA_DAC_LONG; u32 sum10 = 0, avg10; - int i; + int i, val; /* 1.3.1: release LEQ auto-adapt, let it settle from zero */ if (!direct_serdes) rtpcs_sds_write_bits(sds, PAGE_ANA_10G, 0xc, 8, 8, 0x0); rtpcs_sds_write_bits(sds, PAGE_ANA_10G, 0x17, 7, 7, 0x0); - rtpcs_930x_sds_rxcal_leq_manual(sds, false, 0); + rtpcs_930x_sds_rxcal_leq_set_adapt(sds, true); /* 1.3.2: sample the auto-adapted LEQ value 10 times over ~100ms */ for (i = 0; i < 10; i++) { - sum10 += rtpcs_930x_sds_rxcal_leq_read(sds); - mdelay(10); + val = rtpcs_930x_sds_rxcal_leq_get_coef(sds); + if (val < 0) + return; + + sum10 += val; + usleep_range(10000, 11000); } /* rounded average of where auto-adapt settled */ @@ -2519,64 +2514,60 @@ static void rtpcs_930x_sds_rxcal_leq_adapt_lock(struct rtpcs_serdes *sds) /* lock LEQ at corrected value for direct SerDes; PHY-attached stays in auto-adapt */ if (direct_serdes) { - rtpcs_930x_sds_rxcal_leq_offset_manual(sds, 1, 0); rtpcs_sds_write_bits(sds, PAGE_ANA_10G, 0x17, 7, 7, 0x1); - rtpcs_930x_sds_rxcal_leq_manual(sds, true, avg10); + rtpcs_930x_sds_rxcal_leq_set_adapt(sds, false); + rtpcs_930x_sds_rxcal_leq_set_coef(sds, avg10, 0); } - pr_info("SDS %u LEQ = %u", sds->id, rtpcs_930x_sds_rxcal_leq_read(sds)); + pr_info("SDS %u LEQ = %u", sds->id, rtpcs_930x_sds_rxcal_leq_get_coef(sds)); } static void rtpcs_930x_sds_rxcal_vth_tap0_adapt_lock(struct rtpcs_serdes *sds) { - u32 tap0_list[4] = {0}; - u32 vth_list[2] = {0}; + unsigned int vth_p, vth_n; + int tap0; /* run VTH/TAP auto-adapt */ - rtpcs_930x_sds_rxcal_vth_manual(sds, false, vth_list); - rtpcs_930x_sds_rxcal_tap_manual(sds, 0, false, tap0_list); - mdelay(200); + rtpcs_930x_sds_rxcal_vth_set_adapt(sds, true); + rtpcs_930x_sds_rxcal_tap_set_adapt(sds, 0, true); + msleep(200); /* manually set learned VTH */ - rtpcs_930x_sds_rxcal_vth_get(sds, vth_list); - rtpcs_930x_sds_rxcal_vth_manual(sds, true, vth_list); + if (rtpcs_930x_sds_rxcal_vth_get(sds, &vth_p, &vth_n) < 0) + return; + rtpcs_930x_sds_rxcal_vth_set_value(sds, vth_p, vth_n); + rtpcs_930x_sds_rxcal_vth_set_adapt(sds, false); - mdelay(100); + msleep(100); /* manually set learned TAP0 */ - rtpcs_930x_sds_rxcal_tap_get(sds, 0, tap0_list); - rtpcs_930x_sds_rxcal_tap_manual(sds, 0, true, tap0_list); + if (rtpcs_930x_sds_rxcal_tap_get(sds, 0, &tap0, NULL) < 0) + return; + rtpcs_930x_sds_rxcal_tap_set_value(sds, 0, tap0, 0); + rtpcs_930x_sds_rxcal_tap_set_adapt(sds, 0, false); } static void rtpcs_930x_sds_rxcal_dfe_taps_adapt(struct rtpcs_serdes *sds) { - u32 tap1_list[4] = {0}; - u32 tap2_list[4] = {0}; - u32 tap3_list[4] = {0}; - u32 tap4_list[4] = {0}; - /* dfeTap1_4Enable true */ - rtpcs_930x_sds_rxcal_tap_manual(sds, 1, false, tap1_list); - rtpcs_930x_sds_rxcal_tap_manual(sds, 2, false, tap2_list); - rtpcs_930x_sds_rxcal_tap_manual(sds, 3, false, tap3_list); - rtpcs_930x_sds_rxcal_tap_manual(sds, 4, false, tap4_list); + rtpcs_930x_sds_rxcal_tap_set_adapt(sds, 1, true); + rtpcs_930x_sds_rxcal_tap_set_adapt(sds, 2, true); + rtpcs_930x_sds_rxcal_tap_set_adapt(sds, 3, true); + rtpcs_930x_sds_rxcal_tap_set_adapt(sds, 4, true); - mdelay(30); + msleep(30); } static void rtpcs_930x_sds_rxcal_dfe_disable(struct rtpcs_serdes *sds) { - u32 tap1_list[4] = {0}; - u32 tap2_list[4] = {0}; - u32 tap3_list[4] = {0}; - u32 tap4_list[4] = {0}; + int tap_even = 0, tap_odd = 0; - rtpcs_930x_sds_rxcal_tap_manual(sds, 1, true, tap1_list); - rtpcs_930x_sds_rxcal_tap_manual(sds, 2, true, tap2_list); - rtpcs_930x_sds_rxcal_tap_manual(sds, 3, true, tap3_list); - rtpcs_930x_sds_rxcal_tap_manual(sds, 4, true, tap4_list); + for (int i = 1; i <= 4; i++) { + rtpcs_930x_sds_rxcal_tap_set_value(sds, i, tap_even, tap_odd); + rtpcs_930x_sds_rxcal_tap_set_adapt(sds, i, false); + } - mdelay(10); + usleep_range(10000, 11000); } static void rtpcs_930x_sds_do_rx_calibration(struct rtpcs_serdes *sds, @@ -2584,17 +2575,17 @@ static void rtpcs_930x_sds_do_rx_calibration(struct rtpcs_serdes *sds, { u32 latch_sts; - rtpcs_930x_sds_do_rx_calibration_1(sds, hw_mode); + rtpcs_930x_sds_rxcal_init(sds, hw_mode); rtpcs_930x_sds_rxcal_fgcal(sds); rtpcs_930x_sds_rxcal_vth_tap0_adapt_lock(sds); /* Do this only for 10GR mode */ if (hw_mode == RTPCS_SDS_MODE_10GBASER) { rtpcs_930x_sds_rxcal_dfe_taps_adapt(sds); - mdelay(20); + msleep(20); latch_sts = rtpcs_sds_read_bits(sds, PAGE_TGR_STD_0, 1, 2, 2); - mdelay(1); + usleep_range(1000, 2000); latch_sts = rtpcs_sds_read_bits(sds, PAGE_TGR_STD_0, 1, 2, 2); if (latch_sts) { rtpcs_930x_sds_rxcal_dfe_disable(sds); @@ -2694,7 +2685,7 @@ static int rtpcs_930x_sds_check_calibration(struct rtpcs_serdes *sds, /* Count errors during 1ms */ errors1 = rtpcs_930x_sds_sym_err_get(sds, hw_mode); - mdelay(1); + usleep_range(1000, 2000); errors2 = rtpcs_930x_sds_sym_err_get(sds, hw_mode); switch (hw_mode) { @@ -2995,7 +2986,7 @@ static int rtpcs_930x_sds_post_config(struct rtpcs_serdes *sds, enum rtpcs_sds_m do { rtpcs_930x_sds_do_rx_calibration(sds, hw_mode); calib_tries++; - mdelay(50); + msleep(50); } while (rtpcs_930x_sds_check_calibration(sds, hw_mode) && calib_tries < 3); if (calib_tries >= 3) pr_warn("%s: SerDes RX calibration failed\n", __func__); @@ -3241,7 +3232,7 @@ static void rtpcs_931x_sds_rx_reset(struct rtpcs_serdes *sds) rtpcs_sds_write(sds, PAGE_ANA_10G_EXT, 0x2, 0x6010); rtpcs_sds_write(sds, PAGE_ANA_MISC, 0x0, 0xc30); - mdelay(50); + msleep(50); } static int rtpcs_931x_sds_cmu_page_get(enum rtpcs_sds_mode hw_mode) @@ -4164,8 +4155,8 @@ static int rtpcs_probe(struct platform_device *pdev) return ret; } - for_each_child_of_node_scoped(dev->of_node, child) { - ret = of_property_read_u32(child, "reg", &sds_id); + device_for_each_child_node_scoped(dev, child) { + ret = fwnode_property_read_u32(child, "reg", &sds_id); if (ret) return ret; @@ -4173,7 +4164,7 @@ static int rtpcs_probe(struct platform_device *pdev) return -EINVAL; sds = &ctrl->serdes[sds_id]; - sds->fwnode = fwnode_handle_get(of_fwnode_handle(child)); + sds->fwnode = fwnode_handle_get(child); ret = devm_add_action_or_reset(dev, rtpcs_sds_put_fwnode, sds); if (ret) return ret; diff --git a/target/linux/realtek/image/rtl931x.mk b/target/linux/realtek/image/rtl931x.mk index 3d6607a1f00fa0..076d91a24a8448 100644 --- a/target/linux/realtek/image/rtl931x.mk +++ b/target/linux/realtek/image/rtl931x.mk @@ -12,6 +12,17 @@ define Device/hasivo_f5800w-12s-plus endef TARGET_DEVICES += hasivo_f5800w-12s-plus +define Device/hasivo_s1300wp-8xgt-4s-plus + SOC := rtl9313 + DEVICE_VENDOR := Hasivo + DEVICE_MODEL := S1300WP-8XGT-4S+ + IMAGE_SIZE := 29696k + DEVICE_PACKAGES := kmod-phy-realtek kmod-rtc-pcf8563 rtl826x-firmware \ + kmod-hasivo-mcu-wdt kmod-hasivo-mcu-sensor + $(Device/kernel-lzma) +endef +TARGET_DEVICES += hasivo_s1300wp-8xgt-4s-plus + define Device/plasmacloud-common SOC := rtl9312 UIMAGE_MAGIC := 0x93100000 @@ -43,6 +54,16 @@ define Device/plasmacloud_psx28 endef TARGET_DEVICES += plasmacloud_psx28 +define Device/ubnt_usw-pro-xg-8-poe + SOC := rtl9313 + DEVICE_VENDOR := Ubiquiti + DEVICE_MODEL := UniFi USW Pro XG 8 PoE + IMAGE_SIZE := 30272k + DEVICE_PACKAGES := rtl826x-firmware kmod-hwmon-adt7475 + $(Device/kernel-lzma) +endef +TARGET_DEVICES += ubnt_usw-pro-xg-8-poe + define Device/xikestor_sks8300-12x-v1 SOC := rtl9313 DEVICE_VENDOR := XikeStor diff --git a/target/linux/starfive/patches-6.18/0010-uart-8250-add-reset-operation-in-runtime-PM.patch b/target/linux/starfive/patches-6.18/0010-uart-8250-add-reset-operation-in-runtime-PM.patch index 26b3bfbfa11d78..7798360b1d5c79 100644 --- a/target/linux/starfive/patches-6.18/0010-uart-8250-add-reset-operation-in-runtime-PM.patch +++ b/target/linux/starfive/patches-6.18/0010-uart-8250-add-reset-operation-in-runtime-PM.patch @@ -12,7 +12,7 @@ Signed-off-by: William Qiu --- a/drivers/tty/serial/8250/8250_dw.c +++ b/drivers/tty/serial/8250/8250_dw.c -@@ -903,6 +903,8 @@ static int dw8250_runtime_suspend(struct +@@ -905,6 +905,8 @@ static int dw8250_runtime_suspend(struct { struct dw8250_data *data = dev_get_drvdata(dev); @@ -21,7 +21,7 @@ Signed-off-by: William Qiu clk_disable_unprepare(data->clk); clk_disable_unprepare(data->pclk); -@@ -925,6 +927,8 @@ static int dw8250_runtime_resume(struct +@@ -927,6 +929,8 @@ static int dw8250_runtime_resume(struct return ret; } diff --git a/target/linux/starfive/patches-6.18/1002-serial-8250_dw-Add-starfive-jh7100-hsuart-compatible.patch b/target/linux/starfive/patches-6.18/1002-serial-8250_dw-Add-starfive-jh7100-hsuart-compatible.patch index fffbc009e9eed9..856c0beb88e10c 100644 --- a/target/linux/starfive/patches-6.18/1002-serial-8250_dw-Add-starfive-jh7100-hsuart-compatible.patch +++ b/target/linux/starfive/patches-6.18/1002-serial-8250_dw-Add-starfive-jh7100-hsuart-compatible.patch @@ -15,7 +15,7 @@ Signed-off-by: Emil Renner Berthing --- a/drivers/tty/serial/8250/8250_dw.c +++ b/drivers/tty/serial/8250/8250_dw.c -@@ -973,6 +973,7 @@ static const struct of_device_id dw8250_ +@@ -975,6 +975,7 @@ static const struct of_device_id dw8250_ { .compatible = "marvell,armada-38x-uart", .data = &dw8250_armada_38x_data }, { .compatible = "renesas,rzn1-uart", .data = &dw8250_renesas_rzn1_data }, { .compatible = "sophgo,sg2044-uart", .data = &dw8250_skip_set_rate_data },