#!/bin/sh
# Copyright (C) 2025 iopsys Software Solutions AB
# Author: Suvendhu Hansa <suvendhu.hansa@iopsys.eu>
# Purpose: This script is used in airoha platform to test Download speed
#          using fast path via speed_test module
# Limitations:
#    1. speed_test module does not provide error codes when fails, so fallback
#       to curl command to get the exact error code
#    3. Configuration of speed_test module for FTP protocol is unknown, so
#       rely on curl for download test with ftp

. /usr/share/libubox/jshn.sh

ROOT="$(dirname "${0}")"
. "${ROOT}"/bbf_api
. "${ROOT}"/tr143_lock

DOWNLOAD_TIMEOUT=999
# Connection slots the speed_test module has; it refuses any count above this.
MAX_FASTPATH_CONNS=16
TEST_RESULT_PATH=/proc/tc3162/speedtest_single_result
TEST_RESULT_PATH_PER_CON=/proc/tc3162/speedtest_result_tr143_ng
DEBUG_LOGS="/var/log/download_diagnostics.log"
#DEBUG_LOGS="/var/log/download_diagnostics_$(date +%s).log"

# Set to 1 to turn on the speed_test module's verbose printk tracing (logged to dmesg/kmsg).
SPEEDTEST_DEBUG="${SPEEDTEST_DEBUG:-0}"

# Variables shared between functions; everything else is local to its function.

# Result of a run, set by whichever backend ran it and read by publish_result.
RES_ROM_TIME=""
RES_BOM_TIME=""
RES_EOM_TIME=""
RES_TEST_BYTES_RECEIVED=""
RES_TOTAL_BYTES_RECEIVED=""
RES_TOTAL_BYTES_SENT=""
RES_TCP_OPEN_REQUEST_TIME=""
RES_TCP_OPEN_RESPONSE_TIME=""
RES_PERIOD_OF_FULL_LOADING=""
RES_IP_ADDRESS_USED=""
RES_MODE=""
RES_NUM_CON=""
RES_ENABLE_PER_CON=""
RES_PROTO=""

# One per-connection record, set by parse_per_connection.
PC_ROM_TIME=""
PC_BOM_TIME=""
PC_EOM_TIME=""
PC_TEST_BYTES_RECEIVED=""
PC_TOTAL_BYTES_RECEIVED=""
PC_TOTAL_BYTES_SENT=""
PC_TCP_OPEN_REQUEST_TIME=""
PC_TCP_OPEN_RESPONSE_TIME=""

# One timestamp, set by read_time_field.
RT_EPOCH=""
RT_ISO=""

# State of the run in progress, used by debug_log_error.
DEVICE=""
FP_DEVICE=""
FP_HEADER=""
WAN_PROTO=""
DUT_IP=""
DST_IP=""
DST_PORT=""
FILE_PATH=""
DST_PROTO=""
NETMASK=""
NEXTHOP=""
CONNS=""
TIMEOUT=""
STATE=""
EOM=""
ERR=""
SETTLE=""

# Record the state the failing check saw. The proc dump alone only shows where
# the module ended up, not which guard rejected the run.
debug_log_error() {
	{
		echo "##### START error #####"
		echo "Status:       ${1}"
		echo "Origin:       ${2}"
		echo "device:       [${DEVICE}]"
		echo "fp_device:    [${FP_DEVICE}]"
		echo "fp_header:    [${FP_HEADER}]"
		echo "wan_proto:    [${WAN_PROTO}]"
		echo "dut_ip:       [${DUT_IP}]"
		echo "dst_ip:       [${DST_IP}]"
		echo "dst_port:     [${DST_PORT}]"
		echo "file_path:    [${FILE_PATH}]"
		echo "dst_proto:    [${DST_PROTO}]"
		echo "netmask:      [${NETMASK}]"
		echo "nexthop:      [${NEXTHOP}]"
		echo "conns:        [${CONNS}]"
		echo "debug:        [${SPEEDTEST_DEBUG}]"
		echo "poll:         TIMEOUT=[${TIMEOUT}] STATE=[${STATE}] EOM=[${EOM}]"
		# a status string here rather than a number means get_error_reason ran
		echo "err:          [${ERR}] after [${SETTLE}] settle attempt(s)"
		echo "result valid: [$(download_result_is_valid && echo yes || echo no)]"
		echo "${TEST_RESULT_PATH}: exists=[$([ -f "${TEST_RESULT_PATH}" ] && echo yes || echo no)]"
		echo "${TEST_RESULT_PATH_PER_CON}: exists=[$([ -f "${TEST_RESULT_PATH_PER_CON}" ] && echo yes || echo no)]"
		echo "##### STOP error #####"
	} >> "${DEBUG_LOGS}"
}

# True when the result file describes a transfer that actually ran to completion:
# measurement started, ended after it started, and moved data.
download_result_is_valid() {
	local bom EOM test_recv

	[ -f "${TEST_RESULT_PATH}" ] || return 1

	bom=$(grep "^BOMTime:" "${TEST_RESULT_PATH}" | cut -d'(' -f2 | cut -d')' -f1)
	EOM=$(grep "^EOMTime:" "${TEST_RESULT_PATH}" | cut -d'(' -f2 | cut -d')' -f1)
	test_recv=$(read_byte_field "${TEST_RESULT_PATH}" "TestByteReceived")

	[ -z "${bom}" ] || [ "${bom}" = "0:0" ] && return 1
	[ -z "${EOM}" ] || [ "${EOM}" = "0:0" ] && return 1
	[ -z "${test_recv}" ] && return 1

	[ "${test_recv}" -gt 0 ] 2>/dev/null || return 1
	[ "${EOM%%:*}" -ge "${bom%%:*}" ] 2>/dev/null || return 1

	return 0
}

download_error() {
	debug_log_error "$1" "$3"

	json_init
	json_add_string "Status" "$1"
	json_dump

	# Store data in dmmap_diagnostics for both protocols (cwmp/usp)
	[ "$2" = "both_proto" ] && {
		$UCI_SET_BBF_DMMAP dmmap_diagnostics.download.DiagnosticState="$1"
	}

	$UCI_SET_BBF_DMMAP dmmap_diagnostics.download.Status="complete"
	$UCI_COMMIT_BBF_DMMAP
}

# Map a curl exit code to the matching TR-181 diagnostics status. Codes 9-31
# only ever come back on the FTP path. Note 27 is out of memory, not a size
# mismatch: size conditions are 18 (partial), 33 (rejected range), 63 (too large).
curl_exit_to_status() {
	case "${1}" in
		0)             echo "Complete" ;;
		5|6|15)        echo "Error_CannotResolveHostName" ;;
		7|35|64)       echo "Error_InitConnectionFailed" ;;
		8|19|22|52)    echo "Error_NoResponse" ;;
		9|67)          echo "Error_LoginFailed" ;;
		11)            echo "Error_PasswordRequestFailed" ;;
		13|14)         echo "Error_NoPASV" ;;
		17)            echo "Error_NoTransferMode" ;;
		18|33|63)      echo "Error_IncorrectSize" ;;
		12|28)         echo "Error_Timeout" ;;
		45)            echo "Error_NoRouteToHost" ;;
		55|56)         echo "Error_TransferFailed" ;;
		2|23|26|27|43) echo "Error_Internal" ;;
		*)             echo "Error_Other" ;;
	esac
}

# Ask the server why the module could not fetch the file, mapping its answer to a
# diagnostic status; $7 is the fallback reason when the URL turns out to be serving
# fine. Issues a HEAD rather than a GET, which would time out transferring the
# whole test file and always fall through to $7 regardless of the real cause.
get_error_reason() {
	local code curl_proto host status

	curl_proto=""
	if [ "${1}" = "IPv4" ]; then
		curl_proto="--ipv4"
	elif [ "${1}" = "IPv6" ]; then
		curl_proto="--ipv6"
	fi

	# a bare IPv6 literal has to be bracketed to form a valid URL
	host="${3}"
	[ "${1}" = "IPv6" ] && host="[${3}]"

	curl ${curl_proto} --head --fail --silent --max-time 5 --interface "${6}" \
		"${2}://${host}:${4}${5}" --output /dev/null
	code="$?"

	# Not every server answers HEAD; retry with a short GET, treating any data
	# arriving as the URL being fine even if the timeout then cuts it off.
	if [ "${code}" -eq 22 ]; then
		curl ${curl_proto} --fail --silent --max-time 3 --interface "${6}" \
			"${2}://${host}:${4}${5}" --output /dev/null
		code="$?"
		if [ "${code}" -eq 28 ]; then
			code=0
		fi
	fi

	# The URL itself answered, so the failure was on the module's side and the
	# caller's reason is the better answer
	if [ "${code}" -eq 0 ]; then
		echo "${7}"
		return
	fi

	status=$(curl_exit_to_status "${code}")

	# Nothing specific to report either, so keep the caller's reason
	[ "${status}" = "Error_Other" ] && status="${7}"

	echo "${status}"
}

install_speedtest_modules() {
	if [ -f "${DEBUG_LOGS}" ]; then
		rm "${DEBUG_LOGS}"
	fi

	if ! lsmod | grep -qe "^arht_timer "; then
		# do not fail if it is absent
		insmod arht_timer
	fi

	if ! lsmod | grep -qe "^speedtest "; then
		if ! insmod speedtest; then
			rmmod arht_timer
			download_error "Error_Internal" "${1}" "install_speedtest_modules:${LINENO} insmod speedtest failed"
			exit 0
		fi
	fi

	if ! lsmod | grep -qe "^lro_wan "; then
		if ! insmod lro_wan; then
			rmmod speedtest
			rmmod arht_timer
			download_error "Error_Internal" "${1}" "install_speedtest_modules:${LINENO} insmod lro_wan failed"
			exit 0
		fi
	fi

	if ! lsmod | grep -qe "^lro_lan "; then
		if ! insmod lro_lan; then
			rmmod speedtest
			rmmod arht_timer
			rmmod lro_wan
			download_error "Error_Internal" "${1}" "install_speedtest_modules:${LINENO} insmod lro_lan failed"
			exit 0
		fi
	fi
}

remove_speedtest_modules() {
	rmmod speedtest
	rmmod lro_wan
	rmmod lro_lan
	rmmod arht_timer
}

collect_debug_logs() {
	local f

	{
		echo "##### START input #####"
		echo "${1}"
		echo "##### STOP input #####"
	} >> "${DEBUG_LOGS}"

	for f in /proc/tc3162/speed* /etc/bbfdm/dmmap/dmmap_diagnostics; do
		[ -e "${f}" ] || continue
		{
			echo "##### START ${f} #####"
			cat "${f}" 2>/dev/null
			echo "##### STOP ${f} #####"
		} >> "${DEBUG_LOGS}"
	done
}

get_dstip_port_path() {
	local colons dst fqdn full_regex ip_proto len num_colon octet_regex path port_regex
	local resolved tmp tmp_ip tmp_path tmp_port tmp_proto url

	url="${1}"
	ip_proto="${2}"

	tmp_ip=""
	tmp_port=""
	tmp_path=""
	tmp_proto=""

	dst=$(echo "${url}" | cut -d'/' -f 3)
	if [ -z "${dst}" ]; then
		echo "Error_Other"
		return
	fi

	octet_regex="(25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9]?[0-9])"
	port_regex="([1-9]|[1-9][0-9]|[1-9][0-9]{2}|[1-9][0-9]{3}|[1-5][0-9]{4}|6[0-4][0-9]{3}|65[0-4][0-9]{2}|655[0-2][0-9]|6553[0-5])"
	full_regex="^${octet_regex}\\.${octet_regex}\\.${octet_regex}\\.${octet_regex}(:${port_regex})?$"

	if [[ "${dst}" =~ ${full_regex} ]]; then
		# ipv4 ip
		if [ "${ip_proto}" = "IPv6" ]; then
			echo "Error_Other"
			return
		fi

		colons="${dst//[^:]}"
		num_colon="${#colons}"
		if [ "${num_colon}" -eq 1 ]; then
			tmp_ip=$(echo "${dst}" | cut -d':' -f 1)
			tmp_port=$(echo "${dst}" | cut -d':' -f 2)
		else
			tmp_ip="${dst}"
			tmp_port="80"
		fi

		tmp_proto="IPv4"
	else
		colons="${dst//[^:]}"
		num_colon="${#colons}"

		if [ "${num_colon}" -gt 1 ]; then
			# ipv6 ip: [xx::xx]:22, [xx::xx], or bare xx::xx
			if [ "${ip_proto}" = "IPv4" ]; then
				echo "Error_Other"
				return
			fi

			if [ "${dst:0:1}" != "[" ] && [[ "${dst}" =~ .\] ]]; then
				echo "Error_Other"
				return
			elif [ "${dst:0:1}" = "[" ] && [[ "${dst}" != *\]* ]]; then
				echo "Error_Other"
				return
			elif [ "${dst:0:1}" = "[" ] && [[ "${dst}" =~ .\]: ]]; then
				tmp="${dst:1}"
				tmp_ip=$(echo "${tmp}" | cut -d']' -f 1)
				tmp_port=$(echo "${tmp}" | cut -d']' -f 2 | cut -d':' -f 2)
			elif [ "${dst:0:1}" = "[" ] && [[ "${dst}" =~ .\]$ ]]; then
				len="${#dst}"
				len=$(( len - 2 ))
				tmp_ip="${dst:1:$len}"
				tmp_port="80"
			else
				tmp_ip="${dst}"
				tmp_port="80"
			fi

			tmp_proto="IPv6"
		else
			colons="${dst//[^:]}"
			num_colon="${#colons}"
			fqdn="${dst}"
			resolved=""
			tmp_port="80"

			if [ "${num_colon}" -eq 1 ]; then
				fqdn=$(echo "${dst}" | cut -d':' -f 1)
				tmp_port=$(echo "${dst}" | cut -d':' -f 2)
			fi

			if [ "${ip_proto}" = "IPv4" ]; then
				resolved=$(nslookup -type=a "${fqdn}" | grep Address: | tail -n +2 | head -n 1 | awk '{ print $NF }')
				tmp_proto="IPv4"
			elif [ "${ip_proto}" = "IPv6" ]; then
				resolved=$(nslookup -type=aaaa "${fqdn}" | grep Address: | tail -n +2 | head -n 1 | awk '{ print $NF }')
				tmp_proto="IPv6"
			else
				tmp_proto="IPv4"
				resolved=$(nslookup -type=a "${fqdn}" | grep Address: | tail -n +2 | head -n 1 | awk '{ print $NF }')
				if [ -z "${resolved}" ]; then
					resolved=$(nslookup -type=aaaa "${fqdn}" | grep Address: | tail -n +2 | head -n 1 | awk '{ print $NF }')
					tmp_proto="IPv6"
				fi
			fi

			if [ -z "${resolved}" ]; then
				echo "Error_CannotResolveHostName"
				return
			fi

			tmp_ip="${resolved}"
		fi
	fi

	path=$(echo "${url}" | cut -d'/' -f4-)
	if [ -z "${path}" ]; then
		echo "Error_Other"
		return
	fi

	tmp_path="/${path}"

	echo "${tmp_ip} ${tmp_port} ${tmp_path} ${tmp_proto}"
}

itoa() {
	#returns the dotted-decimal ascii form of an IP arg passed in integer format
	echo -n $(($(($(($((${1}/256))/256))/256))%256)).
	echo -n $(($(($((${1}/256))/256))%256)).
	echo -n $(($((${1}/256))%256)).
	echo $((${1}%256))
}

get_subnet_mask() {
	local mask mask_int pow sub

	mask=$(ifstatus "${1}" | jsonfilter -e '@["ipv4-address"][0].mask')
	if [ -z "${mask}" ]; then
		mask=$(ip addr show "${2}" | grep -w inet | awk '{print $2}' | cut -d'/' -f 2)
		if [ -z "${mask}" ]; then
			echo ""
			return
		fi
	fi

	pow=$(( 32 - mask ))
	sub=$(( 2 ** pow - 1 ))
	mask_int=$(( 4294967295 - sub ))

	itoa $mask_int
}

pppoe_l2_header() {
	local dev proto entry peer self sid ppp_proto

	dev="${1}"
	proto="${2}"

	entry=$(awk -v d="${dev}" 'NR>1 && $3==d {print $1" "$2; exit}' /proc/net/pppoe)
	[ -z "${entry}" ] && return 1

	# The kernel prints the session id with %08X on a __be16, so on a little
	# endian host its two bytes come out reversed: 00000100 is session 0x0001.
	sid="${entry%% *}"
	sid="${sid#????}"
	sid="${sid#??}${sid%??}"

	peer=$(echo "${entry#* }" | tr -d ':')
	self=$(tr -d ':' < "/sys/class/net/${dev}/address" 2>/dev/null)
	[ -z "${peer}" ] || [ -z "${self}" ] && return 1

	ppp_proto="0021"
	[ "${proto}" = "IPv6" ] && ppp_proto="0057"

	# ethernet, then PPPoE ver/type 0x11 code 0x00 session length, then the PPP
	# protocol id. The length is per packet, so it is left zero here.
	echo "${peer}${self}88641100${sid}0000${ppp_proto}"
}

extend_v6address() {
	local address colon_count group1 group2 group3 group4 group5 group6 group7 group8
	local pad_index zero_groups

	address="${1}"
	zero_groups=":"
	colon_count=$(echo "${address}" | grep -o ':' | wc -l)

	if [ "${colon_count}" -lt 7 ];then
		for pad_index in $(seq 1 $((8-colon_count)))
		do
			zero_groups=${zero_groups}0:
		done
		address=$(echo "${address}" | sed 's/::/'"${zero_groups}"'/')
	fi

	group1=$(echo "${address}" | awk -F ':' '{print $1}')
	group1=$((0x$group1))

	group2=$(echo "${address}" | awk -F ':' '{print $2}')
	group2=$((0x$group2))

	group3=$(echo "${address}" | awk -F ':' '{print $3}')
	group3=$((0x$group3))

	group4=$(echo "${address}" | awk -F ':' '{print $4}')
	group4=$((0x$group4))

	group5=$(echo "${address}" | awk -F ':' '{print $5}')
	group5=$((0x$group5))

	group6=$(echo "${address}" | awk -F ':' '{print $6}')
	group6=$((0x$group6))

	group7=$(echo "${address}" | awk -F ':' '{print $7}')
	group7=$((0x$group7))

	group8=$(echo "${address}" | awk -F ':' '{print $8}')
	group8=$((0x$group8))

	address=$(printf "%04x:%04x:%04x:%04x:%04x:%04x:%04x:%04x" $group1 $group2 $group3 $group4 $group5 $group6 $group7 $group8)
	echo "${address}"
}

# Convert a nanosecond field to the microseconds the data model reports. Dropping
# the last three digits truncates via string slicing rather than shell arithmetic,
# where a leading-zero field would be misread as octal.
get_usec_from_nsec_floor() {
	local nsec="00${1}" usec

	usec="000000${nsec%???}"

	echo "${usec:$(( ${#usec} - 6 ))}"
}

# Read one "<sec>:<nsec>" field ($2, e.g. "BOMTime" or "3_BOMTime") out of a
# result file ($1), setting RT_EPOCH to the "<sec>.<usec>" epoch float and
# RT_ISO to the data model's time format.
read_time_field() {
	local file="${1}" field="${2}"
	local frac raw rt_sec rt_usec separator_idx sec_part

	raw=$(grep -e "^${field}:" "${file}")
	raw="${raw:$(( ${#field} + 2 )):-1}"

	sec_part=${raw%%[:]*}
	separator_idx=$((${#sec_part}+1))
	frac=${raw:$separator_idx}

	rt_sec=${raw:0:$((separator_idx-1))}
	rt_usec=$(get_usec_from_nsec_floor "${frac}")

	RT_EPOCH="${rt_sec}.${rt_usec}"
	RT_ISO=$(date -u +"%Y-%m-%dT%H:%M:%S.${rt_usec}Z" -d @"${rt_sec}")
}

# Read one "<prefix>:<value>" byte counter out of a result file
read_byte_field() {
	local file="${1}" field="${2}" raw

	raw=$(grep -e "^${field}:" "${file}")
	echo "${raw:$(( ${#field} + 2 )):-1}"
}

# True once the module has begun measuring: BOMTime is stamped and the byte
# counter named in $1 moves. Both are still zero on a run that never connected.
transfer_has_started() {
	local bom bytes

	bom=$(grep "^BOMTime:" /proc/tc3162/speedtest_result 2>/dev/null | cut -d'(' -f2 | cut -d')' -f1)
	[ -n "${bom}" ] && [ "${bom}" != "0:0" ] && return 0

	bytes=$(grep "^${1}:" /proc/tc3162/speedtest_result 2>/dev/null | cut -d'(' -f2 | cut -d')' -f1)
	[ "${bytes:-0}" -gt 0 ] 2>/dev/null && return 0

	return 1
}

# Parse one per-connection record out of TEST_RESULT_PATH_PER_CON into the PC_*
# variables; the module numbers connections from 0.
parse_per_connection() {
	local idx="${1}"
	local pc_bom_epoch pc_eom_epoch

	read_time_field "${TEST_RESULT_PATH_PER_CON}" "${idx}_ROMTime"
	PC_ROM_TIME="${RT_ISO}"

	read_time_field "${TEST_RESULT_PATH_PER_CON}" "${idx}_BOMTime"
	PC_BOM_TIME="${RT_ISO}"
	pc_bom_epoch="${RT_EPOCH}"

	read_time_field "${TEST_RESULT_PATH_PER_CON}" "${idx}_EOMTime"
	PC_EOM_TIME="${RT_ISO}"
	pc_eom_epoch="${RT_EPOCH}"

	# The module misspells the request time field, keep the typo
	read_time_field "${TEST_RESULT_PATH_PER_CON}" "${idx}_TCPRequesetTime"
	PC_TCP_OPEN_REQUEST_TIME="${RT_ISO}"

	read_time_field "${TEST_RESULT_PATH_PER_CON}" "${idx}_TCPResponseTime"
	PC_TCP_OPEN_RESPONSE_TIME="${RT_ISO}"

	PC_TEST_BYTES_RECEIVED=$(read_byte_field "${TEST_RESULT_PATH_PER_CON}" "${idx}_TestByteReceived(Byte)")
	PC_TOTAL_BYTES_RECEIVED=$(read_byte_field "${TEST_RESULT_PATH_PER_CON}" "${idx}_TotalByteReceived(Byte)")
	PC_TOTAL_BYTES_SENT=$(read_byte_field "${TEST_RESULT_PATH_PER_CON}" "${idx}_TotalByteSend(Byte)")
}

# Convert a "<sec>.<usec>" epoch float to the data model time format
epoch_float_to_iso() {
	local epoch_float="${1}"
	local sec_part=${epoch_float%%[.]*}
	local separator_idx=$((${#sec_part}+1))
	local microsec=${epoch_float:$separator_idx}
	local sec=${epoch_float:0:$((separator_idx-1))}

	date -u +"%Y-%m-%dT%H:%M:%S.${microsec}Z" -d @"${sec}"
}

# Emit the diagnostic result on stdout for the USP operate response, and
# additionally persist it into dmmap_diagnostics on the CWMP path. Reads the
# RES_* variables set by the caller.
publish_result() {
	local count pc_section

	json_init
	json_add_string "Status" "Complete"
	json_add_string "IPAddressUsed" "${RES_IP_ADDRESS_USED}"
	json_add_string "ROMTime" "${RES_ROM_TIME}"
	json_add_string "BOMTime" "${RES_BOM_TIME}"
	json_add_string "EOMTime" "${RES_EOM_TIME}"
	json_add_int "TestBytesReceived" "${RES_TEST_BYTES_RECEIVED}"
	json_add_int "TotalBytesReceived" "${RES_TOTAL_BYTES_RECEIVED}"
	json_add_int "TotalBytesSent" "${RES_TOTAL_BYTES_SENT}"
	json_add_int "PeriodOfFullLoading" "${RES_PERIOD_OF_FULL_LOADING}"
	json_add_string "TCPOpenRequestTime" "${RES_TCP_OPEN_REQUEST_TIME}"
	json_add_string "TCPOpenResponseTime" "${RES_TCP_OPEN_RESPONSE_TIME}"

	if [ "${RES_ENABLE_PER_CON}" = "1" ] || [ "${RES_ENABLE_PER_CON}" = "true" ]; then
		json_add_array "DownloadPerConnection"
		count=0
		while [ "${count}" -lt "${RES_NUM_CON}" ]; do
			if [ "${RES_MODE}" = "legacy" ] || [ "${RES_NUM_CON}" -eq 1 ]; then
				# A single real transfer, reported once per configured connection.
				# Also used for a single-connection speed_test run: some devices
				# (e.g. e650) don't expose TEST_RESULT_PATH_PER_CON at all.
				echo "per-connection[${count}]: reused overall result (mode=${RES_MODE} num_con=${RES_NUM_CON})" >> "${DEBUG_LOGS}"
				PC_ROM_TIME="${RES_ROM_TIME}"
				PC_BOM_TIME="${RES_BOM_TIME}"
				PC_EOM_TIME="${RES_EOM_TIME}"
				PC_TEST_BYTES_RECEIVED="${RES_TEST_BYTES_RECEIVED}"
				PC_TOTAL_BYTES_RECEIVED="${RES_TOTAL_BYTES_RECEIVED}"
				PC_TOTAL_BYTES_SENT="${RES_TOTAL_BYTES_SENT}"
				PC_TCP_OPEN_REQUEST_TIME="${RES_TCP_OPEN_REQUEST_TIME}"
				PC_TCP_OPEN_RESPONSE_TIME="${RES_TCP_OPEN_RESPONSE_TIME}"
			else
				echo "per-connection[${count}]: parsed from ${TEST_RESULT_PATH_PER_CON}" >> "${DEBUG_LOGS}"
				parse_per_connection "${count}"
			fi

			json_add_object ""
			json_add_string "ROMTime" "${PC_ROM_TIME}"
			json_add_string "BOMTime" "${PC_BOM_TIME}"
			json_add_string "EOMTime" "${PC_EOM_TIME}"
			json_add_int "TestBytesReceived" "${PC_TEST_BYTES_RECEIVED}"
			json_add_int "TotalBytesReceived" "${PC_TOTAL_BYTES_RECEIVED}"
			json_add_int "TotalBytesSent" "${PC_TOTAL_BYTES_SENT}"
			json_add_string "TCPOpenRequestTime" "${PC_TCP_OPEN_REQUEST_TIME}"
			json_add_string "TCPOpenResponseTime" "${PC_TCP_OPEN_RESPONSE_TIME}"
			json_close_object

			count="$(( count + 1 ))"
		done
		json_close_array
	fi

	json_dump

	# Store data in dmmap_diagnostics for both protocols (cwmp/usp)
	[ "${RES_PROTO}" = "both_proto" ] && {
		$UCI_SET_BBF_DMMAP dmmap_diagnostics.download.DiagnosticState="Complete"
		$UCI_SET_BBF_DMMAP dmmap_diagnostics.download.IPAddressUsed="${RES_IP_ADDRESS_USED}"
		$UCI_SET_BBF_DMMAP dmmap_diagnostics.download.ROMTime="${RES_ROM_TIME}"
		$UCI_SET_BBF_DMMAP dmmap_diagnostics.download.BOMTime="${RES_BOM_TIME}"
		$UCI_SET_BBF_DMMAP dmmap_diagnostics.download.EOMTime="${RES_EOM_TIME}"
		$UCI_SET_BBF_DMMAP dmmap_diagnostics.download.TestBytesReceived="${RES_TEST_BYTES_RECEIVED}"
		$UCI_SET_BBF_DMMAP dmmap_diagnostics.download.TotalBytesReceived="${RES_TOTAL_BYTES_RECEIVED}"
		$UCI_SET_BBF_DMMAP dmmap_diagnostics.download.TotalBytesSent="${RES_TOTAL_BYTES_SENT}"
		$UCI_SET_BBF_DMMAP dmmap_diagnostics.download.PeriodOfFullLoading="${RES_PERIOD_OF_FULL_LOADING}"
		$UCI_SET_BBF_DMMAP dmmap_diagnostics.download.TCPOpenRequestTime="${RES_TCP_OPEN_REQUEST_TIME}"
		$UCI_SET_BBF_DMMAP dmmap_diagnostics.download.TCPOpenResponseTime="${RES_TCP_OPEN_RESPONSE_TIME}"

		if [ "${RES_ENABLE_PER_CON}" = "true" ] || [ "${RES_ENABLE_PER_CON}" = "1" ]; then
			count=0
			while [ "${count}" -lt "${RES_NUM_CON}" ]; do
				if [ "${RES_MODE}" = "legacy" ] || [ "${RES_NUM_CON}" -eq 1 ]; then
					PC_ROM_TIME="${RES_ROM_TIME}"
					PC_BOM_TIME="${RES_BOM_TIME}"
					PC_EOM_TIME="${RES_EOM_TIME}"
					PC_TEST_BYTES_RECEIVED="${RES_TEST_BYTES_RECEIVED}"
					PC_TOTAL_BYTES_RECEIVED="${RES_TOTAL_BYTES_RECEIVED}"
					PC_TOTAL_BYTES_SENT="${RES_TOTAL_BYTES_SENT}"
					PC_TCP_OPEN_REQUEST_TIME="${RES_TCP_OPEN_REQUEST_TIME}"
					PC_TCP_OPEN_RESPONSE_TIME="${RES_TCP_OPEN_RESPONSE_TIME}"
				else
					parse_per_connection "${count}"
				fi

				pc_section="download_$(( count + 1 ))"
				$UCI_SET_BBF_DMMAP "dmmap_diagnostics.${pc_section}=DownloadPerConnection"
				$UCI_SET_BBF_DMMAP "dmmap_diagnostics.${pc_section}.perconnection_instance=$(( count + 1 ))"
				$UCI_SET_BBF_DMMAP dmmap_diagnostics.${pc_section}.ROMTime="${PC_ROM_TIME}"
				$UCI_SET_BBF_DMMAP dmmap_diagnostics.${pc_section}.BOMTime="${PC_BOM_TIME}"
				$UCI_SET_BBF_DMMAP dmmap_diagnostics.${pc_section}.EOMTime="${PC_EOM_TIME}"
				$UCI_SET_BBF_DMMAP dmmap_diagnostics.${pc_section}.TestBytesReceived="${PC_TEST_BYTES_RECEIVED}"
				$UCI_SET_BBF_DMMAP dmmap_diagnostics.${pc_section}.TotalBytesReceived="${PC_TOTAL_BYTES_RECEIVED}"
				$UCI_SET_BBF_DMMAP dmmap_diagnostics.${pc_section}.TotalBytesSent="${PC_TOTAL_BYTES_SENT}"
				$UCI_SET_BBF_DMMAP dmmap_diagnostics.${pc_section}.TCPOpenRequestTime="${PC_TCP_OPEN_REQUEST_TIME}"
				$UCI_SET_BBF_DMMAP dmmap_diagnostics.${pc_section}.TCPOpenResponseTime="${PC_TCP_OPEN_RESPONSE_TIME}"

				count="$(( count + 1 ))"
			done
		fi
	}

	$UCI_SET_BBF_DMMAP dmmap_diagnostics.download.Status="complete"
	$UCI_COMMIT_BBF_DMMAP
}

execute_http_download_test() {
	local bom_epoch dscp enable_per_con eom_epoch eth_prio get_res iface ip_proto ipalloc_type
	local num_of_con overall_period proto tbt_duration test_duration tmp url

	url="${1}"
	iface="${2}"
	dscp="${3}"
	eth_prio="${4}"
	ip_proto="${5}"
	num_of_con="${6}"
	enable_per_con="${7}"
	proto="${8}"
	tbt_duration="${9}"

	if [ -z "${iface}" ]; then
		iface="wan"
	fi

	VERSION=$(grep "version:" /proc/tc3162/speed_test | cut -d: -f 2 | tr -d ' \t\r\n')
	DEVICE=$(ifstatus "${iface}" | jsonfilter -e @.l3_device)
	if [ -z "${DEVICE}" ]; then
		download_error "Error_Other" "${proto}" "execute_http_download_test:${LINENO} wan device lookup failed"
		return
	fi

	WAN_PROTO=$(ifstatus "${iface}" | jsonfilter -e @.proto)
	if [ -z "${WAN_PROTO}" ]; then
		download_error "Error_Other" "${proto}" "execute_http_download_test:${LINENO} wan proto lookup failed"
		return
	fi

	# The speed_test module builds the whole link layer header itself, PPPoE
	# encapsulation included, and transmits it with dev_queue_xmit(). It needs
	# the device that header goes out on, so on PPPoE that is the interface's
	# lower device and not its l3_device, which is the PPP netdev. Handing it
	# the PPP netdev encapsulates every frame twice and panics the kernel; the
	# module refuses a PPP netdev outright on builds that carry that fix.
	# Everything below still uses DEVICE, which stays the l3_device.
	# Deliberately left as "ip" on PPPoE. Writing "ppp" sets the module's isppp,
	# which does not mean "encapsulate as PPPoE" but "the netdev encapsulates for
	# me": every one of its transmit paths reads
	#
	#	if (!isppp) { skb_push(skb, maclen); memcpy(skb->data, machdr, maclen); }
	#	if (maclen > 20) *(__be16 *)(skb->data + maclen - 4) = hdr.tot_len + 2;
	#
	# so with isppp set no link layer header is prepended at all and a bare IP
	# frame reaches dev_queue_xmit, while the length fixup, still enabled by our
	# 22 byte maclen, writes into the destination address instead of the PPPoE
	# length field. Captured on the WAN, the frames left as ethertype 0xc0a8 --
	# the IP header read as an ethernet one -- addressed to 192.168.2.60 rather
	# than 192.168.88.10. That design needs wandev= to be the PPP netdev, which
	# is the arrangement that panics the box, so there is no working combination
	# with it set. Clearing it makes the module prepend the header built below,
	# and skb->data + 18 is then the PPPoE length field the fixup was written
	# for. isppp otherwise only adjusts a receive byte count.
	ipalloc_type="ip"
	FP_DEVICE="${DEVICE}"
	if [ "${WAN_PROTO}" = "pppoe" ]; then
		if [ "${VERSION}" = "1.7.16.004" ]; then
			FP_DEVICE=$(ifstatus "${iface}" | jsonfilter -e @.device)
			if [ -z "${FP_DEVICE}" ]; then
				download_error "Error_Other" "${proto}" "execute_http_download_test:${LINENO} pppoe lower device lookup failed"
				return
			fi
		else
			ipalloc_type="ppp"
		fi
	fi

	get_res=$(get_dstip_port_path "${url}" "${ip_proto}")
	if [ "${get_res:0:6}" = "Error_" ]; then
		download_error "${get_res}" "${proto}" "execute_http_download_test:${LINENO} get_dstip_port_path failed"
		return
	fi

	DST_IP=$(echo "${get_res}" | awk '{print $1}')
	DST_PORT=$(echo "${get_res}" | awk '{print $2}')
	FILE_PATH=$(echo "${get_res}" | awk '{print $3}')
	DST_PROTO=$(echo "${get_res}" | awk '{print $4}')

	if [ -z "${DST_IP}" ] || [ -z "${DST_PORT}" ] || [ -z "${FILE_PATH}" ] || [ -z "${DST_PROTO}" ]; then
		download_error "Error_Other" "${proto}" "execute_http_download_test:${LINENO} dst ip/port/path incomplete"
		return
	fi

	DUT_IP=$(get_ip_addr_used "${url}" "${DST_PROTO}" "${iface}")
	if [ -z "${DUT_IP}" ]; then
		download_error "Error_NoRouteToHost" "${proto}" "execute_http_download_test:${LINENO} get_ip_addr_used failed"
		return
	fi

	NETMASK=""
	NEXTHOP=""
	if [ "${DST_PROTO}" = "IPv4" ]; then
		NETMASK=$(get_subnet_mask "${iface}" "${DEVICE}")
		NEXTHOP=$(ifstatus "${iface}" | jsonfilter -e @.route[0].nexthop)
		if [ -z "${NETMASK}" ]; then
			download_error "Error_Other" "${proto}" "execute_http_download_test:${LINENO} netmask lookup failed"
			return
		fi
	elif [ "${DST_PROTO}" = "IPv6" ]; then
		DST_IP=$(extend_v6address "${DST_IP}")
	else
		download_error "Error_Other" "${proto}" "execute_http_download_test:${LINENO} unsupported dst proto"
		return
	fi

	if [ ! -f /proc/tc3162/speed_test ]; then
		download_error "Error_Other" "${proto}" "execute_http_download_test:${LINENO} speed_test proc missing"
		return
	fi

	# For a GET the module opens one connection fewer than asked, so add one.
	# Clamped: a count above the slot total is refused and the run then stalls.
	CONNS=$(( num_of_con + 1 ))
	[ "${CONNS}" -gt "${MAX_FASTPATH_CONNS}" ] && CONNS="${MAX_FASTPATH_CONNS}"

	# Bounded by the ceiling this backend supports so it still finishes within
	# the poll loop below; the data model itself already refuses anything past it.
	test_duration="${DOWNLOAD_TIMEOUT}"
	if [ "${tbt_duration}" -gt 0 ]; then
		test_duration="${tbt_duration}"
		[ "${test_duration}" -gt "${DOWNLOAD_TIMEOUT}" ] && test_duration="${DOWNLOAD_TIMEOUT}"
	fi

	# now start the speedtest tool
	echo "fin" > /proc/tc3162/speed_test
	echo "reset" > /proc/tc3162/speed_test
	sleep 1
	[ "${SPEEDTEST_DEBUG}" = "1" ] && echo "debug=1" > /proc/tc3162/speed_test
	echo "${DST_PROTO}" > /proc/tc3162/speed_test
	echo "${ipalloc_type}" > /proc/tc3162/speed_test
	echo "wandev=${FP_DEVICE}" > /proc/tc3162/speed_test
	echo "offset=1" > /proc/tc3162/speed_test
	echo "duration=${test_duration}" > /proc/tc3162/speed_test
	echo "usegdm2rx=0" > /proc/tc3162/speed_test
	echo "scale=14" > /proc/tc3162/speed_test
	echo "lro=1" > /proc/tc3162/speed_test
	echo "conns=${CONNS}" > /proc/tc3162/speed_test
	echo "clear_data=1" > /proc/tc3162/speed_test
	if [ -n "${dscp}" ]; then
		echo "dscp=${dscp}" > /proc/tc3162/speed_test
	fi

	if [ -n "${eth_prio}" ]; then
		echo "ethpri=${eth_prio}" > /proc/tc3162/speed_test
	fi

	# Two things the module cannot work out for itself once wandev= is the lower
	# device rather than the PPP netdev.
	#
	# It reads the source address off wandev=, and the lower device carries none,
	# so the lookup comes up empty, localip keeps the zero reset left it and
	# speed_test_update_conn() drops every connection on its src_ip check. It
	# keeps a value it was given, so write the address explicitly.
	#
	# It also transmits with a link layer header it learns by watching the WAN,
	# and it only ever learns one from ARP on the VLAN device, which PPPoE never
	# produces. maclen stays 0 and every frame leaves with no header at all --
	# and the maclen sanity check is skipped when isppp is set, so the run gets
	# under way regardless and simply never moves a byte. Hand it the header too.
	if [ "${WAN_PROTO}" = "pppoe" ] && [ "${VERSION}" = "1.7.16.004" ]; then
		SRC_IP="${DUT_IP}"
		[ "${DST_PROTO}" = "IPv6" ] && SRC_IP=$(extend_v6address "${DUT_IP}")
		echo "localip=${SRC_IP}" > /proc/tc3162/speed_test

		FP_HEADER=$(pppoe_l2_header "${FP_DEVICE}" "${DST_PROTO}")
		if [ -z "${FP_HEADER}" ]; then
			download_error "Error_Other" "${proto}" "execute_http_download_test:${LINENO} pppoe header build failed"
			return
		fi
		echo "header=${FP_HEADER}" > /proc/tc3162/speed_test
	fi
	echo "destip=${DST_IP}" > /proc/tc3162/speed_test
	echo "destport=${DST_PORT}" > /proc/tc3162/speed_test
	if [ "${DST_PROTO}" = "IPv4" ]; then
		echo "mask=${NETMASK}" > /proc/tc3162/speed_test
		if [ -n "${NEXTHOP}" ]; then
			if [ "${WAN_PROTO}" != "pppoe" ] || [ "${VERSION}" != "1.7.16.004" ]; then
				echo "gateway=${NEXTHOP}" > /proc/tc3162/speed_test
			fi
		fi
		echo "host=${DST_IP}" > /proc/tc3162/speed_test
	else
		if [ "${WAN_PROTO}" != "pppoe" ] || [ "${VERSION}" != "1.7.16.004" ]; then
			ping -6 -I "${DEVICE}" -c 2 "${DST_IP}" >/dev/null
		fi
		echo "host=[${DST_IP}]" > /proc/tc3162/speed_test
	fi
	echo "action=GET ${FILE_PATH} ${DST_IP} ${DST_PORT}" > /proc/tc3162/speed_test

	# Duration plus ten seconds for the module to finalise; not a multiple of the
	# sampling interval below, hence -le rather than -eq.
	TIMEOUT=$((test_duration + 10))
	# Samples allowed for the transfer to begin. A healthy run has BOMTime stamped
	# by the first one; a dead one would otherwise poll out the whole budget.
	START_SAMPLES=6
	STATE=""
	EOM="0:0"
	while [ "${TIMEOUT}" -gt 0 ]; do
		sleep 5

		STATE=$(grep "download state" /proc/tc3162/speedtest_state| awk '{ print $NF }')

		tmp=$(grep "EOMTime:" /proc/tc3162/speedtest_result)
		if [ -n "${tmp}" ]; then
			EOM="${tmp:9:-1}"
		fi

		if [ "${STATE}" = "idle" ] || [ "${EOM}" != "0:0" ]; then
			break
		fi

		# Nothing measured yet: the module never got the transfer going, so give up
		# rather than hold the lock for the rest of the budget.
		START_SAMPLES=$(( START_SAMPLES - 1 ))
		if [ "${START_SAMPLES}" -eq 0 ] && ! transfer_has_started TestByteReceived; then
			ERR=$(get_error_reason "${DST_PROTO}" "http" "${DST_IP}" "${DST_PORT}" "${FILE_PATH}" "${DEVICE}" "Error_InitConnectionFailed")
			download_error "${ERR}" "${proto}" "execute_http_download_test:${LINENO} transfer never started"
			return
		fi

		TIMEOUT=$(( TIMEOUT - 5 ))
	done

	if [ "${TIMEOUT}" -le 0 ]; then
		ERR=$(get_error_reason "${DST_PROTO}" "http" "${DST_IP}" "${DST_PORT}" "${FILE_PATH}" "${DEVICE}" "Error_Timeout")
		download_error "${ERR}" "${proto}" "execute_http_download_test:${LINENO} poll timed out"
		return
	fi

	# EOMTime is published before the module finishes finalising, so the first
	# err sample can still carry a code that clears a moment later.
	SETTLE=0
	while [ "${SETTLE}" -lt 3 ]; do
		ERR=$(grep "^err:" /proc/tc3162/speedtest_result | cut -d: -f 2)
		[ "${ERR}" = "0" ] && break
		sleep 1
		SETTLE=$(( SETTLE + 1 ))
	done

	# A complete transfer is proof the run succeeded, so only report a failure
	# when the results do not back the error code up
	if [ "${ERR}" != "0" ] && ! download_result_is_valid; then
		ERR=$(get_error_reason "${DST_PROTO}" "http" "${DST_IP}" "${DST_PORT}" "${FILE_PATH}" "${DEVICE}" "Error_Other")
		download_error "${ERR}" "${proto}" "execute_http_download_test:${LINENO} result invalid after error"
		return
	fi

	if [ ! -f "${TEST_RESULT_PATH}" ]; then
		download_error "Error_Other" "${proto}" "execute_http_download_test:${LINENO} result file missing"
		return
	fi

	read_time_field "${TEST_RESULT_PATH}" "ROMTime"
	RES_ROM_TIME="${RT_ISO}"

	read_time_field "${TEST_RESULT_PATH}" "BOMTime"
	RES_BOM_TIME="${RT_ISO}"
	bom_epoch="${RT_EPOCH}"

	read_time_field "${TEST_RESULT_PATH}" "EOMTime"
	RES_EOM_TIME="${RT_ISO}"
	eom_epoch="${RT_EPOCH}"

	read_time_field "${TEST_RESULT_PATH}" "TCPRequesetTime"
	RES_TCP_OPEN_REQUEST_TIME="${RT_ISO}"

	read_time_field "${TEST_RESULT_PATH}" "TCPResponseTime"
	RES_TCP_OPEN_RESPONSE_TIME="${RT_ISO}"

	RES_IP_ADDRESS_USED="${DUT_IP}"
	RES_TEST_BYTES_RECEIVED=$(read_byte_field "${TEST_RESULT_PATH}" "TestByteReceived")
	RES_TOTAL_BYTES_RECEIVED=$(read_byte_field "${TEST_RESULT_PATH}" "TotalByteReceived")
	RES_TOTAL_BYTES_SENT=$(read_byte_field "${TEST_RESULT_PATH}" "TotalByteSend")
	RES_ENABLE_PER_CON="${enable_per_con}"
	RES_NUM_CON="${num_of_con}"
	RES_PROTO="${proto}"
	RES_MODE="speed_test"

	overall_period=$(echo "${eom_epoch} ${bom_epoch}" | \
		awk '{ d = ($1 - $2) * 1000000; printf "%d", (d > 0 ? d : 0) }')

	RES_PERIOD_OF_FULL_LOADING="${overall_period}"

	debug_log_error "Complete" "execute_http_download_test:${LINENO}"
	publish_result
}

execute_legacy_download_test() {
	local bom_epoch bom_iso bom_sec bom_usec curl_max_time dscp enable_per_con eom_epoch
	local eom_iso eom_sec eom_usec eth_prio exitcode format iface ip_proto num_of_con
	local period_time proto res rom_epoch rom_iso rom_sec rom_usec rx_bytes_end rx_bytes_start
	local separator_idx size_download size_header tbt_duration tcp_req_epoch tcp_req_iso
	local tcp_req_sec tcp_req_usec tcp_resp_epoch tcp_resp_iso tcp_resp_sec tcp_resp_usec
	local test_recv time_appconnect time_connect time_end time_pretransfer time_start
	local time_starttransfer time_total total_recv total_send tx_bytes_end tx_bytes_start url
	local sec_part

	url="${1}"
	iface="${2}"
	dscp="${3}"
	eth_prio="${4}"
	ip_proto="${5}"
	num_of_con="${6}"
	enable_per_con="${7}"
	proto="${8}"
	tbt_duration="${9}"

	[ -z "${url}" ] && {
		download_error "Error_InitConnectionFailed" "${proto}" "execute_legacy_download_test:${LINENO} empty url"
		return
	}

	[ "${url:0:7}" != "http://" ] && [ "${url:0:6}" != "ftp://" ] && {
		download_error "Error_Other" "${proto}" "execute_legacy_download_test:${LINENO} unsupported url scheme"
		return
	}

	if [ -n "${iface}" ]; then
		DEVICE=$(ifstatus "${iface}" | jsonfilter -e '@.l3_device')
		[ -z "${DEVICE}" ] && {
			download_error "Error_NoRouteToHost" "${proto}" "execute_legacy_download_test:${LINENO} wan device lookup failed"
			return
		}
	else
		DEVICE=$(route -n | grep 'UG[ \t]' | awk '{print $8}')
	fi

	DUT_IP=$(get_ip_addr_used "${url}" "${ip_proto}" "${iface}")
	if [ -z "${DUT_IP}" ]; then
		download_error "Error_NoRouteToHost" "${proto}" "execute_legacy_download_test:${LINENO} get_ip_addr_used failed"
		return
	fi

	if [ "$ip_proto" = "IPv4" ]; then ip_proto="--ipv4"; elif [ "$ip_proto" = "IPv6" ]; then ip_proto="--ipv6"; else ip_proto=""; fi

	format='{ "size_download": "%{size_download}",
			  "size_header": "%{size_header}",
			  "time_appconnect": "%{time_appconnect}",
			  "time_connect": "%{time_connect}",
			  "time_pretransfer": "%{time_pretransfer}",
			  "time_starttransfer": "%{time_starttransfer}",
			  "time_total": "%{time_total}",
			  "exitcode": "%{exitcode}" }'

	tx_bytes_start=$(ubus call network.device status "{'name':'$DEVICE'}" | jsonfilter -e @.statistics.tx_bytes)
	rx_bytes_start=$(ubus call network.device status "{'name':'$DEVICE'}" | jsonfilter -e @.statistics.rx_bytes)

	# Bounded by its duration, never longer than a size based test's ceiling.
	curl_max_time="${DOWNLOAD_TIMEOUT}"
	if [ "${tbt_duration}" -gt 0 ]; then
		curl_max_time="${tbt_duration}"
		[ "${curl_max_time}" -gt "${DOWNLOAD_TIMEOUT}" ] && curl_max_time="${DOWNLOAD_TIMEOUT}"
	fi

	time_start=$(date +"%s.282646") # It should be like that time_start=$(date +"%s.%6N") but since OpenWrt busybox has limitations and doesn't support nonoseconds so keep it hardcoded
	if [ -z "${ip_proto}" ]; then
		res=$(curl --fail --silent --max-time "${curl_max_time}" --interface "${DEVICE}" -w "${format}" "${url}" --output /dev/null)
	else
		res=$(curl "${ip_proto}" --fail --silent --max-time "${curl_max_time}" --interface "${DEVICE}" -w "${format}" "${url}" --output /dev/null)
	fi
	time_end=$(date +"%s.282646") # It should be like that time_end=$(date +"%s.%6N") but since OpenWrt busybox has limitations and doesn't support nonoseconds so keep it hardcoded

	tx_bytes_end=$(ubus call network.device status "{'name':'$DEVICE'}" | jsonfilter -e @.statistics.tx_bytes)
	rx_bytes_end=$(ubus call network.device status "{'name':'$DEVICE'}" | jsonfilter -e @.statistics.rx_bytes)
	
	logger -t "tr143_download" "########### ${url} ==> ${res} ###########"
	json_load "${res}"
	json_get_var size_download size_download
	json_get_var size_header size_header
	json_get_var time_appconnect time_appconnect
	json_get_var time_connect time_connect
	json_get_var time_pretransfer time_pretransfer
	json_get_var time_starttransfer time_starttransfer
	json_get_var time_total time_total
	json_get_var exitcode exitcode

	[ -z "${exitcode}" ] && exitcode=-1

	# Hitting the timeout is expected for a time based test, not a failure.
	[ "${tbt_duration}" -gt 0 ] && [ "$exitcode" = "28" ] && exitcode=0
	[ "$exitcode" != "0" ] && {
		download_error "$(curl_exit_to_status "${exitcode}")" "${proto}" "execute_legacy_download_test:${LINENO} curl exitcode ${exitcode}"
		return
	}
	
	[ -z "${time_appconnect}" ] && time_appconnect=0
	tcp_req_epoch=$(echo "${time_start}" "${time_appconnect}" | awk '{printf "%.6f", $1 + $2}')
	[ -z "${time_connect}" ] && time_connect=0
	tcp_resp_epoch=$(echo "${time_start}" "${time_connect}" | awk '{printf "%.6f", $1 + $2}')
	[ -z "${time_pretransfer}" ] && time_pretransfer=0
	rom_epoch=$(echo "${time_start}" "${time_pretransfer}" | awk '{printf "%.6f", $1 + $2}')
	[ -z "${time_starttransfer}" ] && time_starttransfer=0
	bom_epoch=$(echo "${time_start}" "${time_starttransfer}" | awk '{printf "%.6f", $1 + $2}')
	[ -z "${time_total}" ] && time_total=0
	eom_epoch=$(echo "${time_start}" "${time_total}" | awk '{printf "%.6f", $1 + $2}')

	sec_part=${tcp_req_epoch%%[.]*}
	separator_idx=$((${#sec_part}+1))
	tcp_req_usec=${tcp_req_epoch:$separator_idx}
	tcp_req_sec=${tcp_req_epoch:0:$((separator_idx-1))}

	sec_part=${tcp_resp_epoch%%[.]*}
	separator_idx=$((${#sec_part}+1))
	tcp_resp_usec=${tcp_resp_epoch:$separator_idx}
	tcp_resp_sec=${tcp_resp_epoch:0:$((separator_idx-1))}

	sec_part=${rom_epoch%%[.]*}
	separator_idx=$((${#sec_part}+1))
	rom_usec=${rom_epoch:$separator_idx}
	rom_sec=${rom_epoch:0:$((separator_idx-1))}

	sec_part=${bom_epoch%%[.]*}
	separator_idx=$((${#sec_part}+1))
	bom_usec=${bom_epoch:$separator_idx}
	bom_sec=${bom_epoch:0:$((separator_idx-1))}

	sec_part=${eom_epoch%%[.]*}
	separator_idx=$((${#sec_part}+1))
	eom_usec=${eom_epoch:$separator_idx}
	eom_sec=${eom_epoch:0:$((separator_idx-1))}

	tcp_req_iso=$(date -u +"%Y-%m-%dT%H:%M:%S.${tcp_req_usec}Z" -d @"${tcp_req_sec}")
	tcp_resp_iso=$(date -u +"%Y-%m-%dT%H:%M:%S.${tcp_resp_usec}Z" -d @"${tcp_resp_sec}")
	rom_iso=$(date -u +"%Y-%m-%dT%H:%M:%S.${rom_usec}Z" -d @"${rom_sec}")
	bom_iso=$(date -u +"%Y-%m-%dT%H:%M:%S.${bom_usec}Z" -d @"${bom_sec}")
	eom_iso=$(date -u +"%Y-%m-%dT%H:%M:%S.${eom_usec}Z" -d @"${eom_sec}")

	total_send=$((tx_bytes_end-tx_bytes_start))
	total_recv=$((rx_bytes_end-rx_bytes_start))

	[ -z "${size_header}" ] && size_header=0
	[ -z "${size_download}" ] && size_download=0
	test_recv=$((size_download+size_header))
	period_time=$(echo "${time_end}" "${time_start}" | awk '{printf ($1 - $2) * 1000000}')

	RES_IP_ADDRESS_USED="${DUT_IP}"
	RES_ROM_TIME="${rom_iso}"
	RES_BOM_TIME="${bom_iso}"
	RES_EOM_TIME="${eom_iso}"
	RES_TEST_BYTES_RECEIVED="${test_recv}"
	RES_TOTAL_BYTES_RECEIVED="${total_recv}"
	RES_TOTAL_BYTES_SENT="${total_send}"
	RES_TCP_OPEN_REQUEST_TIME="${tcp_req_iso}"
	RES_TCP_OPEN_RESPONSE_TIME="${tcp_resp_iso}"
	RES_ENABLE_PER_CON="${enable_per_con}"
	RES_NUM_CON="${num_of_con}"
	RES_PROTO="${proto}"
	RES_MODE="legacy"

	RES_PERIOD_OF_FULL_LOADING="${period_time}"

	debug_log_error "Complete" "execute_legacy_download_test:${LINENO}"
	publish_result
}

download_launch() {
	local dscp enable_per_con eth_prio iface input ip_proto num_of_con proto sec sections
	local tbt_duration url

	input="$1"

	json_load "${input}"
	
	json_get_var url url
	json_get_var iface iface
	json_get_var dscp dscp
	json_get_var eth_prio eth_prio
	json_get_var ip_proto ip_proto
	json_get_var num_of_con num_of_con
	json_get_var enable_per_con enable_per_con
	json_get_var tbt_duration tbt_duration
	json_get_var proto proto

	# No two transfer diagnostics may run at once, see tr143_lock
	transfer_lock_try "download" "tr143_download" "${proto}" || return

	# A zero duration means the test is size based (historical behaviour).
	[ -z "${tbt_duration}" ] && tbt_duration=0

	# Absent, zero or junk: the data model default is a single connection.
	[ "${num_of_con}" -gt 0 ] 2>/dev/null || num_of_con=1
	# Make sure the dmmap section exists, and mark the run in progress
	[ ! -f /etc/bbfdm/dmmap/dmmap_diagnostics ] && touch /etc/bbfdm/dmmap/dmmap_diagnostics
	# Drop anything a previous run staged but never committed
	$UCI_REVERT_BBF_DMMAP dmmap_diagnostics
	$UCI_SET_BBF_DMMAP dmmap_diagnostics.download='download'
	$UCI_SET_BBF_DMMAP dmmap_diagnostics.download.Status="running"

	# Cleanup per-connection results
	delete_bbf_dmmap_sections dmmap_diagnostics DownloadPerConnection

	$UCI_COMMIT_BBF_DMMAP

	if [ -z "${url}" ]; then
		download_error "Error_InitConnectionFailed" "${proto}" "download_launch:${LINENO} empty url"
		return
	fi

	if [ "${url:0:7}" = "http://" ]; then
		install_speedtest_modules "${proto}"
		execute_http_download_test "${url}" "${iface}" "${dscp}" "${eth_prio}" "${ip_proto}" \
			"${num_of_con}" "${enable_per_con}" "${proto}" "${tbt_duration}"
		collect_debug_logs "${input}"
		remove_speedtest_modules
	elif [ "${url:0:6}" = "ftp://" ]; then
		execute_legacy_download_test "${url}" "${iface}" "${dscp}" "${eth_prio}" "${ip_proto}" \
			"${num_of_con}" "${enable_per_con}" "${proto}" "${tbt_duration}"
	else
		download_error "Error_Other" "${proto}" "download_launch:${LINENO} unsupported url scheme"
	fi

	return
}

if [ -n "$1" ]; then
	download_launch "$1"
else
	download_error "Error_Internal" "" "download_launch:${LINENO} no input provided"
fi
