From e5b2a126a8833090b541b3af036e0280a506f3cc Mon Sep 17 00:00:00 2001
From: Florian Maurer <f.maurer@outlook.de>
Date: Fri, 24 Oct 2025 12:25:40 +0200
Subject: [PATCH] ipq806x: add support for aruba ap325

---
 patches/openwrt/0010-Add-Aruba-AP325.patch    | 710 ++++++++++++++++++
 ...-use-ath10k-ct-and-correct-label-mac.patch |  33 +
 targets/ipq806x-generic                       |   9 +
 3 files changed, 752 insertions(+)
 create mode 100644 patches/openwrt/0010-Add-Aruba-AP325.patch
 create mode 100644 patches/openwrt/0011-ipq806x-aruba_ap-325-use-ath10k-ct-and-correct-label-mac.patch

diff --git a/patches/openwrt/0010-Add-Aruba-AP325.patch b/patches/openwrt/0010-Add-Aruba-AP325.patch
new file mode 100644
index 00000000..7a982646
--- /dev/null
+++ b/patches/openwrt/0010-Add-Aruba-AP325.patch
@@ -0,0 +1,710 @@
+From: Lukas Stockner <lukas@lukasstockner.de>
+Date: Mon, 28 Jul 2025 03:26:28 +0200
+Subject: Add Aruba AP325
+
+diff --git a/package/boot/uboot-envtools/files/ipq806x b/package/boot/uboot-envtools/files/ipq806x
+index 443a0e13d40deb95ba1e5ca44c1cce632baed574..01f20ae72a9b049e992b5bac6bb7d6e507ece276 100644
+--- a/package/boot/uboot-envtools/files/ipq806x
++++ b/package/boot/uboot-envtools/files/ipq806x
+@@ -12,7 +12,7 @@ touch /etc/config/ubootenv
+ board=$(board_name)
+ 
+ ubootenv_mtdinfo () {
+-	UBOOTENV_PART=$(cat /proc/mtd | grep APPSBLENV)
++	UBOOTENV_PART=$(cat /proc/mtd | grep -i APPSBLENV)
+ 	mtd_dev=$(echo $UBOOTENV_PART | awk '{print $1}' | sed 's/:$//')
+ 	mtd_size=$(echo $UBOOTENV_PART | awk '{print "0x"$2}')
+ 	mtd_erase=$(echo $UBOOTENV_PART | awk '{print "0x"$3}')
+@@ -53,7 +53,8 @@ nokia,ac400i)
+ 	ubootenv_add_uci_config "/dev/mtd20" "0x0" "0x040000" "0x20000"
+ 	;;
+ qcom,ipq8064-ap148|\
+-qcom,ipq8064-db149)
++qcom,ipq8064-db149|\
++aruba,ap-325)
+ 	ubootenv_add_uci_config $(ubootenv_mtdinfo)
+ 	;;
+ ubnt,unifi-ac-hd|\
+diff --git a/scripts/aruba-header.py b/scripts/aruba-header.py
+new file mode 100755
+index 0000000000000000000000000000000000000000..7c236679c553e3d6a90438d5701925201f1979da
+--- /dev/null
++++ b/scripts/aruba-header.py
+@@ -0,0 +1,225 @@
++#!/usr/bin/python3
++# SPDX-License-Identifier: GPL-2.0-or-later
++#
++# Copyright (C) 2025 Lukas Stockner <lukas@lukasstockner.de>
++#
++# ./aruba-header.py <ImageFileIn> <ImageFileOut> <build string> <version string> <oem string> <machine type>
++#
++# Generates a header for use with the APBoot bootloader on (some?) Aruba APs.
++
++import argparse
++
++LEN_BUILD = 256
++LEN_VERSION = 24
++LEN_OEM = 32
++
++HEADER_MAGIC = b'ARUBA\0\0\0'
++
++
++class ValidFlag:
++    YES = 1
++
++
++class FormatVersion:
++    CURRENT = 2
++
++
++class ImageType:
++    ELF = 0
++    FPGA_BINARY = 1
++    CPBOOT_BINARY = 2
++    APBOOT_BINARY = 3
++    TPMINFO = 4
++    APBOOT_STAGE1_BINARY = 5
++    APBOOT_STAGE2_BINARY = 6
++    APBOOT_COMBINED_BINARY = 7
++    OS_ANCILLARY_IMAGE = 8
++    XLOADER_BINARY = 9
++    GRUB_BIN = 10
++    LSM_PACKAGE = 11
++
++
++class CompressionType:
++    NONE = 0
++    BZIP2 = 1
++    GZIP = 2
++
++
++MACHINE_TYPES = {
++    'MSWITCH': 0,
++    'CABERNET': 1,
++    'SYRAH': 2,
++    'MERLOT': 3,
++    'MUSCAT': 4,
++    'NEBBIOLO': 5,
++    'MALBEC': 6,
++    'PALOMINO': 7,
++    'GRENACHE': 8,
++    'MOSCATO': 9,
++    'TALISKER': 10,
++    'JURA_R': 11,
++    'SCAPA': 12,
++    'JURA_O': 13,
++    'CORVINA': 14,
++    'ARRAN': 15,
++    'BLUEBLOOD': 16,
++    'MSR2K': 17,
++    'PORFIDIO': 18,
++    'CAZULO': 19,
++    'SCAPA_H': 20,
++    'CARDHU': 21,
++    'BOWMORE': 22,
++    'TAMDHU': 23,
++    'ARDBEG': 24,
++    'ARDMORE': 25,
++    'DALMORE': 26,
++    'K2': 27,
++    'GRAPPA': 28,
++    'SHUMWAY': 29,
++    'SPRINGBANK': 30,
++    'OUZO': 31,
++    'AMARULA': 32,
++    'GROZDOVA': 33,
++    'PALINKA': 34,
++    'HAZELBURN': 35,
++    'TOMATIN': 36,
++    'HAZELBURN_H': 37,
++    'TOMATIN_16': 38,
++    'SPRINGBANK_16': 39,
++    'OCTOMORE': 40,
++    'BALVENIE': 41,
++    'OUZO_PLUS': 42,
++    'X4': 43,
++    'EINAR': 44,
++    'GLENFARCLAS': 45,
++    'GLENFIDDICH': 46,
++    'EIGER': 47,
++    'GLENMORANGIE': 48,
++    'MILAGRO': 49,
++    'OPUSONE': 50,
++    'ABERLOUR': 51,
++    'MILLSTONE': 52,
++    'DEWARS': 53,
++    'BUNKER': 54,
++    'MASTERSON': 55,
++    'SIERRA': 56,
++    'KILCHOMAN': 57,
++    'SPEYBURN': 58,
++    'LAGAVULIN': 59,
++    'LAPHROAIG': 60,
++    'TOBA': 61,
++    'ARRANTA': 62,
++}
++
++
++class NextHeader:
++    NONE = 0x00000000
++    SIGN = 0x01111111
++
++
++class Flags:
++    C_TEST_BUILD = 0x00000001
++    SWATCH = 0x00000002
++    # Preserves the image with "clear all"
++    DONT_CLEAR_ON_PURGE = 0x00000004
++    SECURE_BOOTLOADER = 0x00000008
++    FACTORY_IMAGE = 0x00000010
++    FIPS_CERTIFIED = 0x00000020
++
++
++def make_header(data: bytes, build: str, version: str, oem: str, imageType: int, machine: int) -> bytes:
++    buildBytes = build.encode(encoding='ascii')
++    assert len(buildBytes) < LEN_BUILD
++    buildBytes += b'\0' * (LEN_BUILD - len(buildBytes))
++
++    versionBytes = version.encode(encoding='ascii')
++    assert len(versionBytes) < LEN_VERSION
++    versionBytes += b'\0' * (LEN_VERSION - len(versionBytes))
++
++    oemBytes = oem.encode(encoding='ascii')
++    assert len(oemBytes) < LEN_OEM
++    oemBytes += b'\0' * (LEN_OEM - len(oemBytes))
++
++    header = b''
++    # Payload size, image plus optional signature (which we don't use)
++    header += len(data).to_bytes(4, 'big')
++    # Use what appears to be the current version
++    header += FormatVersion.CURRENT.to_bytes(4, 'big')
++    # Checksum is computed later
++    header += b'\0\0\0\0'
++    # Vendor magic number
++    header += HEADER_MAGIC
++    # Long build information string
++    header += buildBytes
++    # Short version information string
++    header += versionBytes
++    # Image is valid
++    header += ValidFlag.YES.to_bytes(1)
++    # Image type
++    header += imageType.to_bytes(1)
++    # APBoot doesn't appear to actually support compression
++    header += CompressionType.NONE.to_bytes(1)
++    # Machine type
++    header += machine.to_bytes(1)
++    # Image size
++    header += len(data).to_bytes(4, 'big')
++    # Next header (we don't support signing)
++    header += NextHeader.NONE.to_bytes(4, 'big')
++    # MD5 checksum plus fudge factor (to ensure non-zero hash), appears unused
++    header += b'\0' * 16
++    header += b'\0' * 4
++    # No flags are set
++    header += int(0).to_bytes(4, 'big')
++    # No next header is used
++    header += b'\0' * 12
++    # Padding
++    header += b'\0' * 36
++    # OEM string
++    header += oemBytes
++    # Padding
++    header += b'\0' * 96
++
++    assert len(header) == 512
++    assert len(data) % 4 == 0
++
++    # Compute checksum such that the big-endian sum of all 32-bit integers becomes zero.
++    curSum = sum(int.from_bytes(header[i : i + 4], 'big') for i in range(0, 512, 4))
++    curSum += sum(int.from_bytes(data[i : i + 4], 'big') for i in range(0, len(data), 4))
++    checksum = 0x100000000 - (curSum % 0x100000000)
++    header = header[:8] + checksum.to_bytes(4, 'big') + header[12:]
++
++    curSum = sum(int.from_bytes(header[i : i + 4], 'big') for i in range(0, 512, 4))
++    curSum += sum(int.from_bytes(data[i : i + 4], 'big') for i in range(0, len(data), 4))
++    assert (curSum % 0x100000000) == 0
++
++    return header
++
++
++if __name__ == "__main__":
++    parser = argparse.ArgumentParser(description='Generate Aruba header.')
++    parser.add_argument('source', type=argparse.FileType('r+b'))
++    parser.add_argument('dest', type=argparse.FileType('wb'))
++    parser.add_argument('build')
++    parser.add_argument('version')
++    parser.add_argument('oem')
++    parser.add_argument('type', choices=['os', 'boot'])
++    parser.add_argument('machine', choices=MACHINE_TYPES.keys(), type=str.upper)
++    args = parser.parse_args()
++
++    # Parse image type.
++    # The OS image must be type "ELF" even though it's not an ELF file...
++    imageType = {'os': ImageType.ELF, 'boot': ImageType.APBOOT_BINARY}[args.type]
++    # Parse machine type.
++    machineType = MACHINE_TYPES[args.machine]
++
++    image = args.source.read()
++    # Pad image.
++    if len(image) % 4 != 0:
++        image += b'\0' * (4 - len(image) % 4)
++
++    # Generate header.
++    header = make_header(image, args.build, args.version, args.oem, imageType, machineType)
++
++    # Write output.
++    args.dest.write(header)
++    args.dest.write(image)
+diff --git a/target/linux/ipq806x/base-files/etc/board.d/01_leds b/target/linux/ipq806x/base-files/etc/board.d/01_leds
+index 60988dec6b5d829c38173309e1fac4f52c5ec0b2..3cfe832eb4f3aca0795535ca61a2a452a27ed2b7 100644
+--- a/target/linux/ipq806x/base-files/etc/board.d/01_leds
++++ b/target/linux/ipq806x/base-files/etc/board.d/01_leds
+@@ -95,6 +95,10 @@ nokia,ac400i)
+ 	ucidef_set_led_default "ctrl" "CTRL" "green:ctrl" "0"
+ 	ucidef_set_led_default "pwr" "PWR" "green:power" "1"
+ 	;;
++aruba,ap-325)
++	ucidef_set_led_netdev "wan" "wan" "green:wlan5g" "eth0"
++	ucidef_set_led_netdev "lan" "lan" "green:wlan2g" "eth1"
++	;;
+ tplink,ad7200)
+ 	ucidef_set_led_usbport "usb1" "USB 1" "blue:usb1" "usb1-port1" "usb2-port1"
+ 	ucidef_set_led_usbport "usb2" "USB 2" "blue:usb3" "usb3-port1" "usb4-port1"
+diff --git a/target/linux/ipq806x/base-files/etc/board.d/02_network b/target/linux/ipq806x/base-files/etc/board.d/02_network
+index 93ccc961f3813037e44d4f66fe586daf9e72fbc5..23dc6e750442a3240b259b2f2da34d50bc20cb84 100644
+--- a/target/linux/ipq806x/base-files/etc/board.d/02_network
++++ b/target/linux/ipq806x/base-files/etc/board.d/02_network
+@@ -48,7 +48,8 @@ ipq806x_setup_interfaces()
+ 		ucidef_set_network_device_conduit "wan" "eth0"
+ 		;;
+ 	edgecore,ecw5410 |\
+-	extreme,ap3935)
++	extreme,ap3935 |\
++	aruba,ap-325)
+ 		ucidef_set_interfaces_lan_wan "eth1" "eth0"
+ 		;;
+ 	qcom,ipq8064-ap161)
+@@ -100,6 +101,12 @@ ipq806x_setup_macs()
+ 			hw_mac_addr=$(mtd_get_mac_ascii hwconfig HW.WAN.MAC.Address)
+ 			ucidef_set_interface_macaddr "wan" "$(macaddr_add $hw_mac_addr 1)"
+ 		;;
++		aruba,ap-325)
++			# Ideally this should be read from the inventory TLVs on 0:mfginfo,
++			# but parsing those from a shell script is tricky.
++			hw_mac_addr=$(mtd_get_mac_ascii 0:appsblenv ethaddr)
++			ucidef_set_interface_macaddr "lan" "$hw_mac_addr"
++			ucidef_set_interface_macaddr "wan" "$(macaddr_add $hw_mac_addr 1)"
+ 	esac
+ }
+ 
+diff --git a/target/linux/ipq806x/base-files/etc/init.d/bootcount b/target/linux/ipq806x/base-files/etc/init.d/bootcount
+index ef3c6894e44adbaf293e42f8abf57693678a2013..c0960f65810a5e4fb1d5bed0cc8f42b26e1ba3c1 100755
+--- a/target/linux/ipq806x/base-files/etc/init.d/bootcount
++++ b/target/linux/ipq806x/base-files/etc/init.d/bootcount
+@@ -9,7 +9,8 @@ boot() {
+ 	asrock,g10)
+ 		asrock_bootconfig_mangle "bootcheck" && reboot
+ 		;;
+-	edgecore,ecw5410)
++	edgecore,ecw5410 |\
++	aruba,ap-325)
+ 		fw_setenv bootcount 0
+ 		;;
+ 	extreme,ap3935)
+diff --git a/target/linux/ipq806x/base-files/lib/upgrade/platform.sh b/target/linux/ipq806x/base-files/lib/upgrade/platform.sh
+index f26f118d548077c7d2192fee29f1b8ebd453b854..d6f3e775c63ea587c93bf1eeace120c6144e4827 100644
+--- a/target/linux/ipq806x/base-files/lib/upgrade/platform.sh
++++ b/target/linux/ipq806x/base-files/lib/upgrade/platform.sh
+@@ -51,6 +51,17 @@ platform_do_upgrade() {
+ 		CI_KERNPART="PriImg"
+ 		nand_do_upgrade "$1"
+ 		;;
++	aruba,ap-325)
++		part="$(awk -F 'ubi.mtd=' '{printf $2}' /proc/cmdline | sed -e 's/ .*$//')"
++		if [ "$part" = "aos0" ]; then
++			fw_setenv os_partition 1 || exit 1
++			CI_UBIPART="aos1"
++		else
++			fw_setenv os_partition 0 || exit 1
++			CI_UBIPART="aos0"
++		fi
++		nand_do_upgrade "$1"
++		;;
+ 	linksys,ea7500-v1 |\
+ 	linksys,ea8500)
+ 		platform_do_upgrade_linksys "$1"
+diff --git a/target/linux/ipq806x/config-6.6 b/target/linux/ipq806x/config-6.6
+index 7e9558ad75206cb62058f72b9e9d84398f629936..59ab8b3c054009cc8bf96e87717713c1d955e319 100644
+--- a/target/linux/ipq806x/config-6.6
++++ b/target/linux/ipq806x/config-6.6
+@@ -184,6 +184,8 @@ CONFIG_GENERIC_VDSO_32=y
+ CONFIG_GLOB=y
+ CONFIG_GPIOLIB_IRQCHIP=y
+ CONFIG_GPIO_CDEV=y
++CONFIG_GPIO_WATCHDOG=y
++CONFIG_GPIO_WATCHDOG_ARCH_INITCALL=y
+ CONFIG_GRO_CELLS=y
+ CONFIG_HARDEN_BRANCH_PREDICTOR=y
+ CONFIG_HARDIRQS_SW_RESEND=y
+diff --git a/target/linux/ipq806x/files-6.6/arch/arm/boot/dts/qcom/qcom-ipq8068-ap-325.dts b/target/linux/ipq806x/files-6.6/arch/arm/boot/dts/qcom/qcom-ipq8068-ap-325.dts
+new file mode 100644
+index 0000000000000000000000000000000000000000..2d0736fd30d1df8f02adb9a1bee7790f71d15bb6
+--- /dev/null
++++ b/target/linux/ipq806x/files-6.6/arch/arm/boot/dts/qcom/qcom-ipq8068-ap-325.dts
+@@ -0,0 +1,309 @@
++// SPDX-License-Identifier: GPL-2.0-or-later OR MIT
++
++#include "qcom-ipq8064-v2.0.dtsi"
++
++#include <dt-bindings/gpio/gpio.h>
++#include <dt-bindings/input/input.h>
++#include <dt-bindings/leds/common.h>
++
++/ {
++	model = "Aruba AP-325";
++	compatible = "aruba,ap-325", "qcom,ipq8064";
++
++	memory@0 {
++		reg = <0x41500000 0x1eb00000>;
++		device_type = "memory";
++	};
++
++	aliases {
++		serial0 = &gsbi4_serial;
++
++		ethernet0 = &gmac2;
++		ethernet1 = &gmac3;
++
++		led-boot = &led_power_green;
++		led-failsafe = &led_power_red;
++		led-running = &led_power_green;
++		led-upgrade = &led_power_green;
++	};
++
++	chosen {
++		stdout-path = "serial0:115200n8";
++		find-rootblock = "ubi.mtd=";
++		append-rootblock = "ubi.mtd=aos";
++	};
++
++	keys {
++		compatible = "gpio-keys";
++		pinctrl-0 = <&button_pins>;
++		pinctrl-names = "default";
++
++		reset {
++			label = "reset";
++			gpios = <&qcom_pinmux 9 GPIO_ACTIVE_LOW>;
++			linux,code = <KEY_RESTART>;
++			debounce-interval = <60>;
++			wakeup-source;
++		};
++	};
++
++	leds {
++		compatible = "gpio-leds";
++		pinctrl-0 = <&led_pins>;
++		pinctrl-names = "default";
++
++		led_power_green: power_green {
++			function = LED_FUNCTION_POWER;
++			color = <LED_COLOR_ID_GREEN>;
++			gpios = <&qcom_pinmux 28 GPIO_ACTIVE_HIGH>;
++		};
++
++		led_power_orange: power_orange {
++			function = LED_FUNCTION_POWER;
++			color = <LED_COLOR_ID_ORANGE>;
++			gpios = <&qcom_pinmux 29 GPIO_ACTIVE_HIGH>;
++		};
++
++		led_power_red: power_red {
++			function = LED_FUNCTION_POWER;
++			color = <LED_COLOR_ID_RED>;
++			gpios = <&qcom_pinmux 30 GPIO_ACTIVE_HIGH>;
++		};
++
++		led_wlan_green {
++			label = "green:wlan";
++			gpios = <&qcom_pinmux 31 GPIO_ACTIVE_HIGH>;
++			linux,default-trigger = "phy0tpt";
++		};
++
++		led_wlan_orange {
++			label = "orange:wlan";
++			gpios = <&qcom_pinmux 32 GPIO_ACTIVE_HIGH>;
++			linux,default-trigger = "phy1tpt";
++		};
++	};
++
++	watchdog {
++		compatible = "linux,wdt-gpio";
++		gpios = <&qcom_pinmux 61 GPIO_ACTIVE_LOW>;
++		hw_algo = "toggle";
++		hw_margin_ms = <1000>;
++		always-running;
++
++		pinctrl-names = "default";
++		pinctrl-0 = <&watchdog_pins>;
++	};
++};
++
++&qcom_pinmux {
++	led_pins: led_pins {
++		mux {
++			pins = "gpio28", "gpio29", "gpio30", "gpio31", "gpio32";
++			function = "gpio";
++			drive-strength = <12>;
++			bias-pull-up;
++		};
++	};
++
++	button_pins: button_pins {
++		mux {
++			pins = "gpio9";
++			function = "gpio";
++			bias-pull-up;
++		};
++	};
++
++	uart0_pins: uart0_pins {
++		mux {
++			pins = "gpio12", "gpio13";
++			function = "gsbi4";
++			drive-strength = <10>;
++			bias-disable;
++		};
++	};
++
++	watchdog_pins: watchdog_pins {
++		mux {
++			pins = "gpio61";
++			function = "gpio";
++			drive-strength = <10>;
++			bias-disable;
++		};
++	};
++	// Used for USB over-current detection instead.
++	/delete-node/ pcie2_pins;
++};
++
++&gsbi4 {
++	status = "okay";
++	qcom,mode = <GSBI_PROT_I2C_UART>;
++};
++
++&gsbi4_serial {
++	status = "okay";
++	pinctrl-0 = <&uart0_pins>;
++	pinctrl-names = "default";
++};
++
++&gsbi5 {
++	qcom,mode = <GSBI_PROT_SPI>;
++	status = "okay";
++
++	spi4: spi@1a280000 {
++		status = "okay";
++		spi-max-frequency = <50000000>;
++
++		pinctrl-0 = <&spi_pins>;
++		pinctrl-names = "default";
++
++		cs-gpios = <&qcom_pinmux 20 GPIO_ACTIVE_HIGH>;
++
++		mx25u3235f@0 {
++			compatible = "jedec,spi-nor";
++			#address-cells = <1>;
++			#size-cells = <1>;
++			spi-max-frequency = <50000000>;
++			reg = <0>;
++
++			partitions {
++				compatible = "qcom,smem-part";
++
++				partition-art {
++					compatible = "nvmem-cells";
++					#address-cells = <1>;
++					#size-cells = <1>;
++					label = "0:art";
++
++					precal_art_1000: precal@1000 {
++						reg = <0x1000 0x2f20>;
++					};
++
++					precal_art_5000: precal@5000 {
++						reg = <0x5000 0x2f20>;
++					};
++				};
++			};
++		};
++	};
++};
++
++&pcie0 {
++	status = "okay";
++
++	bridge@0,0 {
++		reg = <0x00000000 0 0 0 0>;
++		#address-cells = <3>;
++		#size-cells = <2>;
++		ranges;
++
++		wifi@1,0 {
++			compatible = "qcom,ath10k";
++			status = "okay";
++			reg = <0x00010000 0 0 0 0>;
++
++			nvmem-cells = <&precal_art_1000>;
++			nvmem-cell-names = "pre-calibration";
++		};
++	};
++};
++
++&pcie1 {
++	status = "okay";
++
++	bridge@0,0 {
++		reg = <0x00000000 0 0 0 0>;
++		#address-cells = <3>;
++		#size-cells = <2>;
++		ranges;
++
++		wifi@1,0 {
++			compatible = "qcom,ath10k";
++			status = "okay";
++			reg = <0x00010000 0 0 0 0>;
++
++			nvmem-cells = <&precal_art_5000>;
++			nvmem-cell-names = "pre-calibration";
++		};
++	};
++};
++
++&nand {
++	status = "okay";
++
++	nand@0 {
++		compatible = "qcom,nandcs";
++
++		reg = <0>;
++
++		nand-ecc-strength = <4>;
++		nand-bus-width = <8>;
++		nand-ecc-step-size = <512>;
++
++		partitions {
++			compatible = "fixed-partitions";
++			#address-cells = <1>;
++			#size-cells = <1>;
++
++			aos0@0000000 {
++				label = "aos0";
++				reg = <0x0000000 0x4000000>;
++			};
++
++			aos1@4000000 {
++				label = "aos1";
++				reg = <0x4000000 0x4000000>;
++			};
++		};
++	};
++};
++
++&mdio0 {
++	status = "okay";
++
++	pinctrl-0 = <&mdio0_pins>;
++	pinctrl-names = "default";
++
++	phy0: ethernet-phy@0 {
++		reg = <0>;
++	};
++
++	phy1: ethernet-phy@1 {
++		reg = <1>;
++	};
++};
++
++&gmac2 {
++	status = "okay";
++
++	qcom,id = <2>;
++	mdiobus = <&mdio0>;
++
++	phy-mode = "sgmii";
++	phy-handle = <&phy0>;
++};
++
++&gmac3 {
++	status = "okay";
++
++	qcom,id = <3>;
++	mdiobus = <&mdio0>;
++
++	phy-mode = "sgmii";
++	phy-handle = <&phy1>;
++};
++
++&adm_dma {
++	status = "okay";
++};
++
++&usb3_1 {
++	status = "okay";
++};
++
++&hs_phy_1 {
++	status = "okay";
++};
++
++&ss_phy_1 {
++	status = "okay";
++};
+diff --git a/target/linux/ipq806x/image/generic.mk b/target/linux/ipq806x/image/generic.mk
+index 112793d5fdeff3b08c1357c0806f113ac57d3594..eb19cb1b60461a16b9004afbd7734eb67e32815a 100644
+--- a/target/linux/ipq806x/image/generic.mk
++++ b/target/linux/ipq806x/image/generic.mk
+@@ -47,6 +47,18 @@ define Build/linksys-addfwhdr
+        	;mv "$@.new" "$@"
+ endef
+ 
++define Build/apboot-addfwhdr
++	$(SCRIPT_DIR)/aruba-header.py \
++	$@ $@.new \
++	"$(call toupper,$(LINUX_KARCH)) $(VERSION_DIST) Linux-$(LINUX_VERSION), $(VERSION_NUMBER) $(VERSION_CODE)"\
++	"$(VERSION_NUMBER)" \
++	"$(VERSION_DIST)" \
++	os \
++	"$(word 1,$(1))"
++
++	mv "$@.new" "$@"
++endef
++
+ define Device/DniImage
+ 	KERNEL_SUFFIX := -uImage
+ 	KERNEL = kernel-bin | append-dtb | uImage none
+@@ -101,6 +113,22 @@ define Device/arris_tr4400-v2
+ endef
+ TARGET_DEVICES += arris_tr4400-v2
+ 
++define Device/aruba_ap-325
++	$(call Device/LegacyImage)
++	$(call Device/UbiFit)
++	DEVICE_VENDOR := Aruba
++	DEVICE_MODEL := AP-325
++	SOC := qcom-ipq8068
++	PAGESIZE := 2048
++	BLOCKSIZE := 128k
++	KERNEL_SUFFIX := .ari
++	KERNEL = kernel-bin | append-dtb | uImage none | apboot-addfwhdr Octomore
++	KERNEL_LOADADDR = 0x41508000
++	# Use non-ct firmware for now
++	DEVICE_PACKAGES := -kmod-ath10k-ct kmod-ath10k ath10k-firmware-qca99x0
++endef
++TARGET_DEVICES += aruba_ap-325
++
+ define Device/askey_rt4230w-rev6
+ 	$(call Device/LegacyImage)
+ 	$(Device/dsa-migration)
diff --git a/patches/openwrt/0011-ipq806x-aruba_ap-325-use-ath10k-ct-and-correct-label-mac.patch b/patches/openwrt/0011-ipq806x-aruba_ap-325-use-ath10k-ct-and-correct-label-mac.patch
new file mode 100644
index 00000000..8ff7ce96
--- /dev/null
+++ b/patches/openwrt/0011-ipq806x-aruba_ap-325-use-ath10k-ct-and-correct-label-mac.patch
@@ -0,0 +1,33 @@
+From: Florian Maurer <f.maurer@outlook.de>
+Date: Fri, 24 Oct 2025 12:17:54 +0200
+Subject: ipq806x: aruba_ap-325: use ath10k-ct and correct label mac
+
+Signed-off-by: Florian Maurer <f.maurer@outlook.de>
+
+diff --git a/target/linux/ipq806x/base-files/etc/board.d/02_network b/target/linux/ipq806x/base-files/etc/board.d/02_network
+index 23dc6e750442a3240b259b2f2da34d50bc20cb84..ab9ecbd0400ad658c08122bb2e29d0c602bee9dd 100644
+--- a/target/linux/ipq806x/base-files/etc/board.d/02_network
++++ b/target/linux/ipq806x/base-files/etc/board.d/02_network
+@@ -107,6 +107,7 @@ ipq806x_setup_macs()
+ 			hw_mac_addr=$(mtd_get_mac_ascii 0:appsblenv ethaddr)
+ 			ucidef_set_interface_macaddr "lan" "$hw_mac_addr"
+ 			ucidef_set_interface_macaddr "wan" "$(macaddr_add $hw_mac_addr 1)"
++			ucidef_set_label_macaddr "$hw_mac_addr"
+ 	esac
+ }
+ 
+diff --git a/target/linux/ipq806x/image/generic.mk b/target/linux/ipq806x/image/generic.mk
+index eb19cb1b60461a16b9004afbd7734eb67e32815a..5a2ed982e8e69a015ac5a82dd60ff3ff54dc4e14 100644
+--- a/target/linux/ipq806x/image/generic.mk
++++ b/target/linux/ipq806x/image/generic.mk
+@@ -124,8 +124,8 @@ define Device/aruba_ap-325
+ 	KERNEL_SUFFIX := .ari
+ 	KERNEL = kernel-bin | append-dtb | uImage none | apboot-addfwhdr Octomore
+ 	KERNEL_LOADADDR = 0x41508000
+-	# Use non-ct firmware for now
+-	DEVICE_PACKAGES := -kmod-ath10k-ct kmod-ath10k ath10k-firmware-qca99x0
++	# Use ct firmware to allow mesh
++	DEVICE_PACKAGES := ath10k-firmware-qca99x0-ct
+ endef
+ TARGET_DEVICES += aruba_ap-325
+ 
diff --git a/targets/ipq806x-generic b/targets/ipq806x-generic
index 641d89ce..46b2a93f 100644
--- a/targets/ipq806x-generic
+++ b/targets/ipq806x-generic
@@ -26,6 +26,15 @@ device('tp-link-archer-c2600', 'tplink_c2600', {
 	broken = true,
 })
 
+-- Aruba
+
+device('aruba-ap-325', 'aruba_ap-325', {
+	factory = '-squashfs-nand-factory',
+	sysupgrade = '-squashfs-nand-sysupgrade',
+	packages = QCA9980_PACKAGES,
+})
+
+
 --
 -- QCA9984
 --
-- 
2.47.3

