🚀 Sync 2026-03-08 00:54:34

This commit is contained in:
github-actions[bot] committed 2026-03-08 00:54:34 +08:00
1 parent 97d524986e
commit 5997aee442
22 files changed
+3954 -28

No files matched your search

+20
View File
@@ -0,0 +1,20 @@
MIT License
Copyright (c) 2025 sirpdboy
Hereby grant a license free of charge to anyone who obtains copies
This software and related documentation files ("Software") are used to process
Unrestricted in software, including but not limited to rights
Use, copy, modify, merge, publish, distribute, sublicense, and/or sell
Copy of the software and allow the recipient of the software
Provide based on the following conditions:
The above copyright statement and this license statement should be included in all
Copy or substantial part of the software.
The software is provided "as is" without any form of express or warranty
Implied, including but not limited to warranties of merchantability,
Suitable for specific purposes without causing harm. In any case
The author or copyright holder shall be liable for any claims, damages, or other damages
Liability arising from the following reasons, whether in contract, tort or other litigation,
For or related to software, usage, or other transactions.
+23
View File
@@ -0,0 +1,23 @@
# SPDX-License-Identifier: GPL-3.0-only
#
# Copyright (C) 2021-2026 sirpdboy <herboy2008@gmail.com>
# https://github.com/sirpdboy/luci-app-netspeedtest
# This is free software, licensed under the Apache License, Version 2.0 .
#
include $(TOPDIR)/rules.mk
PKG_NAME:=luci-app-netspeedtest
PKG_VERSION:=5.1.2
PKG_RELEASE:=1
LUCI_TITLE:=LuCI Support for netspeedtest
LUCI_DEPENDS:=+ookla-speedtest +homebox +python3-light +python3-pkg-resources +python3-xml +python3-email \
+python3-urllib +python3-codecs +python3-openssl +jq $(if $(find_package iperf3-ssl),+iperf3-ssl,+iperf3)
LUCI_PKGARCH:=all
PKG_MAINTAINER:=sirpdboy <herboy2008@gmail.com>
include $(TOPDIR)/feeds/luci/luci.mk
# call BuildPackage - OpenWrt buildroot signature
@@ -0,0 +1,165 @@
/* Copyright (C) 2021-2026 sirpdboy herboy2008@gmail.com https://github.com/sirpdboy/luci-app-netspeedtest */
'use strict';
'require view';
'require fs';
'require ui';
'require uci';
'require form';
'require poll';
return view.extend({
render: function() {
var state = {
running: false,
port: 3300
};
var container = E('div');
var statusSection = E('div', { 'class': 'cbi-section' });
var statusIcon = E('span', { 'style': 'margin-right: 5px;' });
var statusText = E('span');
var toggleBtn = E('button', { 'class': 'btn cbi-button' });
var statusMessage = E('div', { style: 'text-align: center; padding: 2em;' }, [
E('img', {
src: 'data:image/svg+xml;base64,PHN2ZyB2ZXJzaW9uPSIxLjEiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgd2lkdGg9IjEwMjQiIGhlaWdodD0iMTAyNCIgdmlld0JveD0iMCAwIDEwMjQgMTAyNCI+PHBhdGggZmlsbD0iI2RmMDAwMCIgZD0iTTk0Mi40MjEgMjM0LjYyNGw4MC44MTEtODAuODExLTE1My4wNDUtMTUzLjA0NS04MC44MTEgODAuODExYy03OS45NTctNTEuNjI3LTE3NS4xNDctODEuNTc5LTI3Ny4zNzYtODEuNTc5LTI4Mi43NTIgMC01MTIgMjI5LjI0OC01MTIgNTEyIDAgMTAyLjIyOSAyOS45NTIgMTk3LjQxOSA4MS41NzkgMjc3LjM3NmwtODAuODExIDgwLjgxMSAxNTMuMDQ1IDE1My4wNDUgODAuODExLTgwLjgxMWM3OS45NTcgNTEuNjI3IDE3NS4xNDcgODEuNTc5IDI3Ny4zNzYgODEuNTc5IDI4Mi43NTIgMCA1MTItMjI5LjI0OCA1MTItNTEyIDAtMTAyLjIyOS0yOS45NTItMTk3LjQxOS04MS41NzktMjc3LjM3NnpNMTk0Ljk0NCA1MTJjMC0xNzUuMTA0IDE0MS45NTItMzE3LjA1NiAzMTcuMDU2LTMxNy4wNTYgNDggMCA5My40ODMgMTAuNjY3IDEzNC4yMjkgMjkuNzgxbC00MjEuNTQ3IDQyMS41NDdjLTE5LjA3Mi00MC43ODktMjkuNzM5LTg2LjI3Mi0yOS43MzktMTM0LjI3MnpNNTEyIDgyOS4wNTZjLTQ4IDAtOTMuNDgzLTEwLjY2Ny0xMzQuMjI5LTI5Ljc4MWw0MjEuNTQ3LTQyMS41NDdjMTkuMDcyIDQwLjc4OSAyOS43ODEgODYuMjcyIDI5Ljc4MSAxMzQuMjI5LTAuMDQzIDE3NS4xNDctMTQxLjk5NSAzMTcuMDk5LTMxNy4wOTkgMzE3LjA5OXoiLz48L3N2Zz4=',
style: 'width: 100px; height: 100px; margin-bottom: 1em;'
}),
E('h2', {}, _('Homebox Service Not Running')),
E('p', {}, _('Please enable the Homebox service'))
]);
var isHttps = window.location.protocol === 'https:';
var iframe;
if (!isHttps) {
iframe = E('iframe', {
src: window.location.origin + ':' + state.port,
style: 'border:none;width: 100%; min-height: 80vh; border: none; border-radius: 3px;overflow:hidden !important;'
});
}
function createHttpsButton() {
return E('div', {
style: 'text-align: center; padding: 2em;'
}, [
E('h2', {}, _('Homebox Control panel')),
E('p', {}, _('Due to browser security policies, the Homebox interface https cannot be embedded directly.')),
E('a', {
href: 'http://' + window.location.hostname + ':' + state.port,
target: '_blank',
class: 'cbi-button cbi-button-apply',
style: 'display: inline-block; margin-top: 1em; padding: 10px 20px; font-size: 16px; text-decoration: none; color: white;'
}, _('Open Web Interface'))
]);
}
async function checkProcess() {
try {
// 尝试使用pgrep
const res = await fs.exec('/usr/bin/pgrep', ['homebox']);
return {
running: res.code === 0,
pid: res.stdout.trim() || null
};
} catch (err) {
// 回退到ps方法
try {
const psRes = await fs.exec('/bin/ps', ['-w', '-C', 'homebox', '-o', 'pid=']);
const pid = psRes.stdout.trim();
return {
running: pid !== '',
pid: pid || null
};
} catch (err) {
return { running: false, pid: null };
}
}
}
function controlService(action) {
var command = action === 'start'
? 'nohup /usr/bin/homebox >> /tmp/netspeedtest.log 2>&1 &'
: '/usr/bin/killall homebox';
return fs.exec('/bin/sh', ['-c', command]);
}
function updateStatus() {
statusIcon.textContent = state.running ? '✓' : '✗';
statusIcon.style.color = state.running ? 'green' : 'red';
statusText.textContent = _('Homebox Server') + (state.running ? _('RUNNING') : _('NOT RUNNING'));
statusText.style.color = state.running ? 'green' : 'red';
statusText.style.fontWeight = 'bold';
statusText.style.fontSize = '0.92rem';
toggleBtn.textContent = state.running ? _('Stop Server') : _('Start Server');
toggleBtn.className = `btn cbi-button cbi-button-${state.running ? 'reset' : 'apply'}`;
// Update container content based on state and protocol
container.textContent = '';
if (state.running) {
if (isHttps) {
container.appendChild(createHttpsButton());
} else {
container.appendChild(iframe);
}
} else {
container.appendChild(statusMessage);
}
}
toggleBtn.addEventListener('click', ui.createHandlerFn(this, function() {
var action = state.running ? 'stop' : 'start';
return controlService(action)
.then(checkProcess)
.then(res => {
state.running = res.running;
updateStatus();
});
}));
statusSection.appendChild(E('div', { 'style': 'margin: 15px' }, [
E('h3', {}, _('Throughput speedtest Homebox')),
E('div', { 'class': 'cbi-map-descr' }, [statusIcon, statusText]),
E('div', {'class': 'cbi-value', 'style': 'margin-top: 20px'}, [
E('div', {'class': 'cbi-value-title'}, _('Homebox service control')),
E('div', {'class': 'cbi-value-field'}, toggleBtn),
E('div', { 'style': 'text-align: right; font-style: italic; margin-top: 20px;' }, [
_('© github '),
E('a', {
'href': 'https://github.com/sirpdboy',
'target': '_blank',
'style': 'text-decoration: none;'
}, 'by sirpdboy')
])
])
]));
// Initial status check
checkProcess().then(res => {
state.running = res.running;
updateStatus();
toggleBtn.disabled = false;
// Start polling
poll.add(() => {
return checkProcess().then(res => {
if (res.running !== state.running) {
state.running = res.running;
updateStatus();
toggleBtn.disabled = false;
}
});
}, 5);
poll.start();
});
return E('div', {}, [
statusSection,
container
]);
},
handleSaveApply: null,
handleSave: null,
handleReset: null
});
@@ -0,0 +1,168 @@
/* Copyright (C) 2021-2026 sirpdboy herboy2008@gmail.com https://github.com/sirpdboy/luci-app-netspeedtest */
'use strict';
'require view';
'require fs';
'require ui';
'require uci';
'require form';
'require poll';
var state = {
running: false,
port: null
};
const logPath = '/tmp/netspeedtest.log';
async function checkProcess() {
try {
const res = await fs.exec('/usr/bin/pgrep', ['iperf3']);
if (res.code === 0 && res.stdout.trim()) {
return { running: true, pid: res.stdout.trim() };
}
// 回退检查
const psRes = await fs.exec('/bin/ps', ['-w', '-C', 'iperf3', '-o', 'pid=']);
const pid = psRes.stdout.trim();
return {
running: !!pid,
pid: pid || null
};
} catch (err) {
console.error('Process check error:', err);
return { running: false, pid: null };
}
}
function controlService(action) {
const commands = {
start: `/usr/bin/iperf3 -s -D -p 5201 --logfile ${logPath} 2>&1`,
stop: '/usr/bin/killall -q iperf3'
};
return (action === 'start'
? fs.exec('/bin/sh', ['-c', `mkdir -p /tmp/netspeedtest && touch ${logPath} && chmod 644 ${logPath}`])
: Promise.resolve()
).then(() => fs.exec('/bin/sh', ['-c', commands[action]]))
.catch(err => {
console.error('Service control error:', err);
throw err;
});
}
return view.extend({
handleSaveApply: null,
handleSave: null,
handleReset: null,
load: function() {
return Promise.all([
uci.load('netspeedtest')
]);
},
render: function() {
// 创建状态元素
const statusIcon = E('span', { 'style': 'margin-right: 5px;' });
const btnGroup = E('div', { 'class': 'cbi-value-field', 'style': 'display: flex; gap: 10px;' });
const statusText = E('span');
const toggleBtn = E('button', {
'class': 'btn cbi-button',
'click': ui.createHandlerFn(this, function() {
const action = state.running ? 'stop' : 'start';
toggleBtn.disabled = true; // 禁用按钮
return controlService(action)
.then(() => checkProcess())
.then(res => {
state.running = res.running;
updateStatus();
toggleBtn.disabled = false; // 恢复按钮
})
.catch(err => {
ui.addNotification(null, E('p', _('Error: ') + err.message), 'error');
toggleBtn.disabled = false; // 出错时也要恢复按钮
});
})
});
function updateStatus() {
statusIcon.textContent = state.running ? '✓' : '✗';
statusIcon.style.color = state.running ? 'green' : 'red';
statusText.textContent = _('Iperf3 Server ') + (state.running ? _('RUNNING') : _('NOT RUNNING'));
statusText.style.color = state.running ? 'green' : 'red';
statusText.style['font-weight'] = 'bold';
statusText.style['font-size'] = '0.92rem';
toggleBtn.textContent = state.running ? _('Stop Server') : _('Start Server');
toggleBtn.className = `btn cbi-button cbi-button-${state.running ? 'reset' : 'apply'}`;
}
// 初始化状态
statusIcon.textContent = '...';
statusText.textContent = _('Checking status...');
toggleBtn.textContent = _('Loading...');
toggleBtn.disabled = true;
// 构建UI
const statusSection = E('div', { 'class': 'cbi-section' }, [
E('div', { 'style': 'margin: 15px' }, [
E('h3', {}, _('Throughput speedtest Iperf3')),
E('div', { 'class': 'cbi-map-descr' }, [statusIcon, statusText]),
E('div', {'class': 'cbi-value', 'style': 'margin-top: 20px'}, [
E('div', {'class': 'cbi-value-title'}, _('Iperf3 service control')),
E('div', {'class': 'cbi-value-field'}, toggleBtn),
E('div', {'class': 'cbi-value-title'}, _('Download iperf3 client')),
E('div', {'class': 'cbi-value-field'}, [
E('div', {
'class': 'cbi-value-field',
'style': 'display: flex;'
}, [
E('button', {
'class': 'btn cbi-button cbi-button-save',
'click': ui.createHandlerFn(this, () => window.open('https://iperf.fr/iperf-download.php', '_blank'))
}, _('Official Website')),
E('button', {
'class': 'btn cbi-button cbi-button-save',
'click': ui.createHandlerFn(this, () => window.open('https://github.com/sirpdboy/luci-app-netspeedtest/releases', '_blank'))
}, _('GitHub'))
])
])
]),
E('div', { 'style': 'text-align: right; font-style: italic; margin-top: 20px;' }, [
_('© github '),
E('a', {
'href': 'https://github.com/sirpdboy/luci-app-netspeedtest',
'target': '_blank',
'style': 'text-decoration: none;'
}, 'by sirpdboy')
])
])
]);
// 初始化状态检查
checkProcess().then(res => {
state.running = res.running;
updateStatus();
toggleBtn.disabled = false;
// 启动轮询
poll.add(() => {
return checkProcess().then(res => {
if (res.running !== state.running) {
state.running = res.running;
updateStatus();
}
});
}, 5);
});
return statusSection;
}
});
@@ -0,0 +1,98 @@
/* Copyright (C) 2021-2026 sirpdboy herboy2008@gmail.com https://github.com/sirpdboy/luci-app-netspeedtest */
'use strict';
'require dom';
'require fs';
'require poll';
'require uci';
'require view';
var scrollPosition = 0;
var userScrolled = false;
var logTextarea;
var log_path;
uci.load('netspeedtest').then(function() {
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.');
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))
});
},
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;
});
var log_textarea_wrapper = E('div', { 'id': 'log_textarea' }, logTextarea);
setTimeout(function() {
poll.add(pollLog);
}, 100);
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('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',
'target': '_blank',
'style': 'text-decoration: none;'
}, 'by sirpdboy')
])
])
]);
}
// handleSaveApply: null,
// handleSave: null,
// handleReset: null
});
@@ -0,0 +1,193 @@
/* Copyright (C) 2021-2026 sirpdboy herboy2008@gmail.com https://github.com/sirpdboy/luci-app-netspeedtest */
'use strict';
'require view';
'require uci';
'require form';
return view.extend({
load() {
return Promise.all([
uci.load('netspeedtest')
]);
},
render(res) {
let m, s, o;
m = new form.Map('netspeedtest', _('Online SpeedTest'));
let currentSpeedTestUrl = uci.get('netspeedtest', 'config', 'speedtest_site') || 'https://plugin.speedtest.cn/taste#/?t=1767018285493';
if (!currentSpeedTestUrl) {
currentSpeedTestUrl = 'https://plugin.speedtest.cn/taste#/?t=1767018285493';
uci.set('netspeedtest', 'config', 'speedtest_site', currentSpeedTestUrl);
uci.save();
}
s = m.section(form.NamedSection, 'config', 'netspeedtest');
s.anonymous = true;
// 下拉选择
o = s.option(form.DummyValue, '_speedtest_select');
o.textvalue = function() {
return '';
};
o.render = function(section_id) {
const sites = [
{url: 'https://plugin.speedtest.cn/taste#/?t=1767018285493', name: 'speedtest.cn'},
{url: 'https://static.hdslb.com/', name: 'hdslb.com'},
{url: 'https://test.ustc.edu.cn/', name: 'ustc.edu.cn'},
{url: 'https://www1.szu.edu.cn/nc/speedtest/', name: 'szu.edu.cn'},
{url: 'https://10000.gd.cn/#/speed', name: 'gd.cn'},
{url: 'https://speed.cloudflare.com/', name: 'cloudflare.com'},
{url: 'https://fast.com/', name: 'Netflix fast.com'},
{url: '//openspeedtest.com/speedtest', name: 'openspeedtest.com'}
];
const container = E('div', {
class: 'cbi-value'
});
const label = E('label', {
class: 'cbi-value-title',
for: 'speedtest-site-select'
}, _('Select speed measurement station'));
const field = E('div', {
class: 'cbi-value-field',
});
const select = E('select', {
id: 'speedtest-site-select',
class: 'cbi-input-select'
});
sites.forEach(site => {
const option = E('option', {
value: site.url,
selected: currentSpeedTestUrl === site.url ? 'selected' : null
}, site.name);
select.appendChild(option);
});
function saveConfig(url) {
try {
uci.set('netspeedtest', 'config', 'speedtest_site', url);
uci.save();
} catch (e) {
}
}
select.addEventListener('change', function() {
const iframe = document.getElementById('speedtest-iframe');
if (iframe && this.value) {
currentSpeedTestUrl = this.value;
iframe.src = this.value;
saveConfig(this.value);
}
});
field.appendChild(select);
container.appendChild(label);
container.appendChild(field);
const saveStatus = E('small', {
id: 'save-status',
style: 'display: block; margin-top: 5px; color: #28a745; opacity: 0; transition: opacity 0.3s;'
});
field.appendChild(saveStatus);
return container;
};
s = m.section(form.NamedSection, '_iframe');
s.anonymous = true;
s.render = function(section_id) {
const container = E('div', {
class: 'speedtest-wrapper mobiliframe',
style: 'width:100%;height:550px;position:relative;overflow:hidden;margin-top:20px;'
});
const header = E('div', {
style: 'display:flex;justify-content:space-between;align-items:center;padding: 0.5rem 1rem;'
}, [
E('span', {
style: 'font-weight:bold;'
}, _('NetSpeedtest')),
]);
const iframeContainer = E('div', {
style: 'height: calc(100% - 33px);'
});
const iframe = E('iframe', {
id: 'speedtest-iframe',
src: currentSpeedTestUrl,
style: 'width:100%;height:100%;border:none;background: #fff'
});
iframeContainer.appendChild(iframe);
const style = E('style', {}, `
.speedtest-wrapper {
transition: all 0.3s ease;
}
.speedtest-wrapper:hover {
box-shadow: 0 4px 20px rgba(0,0,0,0.15);
}
#save-status {
transition: opacity 0.3s ease;
}
@media (prefers-color-scheme: dark) {
.speedtest-wrapper {
background: #2d2d2d;
}
.speedtest-wrapper > div:first-child {
background: #3d3d3d;
border-color: #555;
}
.speedtest-wrapper > div:first-child span {
color: #e0e0e0;
}
}
@media (max-width: 768px) {
.speedtest-wrapper {
height: 500px;
}
.cbi-value-title {
width: 100% !important;
margin-bottom: 5px;
}
.cbi-value-field {
width: 100% !important;
}
#speedtest-site-select {
max-width: 100%;
}
}
`);
container.appendChild(header);
container.appendChild(iframeContainer);
container.appendChild(style);
return container;
};
return m.render();
},
handleSaveApply: null,
handleSave: null,
handleReset: null
});
@@ -0,0 +1,481 @@
// Copyright (C) 2019-2026 sirpdboy
// Fixed version - better Ookla result parsing
'use strict';
'require view';
'require poll';
'require dom';
'require fs';
'require uci';
'require ui';
'require form';
var Timeout = 300 * 1000;
var ResultFile = '/tmp/netspeedtest_result';
var SpeedtestScript = '/usr/bin/netspeedtest.sh';
return view.extend({
load() {
return Promise.all([
L.resolveDefault(fs.stat('/usr/bin/ookla-speedtest'), {}),
L.resolveDefault(fs.stat('/usr/bin/speedtest'), {}),
L.resolveDefault(fs.read(ResultFile), null),
L.resolveDefault(fs.stat(ResultFile), {}),
uci.load('netspeedtest')
]);
},
// 检测可用版本
detectVersions: function(res) {
var hasOokla = !!(res[0] && res[0].path);
var hasPython = !!(res[1] && res[1].path);
return {
ookla: hasOokla,
python: hasPython,
available: hasOokla || hasPython
};
},
// 检查是否真正在测试中(文件新鲜且内容为Testing)
isTesting: function(resultContent, resultMtime) {
if (!resultContent || resultContent.length === 0) return false;
if (resultContent[0].trim() !== 'Testing') return false;
// 检查文件是否在超时时间内
var fileAge = Date.now() - resultMtime;
return fileAge < Timeout;
},
// 解析结果内容
parseResult: function(content) {
if (!content || content.length === 0) {
return { type: 'none', data: null };
}
var firstLine = content[0].trim();
// 检查是否是Testing状态
if (firstLine === 'Testing') {
return { type: 'testing', data: null };
}
// 检查是否是失败状态
if (firstLine === 'Test failed') {
return { type: 'failed', data: null };
}
// 检查是否包含Result URL (Ookla格式)
var resultUrl = null;
for (var i = 0; i < content.length; i++) {
var line = content[i];
if (line.match(/Result URL:/i)) {
var urlMatch = line.match(/https?:\/\/[^\s]+/);
if (urlMatch) {
resultUrl = urlMatch[0];
break;
}
}
}
if (resultUrl) {
return { type: 'url', data: resultUrl };
}
// 检查是否是直接URL (Python格式)
if (firstLine.match(/^https?:\/\//) || firstLine.match(/\.png$/)) {
return { type: 'url', data: firstLine };
}
// 检查是否是速度数据
if (firstLine.match(/Download:/i) || firstLine.match(/Upload:/i)) {
return { type: 'speed', data: content.join('\n') };
}
// 其他情况,可能是完整的速度测试结果
// 检查是否包含Download和Upload信息
var hasDownload = false;
var hasUpload = false;
for (var j = 0; j < content.length; j++) {
if (content[j].match(/Download:/i)) hasDownload = true;
if (content[j].match(/Upload:/i)) hasUpload = true;
}
if (hasDownload && hasUpload) {
return { type: 'speed', data: content.join('\n') };
}
return { type: 'unknown', data: content.join('\n') };
},
poll_status(nodes, res) {
var result_content = res[2] ? res[2].trim().split("\n") : [];
var result_mtime = res[3] ? res[3].mtime * 1000 : 0;
var result_stat = nodes.querySelector('#speedtest_result');
var start_btn = nodes.querySelector('.cbi-button-apply');
// 解析结果
var result = this.parseResult(result_content);
var is_testing = this.isTesting(result_content, result_mtime);
// 更新测试按钮状态
if (start_btn) {
start_btn.disabled = is_testing;
}
// 更新结果状态
if (result_stat) {
if (is_testing) {
result_stat.innerHTML = "<span style='color:green;font-weight:bold;margin-left:20px'>" +
"<img src='/luci-static/resources/icons/loading.svg' height='17' style='vertical-align:middle; margin-right:5px'/> " +
_('Speed testing in progress...') +
"</span>";
return;
}
switch(result.type) {
case 'failed':
result_stat.innerHTML = "<span style='color:red;font-weight:bold;margin-left:20px'>" +
"⚠️ " + _('Test failed. Please check log for details.') +
"</span>";
break;
case 'url':
var imageUrl = result.data;
if (!imageUrl.match(/\.png$/)) {
imageUrl = imageUrl + '.png';
}
// 处理speedtest.net的特殊URL
if (imageUrl.includes('speedtest.net/result/')) {
imageUrl = imageUrl.replace('/result/c/', '/result/');
}
result_stat.innerHTML = "<div style='max-width:500px; margin-left:20px'>" +
"<a href='" + result.data + "' target='_blank'>" +
"<img src='" + result.data + "' style='max-width:100%; border-radius:4px; box-shadow:0 2px 4px rgba(0,0,0,0.1)' " +
"onerror='this.onerror=null; this.src=\"/luci-static/resources/icons/error.png\"'/>" +
"</a><br>" +
"</div>";
break;
case 'speed':
var lines = result.data.split('\n');
var download = '', upload = '', latency = '', packetLoss = '', server = '', isp = '';
lines.forEach(function(line) {
if (line.match(/Download:/i)) download = line;
else if (line.match(/Upload:/i)) upload = line;
else if (line.match(/Latency:/i) || line.match(/Idle Latency:/i)) latency = line;
else if (line.match(/Packet Loss:/i)) packetLoss = line;
else if (line.match(/Server:/i)) server = line;
else if (line.match(/ISP:/i)) isp = line;
});
var html = "<div style='margin-left:20px; padding:10px; background:#f5f5f5; border-radius:4px'>";
html += "<strong>" + _('Test Results:') + "</strong>";
if (server) {
html += "<div style='margin-top:5px; padding:5px; color:#666'>" +
server.trim() + "</div>";
}
if (download) {
html += "<div style='margin-top:10px; padding:8px; background:#e8f5e8; border-radius:4px'>" +
"<span style='font-weight:bold'>⬇️ " + _('Download:') + "</span> " +
"<span style='color:#2e7d32; font-weight:500'>" + download.replace(/Download:/i, '').trim() + "</span>" +
"</div>";
}
if (upload) {
html += "<div style='margin-top:5px; padding:8px; background:#e3f2fd; border-radius:4px'>" +
"<span style='font-weight:bold'>⬆️ " + _('Upload:') + "</span> " +
"<span style='color:#1565c0; font-weight:500'>" + upload.replace(/Upload:/i, '').trim() + "</span>" +
"</div>";
}
if (latency) {
html += "<div style='margin-top:5px; padding:5px; background:#f5f5f5; border-radius:4px'>" +
"<span style='font-weight:bold'>⏱️ " + _('Latency:') + "</span> " +
"<span style='color:#666'>" + latency.replace(/Latency:/i, '').replace(/Idle Latency:/i, '').trim() + "</span>" +
"</div>";
}
if (packetLoss) {
html += "<div style='margin-top:5px; padding:5px; color:#666'>" +
packetLoss.trim() + "</div>";
}
html += "</div>";
result_stat.innerHTML = html;
break;
case 'unknown':
// 未知格式,直接显示
result_stat.innerHTML = "<div style='margin-left:20px; padding:10px; background:#f5f5f5; border-radius:4px'>" +
"<strong>" + _('Test Results:') + "</strong><br>" +
"<pre style='margin:5px 0 0 0; font-family:inherit; white-space:pre-wrap; color:#2e7d32'>" +
escapeHTML(result.data) + "</pre>" +
"</div>";
break;
default:
// 无结果
result_stat.innerHTML = "<span style='color:gray;margin-left:20px'>" +
"<em>" + _('No test results yet. Click "Start Speed Test" to begin.') + "</em>" +
"</span>";
}
}
},
render(res) {
var self = this;
var has_ookla = !!(res[0] && res[0].path);
var has_python = !!(res[1] && res[1].path);
var result_content = res[2] ? res[2].trim().split("\n") : [];
var result_mtime = res[3] ? res[3].mtime * 1000 : 0;
var versions = this.detectVersions(res);
var result = this.parseResult(result_content);
var is_testing = this.isTesting(result_content, result_mtime);
var m, s, o;
m = new form.Map('netspeedtest', _('WAN SpeedTest'));
// 结果显示区域
s = m.section(form.TypedSection, '_result');
s.anonymous = true;
s.render = function(section_id) {
var result_id = 'speedtest_result';
if (is_testing) {
return E('div', { id: result_id, class: 'cbi-section' }, [
E('span', { style: 'color:green;font-weight:bold;margin-left:20px' }, [
E('img', {
src: '/luci-static/resources/icons/loading.svg',
height: '17',
style: 'vertical-align:middle; margin-right:5px'
}),
_('Speed testing in progress...')
])
]);
}
switch(result.type) {
case 'failed':
return E('div', { id: result_id, class: 'cbi-section' }, [
E('span', { style: 'color:red;font-weight:bold;margin-left:20px' }, [
'⚠️ ' + _('Test failed. Please check log for details.')
])
]);
case 'url':
var imageUrl = result.data;
if (!imageUrl.match(/\.png$/)) {
imageUrl = imageUrl + '.png';
}
if (imageUrl.includes('speedtest.net/result/')) {
imageUrl = imageUrl.replace('/result/c/', '/result/');
}
return E('div', { id: result_id, class: 'cbi-section' }, [
E('div', { style: 'max-width:500px; margin-left:20px' }, [
E('a', { href: result.data, target: '_blank' }, [
E('img', {
src: imageUrl,
style: 'max-width:100%; border-radius:4px; box-shadow:0 2px 4px rgba(0,0,0,0.1)'
})
]),
E('br'),
E('small', {}, [
E('a', {
href: result.data,
target: '_blank',
style: 'display:inline-block; margin-top:5px; color:#0066cc'
}, _('View detailed results'))
])
])
]);
case 'speed':
var lines = result.data.split('\n');
var download = '', upload = '', latency = '';
lines.forEach(function(line) {
if (line.match(/Download:/i)) download = line;
else if (line.match(/Upload:/i)) upload = line;
else if (line.match(/Latency:/i) || line.match(/Idle Latency:/i)) latency = line;
});
var children = [
E('strong', {}, _('Test Results:'))
];
if (download) {
children.push(
E('div', {
style: 'margin-top:10px; padding:8px; background:#e8f5e8; border-radius:4px'
}, [
E('span', { style: 'font-weight:bold' }, '⬇️ ' + _('Download:') + ' '),
E('span', { style: 'color:#2e7d32; font-weight:500' }, download.replace(/Download:/i, '').trim())
])
);
}
if (upload) {
children.push(
E('div', {
style: 'margin-top:5px; padding:8px; background:#e3f2fd; border-radius:4px'
}, [
E('span', { style: 'font-weight:bold' }, '⬆️ ' + _('Upload:') + ' '),
E('span', { style: 'color:#1565c0; font-weight:500' }, upload.replace(/Upload:/i, '').trim())
])
);
}
if (latency) {
children.push(
E('div', {
style: 'margin-top:5px; padding:5px; background:#f5f5f5; border-radius:4px'
}, [
E('span', { style: 'font-weight:bold' }, '⏱️ ' + _('Latency:') + ' '),
E('span', { style: 'color:#666' }, latency.replace(/Latency:/i, '').replace(/Idle Latency:/i, '').trim())
])
);
}
return E('div', {
id: result_id,
class: 'cbi-section'
}, [
E('div', { style: 'margin-left:20px; padding:10px; background:#f5f5f5; border-radius:4px' }, children)
]);
case 'unknown':
// 未知格式,直接显示
return E('div', { id: result_id, class: 'cbi-section' }, [
E('div', { style: 'margin-left:20px; padding:10px; background:#f5f5f5; border-radius:4px' }, [
E('strong', {}, _('Test Results:')),
E('br'),
E('pre', {
style: 'margin:5px 0 0 0; font-family:inherit; white-space:pre-wrap; color:#2e7d32'
}, escapeHTML(result.data))
])
]);
default:
// 无结果
return E('div', { id: result_id, class: 'cbi-section' }, [
E('span', { style: 'color:gray;margin-left:20px' }, [
E('em', {}, _('No test results yet. Click "Start Speed Test" to begin.'))
])
]);
}
};
// 配置部分
s = m.section(form.NamedSection, 'config', 'netspeedtest');
s.anonymous = true;
// 版本选择
o = s.option(form.ListValue, 'test_version', _('Select Test Version'));
if (has_ookla) {
o.value('ookla', 'Ookla SpeedTest');
}
if (has_python) {
o.value('python', 'Python speedtest-cli');
}
if (has_ookla) {
o.default = 'ookla';
} else if (has_python) {
o.default = 'python';
}
o.write = function(section_id, formvalue) {
uci.set('netspeedtest', section_id, 'test_version', formvalue);
return uci.save('netspeedtest');
};
// 开始测试按钮
o = s.option(form.Button, 'start', _('Start Speed Test'));
o.inputtitle = _('Click to start speed test');
o.inputstyle = 'apply';
// 只有真正在测试中才禁用按钮
if (is_testing) {
o.readonly = true;
}
o.onclick = function() {
var btn = this;
btn.disabled = true;
// 获取选中的版本
var versionSelect = document.getElementById('widget.cbid.netspeedtest.config.test_version');
if (!versionSelect) {
versionSelect = document.querySelector('select[name="test_version"]');
}
var version = versionSelect ? versionSelect.value : (has_ookla ? 'ookla' : 'python');
// 先写入Testing状态
return fs.write(ResultFile, 'Testing\n')
.then(function() {
// 在后台执行测试脚本
var cmd = 'nohup ' + SpeedtestScript;
if (version) {
cmd += ' --version ' + version;
}
cmd += ' > /dev/null 2>&1 &';
return fs.exec('/bin/sh', ['-c', cmd]);
})
.then(function() {
ui.addNotification(null, E('p', _('Speed test started. Please wait...')), 'info');
})
.catch(function(e) {
console.error('Failed to start test:', e);
ui.addNotification(null, E('p', _('Failed to start speed test: ') + e.message), 'error');
});
return false;
};
return m.render()
.then(L.bind(function(m, nodes) {
nodes.result_mtime = result_mtime; // 保存结果文件修改时间
// 添加轮询 - 每2秒检查一次结果
poll.add(L.bind(function() {
return Promise.all([
L.resolveDefault(fs.stat('/usr/bin/ookla-speedtest'), {}),
L.resolveDefault(fs.stat('/usr/bin/speedtest'), {}),
L.resolveDefault(fs.read(ResultFile), null),
L.resolveDefault(fs.stat(ResultFile), {})
]).then(L.bind(function(res) {
nodes.result_mtime = res[3] ? res[3].mtime * 1000 : 0;
this.poll_status(nodes, res);
}, this));
}, this), 2); // 2秒轮询
return nodes;
}, this, m));
},
handleSaveApply: null,
handleSave: null,
handleReset: null
});
// 辅助函数
function escapeHTML(str) {
if (!str) return '';
return String(str)
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#39;');
}
@@ -0,0 +1,122 @@
#
# Copyright (C) 2021-2025 sirpdboy herboy2008@gmail.com https://github.com/sirpdboy/netspeedtest
# This is free software, licensed under the GNU General Public License v3.
#
msgid ""
msgstr ""
"Project-Id-Version: LuCi Chinese Translation\n"
"Report-Msgid-Bugs-To: \n"
"Language: zh_CN\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=1; plural=0;\n"
"X-Generator: Pootle 2.0.6\n"
msgid "NetSpeedtest"
msgstr ""
msgid "A tool for testing network speed in multiple aspects"
msgstr ""
msgid "Lan Speedtest Iperf3"
msgstr ""
msgid "Lan Speedtest Homebox"
msgstr ""
msgid "Wan Ookla SpeedTest"
msgstr ""
msgid "Wan OpenSpeedTest SpeedTest"
msgstr ""
msgid "Log"
msgstr ""
msgid "Start Server"
msgstr ""
msgid "Stop Server"
msgstr ""
msgid "Loading..."
msgstr ""
msgid "RUNNING"
msgstr ""
msgid "NOT RUNNING"
msgstr ""
msgid "Iperf3 Server"
msgstr ""
msgid "Command failed"
msgstr ""
msgid "Failed to read log file"
msgstr ""
msgid "No log content available"
msgstr ""
msgid "Error: "
msgstr ""
msgid "Iperf3 service control"
msgstr ""
msgid "Listen Port"
msgstr ""
msgid "Invalid format. Use [::]:port or ip:port"
msgstr ""
msgid "Enable Log View"
msgstr ""
msgid "Download iperf3 client"
msgstr ""
msgid "Run Log"
msgstr ""
msgid "Refresh Log"
msgstr ""
msgid "Official Website"
msgstr ""
msgid "Please enable the Homebox service"
msgstr ""
msgid "Homebox service control"
msgstr ""
msgid "Homebox Server"
msgstr ""
msgid "Homebox Service Not Running"
msgstr ""
msgid "Ookla® SpeedTest-CLI"
msgstr ""
msgid "Start Ookla SpeedTest"
msgstr ""
msgid "Click to execute"
msgstr ""
msgid "SpeedTesting in progress..."
msgstr ""
msgid "Test failed."
msgstr ""
msgid "No test results yet."
msgstr ""
msgid "Test Script"
msgstr ""
+1
View File
@@ -0,0 +1 @@
zh_Hans
@@ -0,0 +1,150 @@
#
# Copyright (C) 2020-2026 sirpdboy herboy2008@gmail.com https://github.com/sirpdboy/netspeedtest
# This is free software, licensed under the GNU General Public License v3.
#
msgid ""
msgstr ""
"Project-Id-Version: LuCi Chinese Translation\n"
"Report-Msgid-Bugs-To: \n"
"Language: zh_CN\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=1; plural=0;\n"
"X-Generator: Pootle 2.0.6\n"
msgid "NetSpeedtest"
msgstr "网速测试"
msgid "A tool for testing network speed in multiple aspects"
msgstr "一个用于从多方面测试本地和宽带网络速度的测速工具"
msgid "Throughput speedtest Iperf3"
msgstr "吞吐iperf3测速"
msgid "Throughput speedtest Homebox"
msgstr "吞吐homebox测速"
msgid "Wan SpeedTest"
msgstr "宽带测速"
msgid "Select Test Version"
msgstr "选择测速版本"
msgid "Select Server"
msgstr "选择测速服务节点"
msgid "Auto (Let speedtest decide)"
msgstr "自动(由速度测试决定)"
msgid "Server not obtained"
msgstr "未获取到测速服务节点"
msgid "View detailed results"
msgstr "查看详细结果"
msgid "Online SpeedTest"
msgstr "在线宽带测速"
msgid "Log"
msgstr "日志"
msgid "Start Server"
msgstr "启动服务"
msgid "Stop Server"
msgstr "停止服务"
msgid "Loading..."
msgstr "装载中..."
msgid "RUNNING"
msgstr "运行中"
msgid "NOT RUNNING"
msgstr "未运行"
msgid "Iperf3 Server"
msgstr "Iperf3服务端"
msgid "Command failed"
msgstr "命令无效"
msgid "Failed to read log file"
msgstr "读取日志失败"
msgid "No log content available"
msgstr "没有日志记录"
msgid "Error: "
msgstr "错误: "
msgid "Iperf3 service control"
msgstr "Iperf3 服务控制"
msgid "Listen Port"
msgstr "监听端口"
msgid "Invalid format. Use [::]:port or ip:port"
msgstr "格式错误.如 [::]:端口 或 ip:端口"
msgid "Enable Log View"
msgstr "开启日志显示"
msgid "Download iperf3 client"
msgstr "下载iperf3客户端"
msgid "Run Log"
msgstr "运行日志"
msgid "Refresh Log"
msgstr "刷新日志"
msgid "Official Website"
msgstr "官方网站"
msgid "Please enable the Homebox service"
msgstr "请将homebox服务启用"
msgid "Homebox service control"
msgstr "Homebox服务控制"
msgid "Homebox Server"
msgstr "Homebox服务端"
msgid "Homebox Service Not Running"
msgstr "Homebox服务端未运行"
msgid "Open Web Interface"
msgstr "打开WEB页面"
msgid "Start Speed Test"
msgstr "开始测速"
msgid "Click to start speed test"
msgstr "点击开始测速"
msgid "Speed testing in progress..."
msgstr "测速中,请稍候..."
msgid "Test Failed"
msgstr "测速失败"
msgid "No test results yet."
msgstr "还没有测试结果"
msgid "Test Script"
msgstr "测速脚本"
msgid "Homebox Control panel"
msgstr "Homebox控制台"
msgid "Due to browser security policies, the Homebox interface https cannot be embedded directly."
msgstr "由于浏览器安全策略,Homebox接口https不能直接嵌入。"
msgid "Select speed measurement station"
msgstr "选择测速站点"
msgid ""
msgstr ""
@@ -0,0 +1,2 @@
config netspeedtest 'config'
@@ -0,0 +1,6 @@
#!/bin/sh
chmod +x /usr/bin/netspeedtest.sh /usr/bin/speedtest /usr/libexec/rpcd/luci.netspeedtest
rm -f /tmp/luci-indexcache
exit 0
+338
View File
@@ -0,0 +1,338 @@
#!/bin/bash
# Copyright (C) 2019-2026 sirpdboy
# Fixed version - correct image URL handling
RESULT_FILE='/tmp/netspeedtest_result'
LOG_FILE='/tmp/netspeedtest.log'
LOCK_FILE='/var/run/netspeedtest.lock'
PID_FILE='/var/run/netspeedtest.pid'
TIMEOUT=120
# 默认设置
PREFERRED_VERSION="auto"
SELECTED_SERVER="auto"
# 日志函数
log() {
echo "[$(date '+%Y-%m-%d %H:%M:%S')] $1" >> "$LOG_FILE"
}
# 检查进程锁
check_running() {
if [ -f "$PID_FILE" ]; then
local old_pid=$(cat "$PID_FILE" 2>/dev/null)
if kill -0 "$old_pid" 2>/dev/null; then
log "Another speedtest is already running (PID: $old_pid)"
echo "Testing" > "$RESULT_FILE"
exit 1
else
rm -f "$PID_FILE"
fi
fi
exec 200>"$LOCK_FILE"
if ! flock -n 200; then
log "Another speedtest is already running (flock)"
echo "Testing" > "$RESULT_FILE"
exit 1
fi
echo $$ > "$PID_FILE"
}
# 清理函数
cleanup() {
rm -f "$PID_FILE"
flock -u 200 2>/dev/null
exec 200>&-
}
trap cleanup EXIT INT TERM
# 检测可用的speedtest版本
detect_speedtest() {
local preferred="$1"
local available_versions=""
# 检查Ookla官方版
if [ -f '/usr/bin/ookla-speedtest' ] && [ -x '/usr/bin/ookla-speedtest' ]; then
available_versions="ookla:/usr/bin/ookla-speedtest"
fi
# 检查Python版
if [ -f '/usr/bin/speedtest' ] && [ -x '/usr/bin/speedtest' ]; then
if [ -n "$available_versions" ]; then
available_versions="$available_versions|python:/usr/bin/speedtest"
else
available_versions="python:/usr/bin/speedtest"
fi
fi
# 如果没有找到任何版本
if [ -z "$available_versions" ]; then
log "No speedtest versions found"
return 1
fi
# 如果指定了偏好版本
if [ "$preferred" != "auto" ]; then
IFS='|' read -ra versions <<< "$available_versions"
for ver in "${versions[@]}"; do
if [[ "$ver" == "$preferred:"* ]]; then
echo "$ver"
return 0
fi
done
fi
echo "$available_versions" | cut -d'|' -f1
return 0
}
# 运行Ookla版测试
run_ookla_test() {
local speedtest_bin=$1
local server_id=$2
local cmd="$speedtest_bin --accept-gdpr --accept-license --progress=no"
[ -n "$server_id" ] && [ "$server_id" != "auto" ] && cmd="$cmd --server-id=$server_id"
# 使用临时文件存储输出
local tmp_output=$(mktemp)
if command -v timeout >/dev/null 2>&1; then
timeout $TIMEOUT $cmd > "$tmp_output" 2>&1
local exit_code=$?
if [ $exit_code -eq 124 ]; then
log "Error: Test timed out"
rm -f "$tmp_output"
return 1
fi
else
$cmd > "$tmp_output" 2>&1
fi
cat "$tmp_output"
rm -f "$tmp_output"
return 0
}
# 运行Python版测试
run_python_test() {
local speedtest_bin=$1
local server_id=$2
local cmd="$speedtest_bin --share --simple"
if [ -n "$server_id" ] && [ "$server_id" != "auto" ]; then
cmd="$speedtest_bin --server $server_id --share --simple"
fi
local tmp_output=$(mktemp)
if command -v timeout >/dev/null 2>&1; then
timeout 90 $cmd > "$tmp_output" 2>&1
local exit_code=$?
if [ $exit_code -eq 124 ]; then
log "Error: Python test timed out"
# 尝试使用常用服务器
timeout 90 $speedtest_bin --server 59016 --share --simple > "$tmp_output" 2>&1
if [ $? -eq 124 ]; then
log "Error: Second attempt also timed out"
rm -f "$tmp_output"
return 1
fi
fi
else
$cmd > "$tmp_output" 2>&1
fi
cat "$tmp_output"
rm -f "$tmp_output"
return 0
}
# 解析Python版结果
parse_python_result() {
local result="$1"
# 检查是否包含错误信息
if echo "$result" | grep -q -i "error\|failed\|cannot\|timeout"; then
return 1
fi
# 提取分享链接
local share_url=$(echo "$result" | grep -o 'http://www.speedtest.net/result/[0-9]*\.png' | head -1)
if [ -n "$share_url" ]; then
echo "$share_url"
return 0
fi
# 提取速度数据
local download=$(echo "$result" | grep -i "download" | head -1)
local upload=$(echo "$result" | grep -i "upload" | head -1)
local latency=$(echo "$result" | grep -i "latency" | head -1)
if [ -n "$download" ] && [ -n "$upload" ]; then
{
echo "$download"
echo "$upload"
[ -n "$latency" ] && echo "$latency"
}
return 0
fi
return 1
}
parse_ookla_result() {
local result="$1"
# 提取结果URL
local result_url=$(echo "$result" | grep -i "result url" | grep -o 'https\?://[^ ]*' | head -1)
if [ -n "$result_url" ]; then
echo "$result_url.png"
return 0
fi
local download=$(echo "$result" | grep -i "download" | grep -o "[0-9.]\+ [Mk]bits" | head -1)
local upload=$(echo "$result" | grep -i "upload" | grep -o "[0-9.]\+ [Mk]bits" | head -1)
local latency=$(echo "$result" | grep -i "latency" | grep -o "[0-9.]\+ ms" | head -1)
if [ -n "$download" ] && [ -n "$upload" ]; then
{
echo "Download: $download"
echo "Upload: $upload"
[ -n "$latency" ] && echo "Latency: $latency"
}
return 0
fi
return 1
}
# 主测试函数
main_test() {
log "=== Speedtest started (PID: $$) ==="
# 检测可用的speedtest版本
local version_info=$(detect_speedtest "$PREFERRED_VERSION")
if [ -z "$version_info" ]; then
log "Error: No speedtest version found"
echo "Test failed" > "$RESULT_FILE"
exit 1
fi
local version_type=${version_info%:*}
local speedtest_bin=${version_info#*:}
log "Using $version_type version: $speedtest_bin"
TEST_SUCCESS=0
# 运行测试
case $version_type in
ookla)
RUNTEST=$(run_ookla_test "$speedtest_bin" "$SELECTED_SERVER")
;;
python)
RUNTEST=$(run_python_test "$speedtest_bin" "$SELECTED_SERVER")
;;
esac
echo "$RUNTEST" >> "$LOG_FILE"
# 解析结果
case $version_type in
ookla) RESULT=$(parse_ookla_result "$RUNTEST") ;;
python) RESULT=$(parse_python_result "$RUNTEST") ;;
esac
if [ -n "$RESULT" ]; then
echo "$RESULT" > "$RESULT_FILE"
TEST_SUCCESS=1
log "Test successful"
fi
# 如果失败,尝试自动选择
if [ $TEST_SUCCESS -eq 0 ] && [ "$SELECTED_SERVER" != "auto" ]; then
log "Test failed with selected server, trying automatic server selection"
SELECTED_SERVER="auto"
case $version_type in
ookla)
RUNTEST=$(run_ookla_test "$speedtest_bin" "auto")
;;
python)
RUNTEST=$(run_python_test "$speedtest_bin" "auto")
;;
esac
echo "$RUNTEST" >> "$LOG_FILE"
case $version_type in
ookla) RESULT=$(parse_ookla_result "$RUNTEST") ;;
python) RESULT=$(parse_python_result "$RUNTEST") ;;
esac
if [ -n "$RESULT" ]; then
echo "$RESULT" > "$RESULT_FILE"
log "Test successful with automatic server selection"
TEST_SUCCESS=1
fi
fi
# 最终检查
if [ $TEST_SUCCESS -eq 0 ]; then
echo "Test failed" > "$RESULT_FILE"
log "All tests failed"
fi
log "=== Speedtest completed (PID: $$) ==="
echo "" >> "$LOG_FILE"
}
# 显示帮助信息
show_help() {
echo "Usage: $0 [OPTIONS]"
echo "Options:"
echo " --version VERSION Preferred version (ookla/python/auto)"
echo " --server SERVER_ID Server ID to test with"
echo " --help Show this help message"
}
# 命令行参数处理
while [ $# -gt 0 ]; do
case "$1" in
--version)
if [ -n "$2" ]; then
PREFERRED_VERSION="$2"
shift 2
else
echo "Error: --version requires an argument"
exit 1
fi
;;
--server)
if [ -n "$2" ]; then
SELECTED_SERVER="$2"
shift 2
else
echo "Error: --server requires an argument"
exit 1
fi
;;
--help)
show_help
exit 0
;;
*)
echo "Unknown option: $1"
show_help
exit 1
;;
esac
done
# 运行测试
check_running
main_test
exit 0
File diff suppressed because it is too large. Load diff
@@ -0,0 +1,88 @@
#!/bin/sh
# Copyright (C) 2019-2026 sirpdboy
# RPC plugin for netspeedtest
. /usr/share/libubox/jshn.sh
get_ookla_servers() {
local servers="[]"
if [ -f '/usr/bin/ookla-speedtest' ] && [ -x '/usr/bin/ookla-speedtest' ]; then
local json_output=$(/usr/bin/ookla-speedtest --servers --format=json 2>/dev/null)
if [ -n "$json_output" ]; then
servers=$(echo "$json_output" | sed -n 's/.*"servers":\(\[.*\]\).*/\1/p')
[ -z "$servers" ] && servers="[]"
fi
fi
echo "$servers"
}
get_python_servers() {
local servers="[]"
local temp_file="/tmp/python_servers.tmp"
if [ -f '/usr/bin/speedtest' ] && [ -x '/usr/bin/speedtest' ]; then
/usr/bin/speedtest --list > "$temp_file" 2>/dev/null
if [ -s "$temp_file" ]; then
local json=""
local count=0
while IFS= read -r line; do
if echo "$line" | grep -qE '^[0-9]+\)'; then
local id=$(echo "$line" | sed -E 's/^([0-9]+)\).*/\1/')
local sponsor=$(echo "$line" | sed -E 's/^[0-9]+\)[[:space:]]+([^(]+).*/\1/' | sed -e 's/[[:space:]]*$//' -e 's/"/\\"/g')
local city_country=$(echo "$line" | grep -o '([^)]*)' | head -1 | sed 's/[()]//g')
local city=$(echo "$city_country" | cut -d',' -f1 | sed -e 's/^[[:space:]]*//' -e 's/[[:space:]]*$//' -e 's/"/\\"/g')
local country=$(echo "$city_country" | cut -d',' -f2 | sed -e 's/^[[:space:]]*//' -e 's/[[:space:]]*$//' -e 's/"/\\"/g')
local distance=$(echo "$line" | grep -o '\[[0-9.]* km\]' | sed 's/\[//g' | sed 's/ km\]//g')
[ -z "$distance" ] && distance="0"
if [ $count -gt 0 ]; then
json="$json,"
fi
json="$json{\"id\":$id,\"sponsor\":\"$sponsor\",\"city\":\"$city\",\"country\":\"$country\",\"distance\":$distance}"
count=$((count + 1))
[ $count -ge 10 ] && break
fi
done < "$temp_file"
[ -n "$json" ] && servers="[$json]"
fi
rm -f "$temp_file"
fi
echo "$servers"
}
case "$1" in
list)
echo '{
"get_servers": {
"signature": [],
"help": "Get speedtest servers"
}
}'
;;
call)
case "$2" in
get_servers)
json_init
json_add_string "ookla" "$(get_ookla_servers)"
json_add_string "python" "$(get_python_servers)"
json_dump
;;
*)
echo '{"error": "Method not found"}'
;;
esac
;;
*)
echo '{"error": "Invalid command"}'
;;
esac
@@ -0,0 +1,53 @@
{
"admin/network/netspeedtest": {
"title": "NetSpeedtest",
"order": 90,
"action": {
"type": "firstchild"
},
"depends": {
"acl": [ "luci-app-netspeedtest" ],
"uci": { "netspeedtest": true }
}
},
"admin/network/netspeedtest/iperf3": {
"title": "Throughput speedtest Iperf3",
"order": 10,
"action": {
"type": "view",
"path": "netspeedtest/iperf3"
}
},
"admin/network/netspeedtest/homebox": {
"title": "Throughput speedtest Homebox",
"order": 20,
"action": {
"type": "view",
"path": "netspeedtest/homebox"
}
},
"admin/network/netspeedtest/wanspeedtest": {
"title": "Wan SpeedTest",
"order": 40,
"action": {
"type": "view",
"path": "netspeedtest/wanspeedtest"
}
},
"admin/network/netspeedtest/onlinespeedtest": {
"title": "Online SpeedTest",
"order": 50,
"action": {
"type": "view",
"path": "netspeedtest/onlinespeedtest"
}
},
"admin/network/netspeedtest/logs": {
"title": "Log",
"order": 60,
"action": {
"type": "view",
"path": "netspeedtest/logs"
}
}
}
@@ -0,0 +1,38 @@
{
"luci-app-netspeedtest": {
"description": "Grant access to netspeedtest procedures",
"read": {
"file": {
"/usr/bin/netspeedtest.sh": [ "exec" ],
"/usr/bin/pgrep": [ "exec" ],
"/bin/ps": [ "exec" ],
"/usr/bin/homebox": [ "exec" ],
"/usr/bin/killall": [ "exec" ],
"/bin/sh": [ "exec" ],
"/bin/cat": [ "exec" ],
"/usr/bin/iperf3": [ "exec" ],
"/usr/bin/nohup": [ "exec" ],
"/usr/bin/speedtest": [ "exec" ],
"/usr/bin/ookla-speedtest": [ "exec" ],
"/tmp/netspeedtest_result": [ "read" ],
"/tmp/netspeedtest_progress": [ "read" ],
"/var/run/netspeedtest.pid": [ "read" ],
"/tmp/netspeedtest.log": [ "read" ]
},
"ubus": {
"luci.netspeedtest": [ "get_servers" ]
}
},
"write": {
"file": {
"/tmp/netspeedtest_result": [ "write" ],
"/tmp/netspeedtest_progress": [ "write" ],
"/var/run/netspeedtest.pid": [ "write" ],
"/tmp/netspeedtest.log": [ "write" ]
},
"ubus": {
"luci.netspeedtest": [ "get_servers" ]
}
}
}
}
+38 -28
View File
@@ -2,40 +2,52 @@ include $(TOPDIR)/rules.mk
PKG_NAME:=ookla-speedtest
PKG_VERSION:=1.2.0
PKG_RELEASE:=1
PKG_MAINTAINER:=Konstantine Shevlakov <shevlakov@132lan.ru>
PKG_RELEASE:=2
ifeq ($(ARCH),aarch64)
PKG_HASH:=skip
else ifeq ($(ARCH),arm)
ARM_CPU_FEATURES:=$(word 2,$(subst +,$(space),$(call qstrip,$(CONFIG_CPU_TYPE))))
ifeq ($(ARM_CPU_FEATURES),)
ARCH:=armel
PKG_HASH:=skip
else
ARCH:=armhf
PKG_HASH:=skip
endif
else ifeq ($(ARCH),i386)
PKG_HASH:=skip
else ifeq ($(ARCH),x86_64)
PKG_HASH:=skip
endif
PKG_SOURCE:=ookla-speedtest-$(PKG_VERSION)-linux-$(ARCH).tgz
PKG_SOURCE_URL:=https://install.speedtest.net/app/cli
PKG_BUILD_DIR:=$(BUILD_DIR)/$(PKG_NAME)-$(PKG_VERSION)
PKG_MAINTAINER:=sbwml <admin@cooluc.com>
include $(INCLUDE_DIR)/package.mk
define Package/$(PKG_NAME)
TITLE:=Speedtest from Ookla
SECTION:=net
CATEGORY:=Network
SECTION:=net
CATEGORY:=Network
TITLE:=Speedtest CLI by Ookla
DEPENDS:=@(aarch64||arm||i386||x86_64) +ca-certificates
URL:=https://www.speedtest.net/
endef
ifeq ($(ARCH),x86_64)
BIN_PATH:=./files/x86_64
endif
ifeq ($(ARCH),i386)
BIN_PATH:=./files/i386
endif
ifeq ($(ARCH),arm)
BIN_PATH:=./files/armhf
endif
ifeq ($(ARCH),aarch64)
BIN_PATH:=./files/aarch64
endif
define Package/$(PKG_NAME)/description
Speedtest CLI brings the trusted technology and global server network behind Speedtest to the command line.
Built for software developers, system administrators and computer enthusiasts alike,
Speedtest CLI is the first official Linux-native Speedtest application backed by Ookla®.
The Global Broadband Speed Test
endef
define Build/Prepare
endef
define Build/Configure
( \
pushd $(PKG_BUILD_DIR) ; \
$(TAR) -zxf $(DL_DIR)/ookla-speedtest-$(PKG_VERSION)-linux-$(ARCH).tgz -C . ; \
popd ; \
)
endef
define Build/Compile
@@ -43,9 +55,7 @@ endef
define Package/$(PKG_NAME)/install
$(INSTALL_DIR) $(1)/usr/bin
$(CP) $(BIN_PATH)/speedtest $(1)/usr/bin
$(INSTALL_BIN) $(PKG_BUILD_DIR)/speedtest $(1)/usr/bin/ookla-speedtest
endef
ifeq ($(ARCH),$(filter $(ARCH), x86_64 i386 arm aarch64))
$(eval $(call BuildPackage,$(PKG_NAME)))
endif
$(eval $(call BuildPackage,$(PKG_NAME)))
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.