#!/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
#    2. speed_test module does not work with IPv6, so fallback to curl for
#       testing with v6 network
#    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
TEST_RESULT_PATH_TR143=/proc/tc3162/speedtest_result_tr143
TEST_RESULT_PATH_SINGLE=/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"

# Variables shared between functions. Everything else is local to its function.
#
# The result of a run, set by whichever backend performed it and read by
# publish_result, which is the only writer of the two output sinks.
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 and read by its callers.
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=""
#
# The aggregate result file in use and the suffix its byte fields carry. Both are
# settled by select_result_file once the module is up; until then they name the file
# every module version has, so a failure reported before that still logs sensibly.
TEST_RESULT_PATH="${TEST_RESULT_PATH_TR143}"
BYTE_SUFFIX="(Byte)"
#
# One timestamp, set by read_time_field and read by its callers.
RT_EPOCH=""
RT_ISO=""
#
# State of the run in progress, recorded by debug_log_error when a check fails.
DEVICE=""
WAN_PROTO=""
DUT_IP=""
DST_IP=""
DST_PORT=""
FILE_PATH=""
DST_PROTO=""
NETMASK=""
NEXTHOP=""
CONNS=""
TIMEOUT=""
STATE=""
EOM=""
ERR=""
SETTLE=""

# Pick the file to read the aggregate result from.
#
# Module 2.0.0.0 freezes speedtest_result_tr143 when the session ends and never
# revises it, so it can describe far less than the transfer moved: a 900 MB file came
# back as 644 MB received, with TotalByteReceived below the size of the file itself.
# speedtest_single_result holds that module's finalised figures, timestamps included.
#
# On 1.7.16.004 that same file has its timestamps shifted by a field, so BOMTime
# carries ROMTime's nanoseconds and reads as 1988. tr143 is used there, where the
# shortfall is about a percent.
#
# The two files name their byte fields differently, hence the suffix.
select_result_file() {
	if grep -q "^version:2\.0\.0\.0" /proc/tc3162/speed_test 2>/dev/null; then
		TEST_RESULT_PATH="${TEST_RESULT_PATH_SINGLE}"
		BYTE_SUFFIX=""
	else
		TEST_RESULT_PATH="${TEST_RESULT_PATH_TR143}"
		BYTE_SUFFIX="(Byte)"
	fi
}

# 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 "device:       [${DEVICE}]"
		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 "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${BYTE_SUFFIX}")

	[ -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"

	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 to 31
# describe FTP conditions and only ever come back on the FTP path.
#
# Note curl 27 is an out of memory failure, not a size mismatch: the size
# conditions are a partial transfer (18), a rejected range (33) and a file
# larger than the limit (63).
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, and map its answer to
# a diagnostic status. $7 is the reason to fall back on when the URL turns out to
# be serving fine, i.e. when the failure was on the module's side.
#
# This deliberately issues a HEAD request. A GET would have to transfer the whole
# test file, so on any realistically sized file it always hit the timeout and
# every failure fell 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, and treat data
	# starting to arrive - whether it completes or the timeout cuts it off - as
	# the URL being fine, so a rejected HEAD is not reported as a server error.
	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}"
			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}"
			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}"
			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, possible formats:
			# [xx::xx]:22
			# [xx::xx]
			# 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
			# FQDN
			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
}

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 is the same as dividing by a thousand and truncating, which is
# the rounding wanted, and it keeps the value away from shell arithmetic, where a
# field carrying a leading zero would be read as octal and rejected.
#
# The input is padded first so that the three digits are there to drop, and the
# result is taken from a padded copy at the six digit width the time format needs.
get_usec_from_nsec_floor() {
	local nsec="00${1}" usec

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

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

# Read the time value of one "<sec>:<nsec>" field out of a result file and convert
# it, setting RT_EPOCH to the "<sec>.<usec>" epoch float and RT_ISO to the time
# format the data model reports.
#
# $1: result file, $2: field prefix (e.g. "BOMTime" or "3_BOMTime")
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}"
}

# 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

	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" ]; then
				# A single real transfer, reported once per configured connection
				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

			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" ]; 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

				$UCI_ADD_BBF_DMMAP dmmap_diagnostics DownloadPerConnection
				$UCI_SET_BBF_DMMAP dmmap_diagnostics.@DownloadPerConnection[${count}].ROMTime="${PC_ROM_TIME}"
				$UCI_SET_BBF_DMMAP dmmap_diagnostics.@DownloadPerConnection[${count}].BOMTime="${PC_BOM_TIME}"
				$UCI_SET_BBF_DMMAP dmmap_diagnostics.@DownloadPerConnection[${count}].EOMTime="${PC_EOM_TIME}"
				$UCI_SET_BBF_DMMAP dmmap_diagnostics.@DownloadPerConnection[${count}].TestBytesReceived="${PC_TEST_BYTES_RECEIVED}"
				$UCI_SET_BBF_DMMAP dmmap_diagnostics.@DownloadPerConnection[${count}].TotalBytesReceived="${PC_TOTAL_BYTES_RECEIVED}"
				$UCI_SET_BBF_DMMAP dmmap_diagnostics.@DownloadPerConnection[${count}].TotalBytesSent="${PC_TOTAL_BYTES_SENT}"
				$UCI_SET_BBF_DMMAP dmmap_diagnostics.@DownloadPerConnection[${count}].TCPOpenRequestTime="${PC_TCP_OPEN_REQUEST_TIME}"
				$UCI_SET_BBF_DMMAP dmmap_diagnostics.@DownloadPerConnection[${count}].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

	DEVICE=$(ifstatus "${iface}" | jsonfilter -e @.l3_device)
	if [ -z "${DEVICE}" ]; then
		download_error "Error_Other" "${proto}"
		return
	fi

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

	ipalloc_type="ip"
	if [ "${WAN_PROTO}" = "pppoe" ]; then
		ipalloc_type="ppp"
	fi

	get_res=$(get_dstip_port_path "${url}" "${ip_proto}")
	if [ "${get_res:0:6}" = "Error_" ]; then
		download_error "${get_res}" "${proto}"
		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}"
		return
	fi

	DUT_IP=$(get_ip_addr_used "${url}" "${DST_PROTO}" "${iface}")
	if [ -z "${DUT_IP}" ]; then
		download_error "Error_NoRouteToHost" "${proto}"
		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}"
			return
		fi
	elif [ "${DST_PROTO}" = "IPv6" ]; then
		DST_IP=$(extend_v6address "${DST_IP}")
	else
		download_error "Error_Other" "${proto}"
		return
	fi

	select_result_file

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

	# Airoha treats number of connections as index, it starts with 1
	# +1 since speed_test opens 1 less connection than what is configured
	CONNS=$(( num_of_con + 1 ))

	# A time based test maps onto the module's own duration knob, bounded by the
	# ceiling this backend supports so the module still finishes within the poll
	# loop. The data model refuses anything past that ceiling, so the clamp only
	# holds if the backend is driven directly.
	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
	echo "${DST_PROTO}" > /proc/tc3162/speed_test
	echo "${ipalloc_type}" > /proc/tc3162/speed_test
	echo "wandev=${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

	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
		[ -n "${NEXTHOP}" ] && echo "gateway=${NEXTHOP}" > /proc/tc3162/speed_test
		echo "host=${DST_IP}" > /proc/tc3162/speed_test
	else
		ping -6 -I "${DEVICE}" -c 2 "${DST_IP}" >/dev/null
		echo "host=[${DST_IP}]" > /proc/tc3162/speed_test
	fi
	echo "action=GET ${FILE_PATH} ${DST_IP} ${DST_PORT}" > /proc/tc3162/speed_test

	# The duration bounds the transfer, plus ten seconds for the module to finalise
	# the run and for the next sample to observe it. The budget is deliberately not
	# a multiple of the sampling interval, hence the -le below rather than -eq.
	TIMEOUT=$((test_duration + 10))
	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

		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}"
		return
	fi

	# The module publishes EOMTime before it has finished finalising the run, 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}"
		return
	fi

	if [ ! -f "${TEST_RESULT_PATH}" ]; then
		download_error "Error_Other" "${proto}"
		return
	fi

	# Read the aggregate result
	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${BYTE_SUFFIX}")
	RES_TOTAL_BYTES_RECEIVED=$(read_byte_field "${TEST_RESULT_PATH}" "TotalByteReceived${BYTE_SUFFIX}")
	RES_TOTAL_BYTES_SENT=$(read_byte_field "${TEST_RESULT_PATH}" "TotalByteSend${BYTE_SUFFIX}")
	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}"

	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}"

	# Fail if url is empty
	[ -z "${url}" ] && {
		download_error "Error_InitConnectionFailed" "${proto}"
		return
	}

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

	if [ -n "${iface}" ]; then
		DEVICE=$(ifstatus "${iface}" | jsonfilter -e '@.l3_device')

		# If no device was found, return error
		[ -z "${DEVICE}" ] && {
			download_error "Error_NoRouteToHost" "${proto}"
			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}"
		return
	fi

	# Assign default value
	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)

	# A time based test is bounded by its duration instead of by the file size, and
	# never by longer than the ceiling a size based test would have had
	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 how a time based test is supposed to end, the
	# transfer is deliberately cut short once the duration elapses
	[ "${tbt_duration}" -gt 0 ] && [ "$exitcode" = "28" ] && exitcode=0
	[ "$exitcode" != "0" ] && {
		download_error "$(curl_exit_to_status "${exitcode}")" "${proto}"
		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}"

	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, i.e. run until the file has
	# been fetched, which is the historical behaviour
	[ -z "${tbt_duration}" ] && tbt_duration=0

	[ -z "${num_of_con}" ] && 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
	$UCI_SET_BBF_DMMAP dmmap_diagnostics.download='download'
	$UCI_SET_BBF_DMMAP dmmap_diagnostics.download.Status="running"

	# Cleanup per-connection results
	sections="$(${UCI_SHOW_BBF_DMMAP} dmmap_diagnostics |awk -F'=' '/=DownloadPerConnection$/ {print $1}' |sort -r)"
	for sec in ${sections}; do
		[ -z "${sec}" ] && continue
		$UCI_DELETE_BBF_DMMAP "${sec}"
	done

	$UCI_COMMIT_BBF_DMMAP

	# Fail if url is empty
	if [ -z "${url}" ]; then
		download_error "Error_InitConnectionFailed" "${proto}"
		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}"
	fi

	return
}

if [ -n "$1" ]; then
	download_launch "$1"
else
	download_error "Error_Internal"
fi
