🎁 Sync 2026-03-15 08:51:25

This commit is contained in:
github-actions[bot] 2026-03-15 08:51:25 +08:00
parent 80bd1ecded
commit 5a6ba160b1
10 changed files with 597 additions and 354 deletions

View File

@ -1,41 +1,44 @@
# SPDX-License-Identifier: GPL-3.0-only
# Copyright (c) 2020-2024 sirpdboy herboy2008@gmail.com
# Copyright (c) 2022-2026 sirpdboy herboy2008@gmail.com
#
include $(TOPDIR)/rules.mk
PKG_NAME:=homebox
PKG_VERSION:=0.0.0.2024101306
PKG_REAL_VER:=0.0.0-dev.2024101306
PKG_RELEASE:=3
ifeq ($(ARCH),aarch64)
H_ARCH:=arm64
else ifeq ($(ARCH),arm)
H_ARCH:=arm
PKG_VERSION:=1.0.1
PKG_RELEASE:=4
ifneq ($(filter aarch64%,$(ARCH)),)
PKG_ARCH:=arm64
PKG_HASH:=skip
else ifneq ($(filter arm%,$(ARCH)),)
PKG_ARCH:=armv7
PKG_HASH:=skip
else ifeq ($(ARCH),i386)
H_ARCH:=386
else ifeq ($(ARCH),mips)
H_ARCH:=mips
else ifeq ($(ARCH),mipsel)
H_ARCH:=mips
PKG_ARCH:=386
PKG_HASH:=skip
else ifeq ($(ARCH),x86_64)
H_ARCH:=amd64
PKG_ARCH:=amd64
PKG_HASH:=skip
else
$(error Unsupported architecture: $(ARCH))
endif
PKG_SOURCE:=homebox-linux-$(PKG_ARCH)-musl-v$(PKG_VERSION).tar.gz
PKG_SOURCE_URL:=https://github.com/sirpdboy/homebox/releases/download/v$(PKG_VERSION)/
PKG_BUILD_DIR:=$(BUILD_DIR)/$(PKG_NAME)-$(PKG_VERSION)
include $(INCLUDE_DIR)/package.mk
define Package/$(PKG_NAME)
SECTION:=net
CATEGORY:=Network
TITLE:=A Toolbox for Home Local Networks Speed Test
URL:=https://github.com/XGHeaven/homebox
DEPENDS:=@(i386||x86_64||arm||aarch64||mipsel||mips) +libstdcpp
SECTION:=net
CATEGORY:=Network
SUBMENU:=Speed Test
TITLE:=Home Local Network Speed Test Tool
URL:=https://github.com/XGHeaven/homebox
DEPENDS:=@(i386||x86_64||arm||aarch64)
MAINTAINER:=sirpdboy <herboy2008@gmail.com>
endef
define Package/$(PKG_NAME)/description
@ -43,9 +46,11 @@ define Package/$(PKG_NAME)/description
endef
define Build/Prepare
mkdir -p $(PKG_BUILD_DIR)
[ ! -f $(PKG_BUILD_DIR)/server-linux-$(H_ARCH).tar.gz ] && wget https://github.com/XGHeaven/homebox/releases/download/v$(PKG_REAL_VER)/server-linux-$(H_ARCH).tar.gz -O $(PKG_BUILD_DIR)/server-linux-$(H_ARCH).tar.gz
tar -xzvf $(PKG_BUILD_DIR)/server-linux-$(H_ARCH).tar.gz -C $(PKG_BUILD_DIR)
( \
pushd $(PKG_BUILD_DIR) ; \
$(TAR) -zxf $(DL_DIR)/$(PKG_SOURCE) -C . ; \
popd ; \
)
endef
define Build/Compile
@ -53,7 +58,7 @@ endef
define Package/$(PKG_NAME)/install
$(INSTALL_DIR) $(1)/usr/bin
$(INSTALL_BIN) $(PKG_BUILD_DIR)/server-linux-$(H_ARCH) $(1)/usr/bin/homebox
$(INSTALL_BIN) $(PKG_BUILD_DIR)/$(PKG_NAME) $(1)/usr/bin/homebox
endef
$(eval $(call BuildPackage,$(PKG_NAME)))
$(eval $(call BuildPackage,$(PKG_NAME)))

View File

@ -9,8 +9,8 @@ include $(TOPDIR)/rules.mk
PKG_NAME:=luci-app-netspeedtest
PKG_VERSION:=5.1.3
PKG_RELEASE:=2
PKG_VERSION:=5.2.0
PKG_RELEASE:=3
LUCI_TITLE:=LuCI Support for netspeedtest
LUCI_DEPENDS:=+ookla-speedtest +homebox +python3-light +python3-pkg-resources +python3-xml +python3-email \

View File

@ -1,4 +1,4 @@
/* Copyright (C) 2021-2026 sirpdboy herboy2008@gmail.com https://github.com/sirpdboy/luci-app-netspeedtest */
/* Copyright (C) 2021-2026 sirpdboy herboy2008@gmail.com */
'use strict';
'require view';
'require fs';
@ -6,36 +6,142 @@
'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 = 'nohup /usr/bin/homebox serve --port ' + port + ' > ' + logPath + ' 2>&1 &';
return fs.exec('/bin/sh', ['-c', command]);
});
} else {
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 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 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;
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;'
});
}
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', {
@ -49,57 +155,57 @@ return view.extend({
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.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'}`;
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) {
console.log('Stop confirmed - operation complete');
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) {
console.log('Start confirmed - operation complete');
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;
// Update container content based on state and protocol
container.textContent = '';
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 {
@ -107,59 +213,185 @@ return view.extend({
}
}
toggleBtn.addEventListener('click', ui.createHandlerFn(this, function() {
toggleBtn.addEventListener('click', function(ev) {
ev.preventDefault();
if (toggleBtn.disabled || state.operationInProgress) return;
var action = state.running ? 'stop' : 'start';
return controlService(action)
.then(checkProcess)
.then(res => {
state.running = res.running;
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) {
console.log('Stop success after', Date.now() - startTime, 'ms');
resolve({ running: false });
} else if (action === 'start' && isRunning) {
console.log('Start success after', Date.now() - startTime, 'ms');
checkProcess().then(resolve).catch(resolve);
} else if (checkCount < maxChecks) {
checkCount++;
setTimeout(doCheck, 200);
} else {
console.log('Check timeout, using full check');
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', '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')
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)
])
]));
// Initial status check
checkProcess().then(res => {
checkProcess().then(function(res) {
state.running = res.running;
if (res.port) {
state.port = res.port;
}
updateStatus();
toggleBtn.disabled = false;
// Start polling
poll.add(() => {
return checkProcess().then(res => {
if (res.running !== state.running) {
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();
toggleBtn.disabled = false;
}
});
}, 5);
poll.start();
});
return E('div', {}, [
statusSection,
container
]);
},
handleSaveApply: null,
handleSave: null,
handleReset: null
}
});

View File

@ -9,7 +9,7 @@
var state = {
running: false,
port: null
port: 5201
};
const logPath = '/tmp/netspeedtest.log';
@ -34,10 +34,9 @@ async function checkProcess() {
}
}
function controlService(action) {
function controlService(action, port) {
const commands = {
start: `/usr/bin/iperf3 -s -D -p 5201 --logfile ${logPath} 2>&1`,
start: `/usr/bin/iperf3 -s -D -p ${port} --logfile ${logPath} 2>&1`,
stop: '/usr/bin/killall -q iperf3'
};
@ -50,45 +49,105 @@ function controlService(action) {
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')
]);
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 btnGroup = E('div', { 'class': 'cbi-value-field', 'style': 'display: flex; gap: 10px;' });
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; // 禁用按钮
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; // 出错时也要恢复按钮
});
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';
@ -98,51 +157,54 @@ return view.extend({
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;
// 构建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'))
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'))
])
])
])
])
]),
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 => {
@ -161,8 +223,6 @@ const statusSection = E('div', { 'class': 'cbi-section' }, [
}, 5);
});
return statusSection;
}
});
}
});

View File

@ -78,21 +78,11 @@ return view.extend({
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
handleSaveApply: null,
handleSave: null,
handleReset: null
});

View File

@ -1,5 +1,5 @@
// Copyright (C) 2023-2025 muink
// Copyright (C) 2019-2026 sirpdboy
// Fixed version - better Ookla result parsing
'use strict';
'require view';
@ -25,7 +25,6 @@ return view.extend({
]);
},
// 检测可用版本
detectVersions: function(res) {
var hasOokla = !!(res[0] && res[0].path);
var hasPython = !!(res[1] && res[1].path);
@ -37,17 +36,14 @@ return view.extend({
};
},
// 检查是否真正在测试中文件新鲜且内容为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 };
@ -55,17 +51,13 @@ return view.extend({
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];
@ -82,18 +74,13 @@ return view.extend({
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++) {
@ -114,18 +101,13 @@ return view.extend({
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'>" +
@ -212,7 +194,6 @@ return view.extend({
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'>" +
@ -221,9 +202,8 @@ return view.extend({
break;
default:
// 无结果
result_stat.innerHTML = "<span style='color:gray;margin-left:20px'>" +
"<em>" + _('No test results yet. Click "Start Speed Test" to begin.') + "</em>" +
"<em>" + _('No test results yet.') + "</em>" +
"</span>";
}
}
@ -241,9 +221,8 @@ return view.extend({
var is_testing = this.isTesting(result_content, result_mtime);
var m, s, o;
m = new form.Map('netspeedtest', _('WAN SpeedTest'));
m = new form.Map('netspeedtest', _('Wan SpeedTest'));
// 结果显示区域
s = m.section(form.TypedSection, '_result');
s.anonymous = true;
s.render = function(section_id) {
@ -353,7 +332,6 @@ return view.extend({
]);
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:')),
@ -365,20 +343,17 @@ return view.extend({
]);
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.'))
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) {
@ -404,7 +379,6 @@ return view.extend({
o.inputtitle = _('Click to start speed test');
o.inputstyle = 'apply';
// 只有真正在测试中才禁用按钮
if (is_testing) {
o.readonly = true;
}
@ -413,17 +387,14 @@ return view.extend({
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;
@ -445,9 +416,7 @@ return view.extend({
return m.render()
.then(L.bind(function(m, nodes) {
nodes.result_mtime = result_mtime; // 保存结果文件修改时间
// 添加轮询 - 每2秒检查一次结果
nodes.result_mtime = result_mtime;
poll.add(L.bind(function() {
return Promise.all([
L.resolveDefault(fs.stat('/usr/bin/ookla-speedtest'), {}),
@ -469,7 +438,6 @@ return view.extend({
handleReset: null
});
// 辅助函数
function escapeHTML(str) {
if (!str) return '';
return String(str)

View File

@ -70,41 +70,29 @@ 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 "Service Control"
msgstr "服务控制"
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 every %s seconds."
msgstr "每 %s 秒刷新"
msgid "Refresh Log"
msgstr "刷新日志"
msgid "Clear logs"
msgstr "清除日志"
msgid "Official Website"
msgstr "官方网站"
msgid "Please enable the Homebox service"
msgstr "请将homebox服务启用"
msgid "Please start the Homebox service"
msgstr "请启动homebox服务"
msgid "Homebox service control"
msgstr "Homebox服务控制"
@ -145,6 +133,18 @@ 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 ""
msgstr ""

View File

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

View File

@ -1,88 +0,0 @@
#!/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

View File

@ -1,51 +1,63 @@
#
# Copyright (C) 2024 sbwml <admin@cooluc.com>
# Copyright (c) 2025-2026 sirpdboy <herboy2008@gmail.com>
#
# This is free software, licensed under the GPL-3.0 License.
#
include $(TOPDIR)/rules.mk
PKG_NAME:=ookla-speedtest
PKG_VERSION:=1.2.0
PKG_RELEASE:=2
PKG_RELEASE:=3
ifeq ($(ARCH),aarch64)
PKG_ARCH_SUFFIX:=aarch64
PKG_HASH:=skip
else ifeq ($(ARCH),arm)
ARM_CPU_FEATURES:=$(word 2,$(subst +,$(space),$(call qstrip,$(CONFIG_CPU_TYPE))))
ARM_CPU_FEATURES:=$(word 2,$(subst +,$(space),$(call qstrip,$(CONFIG_CPU_TYPE))))
ifeq ($(ARM_CPU_FEATURES),)
ARCH:=armel
PKG_ARCH_SUFFIX:=armel
PKG_HASH:=skip
else
ARCH:=armhf
PKG_ARCH_SUFFIX:=armhf
PKG_HASH:=skip
endif
else ifeq ($(ARCH),i386)
PKG_ARCH_SUFFIX:=i386
PKG_HASH:=skip
else ifeq ($(ARCH),x86_64)
PKG_ARCH_SUFFIX:=x86_64
PKG_HASH:=skip
endif
PKG_SOURCE:=ookla-speedtest-$(PKG_VERSION)-linux-$(ARCH).tgz
PKG_SOURCE:=ookla-speedtest-$(PKG_VERSION)-linux-$(PKG_ARCH_SUFFIX).tgz
PKG_SOURCE_URL:=https://install.speedtest.net/app/cli
PKG_BUILD_DIR:=$(BUILD_DIR)/$(PKG_NAME)-$(PKG_VERSION)
PKG_BUILD_DIR:=$(BUILD_DIR)/$(PKG_NAME)-$(PKG_VERSION)-$(PKG_ARCH_SUFFIX)
PKG_MAINTAINER:=sbwml <admin@cooluc.com>
include $(INCLUDE_DIR)/package.mk
define Package/$(PKG_NAME)
SECTION:=net
CATEGORY:=Network
SUBMENU:=Speed Test
TITLE:=Speedtest CLI by Ookla
URL:=https://www.speedtest.net/
DEPENDS:=@(aarch64||arm||i386||x86_64) +ca-certificates
URL:=https://www.speedtest.net/
endef
define Package/$(PKG_NAME)/description
The Global Broadband Speed Test
Speedtest CLI by Ookla is the official command line client
for testing internet bandwidth using speedtest.net servers.
endef
define Build/Prepare
( \
pushd $(PKG_BUILD_DIR) ; \
$(TAR) -zxf $(DL_DIR)/ookla-speedtest-$(PKG_VERSION)-linux-$(ARCH).tgz -C . ; \
$(TAR) -zxf $(DL_DIR)/ookla-speedtest-$(PKG_VERSION)-linux-$(PKG_ARCH_SUFFIX).tgz -C . ; \
popd ; \
)
endef
@ -58,4 +70,4 @@ define Package/$(PKG_NAME)/install
$(INSTALL_BIN) $(PKG_BUILD_DIR)/speedtest $(1)/usr/bin/ookla-speedtest
endef
$(eval $(call BuildPackage,$(PKG_NAME)))
$(eval $(call BuildPackage,$(PKG_NAME)))