IceG napisał/a:@Focus możesz mi ten "klon" gdzieś wystawić, chciałbym sobie potestować.
Inna sprawa jak ten status portów zapisać ładnie w naszym pięknym języku. Tutaj mamy UP/DOWN i polecenia
ip link set lanX down
ip link set lanX up
czy one tak dosłownie odpowiadają włączeniu/wyłączeniu portów..
Port status w include ma numerek 29, więc robisz np. 28_ports.js.
Wersja 1

Wersja 2

Kod to tylko odczyt danych (read-only), więc nie wprowadza żadnych zmian.
W konsoli działa np:
ip link set lan3 down (lub up)
lub
ubus call network.interface remove_device '{"interface":"lan", "name":"lan3"}' && ip link set lan3 down
----
ubus call network.interface add_device '{"interface":"lan", "name":"lan3"}' && ip link set lan3 up
lub
ifconfig lan3 down (lub up)
I tu pojawia się problem, bo to działa w konsoli, a nie w LuCi.
ip link set lan3 down jest poleceniem systemowym, więc uprawnienia ACL nic nie dadzą.
AI pisze mi, że w/w polecenia można wywołać przez "exec" lub własny backend ubus.
Niżej kody do obu wersji:
Wersja 1
'use strict';
'require view';
'require rpc';
const REFRESH_MS = 2000;
const callGetBuiltinEthernetPorts = rpc.declare({
object: 'luci',
method: 'getBuiltinEthernetPorts',
expect: { result: [] }
});
const callDeviceStatus = rpc.declare({
object: 'network.device',
method: 'status',
params: ['name']
});
let prevStats = {};
function calcRate(dev, stats) {
const now = Date.now();
const prev = prevStats[dev];
prevStats[dev] = {
rx: stats.rx_bytes || 0,
tx: stats.tx_bytes || 0,
t: now
};
if (!prev)
return { rx: 0, tx: 0 };
const dt = (now - prev.t) / 1000;
if (dt <= 0)
return { rx: 0, tx: 0 };
return {
rx: (stats.rx_bytes - prev.rx) / dt,
tx: (stats.tx_bytes - prev.tx) / dt
};
}
function getPortStatus(port) {
if (!port.device || port.device === 'br-lan')
return Promise.resolve(null);
return callDeviceStatus(port.device)
.catch(() => null)
.then(res => {
if (!res || !res.carrier)
return { activity: 'down' };
const stats = res.statistics || {};
const rate = calcRate(port.device, stats);
if (rate.rx > 0 || rate.tx > 0)
return { activity: 'up' };
return { activity: 'idle' };
});
}
function renderRow(port, idx) {
if (!port.device)
return null;
const rowStyle = (idx % 2) ? 'cbi-rowstyle-2' : 'cbi-rowstyle-1';
const tdStyle = 'border-right:1px solid #ccc; border-bottom:1px solid #ccc; padding:4px 6px;';
return E('tr', { 'class': rowStyle }, [
E('td', { style: tdStyle }, port.device),
E('td', { style: tdStyle }, port.activity.toUpperCase())
]);
}
return view.extend({
title: 'Ports Monitor',
load: function () {
return callGetBuiltinEthernetPorts().then(ports =>
Promise.all(ports.map(p =>
getPortStatus(p).then(st => Object.assign(p, st || {}))
))
);
},
render: function (ports) {
const table = E('table', { style: 'border-collapse:collapse; width:100%;' });
const thStyle = 'border-right:1px solid #ccc; border-bottom:1px solid #ccc; padding:4px 6px; font-weight:bold;';
const thead = E('thead', {}, [
E('tr', {}, [
E('th', { style: thStyle }, 'Port'),
E('th', { style: thStyle }, 'Status')
])
]);
const tbody = E('tbody');
table.appendChild(thead);
table.appendChild(tbody);
ports.forEach((p, i) => {
const row = renderRow(p, i);
if (row)
tbody.appendChild(row);
});
return E('div', { 'class': 'cbi-section' }, [
E('div', { 'class': 'cbi-section-node' }, [ table ])
]);
},
onAfterRender: function () {
const self = this;
setInterval(() => {
self.load().then(data => {
const tbody = document.querySelector('.cbi-section-node tbody');
if (!tbody)
return;
tbody.innerHTML = '';
data.forEach((p, i) => {
const row = renderRow(p, i);
if (row)
tbody.appendChild(row);
});
});
}, REFRESH_MS);
},
handleSave: null,
handleSaveApply: null,
handleReset: null
});
Wersja 2
'use strict';
'require view';
'require rpc';
const REFRESH_MS = 2000;
const callGetBuiltinEthernetPorts = rpc.declare({
object: 'luci',
method: 'getBuiltinEthernetPorts',
expect: { result: [] }
});
const callDeviceStatus = rpc.declare({
object: 'network.device',
method: 'status',
params: ['name']
});
let prevStats = {};
let totalTransfer = {};
function parseSpeedDuplex(speed) {
if (!speed || typeof speed !== 'string')
return { speed: '-', duplex: '-' };
const m = speed.match(/^(\d+)([FH])$/);
if (!m) return { speed: speed, duplex: '-' };
return {
speed: parseInt(m[1], 10) + ' Mbps',
duplex: m[2] === 'F' ? 'full' : 'half'
};
}
function formatMbps(val) {
if (!isFinite(val)) return '0.0';
return (val * 8 / 1e6).toFixed(1);
}
function formatGB(val) {
if (!isFinite(val)) return '0.00';
return (val / 1e9).toFixed(2);
}
function formatErrors(val) {
if (!isFinite(val) || val === 0) return '0';
return val > 10000 ? formatGB(val) : val.toLocaleString();
}
function calcRate(dev, stats) {
const now = Date.now();
const prev = prevStats[dev];
prevStats[dev] = { rx: stats.rx_bytes, tx: stats.tx_bytes, t: now };
if (!prev) return { rx: 0, tx: 0 };
const dt = (now - prev.t) / 1000;
if (dt <= 0) return { rx: 0, tx: 0 };
return {
rx: (stats.rx_bytes - prev.rx) / dt,
tx: (stats.tx_bytes - prev.tx) / dt
};
}
function updateTotal(dev, stats) {
if (!totalTransfer[dev]) totalTransfer[dev] = { rx: 0, tx: 0 };
totalTransfer[dev].rx = stats.rx_bytes;
totalTransfer[dev].tx = stats.tx_bytes;
return totalTransfer[dev];
}
function getPortStatus(port) {
if (!port.device || port.device === 'br-lan')
return Promise.resolve(null);
return callDeviceStatus(port.device)
.catch(() => null)
.then(res => {
if (res?.operstate === 'unknown' && res?.carrier)
res.operstate = 'up';
if (!res || !res.carrier) {
return {
speed: '-',
duplex: '-',
rx: '0.0',
tx: '0.0',
totalGB: '0.00',
activity: 'down',
mtu: '-',
errors: '0',
rx_rate: 0,
tx_rate: 0
};
}
const parsed = parseSpeedDuplex(res.speed);
const stats = res.statistics || {};
const rate = calcRate(port.device, stats);
const total = updateTotal(port.device, stats);
const activity = (rate.rx > 0 || rate.tx > 0) ? 'active' : 'idle';
const errors = (stats.rx_errors || 0) + (stats.tx_errors || 0);
return {
speed: parsed.speed,
duplex: parsed.duplex,
rx: formatMbps(rate.rx),
tx: formatMbps(rate.tx),
totalGB: formatGB(total.rx + total.tx),
activity: activity,
rx_rate: rate.rx,
tx_rate: rate.tx,
mtu: res.mtu || '-',
errors: formatErrors(errors)
};
});
}
// animacja migania diody
const styleEl = document.createElement('style');
styleEl.textContent = `
@keyframes blink {
0%, 50%, 100% { opacity: 1; }
25%, 75% { opacity: 0.2; }
}`;
document.head.appendChild(styleEl);
// funkcja generująca diodę aktywności
function renderActivityLight(rate, portStatus) {
if (!portStatus || portStatus === 'down') {
// PORT DOWN › szara
return E('span', { style: `
display:inline-block;
width:10px; height:10px;
border-radius:50%; margin-right:4px;
background-color:#888;
`});
}
if (portStatus === 'idle') {
// IDLE › pomarańczowa, stała
return E('span', { style: `
display:inline-block;
width:10px; height:10px;
border-radius:50%; margin-right:4px;
background-color:#fa0;
`});
}
if (rate > 0 && rate < 12500) {
// Minimalny transfer › zielona, stała
return E('span', { style: `
display:inline-block;
width:10px; height:10px;
border-radius:50%; margin-right:4px;
background-color:#0c0;
`});
}
// Transfer powyżej 0.1 Mbps › zielona, miganie
// cztery poziomy
let duration = 1.5; // domyślnie wolne miganie
if (rate < 1e6) { // 0.1–1 Mbps › poziom 1
duration = 1.5;
} else if (rate < 10e6) { // 1–10 Mbps › poziom 2
duration = 1.0;
} else if (rate < 100e6) { // 10–100 Mbps › poziom 3
duration = 0.5;
} else { // 100 Mbps–1 Gb/s › poziom 4
duration = 0.2;
}
return E('span', { style: `
display:inline-block;
width:10px; height:10px;
border-radius:50%; margin-right:4px;
background-color:#0c0;
animation: blink ${duration}s infinite;
`});
}
// każda komórka z border-bottom i border-right
function renderRow(port, idx) {
if (!port.device) return null;
const rowStyle = (idx % 2) ? 'cbi-rowstyle-2' : 'cbi-rowstyle-1';
const tdStyle = 'border-right:1px solid #ccc; border-bottom:1px solid #ccc; padding:2px 6px;';
return E('tr', { 'class': rowStyle }, [
E('td', { style: tdStyle }, port.device),
E('td', { style: tdStyle }, port.speed),
E('td', { style: tdStyle }, port.duplex),
E('td', { style: tdStyle }, port.rx),
E('td', { style: tdStyle }, port.tx),
E('td', { style: tdStyle }, port.totalGB),
E('td', { style: tdStyle }, port.mtu),
E('td', { style: tdStyle }, port.errors),
E('td', { style: tdStyle }, [
renderActivityLight(port.rx_rate + port.tx_rate, port.activity),
port.activity
])
]);
}
return view.extend({
title: 'Ports Monitor',
load: function () {
return callGetBuiltinEthernetPorts().then(ports =>
Promise.all(ports.map(p =>
getPortStatus(p).then(st => Object.assign(p, st || {}))
))
);
},
render: function (ports) {
const table = E('table', { style: 'border-collapse:collapse; width:100%;' });
const tdHeaderStyle = 'border-right:1px solid #ccc; border-bottom:1px solid #ccc; padding:4px 6px; font-weight:bold;';
const thead = E('thead', {}, [
E('tr', {}, [
E('th', { style: tdHeaderStyle }, 'Port'),
E('th', { style: tdHeaderStyle }, 'Speed'),
E('th', { style: tdHeaderStyle }, 'Duplex'),
E('th', { style: tdHeaderStyle }, 'Upload Mbps'),
E('th', { style: tdHeaderStyle }, 'Download Mbps'),
E('th', { style: tdHeaderStyle }, 'Total GB'),
E('th', { style: tdHeaderStyle }, 'MTU'),
E('th', { style: tdHeaderStyle }, 'Errors'),
E('th', { style: tdHeaderStyle }, 'Activity')
])
]);
const tbody = E('tbody');
table.appendChild(thead);
table.appendChild(tbody);
ports.forEach((p, i) => {
const row = renderRow(p, i);
if (row) tbody.appendChild(row);
});
return E('div', { 'class': 'cbi-section' }, [
E('div', { 'class': 'cbi-section-node' }, [ table ])
]);
},
onAfterRender: function () {
const self = this;
setInterval(() => {
self.load().then(data => {
const tbody = document.querySelector('.cbi-section-node tbody');
if (!tbody) return;
tbody.innerHTML = '';
data.forEach((p, i) => {
const row = renderRow(p, i);
if (row) tbody.appendChild(row);
});
});
}, REFRESH_MS);
},
handleSave: null,
handleSaveApply: null,
handleReset: null
});