🎉 Sync 2026-11-13 03:15:01

This commit is contained in:
github-actions[bot] committed 2026-11-13 03:15:01 +08:00
1 parent db03d04b97
commit 7f7e02b085
59 files changed
+1601 -1280

No files matched your search

+40 -13
View File
@@ -1,21 +1,48 @@
#
# Copyright (C) 2008-2014 The LuCI Team <luci@lists.subsignal.org>
#
# This is free software, licensed under the Apache License, Version 2.0 .
#
include $(TOPDIR)/rules.mk
LUCI_TITLE:=LuCI support for the generic PWM fan controller
LUCI_DEPENDS:=+luci-base +fancontrol
PKG_NAME:=luci-app-fancontrol
PKG_VERSION:=1.0.0
PKG_RELEASE:=5
PKG_LICENSE:=Apache-2.0
PKG_MAINTAINER:=Dante <dante@seckv.com>
LUCI_TITLE:=LuCI support for BPI-R4 PWM Fan Control
LUCI_DEPENDS:=+luci-base +ucode +ucode-mod-fs +ucode-mod-uci +ucode-mod-ubus
LUCI_PKGARCH:=all
PKG_LICENSE:=Apache-2.0
PKG_MAINTAINER:=JiaY-shi <shi05275@163.com>
define Package/$(PKG_NAME)/conffiles
/etc/config/fancontrol
endef
PKG_NAME:=luci-app-fancontrol
PKG_VERSION:=2.2.0
PKG_RELEASE:=4
define Package/$(PKG_NAME)/postinst
#!/bin/sh
[ -n "$${IPKG_INSTROOT}" ] || {
chmod 755 /etc/init.d/fancontrol /usr/sbin/fancontrold 2>/dev/null
/etc/init.d/rpcd restart
/etc/init.d/fancontrol enable
/etc/init.d/fancontrol restart
}
exit 0
endef
define Package/$(PKG_NAME)/prerm
#!/bin/sh
if [ -z "$${IPKG_INSTROOT}" ]; then
/etc/init.d/fancontrol stop
/etc/init.d/fancontrol disable
fi
exit 0
endef
define Package/$(PKG_NAME)/postrm
#!/bin/sh
if [ -z "$${IPKG_INSTROOT}" ]; then
rm -f /var/log/fancontrol.log*
rm -f /var/run/fancontrol.*
fi
exit 0
endef
include $(TOPDIR)/feeds/luci/luci.mk
# call BuildPackage - OpenWrt buildroot signature
@@ -1,171 +0,0 @@
.fancontrol-runtime {
margin-bottom: 1.5rem;
}
.fancontrol-status {
width: 100%;
max-width: 1200px;
overflow: hidden;
border: 1px solid rgba(127, 127, 127, .28);
border-radius: 6px;
}
.fancontrol-status-head {
display: flex;
align-items: center;
justify-content: space-between;
min-height: 0;
padding: .4rem .7rem;
background: rgba(127, 127, 127, .08);
border-bottom: 1px solid rgba(127, 127, 127, .22);
}
.fancontrol-status-title {
font-size: .88rem;
font-weight: 600;
}
.fancontrol-metrics {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
}
.fancontrol-metric {
min-width: 0;
min-height: 0;
padding: .55rem .7rem;
border-right: 1px solid rgba(127, 127, 127, .2);
}
.fancontrol-metric:last-child {
border-right: 0;
}
.fancontrol-metric-label,
.fancontrol-detail-label {
display: block;
color: var(--text-color-medium, inherit);
font-size: .72rem;
}
.fancontrol-metric-reading {
display: flex;
align-items: baseline;
gap: .3rem;
min-height: 0;
margin-top: .12rem;
white-space: nowrap;
}
.fancontrol-metric-value {
font-size: 1.18rem;
font-weight: 600;
line-height: 1.3;
}
.fancontrol-metric-unit {
overflow: hidden;
color: var(--text-color-medium, inherit);
font-size: .72rem;
text-overflow: ellipsis;
}
.fancontrol-chart-wrap {
height: 72px;
margin-top: .4rem;
}
.fancontrol-chart {
display: block;
width: 100%;
height: 100%;
color: var(--text-color-medium, #666);
background: rgba(127, 127, 127, .025);
border: 1px solid rgba(127, 127, 127, .24);
}
.fancontrol-chart-temperature {
--fancontrol-chart-line: #c74b45;
--fancontrol-chart-fill: rgba(199, 75, 69, .2);
}
.fancontrol-chart-pwm {
--fancontrol-chart-line: #1976d2;
--fancontrol-chart-fill: rgba(25, 118, 210, .2);
}
.fancontrol-chart-rpm {
--fancontrol-chart-line: #2f8a57;
--fancontrol-chart-fill: rgba(47, 138, 87, .2);
}
.fancontrol-details {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: .3rem 1rem;
padding: .42rem .7rem;
background: rgba(127, 127, 127, .04);
border-top: 1px solid rgba(127, 127, 127, .2);
}
.fancontrol-detail {
display: grid;
grid-template-columns: minmax(7rem, .34fr) minmax(0, 1fr);
align-items: baseline;
gap: .45rem;
min-width: 0;
}
.fancontrol-detail-value {
min-width: 0;
overflow-wrap: anywhere;
}
.fancontrol-ok {
color: var(--success-color-medium, #00884a);
}
@media (max-width: 700px) {
.fancontrol-metrics {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
.fancontrol-metric:nth-child(2) {
border-right: 0;
}
.fancontrol-metric:last-child {
grid-column: 1 / -1;
border-top: 1px solid rgba(127, 127, 127, .2);
}
.fancontrol-details {
grid-template-columns: 1fr;
}
}
@media (max-width: 420px) {
.fancontrol-metrics {
grid-template-columns: 1fr;
}
.fancontrol-metric,
.fancontrol-metric:nth-child(2) {
border-right: 0;
border-top: 1px solid rgba(127, 127, 127, .2);
}
.fancontrol-metric:first-child {
border-top: 0;
}
.fancontrol-metric:last-child {
grid-column: auto;
}
.fancontrol-detail {
grid-template-columns: 1fr;
gap: .15rem;
}
}
@@ -1,463 +0,0 @@
'use strict';
'require dom';
'require form';
'require fs';
'require poll';
'require rpc';
'require uci';
'require view';
const callServiceList = rpc.declare({
object: 'service',
method: 'list',
params: [ 'name' ],
expect: { '': {} }
});
const HISTORY_WINDOW_MS = 2 * 60 * 1000;
const TIME_GRID_INTERVAL_MS = 10 * 1000;
const TIME_LABEL_INTERVAL_MS = 30 * 1000;
const VALUE_GRID_DIVISIONS = 4;
function serviceRunning(result) {
const instances = result?.fancontrol?.instances || {};
return Object.keys(instances).some(function(name) {
return instances[name].running === true;
});
}
function parseStatus(text) {
const status = {};
String(text || '').split(/\n/).forEach(function(line) {
const separator = line.indexOf('=');
if (separator > 0)
status[line.substring(0, separator)] = line.substring(separator + 1);
});
return status;
}
function validStatusNumber(value) {
const number = Number(value);
return Number.isFinite(number) && number >= 0 ? number : null;
}
function appendHistory(history, running, status) {
const now = Date.now();
const temperature = validStatusNumber(status.temperature_mc);
const pwm = validStatusNumber(status.pwm);
const rpm = validStatusNumber(status.rpm);
history.push({
time: now,
temperature: running && temperature != null ? temperature / 1000 : null,
pwm: running && pwm != null ? pwm * 100 / 255 : null,
rpm: running ? rpm : null
});
while (history.length && history[0].time < now - HISTORY_WINDOW_MS)
history.shift();
}
function chartScale(history, key, minimumMax, step) {
let maximum = minimumMax;
history.forEach(function(sample) {
if (sample[key] != null)
maximum = Math.max(maximum, sample[key]);
});
return Math.ceil(maximum / step) * step;
}
function drawChart(canvas, history, key, options) {
const style = window.getComputedStyle(canvas);
const width = Math.max(canvas.clientWidth, 1);
const height = Math.max(canvas.clientHeight, 1);
const ratio = Math.min(window.devicePixelRatio || 1, 2);
const context = canvas.getContext('2d');
const padding = { left: 28, right: 5, top: 6, bottom: 14 };
const plotWidth = Math.max(width - padding.left - padding.right, 1);
const plotHeight = Math.max(height - padding.top - padding.bottom, 1);
const plotBottom = padding.top + plotHeight;
const now = history.length ? history[history.length - 1].time : Date.now();
const start = now - HISTORY_WINDOW_MS;
const maximum = chartScale(history, key, options.minimumMax, options.step);
const lineColor = style.getPropertyValue('--fancontrol-chart-line').trim() || '#1976d2';
const fillColor = style.getPropertyValue('--fancontrol-chart-fill').trim() || 'rgba(25, 118, 210, .2)';
const labelColor = style.color || '#666';
const gridColor = 'rgba(127, 127, 127, .2)';
let segment = [];
canvas.width = Math.round(width * ratio);
canvas.height = Math.round(height * ratio);
context.setTransform(ratio, 0, 0, ratio, 0, 0);
context.clearRect(0, 0, width, height);
context.strokeStyle = gridColor;
context.lineWidth = 1;
context.beginPath();
for (let elapsed = 0; elapsed <= HISTORY_WINDOW_MS; elapsed += TIME_GRID_INTERVAL_MS) {
const x = padding.left + plotWidth * elapsed / HISTORY_WINDOW_MS;
context.moveTo(x, padding.top);
context.lineTo(x, plotBottom);
}
for (let division = 0; division <= VALUE_GRID_DIVISIONS; division++) {
const y = padding.top + plotHeight * division / VALUE_GRID_DIVISIONS;
context.moveTo(padding.left, y);
context.lineTo(padding.left + plotWidth, y);
}
context.stroke();
context.fillStyle = labelColor;
context.font = '9px sans-serif';
context.textAlign = 'right';
context.textBaseline = 'top';
context.fillText(options.format(maximum), padding.left - 4, padding.top - 1);
context.textBaseline = 'bottom';
context.fillText(options.format(0), padding.left - 4, plotBottom + 1);
context.textBaseline = 'middle';
context.fillText(options.format(maximum / 2), padding.left - 4,
padding.top + plotHeight / 2);
context.textBaseline = 'bottom';
for (let elapsed = 0; elapsed <= HISTORY_WINDOW_MS; elapsed += TIME_LABEL_INTERVAL_MS) {
const x = padding.left + plotWidth * elapsed / HISTORY_WINDOW_MS;
const remaining = (HISTORY_WINDOW_MS - elapsed) / 1000;
context.textAlign = elapsed === 0 ? 'left' :
elapsed === HISTORY_WINDOW_MS ? 'right' : 'center';
context.fillText(remaining ? '-%ds'.format(remaining) : '0', x, height);
}
function point(sample) {
return {
x: padding.left + (sample.time - start) / HISTORY_WINDOW_MS * plotWidth,
y: plotBottom - sample[key] / maximum * plotHeight
};
}
function drawSegment() {
if (!segment.length)
return;
if (segment.length > 1) {
context.beginPath();
context.moveTo(segment[0].x, plotBottom);
segment.forEach(function(item) { context.lineTo(item.x, item.y); });
context.lineTo(segment[segment.length - 1].x, plotBottom);
context.closePath();
context.fillStyle = fillColor;
context.fill();
context.beginPath();
segment.forEach(function(item, index) {
if (index)
context.lineTo(item.x, item.y);
else
context.moveTo(item.x, item.y);
});
context.strokeStyle = lineColor;
context.lineWidth = 1.5;
context.lineJoin = 'round';
context.stroke();
} else {
context.beginPath();
context.arc(segment[0].x, segment[0].y, 2, 0, Math.PI * 2);
context.fillStyle = lineColor;
context.fill();
}
segment = [];
}
history.forEach(function(sample) {
if (sample[key] == null) {
drawSegment();
return;
}
segment.push(point(sample));
});
drawSegment();
}
function statusMetric(title, value, unit, chartClass) {
const canvas = E('canvas', {
'class': 'fancontrol-chart fancontrol-chart-%s'.format(chartClass),
'role': 'img',
'aria-label': title
});
const children = [
E('span', { 'class': 'fancontrol-metric-label' }, title),
E('div', { 'class': 'fancontrol-metric-reading' }, [
E('span', { 'class': 'fancontrol-metric-value' }, value),
unit ? E('span', { 'class': 'fancontrol-metric-unit' }, unit) : ''
]),
E('div', { 'class': 'fancontrol-chart-wrap' }, canvas)
];
return {
node: E('div', { 'class': 'fancontrol-metric' }, children),
canvas: canvas
};
}
function statusDetail(title, value) {
return E('div', { 'class': 'fancontrol-detail' }, [
E('span', { 'class': 'fancontrol-detail-label' }, title),
E('span', { 'class': 'fancontrol-detail-value' }, value)
]);
}
function renderStatus(node, running, status, history) {
const temp = Number(status.temperature_mc);
const pwm = Number(status.pwm);
const rpm = Number(status.rpm);
const tempValid = Number.isFinite(temp) && temp >= 0;
const pwmValid = Number.isFinite(pwm) && pwm >= 0;
const rpmValid = Number.isFinite(rpm) && rpm >= 0;
const pwmPercent = pwmValid ? Math.round(pwm * 100 / 255) : null;
const fault = status.error
? E('span', { 'class': 'label warning' }, status.error)
: E('span', { 'class': 'fancontrol-ok' }, _('None'));
const temperatureMetric = statusMetric(_('Temperature'),
tempValid ? '%.1f'.format(temp / 1000) : '-', tempValid ? '°C' : '', 'temperature');
const pwmMetric = statusMetric(_('PWM output'), pwmValid ? String(pwm) : '-',
pwmValid ? '/ 255 · %d%%'.format(pwmPercent) : '', 'pwm');
const rpmMetric = statusMetric(_('Fan speed'), rpmValid ? String(rpm) : '-',
rpmValid ? 'RPM' : '', 'rpm');
dom.content(node, E('div', { 'class': 'fancontrol-status' }, [
E('div', { 'class': 'fancontrol-status-head' }, [
E('span', { 'class': 'fancontrol-status-title' }, _('Runtime Status')),
E('span', { 'class': running ? 'label success' : 'label warning' },
running ? _('Running') : _('Stopped'))
]),
E('div', { 'class': 'fancontrol-metrics' }, [
temperatureMetric.node,
pwmMetric.node,
rpmMetric.node
]),
E('div', { 'class': 'fancontrol-details' }, [
statusDetail(_('Temperature input'), status.thermal_file || '-'),
statusDetail(_('PWM device'), status.fan_file || '-'),
statusDetail(_('Fault'), fault)
])
]));
drawChart(temperatureMetric.canvas, history, 'temperature', {
minimumMax: 100,
step: 10,
format: function(value) { return '%d°'.format(value); }
});
drawChart(pwmMetric.canvas, history, 'pwm', {
minimumMax: 100,
step: 100,
format: function(value) { return '%d%%'.format(value); }
});
drawChart(rpmMetric.canvas, history, 'rpm', {
minimumMax: 1000,
step: 500,
format: function(value) { return String(value); }
});
}
function validateAutoPath(sectionId, value) {
if (value === 'auto' || value?.startsWith('/'))
return true;
return _('Enter "auto" or an absolute sysfs path.');
}
return view.extend({
load() {
return uci.load('fancontrol');
},
render() {
let m, s, o;
let startTemp, fullTemp, startSpeed, maxSpeed, kickSpeed, kickMs;
m = new form.Map('fancontrol', _('Fan Control'),
_('Configure a continuous PWM curve based on device temperature. Automatic hardware detection is suitable for most devices.'));
s = m.section(form.TypedSection, 'fancontrol', _('Runtime Status'));
s.anonymous = true;
s.addremove = false;
s.render = function() {
const history = [];
const node = E('div', { 'class': 'cbi-section fancontrol-runtime' }, [
E('link', {
'rel': 'stylesheet',
'href': L.resource('view/fancontrol.css')
}),
E('div', { 'class': 'cbi-section-node' }, _('Collecting data...'))
]);
const statusNode = node.lastElementChild;
poll.add(function() {
return Promise.all([
L.resolveDefault(callServiceList('fancontrol'), {}),
L.resolveDefault(fs.read_direct('/var/run/fancontrol.status'), '')
]).then(function(result) {
const running = serviceRunning(result[0]);
const status = parseStatus(result[1]);
appendHistory(history, running, status);
renderStatus(statusNode, running, status, history);
});
}, 5);
return node;
};
s = m.section(form.NamedSection, 'settings', 'fancontrol', _('Settings'));
s.addremove = false;
s.tab('curve', _('Temperature Curve'),
_('Controls when the fan starts and how its speed rises with temperature.'));
s.tab('hardware', _('Hardware'),
_('Automatic detection works on most devices. Select explicit sysfs entries only when necessary.'));
s.tab('safety', _('Safety'),
_('Controls startup assistance and fail-safe behavior. Full-speed safety defaults are recommended.'));
o = s.taboption('curve', form.Flag, 'enabled', _('Enable'));
o.default = o.disabled;
o.rmempty = false;
o.description = _('Run the controller and apply this temperature curve.');
startTemp = s.taboption('curve', form.Value, 'start_temp', _('Start temperature'));
startTemp.default = '45';
startTemp.datatype = 'range(-40,200)';
startTemp.rmempty = false;
startTemp.description = _('The fan remains off below this temperature and starts at the minimum running PWM when it is reached. Unit: °C.');
fullTemp = s.taboption('curve', form.Value, 'full_speed_temp', _('Full-speed temperature'));
fullTemp.default = '85';
fullTemp.datatype = 'range(-39,250)';
fullTemp.rmempty = false;
fullTemp.description = _('PWM rises linearly and reaches the configured maximum at this temperature. Unit: °C.');
fullTemp.validate = function(sectionId, value) {
if (Number(value) <= Number(startTemp.formvalue(sectionId)))
return _('Full-speed temperature must be higher than start temperature.');
return true;
};
o = s.taboption('curve', form.Value, 'hysteresis', _('Stop hysteresis'));
o.default = '3';
o.datatype = 'range(0,50)';
o.rmempty = false;
o.description = _('After starting, the fan stops only after temperature falls this many degrees below the start temperature. This prevents rapid on/off cycling.');
startSpeed = s.taboption('curve', form.Value, 'start_speed', _('Minimum running PWM'));
startSpeed.default = '64';
startSpeed.datatype = 'range(1,255)';
startSpeed.rmempty = false;
startSpeed.description = _('Lowest PWM used while the fan is running. Increase it if the fan stalls or cannot maintain rotation. Range: 1–255.');
maxSpeed = s.taboption('curve', form.Value, 'max_speed', _('Maximum PWM'));
maxSpeed.default = '255';
maxSpeed.datatype = 'range(1,255)';
maxSpeed.rmempty = false;
maxSpeed.description = _('Highest PWM allowed by the curve. A value of 255 means 100% duty cycle.');
maxSpeed.validate = function(sectionId, value) {
if (Number(value) < Number(startSpeed.formvalue(sectionId)))
return _('Maximum PWM must not be lower than minimum running PWM.');
return true;
};
o = s.taboption('curve', form.Value, 'interval', _('Polling interval'));
o.default = '5';
o.datatype = 'range(1,300)';
o.rmempty = false;
o.description = _('How often the temperature is read and the PWM output is updated. Unit: seconds.');
o = s.taboption('hardware', form.Value, 'thermal_file', _('Temperature input'));
o.default = 'auto';
o.rmempty = false;
o.validate = validateAutoPath;
o.description = _('Use "auto" to select a CPU or SoC thermal sensor, or enter an absolute thermal zone or hwmon tempN_input path.');
o = s.taboption('hardware', form.Value, 'thermal_zone', _('Thermal zone type'));
o.default = 'auto';
o.rmempty = false;
o.description = _('Preferred thermal zone type when temperature input is automatic, for example cpu_top_thermal. Leave as "auto" unless several sensors are available.');
o = s.taboption('hardware', form.Value, 'fan_file', _('PWM output'));
o.default = 'auto';
o.rmempty = false;
o.validate = validateAutoPath;
o.description = _('Use "auto" to select a writable hwmon pwmN output, or enter its absolute path. cooling_device cur_state is not a PWM output.');
o = s.taboption('hardware', form.Value, 'fan_hwmon', _('Hwmon device name'));
o.default = 'auto';
o.rmempty = false;
o.description = _('Preferred hwmon name when PWM output is automatic. The pwmfan driver is preferred by default.');
o = s.taboption('hardware', form.Value, 'enable_file', _('PWM mode control'));
o.default = 'auto';
o.rmempty = false;
o.validate = validateAutoPath;
o.description = _('Optional pwmN_enable path used to select manual PWM mode. With "auto", the matching control is used when available.');
o = s.taboption('hardware', form.Value, 'cooling_device', _('Kernel cooling device'));
o.default = 'auto';
o.value('auto');
o.value('none');
o.rmempty = false;
o.validate = function(sectionId, value) {
return value === 'none' ? true : validateAutoPath(sectionId, value);
};
o.description = _('Use auto for a single pwm-fan device, an explicit cooling_device path for multiple fans, or none when kernel fan control is already handled elsewhere.');
o = s.taboption('hardware', form.Value, 'temp_div', _('Temperature divisor'));
o.default = '1000';
o.value('1');
o.value('1000');
o.datatype = 'range(1,1000000)';
o.rmempty = false;
o.description = _('Divides the raw sensor value before control calculations. Standard Linux temperature inputs use 1000; sensors reporting whole degrees use 1.');
kickSpeed = s.taboption('safety', form.Value, 'kick_speed', _('Start kick PWM'));
kickSpeed.default = '255';
kickSpeed.datatype = 'range(0,255)';
kickSpeed.rmempty = false;
kickSpeed.description = _('Brief PWM applied when a stopped fan starts. It helps fans that cannot start reliably at low duty. Set to 0 together with a zero duration to disable.');
kickMs = s.taboption('safety', form.Value, 'kick_ms', _('Start kick duration'));
kickMs.default = '500';
kickMs.datatype = 'range(0,10000)';
kickMs.rmempty = false;
kickMs.description = _('How long the startup kick is applied. Unit: milliseconds.');
kickMs.validate = function(sectionId, value) {
if (Number(value) > 0 && Number(kickSpeed.formvalue(sectionId)) === 0)
return _('Start kick PWM must be non-zero when a kick duration is configured.');
return true;
};
o = s.taboption('safety', form.Value, 'fail_safe_speed', _('Sensor failure PWM'));
o.default = '255';
o.datatype = 'range(0,255)';
o.rmempty = false;
o.description = _('PWM used when the temperature sensor cannot be read. Keeping 255 is the safest choice.');
o = s.taboption('safety', form.Value, 'exit_speed', _('Service exit PWM'));
o.default = '255';
o.datatype = 'range(0,255)';
o.rmempty = false;
o.description = _('PWM written when the service stops. Keeping the fan at full speed avoids losing cooling after an unexpected exit.');
o = s.taboption('safety', form.Flag, 'debug', _('Debug logging'));
o.default = o.disabled;
o.rmempty = false;
o.description = _('Write temperature, PWM and RPM values to the system log at every polling interval.');
return m.render();
}
});
@@ -0,0 +1,88 @@
'use strict';
'require view';
'require rpc';
'require poll';
'require ui';
var callGetLogs = rpc.declare({
object: 'luci.fancontrol',
method: 'get_logs',
params: [ 'lines' ]
});
var callClearLogs = rpc.declare({
object: 'luci.fancontrol',
method: 'clear_logs'
});
return view.extend({
handleSaveApply: null,
handleSave: null,
handleReset: null,
render: function() {
var area = E('textarea', {
id: 'fancontrol_log_content',
style: 'width:100%; height:460px; background:#f9f9f9; color:#222; padding:12px; border:1px solid #ccc; border-radius:4px; font-family:monospace; font-size:12px; line-height:1.5; overflow-y:auto; white-space:pre-wrap; word-break:break-all;',
readonly: 'readonly'
}, _('正在加载日志...'));
var btnRefresh = E('button', {
'class': 'btn cbi-button cbi-button-action',
'style': 'margin-right:10px;',
click: ui.createHandlerFn(this, 'updateLogs')
}, _('刷新日志'));
var btnClear = E('button', {
'class': 'btn cbi-button cbi-button-reset',
click: ui.createHandlerFn(this, 'handleClearLogs')
}, _('清空日志'));
poll.add(L.bind(this.updateLogs, this), 5);
// 初始加载一次日志
this.updateLogs();
return E('div', { 'class': 'cbi-map' }, [
E('h2', {}, _('风扇控制') + ' - ' + _('运行日志')),
E('div', { 'class': 'cbi-map-descr' }, _('实时展示温控守护进程的调速动作、温度变化与自检日志。')),
E('fieldset', { 'class': 'cbi-section' }, [
E('legend', {}, _('系统日志')),
E('div', { 'class': 'cbi-section-node' }, [
E('div', { 'style': 'margin-bottom:10px;' }, [ btnRefresh, btnClear ]),
area
])
])
]);
},
updateLogs: function() {
return callGetLogs(100).then(function(res) {
var area = document.getElementById('fancontrol_log_content');
if (area) {
if (res && res.logs && res.logs.length > 0) {
area.value = res.logs.join('\n');
area.scrollTop = area.scrollHeight;
} else {
area.value = _('暂无日志记录。');
}
}
}).catch(function(err) {
var area = document.getElementById('fancontrol_log_content');
if (area) area.value = _('读取日志失败: %s').format(err.message);
});
},
handleClearLogs: function() {
return callClearLogs().then(L.bind(function(res) {
if (res && res.success) {
ui.addNotification(null, E('p', _('日志已成功清空。')), 'info');
this.updateLogs();
} else {
ui.addNotification(null, E('p', _('清空日志失败。')), 'error');
}
}, this)).catch(function(err) {
ui.addNotification(null, E('p', _('RPC 请求失败: %s').format(err.message)), 'error');
});
}
});
@@ -0,0 +1,329 @@
'use strict';
'require view';
'require rpc';
'require poll';
'require ui';
'require dom';
var callGetStatus = rpc.declare({
object: 'luci.fancontrol',
method: 'get_status'
});
var callSetLevel = rpc.declare({
object: 'luci.fancontrol',
method: 'set_level',
params: [ 'level' ]
});
var callServiceAction = rpc.declare({
object: 'luci.fancontrol',
method: 'service',
params: [ 'action' ]
});
return view.extend({
handleSaveApply: null,
handleSave: null,
handleReset: null,
load: function() {
return callGetStatus();
},
render: function(status) {
var container = E('div', { 'class': 'cbi-map' }, [
E('h2', {}, _('风扇控制') + ' - ' + _('状态概览')),
E('div', { 'class': 'cbi-map-descr' }, _('实时监控香蕉派 BPI-R4 处理器温度与 PWM 风扇运行档位,提供硬件即时测试功能。'))
]);
// 运行状态卡片
var statusSection = E('fieldset', { 'class': 'cbi-section' }, [
E('legend', {}, _('运行状态')),
E('div', { 'class': 'cbi-section-node' }, [
E('table', { 'class': 'table cbi-section-table' }, [
E('tr', { 'class': 'tr' }, [
E('td', { 'class': 'td left', 'width': '33%' }, _('服务状态')),
E('td', { 'class': 'td left' }, [
E('span', { id: 'fan-service-state', style: 'font-weight:bold; margin-right:15px;' }, '-'),
E('button', {
id: 'fan-restart-btn',
'class': 'btn cbi-button cbi-button-reload',
click: ui.createHandlerFn(this, 'handleServiceAction', 'restart')
}, _('重启服务'))
])
]),
E('tr', { 'class': 'tr' }, [
E('td', { 'class': 'td left' }, _('运行模式')),
E('td', { 'class': 'td left', 'id': 'fan-service-mode', 'style': 'font-weight:bold;' }, '-')
]),
E('tr', { 'class': 'tr' }, [
E('td', { 'class': 'td left' }, _('处理器温度')),
E('td', { 'class': 'td left', 'id': 'fan-cpu-temp', 'style': 'font-size:16px; font-weight:bold;' }, '-')
]),
E('tr', { 'class': 'tr' }, [
E('td', { 'class': 'td left' }, _('当前风扇档位')),
E('td', { 'class': 'td left', 'id': 'fan-cur-level', 'style': 'font-weight:bold;' }, '-')
]),
E('tr', { 'class': 'tr' }, [
E('td', { 'class': 'td left' }, _('脉宽占空比')),
E('td', { 'class': 'td left', 'id': 'fan-pwm-duty' }, '-')
])
])
])
]);
container.appendChild(statusSection);
// 当前策略配置卡片
var policySection = E('fieldset', { 'class': 'cbi-section' }, [
E('legend', {}, _('当前策略配置')),
E('div', { 'class': 'cbi-section-node' }, [
E('table', { 'class': 'table cbi-section-table' }, [
E('tr', { 'class': 'tr', id: 'policy-row-interval' }, [
E('td', { 'class': 'td left', 'width': '33%' }, _('检测轮询周期')),
E('td', { 'class': 'td left', 'id': 'policy-interval' }, '-')
]),
E('tr', { 'class': 'tr', id: 'policy-row-spin' }, [
E('td', { 'class': 'td left' }, _('启动除尘自检')),
E('td', { 'class': 'td left', 'id': 'policy-start-spin' }, '-')
]),
E('tr', { 'class': 'tr', id: 'policy-row-low' }, [
E('td', { 'class': 'td left' }, _('低速触发阈值')),
E('td', { 'class': 'td left', 'id': 'policy-temp-low' }, '-')
]),
E('tr', { 'class': 'tr', id: 'policy-row-med' }, [
E('td', { 'class': 'td left' }, _('中速触发阈值')),
E('td', { 'class': 'td left', 'id': 'policy-temp-med' }, '-')
]),
E('tr', { 'class': 'tr', id: 'policy-row-high' }, [
E('td', { 'class': 'td left' }, _('全速触发阈值')),
E('td', { 'class': 'td left', 'id': 'policy-temp-high' }, '-')
]),
E('tr', { 'class': 'tr', id: 'policy-row-hyst' }, [
E('td', { 'class': 'td left' }, _('换挡回差温差')),
E('td', { 'class': 'td left', 'id': 'policy-hysteresis' }, '-')
]),
E('tr', { 'class': 'tr', id: 'policy-row-manual' }, [
E('td', { 'class': 'td left' }, _('预设运转档位')),
E('td', { 'class': 'td left', 'id': 'policy-manual-level' }, '-')
]),
E('tr', { 'class': 'tr', id: 'policy-row-kernel' }, [
E('td', { 'class': 'td left' }, _('控制驱动策略')),
E('td', { 'class': 'td left', 'id': 'policy-kernel-desc' }, _('系统内核设备树原生驱动接管'))
])
])
])
]);
container.appendChild(policySection);
// 快速测试面板
var testSection = E('fieldset', { 'class': 'cbi-section' }, [
E('legend', {}, _('硬件快速测试')),
E('div', { 'class': 'cbi-section-node' }, [
E('p', { 'style': 'margin-bottom:15px; color:#666;' },
_('点击按钮可即时下发档位进行硬件测试,测试结果立即生效并在状态栏回显。若服务处于运行中,后续温控检测将按设定规则恢复。')),
E('div', { 'style': 'display:flex; gap:10px; flex-wrap:wrap;' }, [
E('button', {
'class': 'btn cbi-button cbi-button-reset',
click: ui.createHandlerFn(this, 'handleSetLevel', 0)
}, _('停止风扇 (0 级)')),
E('button', {
'class': 'btn cbi-button cbi-button-action',
click: ui.createHandlerFn(this, 'handleSetLevel', 1)
}, _('低速测试 (1 级)')),
E('button', {
'class': 'btn cbi-button cbi-button-action',
click: ui.createHandlerFn(this, 'handleSetLevel', 2)
}, _('中速测试 (2 级)')),
E('button', {
'class': 'btn cbi-button cbi-button-apply',
click: ui.createHandlerFn(this, 'handleSetLevel', 3)
}, _('全速测试 (3 级)'))
])
])
]);
container.appendChild(testSection);
// 初始刷新
this.updateStatusView(status);
// 启动定时轮询 (每 3 秒)
poll.add(L.bind(function() {
return callGetStatus().then(L.bind(function(res) {
this.updateStatusView(res);
}, this));
}, this), 3);
return container;
},
updateStatusView: function(status) {
if (!status) return;
var stateEl = document.getElementById('fan-service-state');
if (stateEl) {
if (status.state === 'managed') {
stateEl.textContent = _('运行中 [受控]') + (status.pid ? ' [PID: ' + status.pid + ']' : '');
stateEl.style.color = '#2e7d32';
} else if (status.state === 'unmanaged') {
stateEl.textContent = _('运行中 [非受控]') + (status.pid ? ' [PID: ' + status.pid + ']' : '');
stateEl.style.color = '#ef6c00';
} else {
stateEl.textContent = _('已停止');
stateEl.style.color = '#c62828';
}
}
var modeEl = document.getElementById('fan-service-mode');
if (modeEl) {
if (status.mode === 'auto') {
modeEl.textContent = _('阶梯温控模式');
modeEl.style.color = '#1565c0';
} else if (status.mode === 'manual') {
modeEl.textContent = _('手动定速模式');
modeEl.style.color = '#6a1b9a';
} else if (status.mode === 'kernel') {
modeEl.textContent = _('内核接管模式');
modeEl.style.color = '#455a64';
} else {
modeEl.textContent = status.mode || '-';
modeEl.style.color = '#333';
}
}
var tempEl = document.getElementById('fan-cpu-temp');
if (tempEl) {
var tempVal = parseFloat(status.temp) || 0;
tempEl.textContent = tempVal.toFixed(1) + ' °C';
if (tempVal >= 75) {
tempEl.style.color = '#c62828';
} else if (tempVal >= 60) {
tempEl.style.color = '#ef6c00';
} else {
tempEl.style.color = '#2e7d32';
}
}
var levelEl = document.getElementById('fan-cur-level');
if (levelEl) {
var lvl = parseInt(status.level) || 0;
var lvlName = '';
switch (lvl) {
case 0: lvlName = _('0 级 [停转]'); break;
case 1: lvlName = _('1 级 [低速 31%]'); break;
case 2: lvlName = _('2 级 [中速 50%]'); break;
case 3: lvlName = _('3 级 [全速 100%]'); break;
default: lvlName = lvl + ' ' + _('级'); break;
}
levelEl.textContent = lvlName;
}
var pwmEl = document.getElementById('fan-pwm-duty');
if (pwmEl) {
var pwmVal = parseInt(status.pwm) || 0;
var pct = Math.round((pwmVal / 255) * 100);
pwmEl.textContent = pwmVal + ' / 255 (' + pct + '%)';
}
// 策略配置数据更新与模式显隐
var curMode = status.mode || 'auto';
var isAuto = (curMode === 'auto');
var isManual = (curMode === 'manual');
var isKernel = (curMode === 'kernel');
var setRowDisplay = function(id, visible) {
var el = document.getElementById(id);
if (el) el.style.display = visible ? '' : 'none';
};
setRowDisplay('policy-row-interval', isAuto);
setRowDisplay('policy-row-spin', isAuto || isManual);
setRowDisplay('policy-row-low', isAuto);
setRowDisplay('policy-row-med', isAuto);
setRowDisplay('policy-row-high', isAuto);
setRowDisplay('policy-row-hyst', isAuto);
setRowDisplay('policy-row-manual', isManual);
setRowDisplay('policy-row-kernel', isKernel);
var intervalValEl = document.getElementById('policy-interval');
if (intervalValEl && status.interval) {
intervalValEl.textContent = status.interval + ' ' + _('秒');
}
var spinValEl = document.getElementById('policy-start-spin');
if (spinValEl) {
spinValEl.textContent = (status.start_spin == 1) ? _('已开启 (开机全速 2 秒)') : _('未开启');
}
if (isAuto) {
var low = parseInt(status.temp_low) || 45;
var med = parseInt(status.temp_med) || 60;
var high = parseInt(status.temp_high) || 75;
var hyst = parseInt(status.hysteresis) || 3;
var lowEl = document.getElementById('policy-temp-low');
if (lowEl) lowEl.textContent = low + ' °C (' + _('低于 %d °C 停转').format(low - hyst) + ')';
var medEl = document.getElementById('policy-temp-med');
if (medEl) medEl.textContent = med + ' °C (' + _('低于 %d °C 降回低速').format(med - hyst) + ')';
var highEl = document.getElementById('policy-temp-high');
if (highEl) highEl.textContent = high + ' °C (' + _('低于 %d °C 降回中速').format(high - hyst) + ')';
var hystEl = document.getElementById('policy-hysteresis');
if (hystEl) hystEl.textContent = hyst + ' °C (' + _('防临界点振荡') + ')';
} else if (isManual) {
var mLevelEl = document.getElementById('policy-manual-level');
if (mLevelEl) {
var ml = parseInt(status.manual_level) || 0;
var mlName = '';
switch (ml) {
case 0: mlName = _('0 级 [停转 0%]'); break;
case 1: mlName = _('1 级 [低速 31%]'); break;
case 2: mlName = _('2 级 [中速 50%]'); break;
case 3: mlName = _('3 级 [全速 100%]'); break;
default: mlName = ml + ' ' + _('级'); break;
}
mLevelEl.textContent = mlName;
}
}
},
handleSetLevel: function(lvl) {
ui.showModal(null, [
E('p', { 'class': 'spinning' }, _('正在下发测试档位: %d 级 ...').format(lvl))
]);
return callSetLevel(lvl).then(L.bind(function(res) {
ui.hideModal();
if (res && res.success) {
ui.addNotification(null, E('p', _('风扇测试档位下发成功: %d 级').format(lvl)), 'info');
return callGetStatus().then(L.bind(this.updateStatusView, this));
} else {
ui.addNotification(null, E('p', _('风扇档位下发失败,请检查硬件节点。')), 'error');
}
}, this)).catch(function(err) {
ui.hideModal();
ui.addNotification(null, E('p', _('RPC 请求失败: %s').format(err.message)), 'error');
});
},
handleServiceAction: function(action) {
ui.showModal(null, [
E('p', { 'class': 'spinning' }, _('正在执行服务操作: %s ...').format(action))
]);
return callServiceAction(action).then(L.bind(function(res) {
ui.hideModal();
if (res && res.success) {
ui.addNotification(null, E('p', _('服务操作成功执行。')), 'info');
return callGetStatus().then(L.bind(this.updateStatusView, this));
} else {
ui.addNotification(null, E('p', _('服务操作执行失败。')), 'error');
}
}, this)).catch(function(err) {
ui.hideModal();
ui.addNotification(null, E('p', _('RPC 请求失败: %s').format(err.message)), 'error');
});
}
});
@@ -0,0 +1,76 @@
'use strict';
'require view';
'require form';
return view.extend({
render: function() {
var m, s, o;
m = new form.Map('fancontrol', _('风扇控制') + ' - ' + _('温控配置'),
_('配置香蕉派 BPI-R4 的 PWM 智能风扇调速策略与温度响应区间。'));
s = m.section(form.NamedSection, 'global', 'fancontrol', _('参数设置'));
s.anonymous = false;
o = s.option(form.Flag, 'enabled', _('启用服务'),
_('开启风扇温控自动守护服务。'));
o.rmempty = false;
o.default = '1';
o = s.option(form.ListValue, 'mode', _('工作模式'),
_('选择风扇的控制策略模式。'));
o.value('auto', _('阶梯温控模式'));
o.value('manual', _('手动定速模式'));
o.value('kernel', _('内核接管模式'));
o.default = 'auto';
o = s.option(form.Value, 'interval', _('检测周期 (秒)'),
_('温度轮询检测的时间间隔,推荐 3 到 10 秒。'));
o.datatype = 'range(1, 60)';
o.default = '5';
o.depends('mode', 'auto');
o = s.option(form.Flag, 'start_spin', _('启动自检清洁'),
_('服务启动时全速转动 2 秒,清洁积灰并确保轴承起步运转。'));
o.default = '1';
o.depends('mode', 'auto');
o.depends('mode', 'manual');
// 手动模式档位
o = s.option(form.ListValue, 'manual_level', _('手动常开档位'),
_('指定风扇持续运转的固定档位。'));
o.value('0', _('0 级 [停转 0%]'));
o.value('1', _('1 级 [低速 31%]'));
o.value('2', _('2 级 [中速 50%]'));
o.value('3', _('3 级 [全速 100%]'));
o.default = '2';
o.depends('mode', 'manual');
// 阶梯模式各档温度
o = s.option(form.Value, 'temp_low', _('1 级低速触发温度 (°C)'),
_('温度达到此数值时启动 1 级低速 (约 31% 占空比),低于该值停转。'));
o.datatype = 'range(30, 90)';
o.default = '45';
o.depends('mode', 'auto');
o = s.option(form.Value, 'temp_med', _('2 级中速触发温度 (°C)'),
_('温度达到此数值时提升至 2 级中速 (约 50% 占空比)。'));
o.datatype = 'range(35, 100)';
o.default = '60';
o.depends('mode', 'auto');
o = s.option(form.Value, 'temp_high', _('3 级全速触发温度 (°C)'),
_('温度达到此数值时提升至 3 级全速 (100% 占空比)。'));
o.datatype = 'range(40, 110)';
o.default = '75';
o.depends('mode', 'auto');
o = s.option(form.Value, 'hysteresis', _('换挡回差温度 (°C)'),
_('降温换挡的滞后温差。例如 60°C 升至中速,需降至 57°C 才会降回低速,防止临界点频繁启停。'));
o.datatype = 'range(1, 10)';
o.default = '3';
o.depends('mode', 'auto');
return m.render();
}
});
@@ -0,0 +1,261 @@
msgid ""
msgstr "Content-Type: text/plain; charset=UTF-8\n"
msgid "风扇控制"
msgstr ""
msgid "状态概览"
msgstr ""
msgid "温控配置"
msgstr ""
msgid "运行日志"
msgstr ""
msgid "实时监控香蕉派 BPI-R4 处理器温度与 PWM 风扇运行档位,提供硬件即时测试功能。"
msgstr ""
msgid "运行状态"
msgstr ""
msgid "服务状态"
msgstr ""
msgid "运行中 [受控]"
msgstr ""
msgid "运行中 [非受控]"
msgstr ""
msgid "已停止"
msgstr ""
msgid "重启服务"
msgstr ""
msgid "运行模式"
msgstr ""
msgid "阶梯温控模式"
msgstr ""
msgid "手动定速模式"
msgstr ""
msgid "内核接管模式"
msgstr ""
msgid "处理器温度"
msgstr ""
msgid "当前风扇档位"
msgstr ""
msgid "脉宽占空比"
msgstr ""
msgid "0 级 [停转]"
msgstr ""
msgid "1 级 [低速 31%]"
msgstr ""
msgid "2 级 [中速 50%]"
msgstr ""
msgid "3 级 [全速 100%]"
msgstr ""
msgid "级"
msgstr ""
msgid "硬件快速测试"
msgstr ""
msgid "点击按钮可即时下发档位进行硬件测试,测试结果立即生效并在状态栏回显。若服务处于运行中,后续温控检测将按设定规则恢复。"
msgstr ""
msgid "停止风扇 (0 级)"
msgstr ""
msgid "低速测试 (1 级)"
msgstr ""
msgid "中速测试 (2 级)"
msgstr ""
msgid "全速测试 (3 级)"
msgstr ""
msgid "正在下发测试档位: %d 级 ..."
msgstr ""
msgid "风扇测试档位下发成功: %d 级"
msgstr ""
msgid "风扇档位下发失败,请检查硬件节点。"
msgstr ""
msgid "正在执行服务操作: %s ..."
msgstr ""
msgid "服务操作成功执行。"
msgstr ""
msgid "服务操作执行失败。"
msgstr ""
msgid "RPC 请求失败: %s"
msgstr ""
msgid "配置香蕉派 BPI-R4 的 PWM 智能风扇调速策略与温度响应区间。"
msgstr ""
msgid "参数设置"
msgstr ""
msgid "启用服务"
msgstr ""
msgid "开启风扇温控自动守护服务。"
msgstr ""
msgid "工作模式"
msgstr ""
msgid "选择风扇的控制策略模式。"
msgstr ""
msgid "检测周期 (秒)"
msgstr ""
msgid "温度轮询检测的时间间隔,推荐 3 到 10 秒。"
msgstr ""
msgid "启动自检清洁"
msgstr ""
msgid "服务启动时全速转动 2 秒,清洁积灰并确保轴承起步运转。"
msgstr ""
msgid "手动常开档位"
msgstr ""
msgid "指定风扇持续运转的固定档位。"
msgstr ""
msgid "0 级 [停转 0%]"
msgstr ""
msgid "1 级 [低速 31%]"
msgstr ""
msgid "2 级 [中速 50%]"
msgstr ""
msgid "3 级 [全速 100%]"
msgstr ""
msgid "1 级低速触发温度 (°C)"
msgstr ""
msgid "温度达到此数值时启动 1 级低速 (约 31% 占空比),低于该值停转。"
msgstr ""
msgid "2 级中速触发温度 (°C)"
msgstr ""
msgid "温度达到此数值时提升至 2 级中速 (约 50% 占空比)。"
msgstr ""
msgid "3 级全速触发温度 (°C)"
msgstr ""
msgid "温度达到此数值时提升至 3 级全速 (100% 占空比)。"
msgstr ""
msgid "换挡回差温度 (°C)"
msgstr ""
msgid "降温换挡的滞后温差。例如 60°C 升至中速,需降至 57°C 才会降回低速,防止临界点频繁启停。"
msgstr ""
msgid "正在加载日志..."
msgstr ""
msgid "刷新日志"
msgstr ""
msgid "清空日志"
msgstr ""
msgid "实时展示温控守护进程的调速动作、温度变化与自检日志。"
msgstr ""
msgid "系统日志"
msgstr ""
msgid "暂无日志记录。"
msgstr ""
msgid "读取日志失败: %s"
msgstr ""
msgid "日志已成功清空。"
msgstr ""
msgid "清空日志失败。"
msgstr ""
msgid "当前策略配置"
msgstr ""
msgid "检测轮询周期"
msgstr ""
msgid "启动除尘自检"
msgstr ""
msgid "低速触发阈值"
msgstr ""
msgid "中速触发阈值"
msgstr ""
msgid "全速触发阈值"
msgstr ""
msgid "换挡回差温差"
msgstr ""
msgid "预设运转档位"
msgstr ""
msgid "控制驱动策略"
msgstr ""
msgid "系统内核设备树原生驱动接管"
msgstr ""
msgid "秒"
msgstr ""
msgid "已开启 (开机全速 2 秒)"
msgstr ""
msgid "未开启"
msgstr ""
msgid "低于 %d °C 停转"
msgstr ""
msgid "低于 %d °C 降回低速"
msgstr ""
msgid "低于 %d °C 降回中速"
msgstr ""
msgid "防临界点振荡"
msgstr ""
+238 -270
View File
@@ -1,293 +1,261 @@
msgid ""
msgstr ""
"Project-Id-Version: luci-app-fancontrol 2.2.0\n"
"PO-Revision-Date: 2026-07-30 00:00+0800\n"
"Last-Translator: JiaY-shi <shi05275@163.com>\n"
"Language-Team: Chinese (Simplified Han script)\n"
"Language: zh_Hans\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"
msgstr "Content-Type: text/plain; charset=UTF-8\n"
msgid "Kernel cooling device"
msgstr "内核散热设备"
msgid "Use auto for a single pwm-fan device, an explicit cooling_device path for multiple fans, or none when kernel fan control is already handled elsewhere."
msgstr "单个 pwm-fan 设备使用 auto;多个风扇需指定对应的 cooling_device 路径;已通过其他方式处理内核风扇控制时使用 none。"
#: /home/shi/document/c-service/fancontrol/luci-app-fancontrol/htdocs/luci-static/resources/view/fancontrol.js:173
msgid ""
"After starting, the fan stops only after temperature falls this many degrees "
"below the start temperature. This prevents rapid on/off cycling."
msgstr "风扇启动后,温度需要降到比启动温度低这么多度时才会停转,避免风扇频繁启停。"
#: /home/shi/document/c-service/fancontrol/luci-app-fancontrol/htdocs/luci-static/resources/view/fancontrol.js:143
msgid ""
"Automatic detection works on most devices. Select explicit sysfs entries "
"only when necessary."
msgstr "自动检测适用于大多数设备,仅在必要时手动指定 sysfs 项。"
#: /home/shi/document/c-service/fancontrol/luci-app-fancontrol/htdocs/luci-static/resources/view/fancontrol.js:238
msgid ""
"Brief PWM applied when a stopped fan starts. It helps fans that cannot start "
"reliably at low duty. Set to 0 together with a zero duration to disable."
msgstr "风扇从停止状态启动时短暂使用的 PWM,可帮助低占空比下无法可靠启动的风扇。将此项和助推时长都设为 0 可禁用。"
#: /home/shi/document/c-service/fancontrol/luci-app-fancontrol/htdocs/luci-static/resources/view/fancontrol.js:122
msgid "Collecting data..."
msgstr "正在采集数据..."
#: /home/shi/document/c-service/fancontrol/luci-app-fancontrol/htdocs/luci-static/resources/view/fancontrol.js:111
msgid ""
"Configure a continuous PWM curve based on device temperature. Automatic "
"hardware detection is suitable for most devices."
msgstr "根据设备温度配置连续 PWM 调速曲线。自动硬件检测适用于大多数设备。"
#: /home/shi/document/c-service/fancontrol/luci-app-fancontrol/htdocs/luci-static/resources/view/fancontrol.js:145
msgid ""
"Controls startup assistance and fail-safe behavior. Full-speed safety "
"defaults are recommended."
msgstr "控制启动助推和故障保护行为,建议保留默认的全速安全设置。"
#: /home/shi/document/c-service/fancontrol/luci-app-fancontrol/htdocs/luci-static/resources/view/fancontrol.js:141
msgid "Controls when the fan starts and how its speed rises with temperature."
msgstr "控制风扇何时启动,以及转速如何随温度升高。"
#: /home/shi/document/c-service/fancontrol/luci-app-fancontrol/htdocs/luci-static/resources/view/fancontrol.js:263
msgid "Debug logging"
msgstr "调试日志"
#: /home/shi/document/c-service/fancontrol/luci-app-fancontrol/htdocs/luci-static/resources/view/fancontrol.js:232
msgid ""
"Divides the raw sensor value before control calculations. Standard Linux "
"temperature inputs use 1000; sensors reporting whole degrees use 1."
msgstr "进行控制计算前用于换算传感器原始值。标准 Linux 温度输入使用 1000,直接报告摄氏度的传感器使用 1。"
#: /home/shi/document/c-service/fancontrol/luci-app-fancontrol/htdocs/luci-static/resources/view/fancontrol.js:147
msgid "Enable"
msgstr "启用"
#: /home/shi/document/c-service/fancontrol/luci-app-fancontrol/htdocs/luci-static/resources/view/fancontrol.js:98
msgid "Enter \"auto\" or an absolute sysfs path."
msgstr "请输入 \"auto\" 或 sysfs 绝对路径。"
#: /home/shi/document/c-service/fancontrol/luci-app-fancontrol/htdocs/luci-static/resources/view/fancontrol.js:110
#: /home/shi/document/c-service/fancontrol/luci-app-fancontrol/root/usr/share/luci/menu.d/luci-app-fancontrol.json:3
msgid "Fan Control"
msgid "风扇控制"
msgstr "风扇控制"
#: /home/shi/document/c-service/fancontrol/luci-app-fancontrol/htdocs/luci-static/resources/view/fancontrol.js:84
msgid "Fan speed"
msgstr "风扇转速"
msgid "状态概览"
msgstr "状态概览"
#: /home/shi/document/c-service/fancontrol/luci-app-fancontrol/htdocs/luci-static/resources/view/fancontrol.js:89
msgid "Fault"
msgstr "故障"
msgid "温控配置"
msgstr "温控配置"
#: /home/shi/document/c-service/fancontrol/luci-app-fancontrol/htdocs/luci-static/resources/view/fancontrol.js:158
msgid "Full-speed temperature"
msgstr "全速温度"
msgid "运行日志"
msgstr "运行日志"
#: /home/shi/document/c-service/fancontrol/luci-app-fancontrol/htdocs/luci-static/resources/view/fancontrol.js:165
msgid "Full-speed temperature must be higher than start temperature."
msgstr "全速温度必须高于启动温度。"
msgid "实时监控香蕉派 BPI-R4 处理器温度与 PWM 风扇运行档位,提供硬件即时测试功能。"
msgstr "实时监控香蕉派 BPI-R4 处理器温度与 PWM 风扇运行档位,提供硬件即时测试功能。"
#: /home/shi/document/c-service/fancontrol/luci-app-fancontrol/root/usr/share/rpcd/acl.d/luci-app-fancontrol.json:3
msgid "Grant access to Fan Control"
msgstr "授予风扇控制访问权限"
#: /home/shi/document/c-service/fancontrol/luci-app-fancontrol/htdocs/luci-static/resources/view/fancontrol.js:142
msgid "Hardware"
msgstr "硬件"
#: /home/shi/document/c-service/fancontrol/luci-app-fancontrol/htdocs/luci-static/resources/view/fancontrol.js:185
msgid "Highest PWM allowed by the curve. A value of 255 means 100% duty cycle."
msgstr "温控曲线允许的最高 PWM,255 表示 100% 占空比。"
#: /home/shi/document/c-service/fancontrol/luci-app-fancontrol/htdocs/luci-static/resources/view/fancontrol.js:244
msgid "How long the startup kick is applied. Unit: milliseconds."
msgstr "启动助推持续的时间,单位为毫秒。"
#: /home/shi/document/c-service/fancontrol/luci-app-fancontrol/htdocs/luci-static/resources/view/fancontrol.js:196
msgid ""
"How often the temperature is read and the PWM output is updated. Unit: "
"seconds."
msgstr "读取温度并更新 PWM 输出的时间间隔,单位为秒。"
#: /home/shi/document/c-service/fancontrol/luci-app-fancontrol/htdocs/luci-static/resources/view/fancontrol.js:215
msgid "Hwmon device name"
msgstr "Hwmon 设备名称"
#: /home/shi/document/c-service/fancontrol/luci-app-fancontrol/htdocs/luci-static/resources/view/fancontrol.js:179
msgid ""
"Lowest PWM used while the fan is running. Increase it if the fan stalls or "
"cannot maintain rotation. Range: 1–255."
msgstr "风扇运行时使用的最低 PWM。如果风扇停转或无法持续旋转,请提高此值。范围:1–255。"
#: /home/shi/document/c-service/fancontrol/luci-app-fancontrol/htdocs/luci-static/resources/view/fancontrol.js:181
msgid "Maximum PWM"
msgstr "最大 PWM"
#: /home/shi/document/c-service/fancontrol/luci-app-fancontrol/htdocs/luci-static/resources/view/fancontrol.js:188
msgid "Maximum PWM must not be lower than minimum running PWM."
msgstr "最大 PWM 不得低于最低运行 PWM。"
#: /home/shi/document/c-service/fancontrol/luci-app-fancontrol/htdocs/luci-static/resources/view/fancontrol.js:175
msgid "Minimum running PWM"
msgstr "最低运行 PWM"
#: /home/shi/document/c-service/fancontrol/luci-app-fancontrol/htdocs/luci-static/resources/view/fancontrol.js:71
msgid "None"
msgstr "无"
#: /home/shi/document/c-service/fancontrol/luci-app-fancontrol/htdocs/luci-static/resources/view/fancontrol.js:224
msgid ""
"Optional pwmN_enable path used to select manual PWM mode. With \"auto\", the "
"matching control is used when available."
msgstr "用于选择手动 PWM 模式的可选 pwmN_enable 路径。设为 \"auto\" 时会自动使用匹配的控制项。"
#: /home/shi/document/c-service/fancontrol/luci-app-fancontrol/htdocs/luci-static/resources/view/fancontrol.js:88
msgid "PWM device"
msgstr "PWM 设备"
#: /home/shi/document/c-service/fancontrol/luci-app-fancontrol/htdocs/luci-static/resources/view/fancontrol.js:220
msgid "PWM mode control"
msgstr "PWM 模式控制"
#: /home/shi/document/c-service/fancontrol/luci-app-fancontrol/htdocs/luci-static/resources/view/fancontrol.js:82
#: /home/shi/document/c-service/fancontrol/luci-app-fancontrol/htdocs/luci-static/resources/view/fancontrol.js:209
msgid "PWM output"
msgstr "PWM 输出"
#: /home/shi/document/c-service/fancontrol/luci-app-fancontrol/htdocs/luci-static/resources/view/fancontrol.js:162
msgid ""
"PWM rises linearly and reaches the configured maximum at this temperature. "
"Unit: °C."
msgstr "PWM 随温度线性升高,并在此温度达到设定的最大值,单位为 °C。"
#: /home/shi/document/c-service/fancontrol/luci-app-fancontrol/htdocs/luci-static/resources/view/fancontrol.js:255
msgid ""
"PWM used when the temperature sensor cannot be read. Keeping 255 is the "
"safest choice."
msgstr "温度传感器读取失败时使用的 PWM。保持 255 是最安全的选择。"
#: /home/shi/document/c-service/fancontrol/luci-app-fancontrol/htdocs/luci-static/resources/view/fancontrol.js:261
msgid ""
"PWM written when the service stops. Keeping the fan at full speed avoids "
"losing cooling after an unexpected exit."
msgstr "服务停止时写入的 PWM。保持风扇全速可避免服务意外退出后失去散热。"
#: /home/shi/document/c-service/fancontrol/luci-app-fancontrol/htdocs/luci-static/resources/view/fancontrol.js:192
msgid "Polling interval"
msgstr "轮询间隔"
#: /home/shi/document/c-service/fancontrol/luci-app-fancontrol/htdocs/luci-static/resources/view/fancontrol.js:218
msgid ""
"Preferred hwmon name when PWM output is automatic. The pwmfan driver is "
"preferred by default."
msgstr "自动选择 PWM 输出时优先使用的 hwmon 名称,默认优先选择 pwmfan 驱动。"
#: /home/shi/document/c-service/fancontrol/luci-app-fancontrol/htdocs/luci-static/resources/view/fancontrol.js:207
msgid ""
"Preferred thermal zone type when temperature input is automatic, for example "
"cpu_top_thermal. Leave as \"auto\" unless several sensors are available."
msgstr "自动选择温度输入时优先使用的 thermal zone 类型,例如 cpu_top_thermal。除非存在多个传感器,否则保持 \"auto\"。"
#: /home/shi/document/c-service/fancontrol/luci-app-fancontrol/htdocs/luci-static/resources/view/fancontrol.js:150
msgid "Run the controller and apply this temperature curve."
msgstr "运行风扇控制器并应用此温控曲线。"
#: /home/shi/document/c-service/fancontrol/luci-app-fancontrol/htdocs/luci-static/resources/view/fancontrol.js:77
msgid "Running"
msgstr "运行中"
#: /home/shi/document/c-service/fancontrol/luci-app-fancontrol/htdocs/luci-static/resources/view/fancontrol.js:75
#: /home/shi/document/c-service/fancontrol/luci-app-fancontrol/htdocs/luci-static/resources/view/fancontrol.js:113
msgid "Runtime Status"
msgid "运行状态"
msgstr "运行状态"
#: /home/shi/document/c-service/fancontrol/luci-app-fancontrol/htdocs/luci-static/resources/view/fancontrol.js:144
msgid "Safety"
msgstr "安全"
msgid "服务状态"
msgstr "服务状态"
#: /home/shi/document/c-service/fancontrol/luci-app-fancontrol/htdocs/luci-static/resources/view/fancontrol.js:251
msgid "Sensor failure PWM"
msgstr "传感器故障 PWM"
msgid "运行中 [受控]"
msgstr "运行中 [受控]"
#: /home/shi/document/c-service/fancontrol/luci-app-fancontrol/htdocs/luci-static/resources/view/fancontrol.js:257
msgid "Service exit PWM"
msgstr "服务退出 PWM"
msgid "运行中 [非受控]"
msgstr "运行中 [非受控]"
#: /home/shi/document/c-service/fancontrol/luci-app-fancontrol/htdocs/luci-static/resources/view/fancontrol.js:138
msgid "Settings"
msgstr "设置"
#: /home/shi/document/c-service/fancontrol/luci-app-fancontrol/htdocs/luci-static/resources/view/fancontrol.js:234
msgid "Start kick PWM"
msgstr "启动助推 PWM"
#: /home/shi/document/c-service/fancontrol/luci-app-fancontrol/htdocs/luci-static/resources/view/fancontrol.js:247
msgid "Start kick PWM must be non-zero when a kick duration is configured."
msgstr "配置启动助推时长时,启动助推 PWM 不能为零。"
#: /home/shi/document/c-service/fancontrol/luci-app-fancontrol/htdocs/luci-static/resources/view/fancontrol.js:240
msgid "Start kick duration"
msgstr "启动助推时长"
#: /home/shi/document/c-service/fancontrol/luci-app-fancontrol/htdocs/luci-static/resources/view/fancontrol.js:152
msgid "Start temperature"
msgstr "启动温度"
#: /home/shi/document/c-service/fancontrol/luci-app-fancontrol/htdocs/luci-static/resources/view/fancontrol.js:169
msgid "Stop hysteresis"
msgstr "停转回差"
#: /home/shi/document/c-service/fancontrol/luci-app-fancontrol/htdocs/luci-static/resources/view/fancontrol.js:77
msgid "Stopped"
msgid "已停止"
msgstr "已停止"
#: /home/shi/document/c-service/fancontrol/luci-app-fancontrol/htdocs/luci-static/resources/view/fancontrol.js:80
msgid "Temperature"
msgstr "温度"
msgid "重启服务"
msgstr "重启服务"
#: /home/shi/document/c-service/fancontrol/luci-app-fancontrol/htdocs/luci-static/resources/view/fancontrol.js:140
msgid "Temperature Curve"
msgstr "温控曲线"
msgid "运行模式"
msgstr "运行模式"
#: /home/shi/document/c-service/fancontrol/luci-app-fancontrol/htdocs/luci-static/resources/view/fancontrol.js:226
msgid "Temperature divisor"
msgstr "温度除数"
msgid "阶梯温控模式"
msgstr "阶梯温控模式"
#: /home/shi/document/c-service/fancontrol/luci-app-fancontrol/htdocs/luci-static/resources/view/fancontrol.js:87
#: /home/shi/document/c-service/fancontrol/luci-app-fancontrol/htdocs/luci-static/resources/view/fancontrol.js:198
msgid "Temperature input"
msgstr "温度输入"
msgid "手动定速模式"
msgstr "手动定速模式"
#: /home/shi/document/c-service/fancontrol/luci-app-fancontrol/htdocs/luci-static/resources/view/fancontrol.js:156
msgid ""
"The fan remains off below this temperature and starts at the minimum running "
"PWM when it is reached. Unit: °C."
msgstr "低于此温度时风扇保持停止;达到此温度时,以最低运行 PWM 启动。单位为 °C。"
msgid "内核接管模式"
msgstr "内核接管模式"
#: /home/shi/document/c-service/fancontrol/luci-app-fancontrol/htdocs/luci-static/resources/view/fancontrol.js:204
msgid "Thermal zone type"
msgstr "温度区域类型"
msgid "处理器温度"
msgstr "处理器温度"
#: /home/shi/document/c-service/fancontrol/luci-app-fancontrol/htdocs/luci-static/resources/view/fancontrol.js:202
msgid ""
"Use \"auto\" to select a CPU or SoC thermal sensor, or enter an absolute "
"thermal zone or hwmon tempN_input path."
msgstr "使用 \"auto\" 自动选择 CPU 或 SoC 温度传感器,也可输入 thermal zone 或 hwmon tempN_input 的绝对路径。"
msgid "当前风扇档位"
msgstr "当前风扇档位"
#: /home/shi/document/c-service/fancontrol/luci-app-fancontrol/htdocs/luci-static/resources/view/fancontrol.js:213
msgid ""
"Use \"auto\" to select a writable hwmon pwmN output, or enter its absolute "
"path. cooling_device cur_state is not a PWM output."
msgstr "使用 \"auto\" 自动选择可写的 hwmon pwmN 输出,也可输入其绝对路径。cooling_device cur_state 不是 PWM 输出。"
msgid "脉宽占空比"
msgstr "脉宽占空比"
msgid "0 级 [停转]"
msgstr "0 级 [停转]"
msgid "1 级 [低速 31%]"
msgstr "1 级 [低速 31%]"
msgid "2 级 [中速 50%]"
msgstr "2 级 [中速 50%]"
msgid "3 级 [全速 100%]"
msgstr "3 级 [全速 100%]"
msgid "级"
msgstr "级"
msgid "硬件快速测试"
msgstr "硬件快速测试"
msgid "点击按钮可即时下发档位进行硬件测试,测试结果立即生效并在状态栏回显。若服务处于运行中,后续温控检测将按设定规则恢复。"
msgstr "点击按钮可即时下发档位进行硬件测试,测试结果立即生效并在状态栏回显。若服务处于运行中,后续温控检测将按设定规则恢复。"
msgid "停止风扇 (0 级)"
msgstr "停止风扇 (0 级)"
msgid "低速测试 (1 级)"
msgstr "低速测试 (1 级)"
msgid "中速测试 (2 级)"
msgstr "中速测试 (2 级)"
msgid "全速测试 (3 级)"
msgstr "全速测试 (3 级)"
msgid "正在下发测试档位: %d 级 ..."
msgstr "正在下发测试档位: %d 级 ..."
msgid "风扇测试档位下发成功: %d 级"
msgstr "风扇测试档位下发成功: %d 级"
msgid "风扇档位下发失败,请检查硬件节点。"
msgstr "风扇档位下发失败,请检查硬件节点。"
msgid "正在执行服务操作: %s ..."
msgstr "正在执行服务操作: %s ..."
msgid "服务操作成功执行。"
msgstr "服务操作成功执行。"
msgid "服务操作执行失败。"
msgstr "服务操作执行失败。"
msgid "RPC 请求失败: %s"
msgstr "RPC 请求失败: %s"
msgid "配置香蕉派 BPI-R4 的 PWM 智能风扇调速策略与温度响应区间。"
msgstr "配置香蕉派 BPI-R4 的 PWM 智能风扇调速策略与温度响应区间。"
msgid "参数设置"
msgstr "参数设置"
msgid "启用服务"
msgstr "启用服务"
msgid "开启风扇温控自动守护服务。"
msgstr "开启风扇温控自动守护服务。"
msgid "工作模式"
msgstr "工作模式"
msgid "选择风扇的控制策略模式。"
msgstr "选择风扇的控制策略模式。"
msgid "检测周期 (秒)"
msgstr "检测周期 (秒)"
msgid "温度轮询检测的时间间隔,推荐 3 到 10 秒。"
msgstr "温度轮询检测的时间间隔,推荐 3 到 10 秒。"
msgid "启动自检清洁"
msgstr "启动自检清洁"
msgid "服务启动时全速转动 2 秒,清洁积灰并确保轴承起步运转。"
msgstr "服务启动时全速转动 2 秒,清洁积灰并确保轴承起步运转。"
msgid "手动常开档位"
msgstr "手动常开档位"
msgid "指定风扇持续运转的固定档位。"
msgstr "指定风扇持续运转的固定档位。"
msgid "0 级 [停转 0%]"
msgstr "0 级 [停转 0%]"
msgid "1 级 [低速 31%]"
msgstr "1 级 [低速 31%]"
msgid "2 级 [中速 50%]"
msgstr "2 级 [中速 50%]"
msgid "3 级 [全速 100%]"
msgstr "3 级 [全速 100%]"
msgid "1 级低速触发温度 (°C)"
msgstr "1 级低速触发温度 (°C)"
msgid "温度达到此数值时启动 1 级低速 (约 31% 占空比),低于该值停转。"
msgstr "温度达到此数值时启动 1 级低速 (约 31% 占空比),低于该值停转。"
msgid "2 级中速触发温度 (°C)"
msgstr "2 级中速触发温度 (°C)"
msgid "温度达到此数值时提升至 2 级中速 (约 50% 占空比)。"
msgstr "温度达到此数值时提升至 2 级中速 (约 50% 占空比)。"
msgid "3 级全速触发温度 (°C)"
msgstr "3 级全速触发温度 (°C)"
msgid "温度达到此数值时提升至 3 级全速 (100% 占空比)。"
msgstr "温度达到此数值时提升至 3 级全速 (100% 占空比)。"
msgid "换挡回差温度 (°C)"
msgstr "换挡回差温度 (°C)"
msgid "降温换挡的滞后温差。例如 60°C 升至中速,需降至 57°C 才会降回低速,防止临界点频繁启停。"
msgstr "降温换挡的滞后温差。例如 60°C 升至中速,需降至 57°C 才会降回低速,防止临界点频繁启停。"
msgid "正在加载日志..."
msgstr "正在加载日志..."
msgid "刷新日志"
msgstr "刷新日志"
msgid "清空日志"
msgstr "清空日志"
msgid "实时展示温控守护进程的调速动作、温度变化与自检日志。"
msgstr "实时展示温控守护进程的调速动作、温度变化与自检日志。"
msgid "系统日志"
msgstr "系统日志"
msgid "暂无日志记录。"
msgstr "暂无日志记录。"
msgid "读取日志失败: %s"
msgstr "读取日志失败: %s"
msgid "日志已成功清空。"
msgstr "日志已成功清空。"
msgid "清空日志失败。"
msgstr "清空日志失败。"
msgid "当前策略配置"
msgstr "当前策略配置"
msgid "检测轮询周期"
msgstr "检测轮询周期"
msgid "启动除尘自检"
msgstr "启动除尘自检"
msgid "低速触发阈值"
msgstr "低速触发阈值"
msgid "中速触发阈值"
msgstr "中速触发阈值"
msgid "全速触发阈值"
msgstr "全速触发阈值"
msgid "换挡回差温差"
msgstr "换挡回差温差"
msgid "预设运转档位"
msgstr "预设运转档位"
msgid "控制驱动策略"
msgstr "控制驱动策略"
msgid "系统内核设备树原生驱动接管"
msgstr "系统内核设备树原生驱动接管"
msgid "秒"
msgstr "秒"
msgid "已开启 (开机全速 2 秒)"
msgstr "已开启 (开机全速 2 秒)"
msgid "未开启"
msgstr "未开启"
msgid "低于 %d °C 停转"
msgstr "低于 %d °C 停转"
msgid "低于 %d °C 降回低速"
msgstr "低于 %d °C 降回低速"
msgid "低于 %d °C 降回中速"
msgstr "低于 %d °C 降回中速"
msgid "防临界点振荡"
msgstr "防临界点振荡"
#: /home/shi/document/c-service/fancontrol/luci-app-fancontrol/htdocs/luci-static/resources/view/fancontrol.js:266
msgid ""
"Write temperature, PWM and RPM values to the system log at every polling "
"interval."
msgstr "每次轮询时将温度、PWM 和 RPM 数值写入系统日志。"
@@ -0,0 +1,10 @@
config fancontrol 'global'
option enabled '1'
option mode 'auto'
option interval '5'
option start_spin '1'
option temp_low '45'
option temp_med '60'
option temp_high '75'
option hysteresis '3'
option manual_level '2'
+44
View File
@@ -0,0 +1,44 @@
#!/bin/sh /etc/rc.common
# SPDX-License-Identifier: Apache-2.0
# Copyright (C) 2026 Dante <dante@seckv.com>
USE_PROCD=1
START=95
STOP=10
PROG="/usr/sbin/fancontrold"
CONFIG="fancontrol"
start_service() {
local enabled
enabled=$(uci -q get "${CONFIG}.global.enabled" || echo 0)
[ "${enabled}" = "1" ] || return 0
chmod 755 "${PROG}" 2>/dev/null
# 启动前清理任何可能残留的孤儿进程
killall -q fancontrold 2>/dev/null
procd_open_instance "main"
procd_set_param command "${PROG}"
procd_set_param respawn 3600 5 0
procd_set_param stdout 1
procd_set_param stderr 1
procd_close_instance
}
stop_service() {
killall -q fancontrold 2>/dev/null
if [ -f "/sys/class/thermal/thermal_zone0/policy" ]; then
echo "step_wise" > "/sys/class/thermal/thermal_zone0/policy" 2>/dev/null
fi
}
reload_service() {
stop
start
}
service_triggers() {
procd_add_reload_trigger "${CONFIG}"
}
+249
View File
@@ -0,0 +1,249 @@
#!/bin/sh
# SPDX-License-Identifier: Apache-2.0
# Copyright (C) 2026 Dante <dante@seckv.com>
LOG_FILE="/var/log/fancontrol.log"
CONFIG="fancontrol"
log_msg() {
local level="$1"
local msg="$2"
local timestamp
timestamp="$(date '+%Y-%m-%d %H:%M:%S')"
echo "${timestamp} [${level}] ${msg}" >> "${LOG_FILE}"
# 保持日志行数在 200 行以内
if [ -f "${LOG_FILE}" ]; then
local lines
lines=$(wc -l < "${LOG_FILE}" 2>/dev/null || echo 0)
if [ "${lines}" -gt 200 ]; then
local tmp_log
tmp_log="$(tail -n 100 "${LOG_FILE}")"
echo "${tmp_log}" > "${LOG_FILE}"
fi
fi
}
# 寻找硬件节点
find_nodes() {
THERMAL_ZONE=""
COOLING_DEV=""
HWMON_PWM=""
# 优先寻找 thermal_zone0
if [ -f "/sys/class/thermal/thermal_zone0/temp" ]; then
THERMAL_ZONE="/sys/class/thermal/thermal_zone0"
else
for z in /sys/class/thermal/thermal_zone*; do
if [ -f "${z}/temp" ]; then
THERMAL_ZONE="${z}"
break
fi
done
fi
# 寻找 pwm-fan 冷却设备
for c in /sys/class/thermal/cooling_device*; do
if [ -f "${c}/type" ]; then
local ctype
ctype=$(cat "${c}/type" 2>/dev/null)
if [ "${ctype}" = "pwm-fan" ] || [ "${ctype}" = "pwmfan" ]; then
COOLING_DEV="${c}"
break
fi
fi
done
# 寻找 hwmon pwm 节点
for h in /sys/class/hwmon/hwmon*; do
if [ -f "${h}/name" ]; then
local hname
hname=$(cat "${h}/name" 2>/dev/null)
if [ "${hname}" = "pwmfan" ] && [ -f "${h}/pwm1" ]; then
HWMON_PWM="${h}/pwm1"
break
fi
fi
done
}
get_temperature() {
if [ -n "${THERMAL_ZONE}" ] && [ -f "${THERMAL_ZONE}/temp" ]; then
cat "${THERMAL_ZONE}/temp" 2>/dev/null || echo "0"
else
echo "0"
fi
}
set_fan_level() {
local lvl="$1"
if [ -n "${COOLING_DEV}" ] && [ -w "${COOLING_DEV}/cur_state" ]; then
echo "${lvl}" > "${COOLING_DEV}/cur_state" 2>/dev/null
fi
if [ -n "${HWMON_PWM}" ] && [ -w "${HWMON_PWM}" ]; then
local pwm_val=0
case "${lvl}" in
0) pwm_val=0 ;;
1) pwm_val=80 ;;
2) pwm_val=128 ;;
3) pwm_val=255 ;;
*) pwm_val=0 ;;
esac
echo "${pwm_val}" > "${HWMON_PWM}" 2>/dev/null
fi
}
get_level_name() {
case "$1" in
0) echo "0 级 (停转)" ;;
1) echo "1 级 (低速 31%)" ;;
2) echo "2 级 (中速 50%)" ;;
3) echo "3 级 (全速 100%)" ;;
*) echo "$1 级" ;;
esac
}
calc_level() {
local temp="$1"
local cur="$2"
local l1="$3"
local l2="$4"
local l3="$5"
local h="$6"
if [ "${cur}" -le 0 ]; then
if [ "${temp}" -ge "${l3}" ]; then
echo 3
elif [ "${temp}" -ge "${l2}" ]; then
echo 2
elif [ "${temp}" -ge "${l1}" ]; then
echo 1
else
echo 0
fi
elif [ "${cur}" -eq 1 ]; then
if [ "${temp}" -ge "${l3}" ]; then
echo 3
elif [ "${temp}" -ge "${l2}" ]; then
echo 2
elif [ "${temp}" -lt "$((l1 - h))" ]; then
echo 0
else
echo 1
fi
elif [ "${cur}" -eq 2 ]; then
if [ "${temp}" -ge "${l3}" ]; then
echo 3
elif [ "${temp}" -lt "$((l1 - h))" ]; then
echo 0
elif [ "${temp}" -lt "$((l2 - h))" ]; then
echo 1
else
echo 2
fi
else
if [ "${temp}" -lt "$((l1 - h))" ]; then
echo 0
elif [ "${temp}" -lt "$((l2 - h))" ]; then
echo 1
elif [ "${temp}" -lt "$((l3 - h))" ]; then
echo 2
else
echo 3
fi
fi
}
cleanup() {
if [ -n "${THERMAL_ZONE}" ] && [ -w "${THERMAL_ZONE}/policy" ]; then
echo "step_wise" > "${THERMAL_ZONE}/policy" 2>/dev/null
fi
log_msg "信息" "风扇控制守护服务已停止,已恢复内核温控策略"
exit 0
}
trap cleanup INT TERM QUIT
find_nodes
if [ -z "${COOLING_DEV}" ] && [ -z "${HWMON_PWM}" ]; then
log_msg "错误" "未找到可用的风扇硬件控制节点,服务退出"
exit 1
fi
enabled=$(uci -q get ${CONFIG}.global.enabled || echo 1)
if [ "${enabled}" != "1" ]; then
log_msg "信息" "服务配置为未启用状态,退出"
exit 0
fi
mode=$(uci -q get ${CONFIG}.global.mode || echo "auto")
interval=$(uci -q get ${CONFIG}.global.interval || echo 5)
start_spin=$(uci -q get ${CONFIG}.global.start_spin || echo 1)
temp_low=$(uci -q get ${CONFIG}.global.temp_low || echo 45)
temp_med=$(uci -q get ${CONFIG}.global.temp_med || echo 60)
temp_high=$(uci -q get ${CONFIG}.global.temp_high || echo 75)
hysteresis=$(uci -q get ${CONFIG}.global.hysteresis || echo 3)
manual_level=$(uci -q get ${CONFIG}.global.manual_level || echo 2)
[ "${interval}" -lt 1 ] 2>/dev/null && interval=1
[ "${interval}" -gt 60 ] 2>/dev/null && interval=60
log_msg "信息" "风扇控制守护服务启动,运行模式: ${mode},检测周期: ${interval} 秒"
# 启动自检
if [ "${start_spin}" = "1" ] && [ "${mode}" != "kernel" ]; then
log_msg "信息" "执行启动自检,全速运转 2 秒以清洁防尘"
set_fan_level 3
sleep 2
fi
# 非内核模式下切换为用户空间接管,防止内核 step_wise 依据 40°C 强制拉高转速
if [ "${mode}" != "kernel" ] && [ -n "${THERMAL_ZONE}" ] && [ -w "${THERMAL_ZONE}/policy" ]; then
echo "user_space" > "${THERMAL_ZONE}/policy" 2>/dev/null
fi
current_level=-1
while true; do
case "${mode}" in
"kernel")
# 内核托管模式
if [ -n "${THERMAL_ZONE}" ] && [ -w "${THERMAL_ZONE}/policy" ]; then
echo "step_wise" > "${THERMAL_ZONE}/policy" 2>/dev/null
fi
sleep "${interval}"
;;
"manual")
if [ "${current_level}" -ne "${manual_level}" ]; then
set_fan_level "${manual_level}"
current_level="${manual_level}"
lvl_name=$(get_level_name "${manual_level}")
log_msg "信息" "手动定速模式生效,当前档位设定为: ${lvl_name}"
fi
sleep "${interval}"
;;
"auto"|*)
raw_t=$(get_temperature)
# 单位毫摄氏度转整数摄氏度
t_c=$((raw_t / 1000))
t_dec=$(((raw_t % 1000) / 100))
target_lvl=$(calc_level "${t_c}" "${current_level}" "${temp_low}" "${temp_med}" "${temp_high}" "${hysteresis}")
if [ "${target_lvl}" -ne "${current_level}" ]; then
set_fan_level "${target_lvl}"
old_name=$(get_level_name "${current_level}")
new_name=$(get_level_name "${target_lvl}")
if [ "${current_level}" -eq -1 ]; then
log_msg "信息" "初始化风扇档位: 当前温度 ${t_c}.${t_dec} °C,设定为 ${new_name}"
else
log_msg "信息" "温度变化 (${t_c}.${t_dec} °C): 档位由 ${old_name} 调整为 ${new_name}"
fi
current_level="${target_lvl}"
fi
sleep "${interval}"
;;
esac
done
@@ -1,13 +1,37 @@
{
"admin/services/fancontrol": {
"title": "Fan Control",
"order": 90,
"admin/system/fancontrol": {
"title": "风扇控制",
"order": 65,
"action": {
"type": "view",
"path": "fancontrol"
"type": "firstchild"
},
"depends": {
"acl": [ "luci-app-fancontrol" ]
"acl": [ "luci-app-fancontrol" ],
"uci": { "fancontrol": true }
}
},
"admin/system/fancontrol/overview": {
"title": "状态概览",
"order": 10,
"action": {
"type": "view",
"path": "fancontrol/overview"
}
},
"admin/system/fancontrol/settings": {
"title": "温控配置",
"order": 20,
"action": {
"type": "view",
"path": "fancontrol/settings"
}
},
"admin/system/fancontrol/log": {
"title": "运行日志",
"order": 30,
"action": {
"type": "view",
"path": "fancontrol/log"
}
}
}
@@ -1,17 +1,21 @@
{
"luci-app-fancontrol": {
"description": "Grant access to Fan Control",
"description": "Grant access to BPI-R4 fan control procedures",
"read": {
"uci": [ "fancontrol" ],
"ubus": {
"service": [ "list" ]
"luci.fancontrol": [ "*" ]
},
"file": {
"/var/run/fancontrol.status": [ "read" ]
}
"uci": [
"fancontrol"
]
},
"write": {
"uci": [ "fancontrol" ]
"ubus": {
"luci.fancontrol": [ "*" ]
},
"uci": [
"fancontrol"
]
}
}
}
@@ -0,0 +1,223 @@
'use strict';
import { access, readfile, writefile, stat, popen } from 'fs';
import { cursor } from 'uci';
import { connect } from 'ubus';
const UCI_CONFIG = 'fancontrol';
const LOG_FILE = '/var/log/fancontrol.log';
const THERMAL_ZONE_TEMP = '/sys/class/thermal/thermal_zone0/temp';
const COOLING_DEV_CUR = '/sys/class/thermal/cooling_device0/cur_state';
const HWMON_PWM = '/sys/class/hwmon/hwmon1/pwm1';
function get_procd_status() {
let u = connect();
if (!u) return { running: false, pid: null, state: 'stopped' };
let res = u.call('service', 'list', { name: 'fancontrol' });
u.disconnect();
if (res && res['fancontrol'] && res['fancontrol'].instances) {
let instances = res['fancontrol'].instances;
for (let name in instances) {
let inst = instances[name];
if (inst && inst.running) {
return { running: true, pid: inst.pid, state: 'managed' };
}
}
}
// 检查是否有残留孤儿进程
let p = popen('pidof /usr/sbin/fancontrold', 'r');
if (p) {
let pid_str = p.read('all');
p.close();
if (pid_str && length(trim(pid_str)) > 0) {
let pids = split(trim(pid_str), /\s+/);
if (length(pids) > 0 && pids[0] != '') {
return { running: true, pid: int(pids[0]), state: 'unmanaged' };
}
}
}
return { running: false, pid: null, state: 'stopped' };
}
function get_hardware_info() {
let temp_c = 0.0;
if (access(THERMAL_ZONE_TEMP, 'r')) {
let raw_t = readfile(THERMAL_ZONE_TEMP);
if (raw_t) {
let raw_val = int(trim(raw_t));
temp_c = sprintf('%.1f', raw_val / 1000.0);
}
}
let cur_level = 0;
if (access(COOLING_DEV_CUR, 'r')) {
let raw_lvl = readfile(COOLING_DEV_CUR);
if (raw_lvl) cur_level = int(trim(raw_lvl));
}
let pwm_val = 0;
if (access(HWMON_PWM, 'r')) {
let raw_pwm = readfile(HWMON_PWM);
if (raw_pwm) pwm_val = int(trim(raw_pwm));
} else {
// 依据档位估算
if (cur_level == 1) pwm_val = 80;
else if (cur_level == 2) pwm_val = 128;
else if (cur_level == 3) pwm_val = 255;
}
return {
temp: temp_c,
level: cur_level,
pwm: pwm_val
};
}
const methods = {
get_status: {
call: function() {
let proc = get_procd_status();
let hw = get_hardware_info();
let uci_c = cursor();
let enabled = int(uci_c.get(UCI_CONFIG, 'global', 'enabled') || '0');
let mode = uci_c.get(UCI_CONFIG, 'global', 'mode') || 'auto';
let interval = int(uci_c.get(UCI_CONFIG, 'global', 'interval') || '5');
let start_spin = int(uci_c.get(UCI_CONFIG, 'global', 'start_spin') || '1');
let temp_low = int(uci_c.get(UCI_CONFIG, 'global', 'temp_low') || '45');
let temp_med = int(uci_c.get(UCI_CONFIG, 'global', 'temp_med') || '60');
let temp_high = int(uci_c.get(UCI_CONFIG, 'global', 'temp_high') || '75');
let hysteresis = int(uci_c.get(UCI_CONFIG, 'global', 'hysteresis') || '3');
let manual_level = int(uci_c.get(UCI_CONFIG, 'global', 'manual_level') || '2');
return {
running: proc.running,
state: proc.state,
pid: proc.pid,
enabled: enabled,
mode: mode,
interval: interval,
start_spin: start_spin,
temp_low: temp_low,
temp_med: temp_med,
temp_high: temp_high,
hysteresis: hysteresis,
manual_level: manual_level,
temp: hw.temp,
level: hw.level,
pwm: hw.pwm
};
}
},
set_level: {
args: { level: 0 },
call: function(req) {
let lvl = int(req.args?.level ?? 0);
if (lvl < 0) lvl = 0;
if (lvl > 3) lvl = 3;
let applied = false;
if (access(COOLING_DEV_CUR, 'w')) {
try {
writefile(COOLING_DEV_CUR, lvl + "\n");
applied = true;
} catch (e) {}
}
if (!applied && access(HWMON_PWM, 'w')) {
let pwm_val = 0;
if (lvl == 1) pwm_val = 80;
else if (lvl == 2) pwm_val = 128;
else if (lvl == 3) pwm_val = 255;
try {
writefile(HWMON_PWM, pwm_val + "\n");
applied = true;
} catch (e) {}
}
// 记录测试日志
try {
let p = popen('date "+%Y-%m-%d %H:%M:%S"', 'r');
let ts = p ? trim(p.read('all')) : '';
if (p) p.close();
let line = ts + ' [测试] 手动即时下发风扇档位: ' + lvl + ' 级\n';
let old_content = readfile(LOG_FILE) || '';
writefile(LOG_FILE, old_content + line);
} catch (e) {}
return { success: applied, level: lvl };
}
},
get_logs: {
args: { lines: 100 },
call: function(req) {
let lines_limit = int(req.args?.lines ?? 100);
let logs = [];
try {
if (access(LOG_FILE, 'r')) {
let log_stat = stat(LOG_FILE);
let content = '';
if (log_stat && log_stat.size > 65536) {
let fd = popen('tail -c 65536 ' + LOG_FILE, 'r');
if (fd) {
content = fd.read('all');
fd.close();
}
} else {
content = readfile(LOG_FILE);
}
if (content) {
let all_lines = split(content, "\n");
let total = length(all_lines);
let start = (total > lines_limit) ? (total - lines_limit) : 0;
for (let i = start; i < total; i++) {
if (all_lines[i] && length(trim(all_lines[i])) > 0) {
push(logs, all_lines[i]);
}
}
}
}
} catch (e) {}
return { logs: logs };
}
},
clear_logs: {
call: function() {
try {
writefile(LOG_FILE, '');
return { success: true };
} catch (e) {
return { success: false, error: e };
}
}
},
service: {
args: { action: 'restart' },
call: function(req) {
let action = req.args?.action || 'restart';
if (action != 'start' && action != 'stop' && action != 'restart') {
return { success: false, error: 'Invalid action' };
}
if (action == 'restart' || action == 'stop') {
// 强杀孤儿残留进程
let p = popen('killall -9 fancontrold', 'r');
if (p) p.close();
}
let rc = system('/etc/init.d/fancontrol ' + action + ' >/dev/null 2>&1');
system('sleep 1');
return { success: (rc === 0) };
}
}
};
return { 'luci.fancontrol': methods };
File renamed without changes.
@@ -8,7 +8,7 @@ include $(TOPDIR)/rules.mk
PKG_NAME:=luci-app-modemdata
PKG_VERSION:=1.0.32
PKG_RELEASE:=20260709
PKG_RELEASE:=1
LUCI_TITLE:=LuCI JS Support for modemdata application
MAINTAINER:=Rafał Wabik <4Rafal@gmail.com>
LUCI_DESCRIPTION:=Detailed information about the cellular modem and parameters of the mobile internet connection.
File renamed without changes.

Before

Width:  |  Height:  |  Size: 3.5 KiB

After

Width:  |  Height:  |  Size: 3.5 KiB

File renamed without changes.

Before

Width:  |  Height:  |  Size: 1.2 KiB

After

Width:  |  Height:  |  Size: 1.2 KiB

File renamed without changes.

Before

Width:  |  Height:  |  Size: 1.5 KiB

After

Width:  |  Height:  |  Size: 1.5 KiB

File renamed without changes.

Before

Width:  |  Height:  |  Size: 1.5 KiB

After

Width:  |  Height:  |  Size: 1.5 KiB

File renamed without changes.

Before

Width:  |  Height:  |  Size: 1.6 KiB

After

Width:  |  Height:  |  Size: 1.6 KiB

File renamed without changes.

Before

Width:  |  Height:  |  Size: 1.6 KiB

After

Width:  |  Height:  |  Size: 1.6 KiB

File renamed without changes.

Before

Width:  |  Height:  |  Size: 1.7 KiB

After

Width:  |  Height:  |  Size: 1.7 KiB

File renamed without changes.

Before

Width:  |  Height:  |  Size: 4.6 KiB

After

Width:  |  Height:  |  Size: 4.6 KiB

File renamed without changes.

Before

Width:  |  Height:  |  Size: 4.0 KiB

After

Width:  |  Height:  |  Size: 4.0 KiB

File renamed without changes.

Before

Width:  |  Height:  |  Size: 4.0 KiB

After

Width:  |  Height:  |  Size: 4.0 KiB

+1
View File
@@ -0,0 +1 @@
zh_Hans
File renamed without changes.
File renamed without changes.
File renamed without changes.
-328
View File
@@ -1,328 +0,0 @@
# luci-app-modemdata
![GitHub release (latest by date)](https://img.shields.io/github/v/release/4IceG/luci-app-modemdata?style=flat-square)
![GitHub stars](https://img.shields.io/github/stars/4IceG/luci-app-modemdata?style=flat-square)
![GitHub forks](https://img.shields.io/github/forks/4IceG/luci-app-modemdata?style=flat-square)
![GitHub All Releases](https://img.shields.io/github/downloads/4IceG/luci-app-modemdata/total)
### <img src="https://raw.githubusercontent.com/4IceG/Personal_data/master/dooffy_design_icons_EU_flags_United_Kingdom.png" height="24"> Languages available / <img src="https://raw.githubusercontent.com/4IceG/Personal_data/master/dooffy_design_icons_EU_flags_Poland.png" height="24"> Dostępne języki
<img src="https://hatscripts.github.io/circle-flags/flags/cz.svg" width="36" title="Czech (Čeština)" alt="Czech language" /> <img src="https://hatscripts.github.io/circle-flags/flags/dk.svg" width="36" title="Danish (Dansk)" alt="Danish language" /> <img src="https://hatscripts.github.io/circle-flags/flags/de.svg" width="36" title="German (Deutsch)" alt="German language" />
<img src="https://hatscripts.github.io/circle-flags/flags/es.svg" width="36" title="Spanish (Español)" alt="Spanish language" /> <img src="https://hatscripts.github.io/circle-flags/flags/fr.svg" width="36" title="French (Français)" alt="French language" /> <img src="https://hatscripts.github.io/circle-flags/flags/it.svg" width="36" title="Italian (Italiano)" alt="Italian language" />
<img src="https://hatscripts.github.io/circle-flags/flags/jp.svg" width="36" title="Japanese (日本語)" alt="Japanese language" /> <img src="https://hatscripts.github.io/circle-flags/flags/kr.svg" width="36" title="Korean (한국어)" alt="Korean language" /> <img src="https://hatscripts.github.io/circle-flags/flags/nl.svg" width="36" title="Dutch (Nederlands)" alt="Dutch language" />
<img src="https://hatscripts.github.io/circle-flags/flags/pl.svg" width="36" title="Polish (Polski)" alt="Polish language" /> <img src="https://hatscripts.github.io/circle-flags/flags/br.svg" width="36" title="Brazilian Portuguese (Português Brasil)" alt="Brazilian Portuguese language" /> <img src="https://hatscripts.github.io/circle-flags/flags/ru.svg" width="36" title="Russian (Русский)" alt="Russian language" /> <img src="https://hatscripts.github.io/circle-flags/flags/vn.svg" width="36" title="Vietnamese (Tiếng Việt)" alt="Vietnamese language" /> <img src="https://hatscripts.github.io/circle-flags/flags/cn.svg" width="36" title="Simplified Chinese (简体中文)" alt="Simplified Chinese language" /> <img src="https://hatscripts.github.io/circle-flags/flags/tw.svg" width="36" title="Traditional Chinese (繁體中文 - 台灣)" alt="Traditional Chinese language" /> <img src="https://hatscripts.github.io/circle-flags/flags/hk.svg" width="36" title="Traditional Chinese (繁體中文 - 香港)" alt="Traditional Chinese language" />
> [!NOTE]
> <img src="https://raw.githubusercontent.com/4IceG/Personal_data/master/dooffy_design_icons_EU_flags_United_Kingdom.png" height="24">
LuCI JS interface for [modemdata](https://github.com/obsy/modemdata) package. In the future, it will replace luci-app-3ginfo-lite.
>
> <img src="https://raw.githubusercontent.com/4IceG/Personal_data/master/dooffy_design_icons_EU_flags_Poland.png" height="24"> Interfejs LuCI JS dla pakietu [modemdata](https://github.com/obsy/modemdata). W przyszłości zastąpi on pakiet luci-app-3ginfo-lite.
> [!IMPORTANT]
> ***Due to the layout, the package is dedicated to the luci-theme-bootstrap.***
>
> ***Ze względu na układ pakiet dedykowany jest dla motywu luci-theme-bootstrap.***
> [!IMPORTANT]
> ***Required Packages:***
>
> ***serial:***
> sms-tool, kmod-usb-serial-option, kmod-usb-serial
>
> ***QMI/MBIM protocol:***
> kmod-usb-net-qmi-wwan / kmod-usb-net-cdc-mbim, uqmi
>
> ***ModemManager protocol:***
> modemmanager
>
> ***hilink (ecm):***
> wget-ssl
### Supported usb devices (serial mode):
<details>
<summary>Pokaż | Show me</summary>
``` bash
03f0581d
# Huawei E3272/E3276
# Huawei ME906s-158
# HP lt4112 Gobi 4G Module (Huawei ME906E)
# HP lt4132 LTE/HSPA+ 4G Module (Huawei ME906s)
03f0a31d
# Huawei E3272/E3276
# Huawei ME906s-158
# HP lt4112 Gobi 4G Module (Huawei ME906E)
# HP lt4132 LTE/HSPA+ 4G Module (Huawei ME906s)
05c69026
# ASKEY WWHC050
05c69215
# Quectel EC20
# Quectel EC25
05c69625
# Yuge CLM920 NC_5
05c6f601
# MEIG SLM750-V
0e8d7126
# Fibocom FM350-GL
0e8d7127
# Fibocom FM350-GL
119968a2
# Sierra Wireless MC7710
11999071
# DW5809e Dell Wireless 5809e Gobi 4G LTE Mobile Broadband Card (EM7305)
# DW5811e Snapdragon X7 LTE (EM7455B)
# Sierra Wireless EM7455
11999091
# Sierra Wireless EM7565 (generic)
119990d3
# Sierra Wireless EM9190
12d11506
# Huawei E3272/E3276
# Huawei ME906s-158
# HP lt4112 Gobi 4G Module (Huawei ME906E)
# HP lt4132 LTE/HSPA+ 4G Module (Huawei ME906s)
12d1155e
# Huawei E3272/E3276
# Huawei ME906s-158
# HP lt4112 Gobi 4G Module (Huawei ME906E)
# HP lt4132 LTE/HSPA+ 4G Module (Huawei ME906s)
12d1156c
# Huawei E3272/E3276
# Huawei ME906s-158
# HP lt4112 Gobi 4G Module (Huawei ME906E)
# HP lt4132 LTE/HSPA+ 4G Module (Huawei ME906s)
12d115c1
# Huawei E3272/E3276
# Huawei ME906s-158
# HP lt4112 Gobi 4G Module (Huawei ME906E)
# HP lt4132 LTE/HSPA+ 4G Module (Huawei ME906s)
1435d181
# WNC D18QC
# WNC D19QA
1435d191
# WNC D18QC
# WNC D19QA
16907588
# ASKEY WWHC050
19d20167
# ZTE MF821
19d20189
# ZTE MF28D
# ZTE MF290
19d21275
# ZTE P685M
# ZTE ZM8630A
19d21432
# ZTE MF286
# ZTE MF286A
19d21485
# ZTE MF286D
# ZTE MF289F
19d21489
# ZTE MF286R
1bc71040
# Telit LM940
1bc71201
# Telit LE910-EUG
1e0e9000
# SIMCOM SIM7906
# SIMCOM SIM8200EA-M2
1e0e9001
# SIMCOM SIM7906
# SIMCOM SIM8200EA-M2
1e0e9003
# SIMCOM SIM7906
# SIMCOM SIM8200EA-M2
1e2d00b3
# Cinterion MV31-W
# Thales MV31-W
# T99W175
1e2d00b7
# Cinterion MV31-W
# Thales MV31-W
# T99W175
20202033
# BroadMobi BM806U
2c7c0125
# Quectel EC20
# Quectel EC25
2c7c0306
# Quectel EP06
# Quectel EG06
# Quectel EM06
2c7c030b
# Quectel EP06
# Quectel EG06
# Quectel EM06
2c7c0512
# Quectel EG18-EA
# Quectel EM12-G
# Quectel EM160R-GL
2c7c0620
# Quectel EG18-EA
# Quectel EM12-G
# Quectel EM160R-GL
2c7c0800
# Quectel RG500Q-EA
# Quectel RG502Q-EA
# Quectel RM500U-CNV
# Quectel RM520N-GL
2c7c0801
# Quectel RG500Q-EA
# Quectel RG502Q-EA
# Quectel RM500U-CNV
# Quectel RM520N-GL
2c7c0900
# Quectel RG500Q-EA
# Quectel RG502Q-EA
# Quectel RM500U-CNV
# Quectel RM520N-GL
2c7c6026
# Quectel EC200T-EU
2cb70007
# Fibocom L850
2cb70104
# Fibocom FM150
2cb70104
# Fibocom FM190
2cd20001
# Mikrotik R11e-LTE
# COPS
2cd20004
# Mikrotik R11e-LTE6
413c81b1
# DW5809e Dell Wireless 5809e Gobi 4G LTE Mobile Broadband Card (EM7305)
# DW5811e Snapdragon X7 LTE (EM7455B)
# Sierra Wireless EM7455
413c81b6
# DW5809e Dell Wireless 5809e Gobi 4G LTE Mobile Broadband Card (EM7305)
# DW5811e Snapdragon X7 LTE (EM7455B)
# Sierra Wireless EM7455
413c81d7
# DW5821e Snapdragon X20 LTE
8087095a
# Fibocom L860
```
</details>
### Supported pci devices (serial mode):
<details>
<summary>Pokaż | Show me</summary>
``` bash
105be0b0
# Dell DW5930e
# Foxconn T99W175
17cb0306
# Quectel RM500Q-AE
17cb0308
# Quectel RG500Q-EA
# Quectel RG502Q-EA
# Quectel RM500U-CNV
# Quectel RM520N-GL
# Quectel RM520N-GLAP
17cb5201
# Quectel RG500Q-EA
# Quectel RG502Q-EA
# Quectel RM500U-CNV
# Quectel RM520N-GL
1eac1004
# Quectel RM520N-GLAP
```
</details>
### <img src="https://raw.githubusercontent.com/4IceG/Personal_data/master/dooffy_design_icons_EU_flags_United_Kingdom.png" height="24"> Design concept / <img src="https://raw.githubusercontent.com/4IceG/Personal_data/master/dooffy_design_icons_EU_flags_Poland.png" height="24"> Koncepcja wyglądu
> "Modem(s)" window / Okno Modem(-ów):
![](https://github.com/4IceG/Personal_data/blob/master/zrzuty/modemdata/mdlast3a.png?raw=true)
<img width="1045" height="779" alt="dark" src="https://github.com/user-attachments/assets/6730506d-0103-4e0e-a63d-a6b6d4698f53" />
![](https://github.com/4IceG/Personal_data/blob/master/zrzuty/modemdata/mdlast3b.png?raw=true)
![](https://github.com/4IceG/Personal_data/blob/master/zrzuty/modemdata/BTSearch.png?raw=true)
![](https://github.com/4IceG/Personal_data/blob/master/zrzuty/modemdata/modemdata_upd_modal.png?raw=true)
> "Diagnostics" window / Okno diagnostyki:
![](https://github.com/4IceG/Personal_data/blob/master/zrzuty/modemdata/mdd1.png?raw=true)
> "Defined modems" window / Okno zdefiniowanych modem(-ów):
![](https://github.com/4IceG/Personal_data/blob/master/zrzuty/modemdata/mlaswer.png?raw=true)
![](https://github.com/4IceG/Personal_data/blob/master/zrzuty/modemdata/masdad.png?raw=true)
![](https://github.com/4IceG/Personal_data/blob/master/zrzuty/modemdata/md3d.png?raw=true)
> "Configuration" window / Okno konfiguracji pakietu:
![](https://github.com/4IceG/Personal_data/blob/master/zrzuty/modemdata/Package%20configuration%20bts.png?raw=true)
> "Package update and support" window / Okno aktualizacji pakietu i wsparcia:
![](https://github.com/4IceG/Personal_data/blob/master/zrzuty/modemdata/md5b.png?raw=true)
### <img src="https://raw.githubusercontent.com/4IceG/Personal_data/master/dooffy_design_icons_EU_flags_United_Kingdom.png" height="24"> Thanks to / <img src="https://raw.githubusercontent.com/4IceG/Personal_data/master/dooffy_design_icons_EU_flags_Poland.png" height="24"> Podziękowania dla
- [obsy (Cezary Jackiewicz)](https://github.com/obsy)
- [Users of the eko.one.pl forum](https://eko.one.pl/forum/viewtopic.php?id=20096)
-21
View File
@@ -1,21 +0,0 @@
MIT License
Copyright (c) 2025 Rafał Wabik (IceG)
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.