update 2025-05-14 04:25:50

This commit is contained in:
kenzok8 2025-05-14 04:25:50 +08:00
parent 310e9dd234
commit 445f0b7a67
11 changed files with 257 additions and 192 deletions

View File

@ -9,8 +9,8 @@ include $(TOPDIR)/rules.mk
PKG_NAME:=luci-app-netspeedtest
PKG_VERSION:=5.0.1
PKG_RELEASE:=20250512
PKG_VERSION:=5.0.2
PKG_RELEASE:=20250513
LUCI_TITLE:=LuCI Support for netspeedtest
LUCI_DEPENDS:=+speedtest-cli +homebox +iperf3-ssl

View File

@ -30,7 +30,7 @@ return view.extend({
var iframe = E('iframe', {
src: window.location.origin + ':' + state.port,
style: 'width: 100%; min-height: 80vh; border: none; border-radius: 3px;'
style: 'border:none;width: 100%; min-height: 80vh; border: none; border-radius: 3px;overflow:hidden !important;'
});
function checkProcess() {
@ -42,7 +42,7 @@ return view.extend({
function controlService(action) {
var command = action === 'start'
? 'nohup /usr/bin/homebox > /tmp/homebox.log 2>&1 &'
? 'nohup /usr/bin/homebox > /tmp/netspeedtest.log 2>&1 &'
: '/usr/bin/killall homebox';
return fs.exec('/bin/sh', ['-c', command]);
}

View File

@ -12,7 +12,7 @@ var state = {
port: null
};
const logPath = '/var/log/iperf3.log';
const logPath = '/tmp/netspeedtest.log';
function checkProcess() {
return fs.exec('/bin/pidof', ['iperf3']).then(res => ({
@ -172,7 +172,7 @@ const statusSection = E('div', { 'class': 'cbi-section' }, [
return E('div', [
statusSection,
E('div', { 'class': 'cbi-section' }, [
E('h3', {}, _('Iperf3 Run Log')),
E('h3', {}, _('Run Log')),
logTextarea,
E('div', { 'style': 'text-align: right; font-size: small; margin-top: 5px;' },
_('Refresh every 5 seconds.')

View File

@ -1,3 +1,4 @@
/* Copyright (C) 2021-2025 sirpdboy herboy2008@gmail.com https://github.com/sirpdboy/luci-app-netspeedtest */
'use strict';
'require dom';
'require fs';
@ -11,76 +12,87 @@ var logTextarea;
var log_path;
uci.load('netspeedtest').then(function() {
log_path = '/var/log/netspeedtest.log';
log_path = '/tmp/netspeedtest.log';
});
function pollLog() {
return Promise.all([
fs.read_direct(log_path, 'text').then(function (res) {
return res.trim().split(/\n/).join('\n').replace(/\u001b\[33mWARN\u001b\[0m/g, '').replace(/\u001b\[36mINFO\u001b\[0m/g, '').replace(/\u001b\[31mERRO\u001b\[0m/g, '');
}),
]).then(function (data) {
logTextarea.value = data[0] || _('No log data.');
return Promise.all([
fs.read_direct(log_path, 'text').then(function(res) {
return res.trim()
.split(/\n/).join('\n')
.replace(/\u001b\[33mWARN\u001b\[0m/g, '')
.replace(/\u001b\[36mINFO\u001b\[0m/g, '')
.replace(/\u001b\[31mERRO\u001b\[0m/g, '');
}),
]).then(function(data) {
logTextarea.value = data[0] || _('No log data.');
if (!userScrolled) {
logTextarea.scrollTop = logTextarea.scrollHeight;
} else {
logTextarea.scrollTop = scrollPosition;
}
});
};
if (!userScrolled) {
logTextarea.scrollTop = logTextarea.scrollHeight;
} else {
logTextarea.scrollTop = scrollPosition;
}
});
}
return view.extend({
handleCleanLogs: function () {
return fs.write(log_path, '')
.catch(function (e) { ui.addNotification(null, E('p', e.message)) });
},
handleCleanLogs: function() {
return fs.write(log_path, '')
.catch(function(e) {
ui.addNotification(null, E('p', e.message))
});
},
render: function () {
logTextarea = E('textarea', {
'class': 'cbi-input-textarea',
'wrap': 'off',
'readonly': 'readonly',
'style': 'width: calc(100% - 20px);height: 535px;margin: 10px;overflow-y: scroll;',
});
render: function() {
logTextarea = E('textarea', {
'class': 'cbi-input-textarea',
'wrap': 'off',
'readonly': 'readonly',
'style': 'width: calc(100% - 20px); height: 535px; margin: 10px; overflow-y: scroll;'
});
logTextarea.addEventListener('scroll', function () {
userScrolled = true;
scrollPosition = logTextarea.scrollTop;
});
logTextarea.addEventListener('scroll', function() {
userScrolled = true;
scrollPosition = logTextarea.scrollTop;
});
var log_textarea_wrapper = E('div', { 'id': 'log_textarea' }, logTextarea);
var log_textarea_wrapper = E('div', { 'id': 'log_textarea' }, logTextarea);
setTimeout(function () {
poll.add(pollLog);
}, 100);
setTimeout(function() {
poll.add(pollLog);
}, 100);
var clear_logs_button = E('input', { 'class': 'btn cbi-button-action', 'type': 'button', 'style': 'margin-left: 10px; margin-top: 10px;', 'value': _('Clear logs') });
clear_logs_button.addEventListener('click', this.handleCleanLogs.bind(this));
var clear_logs_button = E('input', {
'class': 'btn cbi-button-action',
'type': 'button',
'style': 'margin-left: 20px; margin-top: 10px;',
'value': _('Clear logs')
});
clear_logs_button.addEventListener('click', this.handleCleanLogs.bind(this));
return E([
E('div', { 'class': 'cbi-map' }, [
E('div', { 'class': 'cbi-section' }, [
clear_logs_button,
log_textarea_wrapper,
E('div', { 'style': 'text-align:right' },
E('small', {}, _('Refresh every %s seconds.').format(L.env.pollinterval)),
E('div', { 'class': 'cbi-section-actions cbi-section-actions-right' })
]),
E('div', { 'style': 'text-align: right; font-style: italic;' }, [
return E('div', { 'class': 'cbi-map' }, [
E('div', { 'class': 'cbi-section' }, [
clear_logs_button,
log_textarea_wrapper,
E('div', { 'style': 'text-align: right' }, [
E('small', {}, _('Refresh every %s seconds.').format(L.env.pollinterval))
]),
E('div', { 'class': 'cbi-section-actions cbi-section-actions-right' })
]),
E('div', { 'style': 'text-align: right; font-style: italic; margin-top: 10px;' }, [
E('span', {}, [
_('© github '),
E('a', {
'href': 'https://github.com/sirpdboy',
E('a', {
'href': 'https://github.com/sirpdboy',
'target': '_blank',
'style': 'text-decoration: none;'
}, 'by sirpdboy')
])
])
]);
}
])
])
]);
}
// handleSaveApply: null,
// handleSave: null,
// handleSave: null,
// handleReset: null
});
});

View File

@ -1,3 +1,4 @@
/* Copyright (C) 2021-2025 sirpdboy herboy2008@gmail.com https://github.com/sirpdboy/luci-app-netspeedtest */
'use strict';
'require view';
'require uci';
@ -22,7 +23,7 @@ return view.extend({
s.render = function (section_id) {
return E('iframe', {
src: '//openspeedtest.com/speedtest',
style: 'border:none;width:100%;height:100%;min-height:360px;border:none;overflow:hidden !important;'
style: 'width:100%;height:100%;min-height:360px;border:none;overflow:hidden !important;'
});
};

View File

@ -1,3 +1,4 @@
/* Copyright (C) 2021-2025 sirpdboy herboy2008@gmail.com https://github.com/sirpdboy/luci-app-netspeedtest */
'use strict';
'require view';
'require poll';
@ -17,7 +18,7 @@ return view.extend({
// handleSaveApply: null,
// handleSave: null,
// handleReset: null,
load: function () {
load() {
return Promise.all([
L.resolveDefault(fs.stat(SpeedtestCli), {}),
L.resolveDefault(fs.read(ResultFile), null),
@ -26,76 +27,94 @@ return view.extend({
]);
},
poll_status: function (nodes, res) {
var has_ookla = res[0].path,
poll_status(nodes, res) {
var has_ookla = res[0].path,
result_content = res[1] ? res[1].trim().split("\n") : [];
var ookla_stat = nodes.querySelector('#ookla_status'),
var ookla_stat = nodes.querySelector('#ookla_status'),
result_stat = nodes.querySelector('#speedtest_result');
// Update status indicators
ookla_stat.style.color = has_ookla ? 'green' : 'red';
dom.content(ookla_stat, [_(has_ookla ? 'Installed' : 'Not Installed')]);
// Update result display
if (result_content.length) {
if (result_content[0] == 'Testing') {
result_stat.innerHTML = "<span style='color:green;font-weight:bold'>" +
"<img src='/luci-static/resources/icons/loading.gif' height='17' style='vertical-align:middle'/> " +
_('Testing in progress...') +
"</span>";
} else if (result_content[0].match(/https?:\S+/)) {
result_stat.innerHTML = "<div style='max-width:500px'><a href='" +
result_content[0] + "' target='_blank'><img src='" +
result_content[0] + '.png' + "' style='max-width:100%'></a></div>";
} else if (result_content[0] == 'Test failed') {
result_stat.innerHTML = "<span style='color:red;font-weight:bold'>" +
_('Test failed.') + "</span>";
}
} else {
result_stat.innerHTML = "<span style='color:gray'>" +
_('No test results yet.') + "</span>";
// 获取版本号(新增部分)
var version_info = '';
if (has_ookla) {
fs.exec_direct('/usr/bin/speedtest', ['--version'])
.then(function(res) {
if (res.stdout) {
var version_match = res.stdout.match(/Speedtest (\d+\.\d+\.\d+)/);
if (version_match) {
version_info = ' ver:' + version_match[1];
}
}
// 更新状态显示(包含版本号)
ookla_stat.style.color = 'green';
dom.content(ookla_stat, [_(has_ookla ? 'Installed' + version_info : 'Not Installed')]);
})
.catch(function() {
// 如果获取版本失败,仍显示基本状态
ookla_stat.style.color = has_ookla ? 'green' : 'red';
dom.content(ookla_stat, [_(has_ookla ? 'Installed' : 'Not Installed')]);
});
} else {
// 未安装时的显示保持不变
ookla_stat.style.color = 'red';
dom.content(ookla_stat, [_('Not Installed')]);
}
if (result_content.length) {
if (result_content[0] == 'Testing') {
result_stat.innerHTML = "<span style='color:green;font-weight:bold'>" +
"<img src='/luci-static/resources/icons/loading.gif' height='17' style='vertical-align:middle ;margin-left:20px'/> " +
_('SpeedTesting in progress...') +
"</span>";
} else if (result_content[0].match(/https?:\S+/)) {
result_stat.innerHTML = "<div style='max-width:500px'><a href='" +
result_content[0] + "' target='_blank'><img src='" +
result_content[0] + '.png' + "' style='max-width:100%;margin-left:20px'></a></div>";
} else if (result_content[0] == 'Test failed') {
result_stat.innerHTML = "<span style='color:red;font-weight:bold;margin-left:20px'>" +
_('Test failed.') + "</span>";
}
},
} else {
result_stat.innerHTML = "<span style='color:gray;margin-left:20px'>" +
_('No test results yet.') + "</span>";
}
},
render: function (res) {
render(res) {
var has_ookla = res[0].path,
result_content = res[1] ? res[1].trim().split("\n") : [],
result_mtime = res[2] ? res[2].mtime * 1000 : 0,
date = new Date();
var m, s, o;
m = new form.Map('netspeedtest', _('WAN Ookla SpeedTest'));
m = new form.Map('netspeedtest', _('Wan Ookla SpeedTest'));
// Result display section
s = m.section(form.TypedSection, '_result');
s.anonymous = true;
s.render = function () {
var content;
if (result_content.length) {
if (result_content[0] == 'Testing') {
content = E('span', { style: 'color:green;font-weight:bold' }, [
E('img', { src: '/luci-static/resources/icons/loading.gif', height: '20' }),
' ', _('Testing in progress...')
]);
} else if (result_content[0].match(/https?:\S+/)) {
content = E('div', { style: 'max-width:500px' }, [
E('a', { href: result_content[0], target: '_blank' }, [
E('img', { src: result_content[0] + '.png', style: 'max-width:100%' })
])
]);
} else {
content = E('span', { style: 'color:red;font-weight:bold' },
_('Test failed.'));
}
} else {
content = E('span', { style: 'color:gray' },
_('No test results yet.'));
}
return E('div', { id: 'speedtest_result' }, content);
};
s.render = function (section_id) {
if (result_content.length) {
if (result_content[0] == 'Testing') {
return E('div', { 'id': 'speedtest_result' }, [ E('span', { 'style': 'color:yellow;font-weight:bold' }, [
E('img', { 'src': L.resource(['icons/loading.gif']), 'height': '20', 'style': 'vertical-align:middle' }, []),
_('Testing in progress...')
]) ])
};
if (result_content[0].match(/https?:\S+/)) {
return E('div', { 'id': 'speedtest_result' }, [ E('div', { 'style': 'max-width:500px' }, [
E('a', { 'href': result_content[0], 'target': '_blank' }, [
E('img', { 'src': result_content[0] + '.png', 'style': 'max-width:100%;max-height:100%;vertical-align:middle' }, [])
]) ]) ])
};
if (result_content[0] == 'Test failed') {
return E('div', { 'id': 'speedtest_result' }, [ E('span', { 'style': 'color:red;font-weight:bold' }, [ _('Test failed.') ]) ])
}
} else {
return E('div', { 'id': 'speedtest_result' }, [ E('span', { 'style': 'color:red;font-weight:bold;display:none' }, [ _('No result.') ]) ])
}
};
// Configuration section
s = m.section(form.NamedSection, 'config', 'netspeedtest');
s.anonymous = true;
// Start test button
o = s.option(form.Button, '_start', _('Start Ookla SpeedTest'));

View File

@ -17,7 +17,7 @@ msgid "NetSpeedtest"
msgstr "网速测试"
msgid "A tool for testing network speed in multiple aspects"
msgstr "一个用于从多方面测试本地和宽带网络速度的工具"
msgstr "一个用于从多方面测试本地和宽带网络速度的测速工具"
msgid "Lan Speedtest Iperf3"
msgstr "本地iperf3吞吐测速"
@ -26,7 +26,7 @@ msgid "Lan Speedtest Homebox"
msgstr "本地homebox网页测速"
msgid "Wan Ookla SpeedTest"
msgstr "宽带Ookla网速测"
msgstr "宽带Ookla网速测"
msgid "Wan OpenSpeedTest"
msgstr "宽带OpenSpeedTest测速"
@ -79,8 +79,8 @@ msgstr "开启日志显示"
msgid "Download iperf3 client"
msgstr "下载iperf3客户端"
msgid "Iperf3 Run Log"
msgstr "Iperf3运行日志"
msgid "Run Log"
msgstr "运行日志"
msgid "Refresh Log"
msgstr "刷新日志"
@ -109,8 +109,8 @@ msgstr "开始宽带测速"
msgid "Click to execute"
msgstr "点击执行"
msgid "Testing in progress..."
msgstr "测试正在进行中..."
msgid "SpeedTesting in progress..."
msgstr "测速中,请稍候..."
msgid "Test failed."
msgstr "测速失败"

View File

@ -6,67 +6,72 @@
START=99
USE_PROCD=1
EXTRA_COMMANDS="download_ookla ookla_verify"
EXTRA_COMMANDS="download verify"
EXTRA_HELP=\
" download_ookla Download Ookla Speedtest-CLI
ookla_verify Verify Ookla Speedtest-CLI integrity"
" download Download Ookla Speedtest-CLI
verify Verify Ookla Speedtest-CLI integrity"
#
OOKLA_SPEEDTEST='/usr/bin/speedtest'
# uci
CONFIG='netspeedtest'
NAMEDDSECTION='config'
log='/tmp/netspeedtest.log'
get_config() {
config_load netspeedtest
config_get "proxy_enabled" "config" "proxy_enabled" "0"
download() {
. /etc/openwrt_release
local url arch=$1
local proxy=$2
[ -z "$arch" ] && arch=$DISTRIB_ARCH
[ -z "$proxy" ] && proxy='0'
[ "$proxy" == "1" ] && export ALL_PROXY="http://192.168.10.8:1080"
UA='Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/127.0.0.0 Safari/537.36'
url=$( \
curl --connect-timeout 10 --retry 3 -sSL 'https://www.speedtest.net/apps/cli' \
--user-agent "$UA" \
| grep "Download for Linux" \
| sed 's|<|\n<|g' \
| sed -n '/Download for Linux/,/<\/div>/p' \
| sed -En "s|.*<a href=\"([^\"]+)\">${arch}|\1|p" \
)
[ -z "$url" ] && { echo "Failed to get download URL" >> "$log"; return 1; }
# Backup existing file
[ -f "$OOKLA_SPEEDTEST" ] && mv "$OOKLA_SPEEDTEST" "${OOKLA_SPEEDTEST}.bak"
if curl -sSL "$url" --user-agent "$UA" | tar -xvz -C /tmp; then
mkdir -p "${OOKLA_SPEEDTEST%/*}" 2>/dev/null
cp -f /tmp/speedtest "$OOKLA_SPEEDTEST"
rm -rf /tmp/speedtest
chmod 755 "$OOKLA_SPEEDTEST"
if verify; then
echo "Download successful: $($OOKLA_SPEEDTEST --version | awk '/Speedtest/{print $1" ver:"$4}')" >> "$log"
else
echo "Download failed: binary verification failed" >> "$log"
[ -f "${OOKLA_SPEEDTEST}.bak" ] && mv "${OOKLA_SPEEDTEST}.bak" "$OOKLA_SPEEDTEST"
return 1
fi
else
echo "Download failed: could not retrieve package" >> "$log"
[ -f "${OOKLA_SPEEDTEST}.bak" ] && mv "${OOKLA_SPEEDTEST}.bak" "$OOKLA_SPEEDTEST"
return 1
fi
unset ALL_PROXY
}
download_ookla() {
local url arch=$1
[ -z "$arch" ] && return 1
[ "$(uci -q get $CONFIG.$NAMEDDSECTION.proxy_enabled)" == "1" ] && \
export ALL_PROXY=$(uci -q get $CONFIG.$NAMEDDSECTION.proxy_protocol)://$(uci -q get $CONFIG.$NAMEDDSECTION.proxy_server)
UA='Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/127.0.0.0 Safari/537.36'
url=$( \
curl --connect-timeout 10 --retry 3 -sSL 'https://www.speedtest.net/apps/cli' \
--user-agent "$UA" \
| grep "Download for Linux" \
| sed 's|<|\n<|g' \
| sed -n '/Download for Linux/,/<\/div>/p' \
| sed -En "s|.*<a href=\"([^\"]+)\">${arch}|\1|p" \
)
[ -z "$url" ] && return 1
[ -n "$url" ] && curl -sSL $url --user-agent "$UA" | tar -xvz -C /tmp
mkdir -p ${OOKLA_SPEEDTEST%/*} 2>/dev/null
cp -f /tmp/speedtest $OOKLA_SPEEDTEST
chmod 755 $OOKLA_SPEEDTEST
ookla_verify || rm -f $OOKLA_SPEEDTEST
unset ALL_PROXY
verify() {
[ -x "$OOKLA_SPEEDTEST" ] && "$OOKLA_SPEEDTEST" --version >/dev/null 2>&1
}
ookla_verify() {
if [ -x "$OOKLA_SPEEDTEST" ]; then
return 0
else
return 1
fi
start() {
if ! verify; then
download || {
echo "Critical: Failed to install Speedtest CLI" >> "$log"
exit 1
}
fi
}
start_service() {
ookla_verify
}
service_triggers() {
procd_add_reload_trigger "netspeedtest"
}
reload_service() {
stop
sleep 1
start
restart() {
start
}

View File

@ -3,35 +3,54 @@ mkdir -p /etc/speedtest
export HOME='/etc/speedtest'
SPEEDTEST_CLI='/usr/bin/speedtest'
SPEEDTEST_RESULT='/tmp/speedtest_result'
LOG='/tmp/netspeedtest.log'
# 记录开始时间
echo "=== Speedtest started at $(date) ===" >> "$LOG"
[ -n "$(pgrep -f "$SPEEDTEST_CLI")" ] && exit 1
echo "Start Testing" > "$SPEEDTEST_RESULT"
LOCAL_IP=$(curl -s -4 --connect-timeout 3 http://ip.3322.net)
echo "Local IP: $LOCAL_IP" >> "$LOG"
BAIDU_SK="LHHGlmhcb4ENvIXpR9QQ2tBYa6ooUowX hYCENCEx1nXO0Nt46ldexfG9oI49xBGh 0kKZnWWhXEPfzIkklmzAa3dZ"
if [ -n "$LOCAL_IP" ]; then
for SK in $BAIDU_SK
do
INFO=$(curl -sk --connect-timeout 3 "https://api.map.baidu.com/location/ip?ip="$LOCAL_IP"&coor=bd09ll&ak=$SK")
if [ "$(echo $INFO | jsonfilter -e "@['status']")" = 0 ]; then
status=0
break
fi
done
if [ "$status" = 0 ]; then
lon=$(echo $INFO | jsonfilter -e "@['content']['point']['x']")
lat=$(echo $INFO | jsonfilter -e "@['content']['point']['y']")
server_id=$(curl -sk --connect-timeout 3 "https://www.speedtest.net/api/ios-config.php?lon=$lon&lat=$lat" | grep "server url" | head -n1 | sed 's/.*id="//;s/".*//')
[ -n "$server_id" ] && ARG="-s $server_id"
fi
for SK in $BAIDU_SK
do
INFO=$(curl -sk --connect-timeout 3 "https://api.map.baidu.com/location/ip?ip="$LOCAL_IP"&coor=bd09ll&ak=$SK")
if [ "$(echo $INFO | jsonfilter -e "@['status']")" = 0 ]; then
status=0
break
fi
done
if [ "$status" = 0 ]; then
lon=$(echo $INFO | jsonfilter -e "@['content']['point']['x']")
lat=$(echo $INFO | jsonfilter -e "@['content']['point']['y']")
server_id=$(curl -sk --connect-timeout 3 "https://www.speedtest.net/api/ios-config.php?lon=$lon&lat=$lat" | grep "server url" | head -n1 | sed 's/.*id="//;s/".*//')
[ -n "$server_id" ] && ARG="-s $server_id"
echo "Selected server ID: $server_id" >> "$LOG"
fi
fi
echo "Testing" > "$SPEEDTEST_RESULT"
RUNTEST=$($SPEEDTEST_CLI --accept-gdpr --accept-license --progress=no $ARG 2>&1)
if [ $(echo $RUNTEST | grep -c "No servers defined") -ge 1 ] || [ $(echo $RUNTEST | grep -c "error") -ge 1 ]; then
RUNTEST=$($SPEEDTEST_CLI --accept-gdpr --accept-license --progress=no 2>&1)
echo "Fallback to default server selection" >> "$LOG"
RUNTEST=$($SPEEDTEST_CLI --accept-gdpr --accept-license --progress=no 2>&1)
fi
# 将完整测试结果记录到日志
echo "$RUNTEST" >> "$LOG"
RESULT=$(echo "$RUNTEST" | grep "Result URL" | awk '{print $NF}')
[ -n "$RESULT" ] && echo "$RESULT" > "$SPEEDTEST_RESULT" || echo "Test failed" > "$SPEEDTEST_RESULT"
if [ -n "$RESULT" ]; then
echo "$RESULT" > "$SPEEDTEST_RESULT"
else
echo "Test failed" > "$SPEEDTEST_RESULT"
echo "Test failed" >> "$LOG"
fi
# 记录结束时间
echo "=== Speedtest completed at $(date) ===" >> "$LOG"
echo "" >> "$LOG" # 添加空行分隔不同测试记录

View File

@ -41,5 +41,13 @@
"type": "view",
"path": "netspeedtest/openspeedtest"
}
},
"admin/network/netspeedtest/logs": {
"title": "Log",
"order": 6,
"action": {
"type": "view",
"path": "netspeedtest/logs"
}
}
}

View File

@ -5,15 +5,16 @@
"file": {
"/etc/init.d/netspeedtest": [ "exec" ],
"/usr/lib/netspeedtest/speedtest": [ "exec" ],
"/tmp/speedtest_result": [ "read" ]
"/tmp/speedtest_result": [ "read" ],
"/tmp/netspeedtest.log": [ "read" ]
},
"ubus": {
"service": [ "list" ]
},
"uci": [ "netspeedtest" ]
"uci": [ "netspeedtest" ,"netspeedtest"]
},
"write": {
"uci": [ "netspeedtest" ]
"uci": [ "netspeedtest","netspeedtest" ]
}
}
}