qualcommax: ipq60xx: add TP-Link EAP610-Outdoor support
authorAlexandru Gagniuc <mr.nuke.me@gmail.com>
Sun, 7 Aug 2022 15:15:35 +0000 (10:15 -0500)
committerRobert Marko <robimarko@gmail.com>
Sun, 12 Jan 2025 15:54:51 +0000 (16:54 +0100)
TP-Link EAP610-Outdoor is a 802.11ax AP claiming AX1800 support. It is
wall or pole mountable, and rated for outdoor use. It can only be
powered via PoE.

Specifications:
---------------
* CPU: Qualcomm IPQ6018 Quad core Cortex-A53
* RAM: 512 MB
* Storage: ESMT PSR1GA30DT 128MB NAND
* Ethernet:
  * Gigabit RJ45 port with PoE input
* WLAN:
  * 2.4GHz/5GHz
* LEDs:
  * Multi-color System LED (Green/Amber)
* Buttons:
  * 1x Reset
* UART: 4-pin unpopulated header
  * 1.8 V level, Pinout 1 - TX, 2 - RX, 3 - GND, 4 - 1.8V

Installation:
=============

Web UI method
-------------

Set up the device using the vendor's web UI. After that go to
Management->SSH and enable the "SSH Login" checkbox. Select "Save".
The connect to the machine via SSH:

    ssh -o hostkeyalgorithms=ssh-rsa <ip_of_device>

Disable signature verification:

    cliclientd stopcs

Rename the "-web-ui-factory" image to something less than 63
characters, maintaining the ".bin" suffix.
 * Go to System -> Firmware Update.
 * Under "New Firmware File", click "Browse" and select the image
 * Select "Update" and confirm by clicking "OK".

If the update fails, the web UI should show an error message.
Otherwise, the device should reboot into OpenWRT.

TFTP method
-----------

To flash via tftp, first place the initramfs image on the TFTP server.

    setenv serverip <ip of tftp server>
    setenv ipaddr <ip in same subnet as tftp server>
    tftpboot tplink_eap610-outdoor-initramfs-uImage.itb
    bootm

This should boot OpenWRT. Once booted, flash the sysupgrade.bin image
using either luci or the commandline.

The tplink2022 image format
============================

The vendor images of this device are packaged in a format that does
not match any previous tplink formats. In order for flashing to work
from the vendor's web UI, firmware updates need to be packaged in
this format. The `tplink-mkimage-2022.py` is provided for this
purpose.

This script can also analyze vendor images, and extract the required
"support" string. This string is checked by the vendor firmware, and
images with a missing or incorrect string are rejected.

Signed-off-by: Alexandru Gagniuc <mr.nuke.me@gmail.com>
Link: https://github.com/openwrt/openwrt/pull/14922
Signed-off-by: Robert Marko <robimarko@gmail.com>
include/image-commands.mk
package/boot/uboot-envtools/files/qualcommax_ipq60xx
package/firmware/ipq-wifi/Makefile
scripts/tplink-mkimage-2022.py [new file with mode: 0755]
target/linux/qualcommax/files/arch/arm64/boot/dts/qcom/ipq6018-eap610-outdoor.dts [new file with mode: 0644]
target/linux/qualcommax/image/ipq60xx.mk
target/linux/qualcommax/ipq60xx/base-files/etc/board.d/02_network
target/linux/qualcommax/ipq60xx/base-files/etc/hotplug.d/firmware/11-ath11k-caldata
target/linux/qualcommax/ipq60xx/base-files/lib/preinit/09_mount_factory_data [new file with mode: 0644]
target/linux/qualcommax/ipq60xx/base-files/lib/upgrade/platform.sh

index 2e129e0347d7038a1b9c3c448f5c104d1d8a2b2e..aa48e19399ecd8afed62d4a82f42145dccb2d312 100644 (file)
@@ -626,6 +626,14 @@ define Build/sysupgrade-tar
                $@
 endef
 
+define Build/tplink-image-2022
+       $(TOPDIR)/scripts/tplink-mkimage-2022.py  \
+               --create $@.new \
+               --rootfs $@ \
+               --support "$(TPLINK_SUPPORT_STRING)"
+       @mv $@.new $@
+endef
+
 define Build/tplink-safeloader
        -$(STAGING_DIR_HOST)/bin/tplink-safeloader \
                -B $(TPLINK_BOARD_ID) \
index eec99ce8f979b9c67f7ba40782aceb364f890c5b..77c96da24acc9f5e33936eddf873432f8bb881ba 100644 (file)
@@ -25,6 +25,10 @@ netgear,wax214)
        [ -n "$idx" ] && \
                ubootenv_add_uci_config "/dev/mtd$idx" "0x0" "0x40000" "0x20000"
        ;;
+tplink,eap610-outdoor)
+       idx="$(find_mtd_index 0:appsblenv)"
+       [ -n "$idx" ] && \
+               ubootenv_add_uci_config "/dev/mtd$idx" "0x0" "0x40000" "0x20000"
 yuncore,fap650)
        idx="$(find_mtd_index 0:appsblenv)"
        [ -n "$idx" ] && \
index 679a14421157b30f5f4747904b6dca6ac5d6a817..9821e39a85fe9ccf8a6c98a3dad138e4502c8595 100644 (file)
@@ -55,6 +55,7 @@ ALLWIFIBOARDS:= \
        redmi_ax6 \
        skspruce_wia3300-20 \
        spectrum_sax1v1k \
+       tplink_eap610-outdoor \
        tplink_eap620hd-v1 \
        tplink_eap660hd-v1 \
        wallys_dr40x9 \
@@ -185,6 +186,7 @@ $(eval $(call generate-ipq-wifi-package,prpl_haze,prpl Haze))
 $(eval $(call generate-ipq-wifi-package,redmi_ax6,Redmi AX6))
 $(eval $(call generate-ipq-wifi-package,skspruce_wia3300-20,SKSpruce WIA3300-20))
 $(eval $(call generate-ipq-wifi-package,spectrum_sax1v1k,Spectrum SAX1V1K))
+$(eval $(call generate-ipq-wifi-package,tplink_eap610-outdoor,TPLink EAP610-Outdoor))
 $(eval $(call generate-ipq-wifi-package,tplink_eap620hd-v1,TP-Link EAP620 HD v1))
 $(eval $(call generate-ipq-wifi-package,tplink_eap660hd-v1,TP-Link EAP660 HD v1))
 $(eval $(call generate-ipq-wifi-package,wallys_dr40x9,Wallys DR40X9))
diff --git a/scripts/tplink-mkimage-2022.py b/scripts/tplink-mkimage-2022.py
new file mode 100755 (executable)
index 0000000..4db6940
--- /dev/null
@@ -0,0 +1,198 @@
+#!/usr/bin/env python3
+
+'''A program for manipulating tplink2022 images.
+
+A tplink2022 is an image format encountered on TP-Link devices around the year
+2022. This was seen at least on the EAP610-Outdoor. The format is a container
+for a rootfs, and has optional fields for the "software" version. It also
+ requires a "support" string that describes the list of compatible devices.
+
+This module is intended for creating such images with an OpenWRT UBI image, but
+also supports analysis and extraction of vendor images. Altough tplink2022
+images can be signed, this program does not support signing image.
+
+To get an explanation of the commandline arguments, run this program with the
+"--help" argument.
+'''
+
+import argparse
+import hashlib
+import os
+import pprint
+import struct
+
+def decode_header(datafile):
+    '''Read the tplink2022 image header anbd decode it into a dictionary'''
+    header = {}
+    fmt = '>2I'
+
+    datafile.seek(0x1014)
+    raw_header = datafile.read(8)
+    fields = struct.unpack(fmt, raw_header)
+
+    header['rootfs_size'] = fields[0]
+    header['num_items'] = fields[1]
+    header['items'] = []
+
+    rootfs = {}
+    rootfs['name'] = 'rootfs.ubi'
+    rootfs['offset'] = 0
+    rootfs['size'] = header['rootfs_size']
+    header['items'].append(rootfs)
+
+    for _ in range(header['num_items']):
+        entry = datafile.read(0x2c)
+        fmt = '>I32s2I'
+        fields = struct.unpack(fmt, entry)
+
+        section = {}
+        section['name'] = fields[1].decode("utf-8").rstrip('\0')
+        section['type'] = fields[0]
+        section['offset'] = fields[2]
+        section['size'] = fields[3]
+        header['items'].append(section)
+    return header
+
+def extract(datafile):
+    '''Extract the sections of the tplink2022 image to separate files'''
+    header = decode_header(datafile)
+
+    pretty = pprint.PrettyPrinter(indent=4, sort_dicts=False)
+    pretty.pprint(header)
+
+    for section in header['items']:
+        datafile.seek(0x1814 + section['offset'])
+        section_contents = datafile.read(section['size'])
+
+        with open(f"{section['name']}.bin", 'wb') as section_file:
+            section_file.write(section_contents)
+
+    with open('leftover.bin', 'wb') as extras_file:
+        extras_file.write(datafile.read())
+
+def get_section_contents(section):
+    '''I don't remember what this does. It's been a year since I wrote this'''
+    if section.get('data'):
+        data = section['data']
+    elif section.get('file'):
+        with open(section['file'], 'rb') as section_file:
+            data = section_file.read()
+    else:
+        data = bytes()
+
+    if section['size'] != len(data):
+        raise ValueError("Wrong section size", len(data))
+
+    return data
+
+def write_image(output_image, header):
+    '''Write a tplink2022 image with the contents in the "header" dictionary'''
+    with open(output_image, 'w+b') as out_file:
+        # header MD5
+        salt = [ 0x7a, 0x2b, 0x15, 0xed,
+             0x9b, 0x98, 0x59, 0x6d,
+             0xe5, 0x04, 0xab, 0x44,
+             0xac, 0x2a, 0x9f, 0x4e
+        ]
+
+        out_file.seek(4)
+        out_file.write(bytes(salt))
+
+        # unknown section
+        out_file.write(bytes([0xff] * 0x1000))
+
+        # Table of contents
+        raw_header = struct.pack('>2I', header['rootfs_size'],
+                        header['num_items'])
+        out_file.write(raw_header)
+
+        for section in header['items']:
+            if section['name'] == 'rootfs.ubi':
+                continue
+
+            hdr = struct.pack('>I32s2I',
+                section.get('type', 0),
+                section['name'].encode('utf-8'),
+                section['offset'],
+                section['size']
+            )
+
+            out_file.write(hdr)
+
+        for section in header['items']:
+            out_file.seek(0x1814 + section['offset'])
+            out_file.write(get_section_contents(section))
+
+        size = out_file.tell()
+
+        out_file.seek(4)
+        md5_sum = hashlib.md5(out_file.read())
+
+        out_file.seek(0)
+        out_file.write(struct.pack('>I16s', size, md5_sum.digest()))
+
+def encode_soft_verson():
+    '''Not sure of the meaning of version. Also doesn't appear to be needed.'''
+    return struct.pack('>4B1I2I', 0xff, 1, 0 ,0, 0x2020202, 30000, 1)
+
+def create_image(output_image, root, support):
+    '''Create an image with a ubi "root" and a "support" string.'''
+    header = {}
+
+    header['rootfs_size'] = os.path.getsize(root)
+    header['items'] = []
+
+    rootfs = {}
+    rootfs['name'] = 'rootfs.ubi'
+    rootfs['file'] = root
+    rootfs['offset'] = 0
+    rootfs['size'] = header['rootfs_size']
+    header['items'].append(rootfs)
+
+    support_list = {}
+    support_list['name'] = 'support-list'
+    support_list['data'] = support.replace(" ", "\r\n").encode('utf-8')
+    support_list['offset'] = header['rootfs_size']
+    support_list['size'] = len(support_list['data'])
+    header['items'].append(support_list)
+
+    sw_version = {}
+    sw_version['name'] = 'soft-version'
+    sw_version['type'] = 1
+    sw_version['data'] = encode_soft_verson()
+    sw_version['offset'] = support_list['offset'] + support_list['size']
+    sw_version['size'] = len(sw_version['data'])
+    header['items'].append(sw_version)
+
+    header['num_items'] = len(header['items']) - 1
+    write_image(output_image, header)
+
+def main(args):
+    '''We support image analysis,extraction, and creation'''
+    if args.extract:
+        with open(args.image, 'rb') as image:
+            extract(image)
+    elif args.create:
+        if not args.rootfs or not args.support:
+            raise ValueError('To create an image, specify rootfs and support list')
+        create_image(args.image, args.rootfs, args.support)
+    else:
+        with open(args.image, 'rb') as image:
+            header = decode_header(image)
+
+            pretty = pprint.PrettyPrinter(indent=4, sort_dicts=False)
+            pretty.pprint(header)
+
+if __name__ == "__main__":
+    parser = argparse.ArgumentParser(description='EAP extractor')
+    parser.add_argument('--info', action='store_true')
+    parser.add_argument('--extract', action='store_true')
+    parser.add_argument('--create', action='store_true')
+    parser.add_argument('image', type=str,
+                    help='Name of image to create or decode')
+    parser.add_argument('--rootfs', type=str,
+                    help='When creating an EAP image, UBI image with rootfs and kernel')
+    parser.add_argument('--support', type=str,
+                    help='String for the "support-list" section')
+
+    main(parser.parse_args())
diff --git a/target/linux/qualcommax/files/arch/arm64/boot/dts/qcom/ipq6018-eap610-outdoor.dts b/target/linux/qualcommax/files/arch/arm64/boot/dts/qcom/ipq6018-eap610-outdoor.dts
new file mode 100644 (file)
index 0000000..165fc3e
--- /dev/null
@@ -0,0 +1,151 @@
+// SPDX-License-Identifier: GPL-2.0-or-later OR MIT
+
+/dts-v1/;
+
+#include "ipq6018.dtsi"
+#include "ipq6018-cp-cpu.dtsi"
+#include "ipq6018-ess.dtsi"
+
+#include <dt-bindings/gpio/gpio.h>
+#include <dt-bindings/input/input.h>
+#include <dt-bindings/leds/common.h>
+
+/ {
+       model = "TP-Link EAP610-Outdoor";
+       compatible = "tplink,eap610-outdoor", "qcom,ipq6018";
+
+       aliases {
+               serial0 = &blsp1_uart3;
+               led-boot = &led_sys_green;
+               led-failsafe = &led_sys_amber;
+               led-running = &led_sys_green;
+               led-upgrade = &led_sys_amber;
+       };
+
+       chosen {
+               stdout-path = "serial0:115200n8";
+               bootargs-append = " ubi.block=0,rootfs root=/dev/ubiblock0_1";
+       };
+
+       keys {
+               compatible = "gpio-keys";
+
+               reset {
+                       label = "reset";
+                       gpios = <&tlmm 9 GPIO_ACTIVE_LOW>;
+                       linux,code = <KEY_RESTART>;
+               };
+       };
+
+       leds {
+               compatible = "gpio-leds";
+
+               led_sys_amber: led-0 {
+                       function = "system";
+                       color = <LED_COLOR_ID_AMBER>;
+                       gpios = <&tlmm 35 GPIO_ACTIVE_HIGH>;
+               };
+
+               led_sys_green: led-1 {
+                       function = "system";
+                       color = <LED_COLOR_ID_GREEN>;
+                       gpios = <&tlmm 37 GPIO_ACTIVE_HIGH>;
+               };
+       };
+
+       gpio-restart {
+               compatible = "gpio-restart";
+               gpios = <&tlmm 61 GPIO_ACTIVE_LOW>;
+               open-source;
+       };
+};
+
+&blsp1_uart3 {
+       pinctrl-0 = <&serial_3_pins>;
+       pinctrl-names = "default";
+       status = "okay";
+};
+
+&tlmm {
+       mdio_pins: mdio-pins {
+               mdc {
+                       pins = "gpio64";
+                       function = "mdc";
+                       drive-strength = <8>;
+                       bias-pull-up;
+               };
+
+               mdio {
+                       pins = "gpio65";
+                       function = "mdio";
+                       drive-strength = <8>;
+                       bias-pull-up;
+               };
+       };
+
+       led_enable {
+               gpio-hog;
+               output-high;
+               gpios = <36 GPIO_ACTIVE_HIGH>;
+               line-name = "enable-leds";
+       };
+};
+
+&dp5 {
+       phy-handle = <&rtl8211f_4>;
+       phy-mode = "sgmii";
+       label = "lan";
+       status = "okay";
+};
+
+&edma {
+       status = "okay";
+};
+
+&mdio {
+       pinctrl-0 = <&mdio_pins>;
+       pinctrl-names = "default";
+       reset-gpios = <&tlmm 77 GPIO_ACTIVE_LOW>;
+       reset-delay-us = <10000>;
+       reset-post-delay-us = <50000>;
+       status = "okay";
+
+       rtl8211f_4: ethernet-phy@4 {
+               reg = <4>;
+       };
+};
+
+&switch {
+       switch_lan_bmp = <ESS_PORT5>;
+       switch_mac_mode1 = <MAC_MODE_SGMII_CHANNEL0>;
+       status = "okay";
+
+       qcom,port_phyinfo {
+               port@4 {
+                       port_id = <5>;
+                       phy_address = <4>;
+               };
+       };
+};
+
+&qpic_bam {
+       status = "okay";
+};
+
+&qpic_nand {
+       status = "okay";
+
+       nand@0 {
+               reg = <0>;
+
+               nand-ecc-strength = <4>;
+               nand-ecc-step-size = <512>;
+               nand-bus-width = <8>;
+       };
+};
+
+&wifi {
+       ieee80211-freq-limit = <2402000 5835000>;
+       qcom,ath11k-calibration-variant = "TP-Link-EAP610-Outdoor";
+       status = "okay";
+};
index 0b7cd413c36a6607e096bf30ff4fb1abf1e9f940..c7e0be1181e49ac1feeb2c803c0d5c508a1e944f 100644 (file)
@@ -68,6 +68,24 @@ define Device/qihoo_360v6
 endef
 TARGET_DEVICES += qihoo_360v6
 
+define Device/tplink_eap610-outdoor
+       $(call Device/FitImage)
+       $(call Device/UbiFit)
+       DEVICE_VENDOR := TP-Link
+       DEVICE_MODEL := EAP610-Outdoor
+       BLOCKSIZE := 128k
+       PAGESIZE := 2048
+       SOC := ipq6018
+       DEVICE_PACKAGES := ipq-wifi-tplink_eap610-outdoor
+       IMAGES += web-ui-factory.bin
+       IMAGE/web-ui-factory.bin := append-ubi | tplink-image-2022
+       TPLINK_SUPPORT_STRING := SupportList: \
+               EAP610-Outdoor(TP-Link|UN|AX1800-D):1.0 \
+               EAP610-Outdoor(TP-Link|JP|AX1800-D):1.0 \
+               EAP610-Outdoor(TP-Link|CA|AX1800-D):1.0
+endef
+TARGET_DEVICES += tplink_eap610-outdoor
+
 define Device/yuncore_fap650
        $(call Device/FitImage)
        $(call Device/UbiFit)
index ecc9e57117b98ffe2376285fcf08447eb1b2ba2a..44c0ba7049e1f01861dea164172400f829db9686 100644 (file)
@@ -23,6 +23,9 @@ ipq60xx_setup_interfaces()
        qihoo,360v6)
                ucidef_set_interfaces_lan_wan "lan1 lan2 lan3" "wan"
                ;;
+       tplink,eap610-outdoor)
+               ucidef_set_interface_lan "lan" "dhcp"
+               ;;
        linksys,mr7350|\
        yuncore,fap650)
                ucidef_set_interfaces_lan_wan "lan1 lan2 lan3 lan4" "wan"
@@ -51,6 +54,10 @@ ipq60xx_setup_macs()
                wan_mac=$(macaddr_add "$lan_mac" 1)
                label_mac=$lan_mac
                ;;
+       tplink,eap610-outdoor)
+               label_mac=$(get_mac_binary /tmp/factory_data/default-mac 0)
+               lan_mac=$label_mac
+               ;;
        esac
 
        [ -n "$lan_mac" ] && ucidef_set_interface_macaddr "lan" $lan_mac
index 3380cc8653f674d3b9b5158c01bf35125ab0c186..cf3e400586b7151071bfda24f8d793504dbc5594 100644 (file)
@@ -32,6 +32,13 @@ case "$FIRMWARE" in
                ath11k_patch_mac $(macaddr_add $label_mac 2) 1
                ath11k_set_macflag
                ;;
+       tplink,eap610-outdoor)
+               caldata_from_file "/tmp/factory_data/radio" 0 0x10000
+               label_mac=$(get_mac_binary /tmp/factory_data/default-mac 0)
+               ath11k_patch_mac $label_mac 1
+               ath11k_patch_mac $(macaddr_add $label_mac 1) 0
+               ath11k_set_macflag
+               ;;
        yuncore,fap650)
                caldata_extract "0:art" 0x1000 0x20000
                ;;
diff --git a/target/linux/qualcommax/ipq60xx/base-files/lib/preinit/09_mount_factory_data b/target/linux/qualcommax/ipq60xx/base-files/lib/preinit/09_mount_factory_data
new file mode 100644 (file)
index 0000000..97608db
--- /dev/null
@@ -0,0 +1,19 @@
+#!/bin/sh
+
+preinit_mount_factory_data() {
+       local mtd_path
+
+       . /lib/functions.sh
+       . /lib/functions/system.sh
+
+       case $(board_name) in
+       tplink,eap610-outdoor)
+               mtd_path=$(find_mtd_chardev "factory_data")
+               ubiattach --dev-path="$mtd_path" --devn=1
+               mkdir /tmp/factory_data
+               mount -o ro,noatime -t ubifs ubi1:ubi_factory_data /tmp/factory_data
+               ;;
+       esac
+}
+
+boot_hook_add preinit_main preinit_mount_factory_data
index 411570715c20ff04c69b9199914d8d1b8204dfab..f9d446ff1f70f7b3e2a9b73958c069e3f92c6283 100644 (file)
@@ -4,6 +4,79 @@ REQUIRE_IMAGE_METADATA=1
 RAMFS_COPY_BIN='fw_printenv fw_setenv head'
 RAMFS_COPY_DATA='/etc/fw_env.config /var/lock/fw_printenv.lock'
 
+remove_oem_ubi_volume() {
+       local oem_volume_name="$1"
+       local oem_ubivol
+       local mtdnum
+       local ubidev
+
+       mtdnum=$(find_mtd_index "$CI_UBIPART")
+       if [ ! "$mtdnum" ]; then
+               return
+       fi
+
+       ubidev=$(nand_find_ubi "$CI_UBIPART")
+       if [ ! "$ubidev" ]; then
+               ubiattach --mtdn="$mtdnum"
+               ubidev=$(nand_find_ubi "$CI_UBIPART")
+       fi
+
+       if [ "$ubidev" ]; then
+               oem_ubivol=$(nand_find_volume "$ubidev" "$oem_volume_name")
+               [ "$oem_ubivol" ] && ubirmvol "/dev/$ubidev" --name="$oem_volume_name"
+       fi
+}
+
+tplink_get_boot_part() {
+       local cur_boot_part
+       local args
+
+       # Try to find rootfs from kernel arguments
+       read -r args < /proc/cmdline
+       for arg in $args; do
+               local ubi_mtd_arg=${arg#ubi.mtd=}
+               case "$ubi_mtd_arg" in
+               rootfs|rootfs_1)
+                       echo "$ubi_mtd_arg"
+                       return
+               ;;
+               esac
+       done
+
+       # Fallback to u-boot env (e.g. when running initramfs)
+       cur_boot_part="$(/usr/sbin/fw_printenv -n tp_boot_idx)"
+       case $cur_boot_part in
+       1)
+               echo rootfs_1
+               ;;
+       0|*)
+               echo rootfs
+               ;;
+       esac
+}
+
+tplink_do_upgrade() {
+       local new_boot_part
+
+       case $(tplink_get_boot_part) in
+       rootfs)
+               CI_UBIPART="rootfs_1"
+               new_boot_part=1
+       ;;
+       rootfs_1)
+               CI_UBIPART="rootfs"
+               new_boot_part=0
+       ;;
+       esac
+
+       fw_setenv -s - <<-EOF
+               tp_boot_idx $new_boot_part
+       EOF
+
+       remove_oem_ubi_volume ubi_rootfs
+       nand_do_upgrade "$1"
+}
+
 platform_check_image() {
        return 0;
 }
@@ -55,6 +128,9 @@ platform_do_upgrade() {
        qihoo,360v6)
                nand_do_upgrade "$1"
                ;;
+       tplink,eap610-outdoor)
+               tplink_do_upgrade "$1"
+               ;;
        yuncore,fap650)
                [ "$(fw_printenv -n owrt_env_ver 2>/dev/null)" != "7" ] && yuncore_fap650_env_setup
                local active="$(fw_printenv -n owrt_slotactive 2>/dev/null)"