update 2026-05-10 09:48:30

This commit is contained in:
action committed 2026-05-10 09:48:30 +08:00
1 parent 95ac96413e
commit 0b362674f1
6244 files changed
+2992248

No files matched your search

+20
View File
@@ -0,0 +1,20 @@
MIT License
Copyright (c) 2022-2026 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.2.1
PKG_RELEASE:=20260322
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,393 @@
/* Copyright (C) 2021-2026 sirpdboy herboy2008@gmail.com */
'use strict';
'require view';
'require fs';
'require ui';
'require uci';
'require form';
'require poll';
var state = {
running: false,
port: 3300,
enabled: false,
operationInProgress: false,
operationType: null // 'start' 或 'stop'
};
const logPath = '/tmp/netspeedtest.log';
function checkProcess(quick = false) {
if (quick) {
return fs.exec('/usr/bin/pgrep', ['homebox'])
.then(function(res) {
return res.code === 0 && res.stdout.trim() !== '';
})
.catch(function() {
return false;
});
} else {
return fs.exec('/usr/bin/pgrep', ['homebox'])
.then(function(res) {
if (res.code === 0 && res.stdout.trim()) {
return {
running: true,
pid: res.stdout.trim()
};
}
return fs.exec('/bin/ps', ['-w', '-C', 'homebox', '-o', 'pid='])
.then(function(psRes) {
var pid = psRes.stdout.trim();
return {
running: pid !== '',
pid: pid || null
};
});
})
.catch(function(err) {
return { running: false, pid: null };
});
}
}
function controlService(action, port) {
if (action === 'start') {
return fs.exec('/usr/bin/killall', ['homebox'])
.catch(function() { return Promise.resolve(); })
.then(function() {
var command = '/usr/bin/homebox serve --port ' + port + ' > ' + logPath + ' 2>&1 &';
return fs.exec('/bin/sh', ['-c', command]);
});
} else {
fs.exec('/usr/bin/killall', ['homebox']);
return fs.exec('/etc/init.d/netspeedtest', ['stop']);
}
}
function saveConfiguration(newPort, enabled) {
const uciContent = `config netspeedtest 'config'
\toption homebox_port '${newPort}'
\toption homebox_enabled '${enabled ? '1' : '0'}'
`;
return fs.write('/etc/config/netspeedtest', uciContent)
.then(() => {
if (enabled) {
return fs.exec('/etc/init.d/netspeedtest', ['enable']);
} else {
return fs.exec('/etc/init.d/netspeedtest', ['disable']);
}
})
.then(() => {
return uci.load('netspeedtest');
});
}
return view.extend({
handleSaveApply: null,
handleSave: null,
handleReset: null,
load: function() {
return Promise.all([
uci.load('netspeedtest')
]).then(() => {
var port = uci.get('netspeedtest', 'config', 'homebox_port');
if (port) {
state.port = parseInt(port);
}
var enabled = uci.get('netspeedtest', 'config', 'homebox_enabled');
state.enabled = enabled === '1';
});
},
render: function() {
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 saveBtn = E('button', {
'class': 'btn cbi-button cbi-button-apply',
'style': 'margin-left: 10px;'
}, _('Save'));
var enableCheckbox = E('input', {
'type': 'checkbox',
'id': 'homebox_enable',
'class': 'cbi-input-checkbox'
});
var statusMessage = E('div', { style: 'text-align: center; padding: 2em;' }, [
E('div', { style: 'font-size: 5em; color: #f39c12; margin-bottom: 0.2em;' }, '⚠️'),
E('h2', {}, _('Homebox Service Not Running')),
E('p', { style: 'color: #666; margin-top: 1em;' }, _('Please start the Homebox service'))
]);
var isHttps = window.location.protocol === 'https:';
var iframe;
var portInput = E('input', {
'type': 'number',
'class': 'cbi-input-text',
'style': 'width: 100px;',
'value': state.port,
'min': 1024,
'max': 65535,
'placeholder': '3300'
});
var portError = E('span', {
'class': 'error',
'style': 'color: red; margin-left: 10px; display: none;'
}, _('Port range must be 1024-65535'));
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'))
]);
}
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';
if (state.operationInProgress) {
if (state.operationType === 'start') {
toggleBtn.textContent = _('Starting...');
} else if (state.operationType === 'stop') {
toggleBtn.textContent = _('Stopping...');
}
toggleBtn.disabled = true;
if (state.operationType === 'stop' && !state.running) {
state.operationInProgress = false;
state.operationType = null;
toggleBtn.textContent = state.running ? _('Stop Server') : _('Start Server');
toggleBtn.className = 'btn cbi-button cbi-button-' + (state.running ? 'reset' : 'apply');
toggleBtn.disabled = false;
} else if (state.operationType === 'start' && state.running) {
state.operationInProgress = false;
state.operationType = null;
toggleBtn.textContent = state.running ? _('Stop Server') : _('Start Server');
toggleBtn.className = 'btn cbi-button cbi-button-' + (state.running ? 'reset' : 'apply');
toggleBtn.disabled = false;
}
} else {
toggleBtn.textContent = state.running ? _('Stop Server') : _('Start Server');
toggleBtn.className = 'btn cbi-button cbi-button-' + (state.running ? 'reset' : 'apply');
toggleBtn.disabled = false;
}
enableCheckbox.checked = state.enabled;
portInput.value = state.port;
container.innerHTML = '';
if (state.running) {
if (isHttps) {
container.appendChild(createHttpsButton());
} else {
iframe = E('iframe', {
src: 'http://' + window.location.hostname + ':' + state.port,
style: 'border:none;width: 100%; min-height: 80vh; border: none; border-radius: 3px;overflow:hidden !important;'
});
container.appendChild(iframe);
}
} else {
container.appendChild(statusMessage);
}
}
toggleBtn.addEventListener('click', function(ev) {
ev.preventDefault();
if (toggleBtn.disabled || state.operationInProgress) return;
var action = state.running ? 'stop' : 'start';
var startTime = Date.now();
state.operationInProgress = true;
state.operationType = action;
updateStatus();
controlService(action, state.port)
.then(function() {
return new Promise(function(resolve, reject) {
var checkCount = 0;
var maxChecks = 30;
function doCheck() {
checkProcess(true).then(function(isRunning) {
if (action === 'stop' && !isRunning) {
resolve({ running: false });
} else if (action === 'start' && isRunning) {
checkProcess().then(resolve).catch(resolve);
} else if (checkCount < maxChecks) {
checkCount++;
setTimeout(doCheck, 200);
} else {
checkProcess().then(resolve).catch(resolve);
}
}).catch(function() {
if (checkCount < maxChecks) {
checkCount++;
setTimeout(doCheck, 100);
} else {
checkProcess().then(resolve).catch(resolve);
}
});
}
doCheck();
});
})
.then(function(res) {
if (res) {
if (typeof res === 'boolean') {
state.running = res;
} else {
state.running = res.running || false;
if (res.port) {
state.port = res.port;
}
}
}
state.operationInProgress = false;
state.operationType = null;
updateStatus();
var message = action === 'start' ?
_('Homebox server started on port %s').replace('%s', state.port) :
_('Homebox server stopped');
})
.catch(function(err) {
console.error('Service control error:', err);
checkProcess().then(function(res) {
state.running = res.running || false;
if (res.port) state.port = res.port;
state.operationInProgress = false;
state.operationType = null;
updateStatus();
}).catch(function() {
state.operationInProgress = false;
state.operationType = null;
updateStatus();
});
});
});
saveBtn.addEventListener('click', function(ev) {
ev.preventDefault();
var newPort = parseInt(portInput.value, 10);
if (isNaN(newPort) || newPort < 1024 || newPort > 65535) {
portError.style.display = 'inline';
return;
}
portError.style.display = 'none';
saveBtn.disabled = true;
saveBtn.textContent = _('Saving...');
saveConfiguration(newPort, enableCheckbox.checked)
.then(function() {
state.port = newPort;
state.enabled = enableCheckbox.checked;
updateStatus();
})
.catch(function(err) {
})
.finally(function() {
saveBtn.disabled = false;
saveBtn.textContent = _('Save');
});
});
enableCheckbox.addEventListener('change', function(ev) {
saveConfiguration(state.port, enableCheckbox.checked)
.then(function() {
state.enabled = enableCheckbox.checked;
updateStatus();
var message = enableCheckbox.checked ?
_('Auto start enabled') :
_('Auto start disabled');
})
.catch(function(err) {
enableCheckbox.checked = state.enabled;
});
});
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'}, [
E('div', {'class': 'cbi-value-title'}, _('Auto Start')),
E('div', {'class': 'cbi-value-field'}, [
enableCheckbox,
E('label', {'for': 'homebox_enable', 'style': 'margin-left: 5px;'})
])
]),
E('div', {'class': 'cbi-value' }, [
E('div', {'class': 'cbi-value-title'}, _('Port Setting')),
E('div', {'class': 'cbi-value-field', 'style': 'align-items: center;'}, [
portInput,
saveBtn,
portError
])
]),
E('div', {'class': 'cbi-value'}, [
E('div', {'class': 'cbi-value-title'}, _('Service Control')),
E('div', {'class': 'cbi-value-field'}, toggleBtn)
])
]));
checkProcess().then(function(res) {
state.running = res.running;
if (res.port) {
state.port = res.port;
}
updateStatus();
poll.add(function() {
return checkProcess().then(function(res) {
if (res.running !== state.running || (res.port && res.port !== state.port)) {
state.running = res.running;
if (res.port) {
state.port = res.port;
}
updateStatus();
}
});
}, 5);
});
return E('div', {}, [
statusSection,
container
]);
}
});
@@ -0,0 +1,226 @@
/* 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: 5201
};
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, port) {
const commands = {
start: `/usr/bin/iperf3 -s -D -p ${port} --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;
});
}
function saveConfiguration(newPort, enabled) {
const uciContent = `config netspeedtest 'config'
\toption iperf3port '${newPort}'
\toption iperf3_enabled '${enabled ? '1' : '0'}'
`;
return fs.write('/etc/config/netspeedtest', uciContent)
.then(() => {
if (enabled) {
return fs.exec('/etc/init.d/netspeedtest', ['enable']);
} else {
return fs.exec('/etc/init.d/netspeedtest', ['disable']);
}
});
}
return view.extend({
handleSaveApply: null,
handleSave: null,
handleReset: null,
load: function() {
return Promise.all([
uci.load('netspeedtest')
]).then(() => {
const port = uci.get('netspeedtest', 'config', 'iperf3port');
if (port) {
state.port = parseInt(port);
}
});
},
render: function() {
const statusIcon = E('span', { 'style': 'margin-right: 5px;' });
const statusText = E('span');
const portInput = E('input', {
'type': 'number',
'class': 'cbi-input-text',
'style': 'width: 100px;',
'value': state.port,
'min': 1024,
'max': 65535,
'placeholder': '5201'
});
const enableCheckbox = E('input', {
'type': 'checkbox',
'id': 'iperf3_enable',
'class': 'cbi-input-checkbox',
'checked': state.enabled
});
const portError = E('span', {
'class': 'error',
'style': 'color: red; margin-left: 10px; display: none;'
}, _('Port range must be 1024-65535'));
const statusContainer = E('div', { 'class': 'cbi-map-descr' }, [
statusIcon,
statusText
]);
const toggleBtn = E('button', {
'class': 'btn cbi-button',
'click': ui.createHandlerFn(this, function() {
const action = state.running ? 'stop' : 'start';
toggleBtn.disabled = true;
controlService(action, state.port)
.then(() => {
return new Promise(resolve => setTimeout(resolve, 500));
})
.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;
});
})
});
const savePortBtn = E('button', {
'class': 'btn cbi-button cbi-button-apply',
'style': 'margin-left: 10px;',
'click': ui.createHandlerFn(this, function() {
const newPort = parseInt(portInput.value);
if (isNaN(newPort) || newPort < 1024 || newPort > 65535) {
portError.style.display = 'inline';
return;
}
portError.style.display = 'none';
saveConfiguration(newPort,state.enabled)
})
}, _('Save'));
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'}`;
portInput.value = state.port;
}
statusIcon.textContent = '...';
statusText.textContent = _('Checking status...');
toggleBtn.textContent = _('Loading...');
toggleBtn.disabled = true;
const statusSection = E('div', { 'class': 'cbi-section' }, [
E('div', { 'style': 'margin: 15px' }, [
E('h3', {}, _('Throughput speedtest Iperf3')),
statusContainer,
E('div', {'class': 'cbi-value', 'style': 'margin-top: 20px'}, [
E('div', {'class': 'cbi-value-title'}, _('Port Setting')),
E('div', {'class': 'cbi-value-field'}, [
portInput,
savePortBtn,
portError
])
]),
E('div', {'class': 'cbi-value'}, [
E('div', {'class': 'cbi-value-title'}, _('Service Control')),
E('div', {'class': 'cbi-value-field'}, toggleBtn),
]),
E('div', {'class': 'cbi-value'}, [
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/netspeedtest/releases', '_blank'))
}, _('GitHub'))
])
])
])
])
]);
// 初始化状态检查
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,88 @@
/* 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' })
])
]);
},
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,453 @@
// Copyright (C) 2023-2025 muink
// Copyright (C) 2019-2026 sirpdboy
'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
};
},
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();
if (firstLine === 'Testing') {
return { type: 'testing', data: null };
}
if (firstLine === 'Test failed') {
return { type: 'failed', data: null };
}
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 };
}
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') };
}
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') };
},
startSpeedTest: function(version) {
return fs.write(ResultFile, 'Testing\n')
.then(function() {
var shellCmd = SpeedtestScript;
if (version) {
shellCmd += ' --version ' + version;
}
shellCmd += ' > /dev/null 2>&1 &';
console.log('Starting test with sh -c:', shellCmd);
return fs.exec('/bin/ash', ['-c', shellCmd])
.catch(function() {
return fs.exec('/bin/sh', ['-c', shellCmd]);
});
});
},
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';
}
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)' " +
"</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.') + "</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.'))
])
]);
}
};
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');
self.startSpeedTest(version)
.then(function() {
})
.catch(function(e) {
});
return false;
};
return m.render()
.then(L.bind(function(m, nodes) {
nodes.result_mtime = result_mtime;
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 ""
@@ -0,0 +1,156 @@
#
# 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 "Error: "
msgstr "错误: "
msgid "Service Control"
msgstr "服务控制"
msgid "Download iperf3 client"
msgstr "下载iperf3客户端"
msgid "Refresh every %s seconds."
msgstr "每 %s 秒刷新"
msgid "Clear logs"
msgstr "清除日志"
msgid "Official Website"
msgstr "官方网站"
msgid "Please start 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 "Iperf3 Port"
msgstr "Iperf3监听端口"
msgid "Port range must be 1024-65535"
msgstr "端口范围1024-65535"
msgid "Auto Start"
msgstr "自动启动"
msgid "Port Setting"
msgstr "监听端口"
msgid "Starting..."
msgstr "启动中"
msgid "Stopping..."
msgstr "停止中"
msgid "Test failed. Please check log for details."
msgstr "测速失败,请查看日志"
@@ -0,0 +1,2 @@
config netspeedtest 'config'
@@ -0,0 +1,64 @@
#!/bin/sh /etc/rc.common
# netspeedtest init script
START=95
STOP=15
USE_PROCD=1
PATH=/usr/bin:/bin:/usr/sbin:/sbin
start_service() {
START_IPERF3=$(uci get netspeedtest.config.iperf3_enabled 2>/dev/null || echo "0")
START_HOMEBOX=$(uci get netspeedtest.config.homebox_enabled 2>/dev/null || echo "0")
[ "$START_HOMEBOX" = "1" ] || return 0
port=$(uci get netspeedtest.config.homebox_port 2>/dev/null || echo "3300")
procd_open_instance
procd_set_param command /usr/bin/homebox serve --port "$port"
procd_set_param stdout 1
procd_set_param stderr 1
procd_set_param respawn
procd_close_instance
}
stop_service() {
logger -t netspeedtest "Stopping homebox"
ubus call service delete '{ "name": "netspeedtest" }' 2>/dev/null
killall homebox 2>/dev/null || true
local retries=5
while pidof homebox >/dev/null 2>&1 && [ $retries -gt 0 ]; do
sleep 0.2
retries=$((retries-1))
done
}
service_triggers() {
procd_add_reload_trigger "netspeedtest"
}
reload_service() {
stop
start
}
status() {
START_HOMEBOX=$(uci get netspeedtest.config.homebox_enabled 2>/dev/null || echo "0")
if [ "$START_HOMEBOX" != "1" ]; then
return 3
fi
_procd_status
}
boot() {
sleep 2
start
}
@@ -0,0 +1,6 @@
#!/bin/sh
chmod +x /etc/init.d/netspeedtest /usr/bin/netspeedtest.sh /usr/bin/speedtest /usr/libexec/rpcd/luci.netspeedtest
/etc/init.d/rpcd restart
rm -f /tmp/luci-indexcache
exit 0
@@ -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,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" ]
}
}
}
}