Files

2292 lines
89 KiB
JavaScript

'use strict';
'require baseclass';
'require form';
'require fs';
'require view';
'require ui';
'require uci';
'require poll';
'require dom';
/*
Copyright 2025-2026 Rafał Wabik - IceG - From eko.one.pl forum
MIT License
*/
let refresh = {
interval: 5, // normal => 5s
labelEl: null,
selectEl: null,
lastSec: null,
remaining: null
};
let _latestJsonByIndex = {};
let _mainPollWasActive = false;
let _sigModalOpen = false;
let _sigModalTimer = null;
let _sigInflight = false;
let _sigModalIndex = null;
function addDarkModeStyles() {
const style = document.createElement('style');
style.type = 'text/css';
style.textContent = `
:root {
/* normal */
--connection-connected-bg: #34c759;
--connection-disconnected-bg: #7f8c8d;
--connection-text: #ffffff;
--signal-excellent-bg: #34c759;
--signal-good-bg: #FFFF00;
--signal-fair-bg: #FFA500;
--signal-poor-bg: #e74c3c;
--signal-nodata-bg: #7f8c8d;
--signal-text-light: #000000;
--signal-text-dark: #ffffff;
--signal-shadow: 3px 3px 8px rgba(0, 0, 0, 0.7);
--signal-yellow-shadow: 2px 2px 5px rgba(128, 128, 128, 0.5);
}
:root[data-darkmode="true"] {
--connection-connected-bg: rgba(46, 204, 113, 0.28);
--connection-disconnected-bg: rgba(255, 255, 255, 0.12);
--connection-text: #e5e7eb;
--signal-excellent-bg: rgba(52, 199, 89, 0.25);
--signal-good-bg: rgba(255, 255, 0, 0.22);
--signal-fair-bg: rgba(255, 165, 0, 0.25);
--signal-poor-bg: rgba(231, 76, 60, 0.25);
--signal-nodata-bg: rgba(255, 255, 255, 0.1);
--signal-text-light: #e5e7eb;
--signal-text-dark: #e5e7eb;
--signal-shadow: 0 1px 2px rgba(0,0,0,.35), 0 2px 6px rgba(0,0,0,.22);
--signal-yellow-shadow: 0 1px 2px rgba(0,0,0,.35), 0 2px 6px rgba(0,0,0,.22);
}
.connection-status {
background-color: var(--connection-disconnected-bg);
color: var(--connection-text);
text-shadow: 0 1px 2px rgba(0,0,0,.4), 0 2px 6px rgba(0,0,0,.25);
padding: 2px 5px;
border-radius: 4px;
min-width: 92px;
max-width: 92px;
text-align: center;
white-space: nowrap;
font-weight: 500;
font-size: 12px;
display: inline-block;
border: 1px solid transparent;
}
:root[data-darkmode="true"] .connection-status--connected {
background-color: var(--connection-connected-bg);
border: 1px solid rgba(46, 204, 113, 0.6);
}
:root[data-darkmode="true"] .connection-status--disconnected {
background-color: var(--connection-disconnected-bg);
border: 1px solid rgba(255, 255, 255, 0.3);
}
.connection-status--connected {
background-color: var(--connection-connected-bg);
}
.connection-status--disconnected {
background-color: var(--connection-disconnected-bg);
}
.signal-badge {
padding: 3px 10px;
border-radius: 12px;
min-width: 80px;
width: 80px;
text-align: center;
white-space: nowrap;
font-weight: 500;
font-size: 12px;
display: inline-block;
border: 1px solid transparent;
}
.signal-badge--excellent {
background-color: var(--signal-excellent-bg);
color: var(--signal-text-dark);
text-shadow: var(--signal-shadow);
}
.signal-badge--good {
background-color: var(--signal-good-bg);
color: var(--signal-text-light);
text-shadow: var(--signal-yellow-shadow);
}
.signal-badge--fair {
background-color: var(--signal-fair-bg);
color: var(--signal-text-dark);
text-shadow: var(--signal-shadow);
}
.signal-badge--poor {
background-color: var(--signal-poor-bg);
color: var(--signal-text-dark);
text-shadow: var(--signal-shadow);
}
.signal-badge--nodata {
background-color: var(--signal-nodata-bg);
color: var(--signal-text-dark);
text-shadow: var(--signal-shadow);
}
.table th, .table td {
min-width: 0;
padding: 4px 1px;
}
.signal-cell {
display: flex;
align-items: center;
gap: 6px;
overflow: hidden;
box-sizing: border-box;
}
.signal-cell .signal-badge {
flex: 0 0 80px;
min-width: 56px;
max-width: 80px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
box-sizing: border-box;
}
.signal-cell .signal-label {
flex: 1 1 0;
min-width: 55px;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
font-size: 12px;
line-height: 1.2;
}
:root[data-darkmode="true"] .signal-badge--excellent {
border: 1px solid rgba(52, 199, 89, 0.6);
}
:root[data-darkmode="true"] .signal-badge--good {
border: 1px solid rgba(255, 255, 0, 0.5);
}
:root[data-darkmode="true"] .signal-badge--fair {
border: 1px solid rgba(255, 165, 0, 0.6);
}
:root[data-darkmode="true"] .signal-badge--poor {
border: 1px solid rgba(231, 76, 60, 0.6);
}
:root[data-darkmode="true"] .signal-badge--nodata {
border: 1px solid rgba(255, 255, 255, 0.4);
}
/* Fix checkbox vertical alignment issue across different themes */
label.cbi-checkbox {
display: inline-flex !important; /* Use flexbox for alignment */
align-items: center !important; /* Vertically center checkbox and text */
gap: 0.25em !important; /* Space between checkbox and text */
line-height: 1.6 !important; /* Consistent line height */
vertical-align: middle !important; /* Align with surrounding text */
min-height: 24px !important; /* Minimum height for touch targets */
}
label.cbi-checkbox input[type="checkbox"] {
margin: 0 !important; /* Remove default margins */
margin-right: 0.5em !important; /* Space after checkbox */
vertical-align: middle !important; /* Align with text baseline */
flex-shrink: 0 !important; /* Prevent checkbox from shrinking */
position: relative !important; /* Keep relative for focus effects */
top: 0 !important; /* Reset vertical offset (fix Argon 6.4px, Bootstrap 2px) */
bottom: 0 !important; /* Reset bottom offset */
left: 0 !important; /* Reset horizontal offset */
right: 0 !important; /* Reset right offset */
}
/* Completely hide custom checkbox label and its pseudo-elements to prevent sweep animation */
label.cbi-checkbox > label[for] {
display: none !important; /* Hide the element */
visibility: hidden !important; /* Make invisible */
opacity: 0 !important; /* Fully transparent */
pointer-events: none !important; /* Disable mouse events */
max-width: 0 !important; /* Constrain width */
max-height: 0 !important; /* Constrain height */
overflow: hidden !important; /* Hide overflow content */
}
label.cbi-checkbox > label[for]::before,
label.cbi-checkbox > label[for]::after {
display: none !important; /* Hide pseudo-elements */
content: none !important; /* Remove content */
visibility: hidden !important; /* Make invisible */
max-width: 0 !important; /* Constrain width to prevent sweep animation */
max-height: 0 !important; /* Constrain height to prevent sweep animation */
}
/* Fix ifacebox card layout - make body fill the entire card */
.ifacebox {
min-width: 23% !important;
display: flex !important; /* Use flexbox layout for consistent card structure */
flex-direction: column !important; /* Stack header and body vertically */
}
.ifacebox-body {
flex: 1 !important; /* Fill remaining vertical space to match card height */
width: 100% !important; /* Take full width of parent (fix width compression) */
box-sizing: border-box !important; /* Include padding in width calculation */
}
.ifacebox-head port-label {
width: 100% !important; /* Take full width of parent */
box-sizing: border-box !important; /* Include padding in width calculation */
}
/* Fix progress bar width consistency - use theme's default layout */
.cbi-modal .cbi-progressbar {
width: 100% !important; /* Take full available width */
box-sizing: border-box !important; /* Include padding in width */
}
/* Fix modal window width for signal levels dialog */
.cbi-modal {
max-width: 600px !important; /* Limit modal width to 600px */
}
/* On narrow screens themes turn table rows into wrapping flex cards with
about 10% per cell, which squeezes the band tables' 5-8 cells and their
signal badges onto one line. Give each cell room for a badge and its
label: two per line on phones, four on tablets. Ignored while the row
is a normal table row. */
.table[id^="lteTable_"] .tr .td,
.table[id^="wcdmaTable_"] .tr .td {
flex: 1 1 22%;
min-width: 150px;
}
/* Increase label width to prevent text wrapping on desktop */
@media screen and (min-width: 481px) {
.cbi-modal .cbi-value-title {
min-width: 180px !important; /* Minimum width for single-line labels */
white-space: nowrap !important; /* Prevent text wrapping */
}
}
`;
document.head.appendChild(style);
}
function setUpdateMessage(el, sec) {
if (!el) return;
if (sec < 0) {
el.textContent = _('Disabled.');
return;
}
if (sec === 0) {
el.textContent = '';
el.appendChild(E('em', { 'class': 'spinning' }, _('Refreshing') + '..'));
return;
}
let tmpl = _('Updating again in %s second(s).');
let parts = String(tmpl).split('%s');
el.textContent = '';
el.appendChild(document.createTextNode(parts[0] || ''));
el.appendChild(E('b', {}, String(sec)));
el.appendChild(document.createTextNode(parts[1] || ''));
}
async function getSavedUpdateInterval() {
try {
const raw = await uci.get('modemdata', '@modemdata[0]', 'updtime');
let n = raw != null ? parseInt(raw, 10) : 5;
if (!Number.isFinite(n)) n = 5;
return (n === -1 || n >= 0) ? n : 5;
} catch (e) {
console.error('getSavedUpdateInterval error:', e);
return 5;
}
}
async function loadUCInterval(opts = { applyToGlobal: true }) {
const interval = await getSavedUpdateInterval();
const selectElement = document.getElementById('selectInterval');
if (selectElement) selectElement.value = String(interval);
refresh.interval = interval;
if (opts.applyToGlobal) {
if (interval > 0) {
refresh.remaining = interval;
refresh.lastSec = null;
if (!poll.active()) poll.start();
} else {
poll.stop();
refresh.remaining = null;
setUpdateMessage(refresh.labelEl, -1);
}
}
return interval;
}
// 1-sec tick
function updateDataTick(runFetchFn) {
let tick = poll.tick || 0;
let interval = refresh.interval > 0 ? refresh.interval : 0;
let sec = interval > 0 ? interval - (tick % interval || interval) : -1;
if (refresh.labelEl && sec !== refresh.lastSec) {
setUpdateMessage(refresh.labelEl, sec);
refresh.lastSec = sec;
}
if (interval && sec === 0 && typeof runFetchFn === 'function') {
return runFetchFn();
}
return Promise.resolve();
}
function popTimeout(a, message, timeout, severity) {
ui.addTimeLimitedNotification(a, message, timeout, severity)
}
function clickToSelectInterval(ev) {
let v = parseInt(ev.target.value, 10);
if (isNaN(v)) return;
refresh.interval = v;
uci.set('modemdata', '@modemdata[0]', 'updtime', v.toString());
uci.save();
uci.apply();
if (v > 0) {
refresh.remaining = v;
refresh.lastSec = null;
if (!poll.active()) poll.start();
} else {
poll.stop();
refresh.remaining = null;
setUpdateMessage(refresh.labelEl, -1);
}
}
function clamp(n, min, max){ return Math.max(min, Math.min(max, n)); }
function toNumber(val){
if (val == null) return null;
let s = String(val).replace(',', '.');
let m = s.match(/-?\d+(\.\d+)?/);
return m ? parseFloat(m[0]) : null;
}
function pctFromRange(val, min, max){
if (val == null) return 0;
return clamp(Math.round(((val - min) / (max - min)) * 100), 0, 100);
}
const SIGNAL_RANGES = {
RSSI: { min: -110, max: -50, unit: 'dBm' },
RSRP: { min: -120, max: -80, unit: 'dBm' },
RSRQ: { min: -20, max: -3, unit: 'dB' },
SINR: { min: -5, max: 25, unit: 'dB' },
SNR: { min: 0, max: 30, unit: 'dB' },
RSSI_wcdma: { min: -110, max: -50, unit: 'dBm' },
RSCP: { min: -120, max: -60, unit: 'dBm' },
ECIO: { min: -24, max: 0, unit: 'dB' }
};
const SIG_COLORS = {
green: '#34c759',
yellow: '#FFFF00',
orange: '#FFA500',
red: '#e74c3c',
gray: '#7f8c8d'
};
function getCSQLabel(csqVal) {
let n = toNumber(csqVal);
if (n == null || n === 99) return { label: _('No data'), color: 'gray' };
if (n >= 20) return { label: _('Excellent'), color: 'green' };
if (n >= 15) return { label: _('Good'), color: 'yellow' };
if (n >= 10) return { label: _('Fair'), color: 'orange' };
return { label: _('Poor'), color: 'red' };
}
function _progressValueDiv(valueId, labelText, descr, barId, showInitially) {
return E('div', {
'class': 'cbi-value',
'id': valueId,
'style': (showInitially === false ? 'display:none;' : '')
}, [
E('label', { 'class': 'cbi-value-title' }, [
_(labelText)
]),
E('div', { 'class': 'cbi-value-field' }, [
E('div', {
'id': barId,
'class': 'cbi-progressbar',
'title': '-',
'style': 'width:100%;max-width:200px;'
}, E('div')),
E('div', {
'id': barId + '_label',
'style': 'text-align:left;font-size:11px;opacity:0.8;margin-top:2px;'
}, _(descr))
])
]);
}
function _qualityFor(metricKey, rawValue){
if (metricKey === 'CSQ') return getCSQLabel(rawValue);
let v = rawValue;
if (v == null || String(v).trim()==='') return { label: _('No data'), color: 'gray' };
return getSignalLabel(v, metricKey);
}
function _setProgressBarRich(barId, metricKey, rawValue){
let wrap = document.getElementById(barId);
if (!wrap) return;
let inner = wrap.firstElementChild || null;
if (!inner) return;
if (!wrap._maxWidthSet) {
wrap.style.maxWidth = '200px';
wrap._maxWidthSet = true;
}
let oldHint = wrap.querySelector('.bar-hint');
if (oldHint) oldHint.remove();
let percent = 0;
if (metricKey === 'CSQ') {
let v = toNumber(rawValue);
if (v == null || v === 99) percent = 0;
else percent = clamp(Math.round((v / 31) * 100), 0, 100);
} else {
let rng = SIGNAL_RANGES[metricKey];
let numeric = toNumber(rawValue);
percent = rng ? pctFromRange(numeric, rng.min, rng.max) : 0;
}
let q = _qualityFor(metricKey, rawValue);
let colorHex = SIG_COLORS[q.color] || SIG_COLORS.gray;
if (inner._lastWidth !== percent) {
inner.style.width = percent + '%';
inner._lastWidth = percent;
}
if (inner._lastColor !== colorHex) {
inner.style.backgroundColor = colorHex;
inner._lastColor = colorHex;
}
let unit = '';
if (metricKey === 'RSSI' || metricKey === 'RSRP' || metricKey === 'RSCP') unit = 'dBm';
else if (metricKey === 'RSRQ' || metricKey === 'SINR' || metricKey === 'SNR' || metricKey === 'ECIO') unit = 'dB';
let vtxt = (rawValue == null || String(rawValue).trim()==='') ? '-' : String(rawValue);
if (unit && vtxt !== '-' && !/\bdB(m)?\b/i.test(vtxt)) vtxt += ' ' + unit;
let titleTxt;
if (vtxt === '-' && q.label === _('No data')) {
titleTxt = q.label;
} else {
titleTxt = '%s'.format(vtxt) + ' | ' + q.label;
}
wrap.setAttribute('title', titleTxt);
let labelDiv = document.getElementById(barId + '_label');
if (labelDiv) {
let currentLabel = labelDiv.textContent || '';
if (!currentLabel) {
labelDiv.textContent = vtxt + ' (' + q.label + ')';
}
}
}
async function _readSignalsForIndex(idx, forceFresh) {
if (idx == null) return null;
let cached = _latestJsonByIndex[idx];
if (forceFresh || !cached) {
try {
await uci.load('defmodems');
let defs = uci.sections('defmodems', 'defmodems') || [];
let m = defs[idx];
if (!m) return null;
let res = '';
if (m.modemdata === 'serial' || m.modemdata === 'ecm')
res = await L.resolveDefault(fs.exec_direct('/usr/bin/md_serial_ecm', [m.comm_port, m.network, m.forced_plmn || '-']));
else if (m.modemdata === 'uqmi')
res = await L.resolveDefault(fs.exec_direct('/usr/bin/md_uqmi', [m.comm_port, m.network, m.forced_plmn || '-', m.onproxy || '-']));
else if (m.modemdata === 'mm')
res = await L.resolveDefault(fs.exec_direct('/usr/bin/md_modemmanager', [m.comm_port, m.network, m.forced_plmn || '-']));
if (!res) return null;
let jsonraw = JSON.parse(res.replace(/[\u0000-\u001F\u007F-\u009F]/g, ""));
let arr = Object.values(jsonraw);
if (!arr || arr.length < 3 || !arr[2]) return null;
cached = { json0: arr[0], json1: arr[1], json2: arr[2] };
_latestJsonByIndex[idx] = {
json0: arr[0],
json1: arr[1],
json2: arr[2],
hash: JSON.stringify(arr[2]).length + '',
ts: Date.now()
};
} catch(e){
console.error('signals read error:', e);
return null;
}
}
let j2 = cached.json2 || {};
let addon = j2.addon || [];
let byKey = function(k){ let o = addon.find(function(i){ return i.key===k; }); return o ? o.value : null; };
let csqRaw = (j2 && (j2.csq != null)) ? j2.csq : null;
function fmt(unit, v) {
if (v == null || String(v).trim()==='') return null;
let s = String(v);
if (unit && !/\bdB(m)?\b/i.test(s)) s = s + ' ' + unit;
return s;
}
return {
MODE: j2.mode || '',
CSQ: csqRaw,
RSSI: fmt('dBm', byKey('RSSI')),
RSRP: fmt('dBm', byKey('RSRP') || byKey('RSRP (avg)')),
RSRQ: fmt('dB', byKey('RSRQ')),
SINR: fmt('dB', byKey('SINR')),
SNR: fmt('dB', byKey('SNR')),
RSCP: fmt('dBm', byKey('RSCP')),
ECIO: fmt('dB', byKey('ECIO'))
};
}
function _csqFromRssiFallback(rssiRaw) {
let rssi = toNumber(rssiRaw);
if (rssi == null || isNaN(rssi)) return null;
let csq = Math.round((rssi + 113) / 2);
return clamp(csq, 0, 31);
}
async function _updateBasicSignalsModal(){
if (!_sigModalOpen) return;
if (_sigInflight) return;
if (_sigModalIndex == null) return;
_sigInflight = true;
try {
let d = await _readSignalsForIndex(_sigModalIndex, true);
if (!d) return;
let csqEmpty = (d.CSQ == null || String(d.CSQ).trim() === '' || String(d.CSQ) === '99');
if (csqEmpty) {
let csqCalc = _csqFromRssiFallback(d.RSSI);
if (csqCalc != null) {
d.CSQ = String(csqCalc);
}
}
_setProgressBarRich('bs_bar_csq', 'CSQ', d.CSQ);
let mode = (d.MODE || '').toUpperCase();
let isLTE5G = (mode.indexOf('LTE') >= 0 || mode.indexOf('5G') >= 0);
let show = function(id, vis){
let el = document.getElementById(id);
if (el) el.style.display = vis ? '' : 'none';
};
if (isLTE5G) {
show('bs_rssi', true); show('bs_rsrp', true); show('bs_rsrq', true); show('bs_sxx', true);
show('bs_w_rssi', false); show('bs_rscp', false); show('bs_ecio', false);
_setProgressBarRich('bs_bar_rssi', 'RSSI', d.RSSI);
_setProgressBarRich('bs_bar_rsrp', 'RSRP', d.RSRP);
_setProgressBarRich('bs_bar_rsrq', 'RSRQ', d.RSRQ);
let sType = (d.SINR != null && String(d.SINR).trim() !== '') ? 'SINR' :
((d.SNR != null && String(d.SNR ).trim() !== '') ? 'SNR' : 'SINR');
let sVal = (sType === 'SINR') ? d.SINR : d.SNR;
_setProgressBarRich('bs_bar_sxx', sType, sVal);
let sxxTitleLabel = document.querySelector('#bs_sxx .cbi-value-title');
if (sxxTitleLabel) {
sxxTitleLabel.firstChild.textContent = sType;
}
} else {
show('bs_rssi', false); show('bs_rsrp', false); show('bs_rsrq', false); show('bs_sxx', false);
show('bs_w_rssi', true); show('bs_rscp', true); show('bs_ecio', true);
_setProgressBarRich('bs_bar_w_rssi', 'RSSI_wcdma', d.RSSI);
_setProgressBarRich('bs_bar_rscp', 'RSCP', d.RSCP);
_setProgressBarRich('bs_bar_ecio', 'ECIO', d.ECIO);
}
} catch(e){
console.error('BasicSignals modal update error:', e);
} finally {
_sigInflight = false;
}
}
async function openCellIdModal(modemIndex) {
if (typeof modemIndex !== 'number') return;
let wasMainPollActive = poll.active();
if (wasMainPollActive) poll.stop();
try {
let d = await _readSignalsForIndex(modemIndex, true);
if (!d) {
if (wasMainPollActive && refresh.interval > 0 && !poll.active()) {
refresh.lastSec = null;
poll.start();
}
return;
}
let cached = _latestJsonByIndex[modemIndex];
if (!cached || !cached.json2) {
if (wasMainPollActive && refresh.interval > 0 && !poll.active()) {
refresh.lastSec = null;
poll.start();
}
return;
}
let json2 = cached.json2;
let cidDec = json2.cid_dec || '0';
let cidHex = json2.cid_hex || '0';
let cidHexClean = cidHex.replace(/^0x/i, '').toUpperCase();
let enbh = '', sech = '', enb = 0, sec = 0;
let cidHexCellmapper = '', cidDecCellmapper = '';
if (cidHexClean && cidHexClean.length >= 2) {
enbh = cidHexClean.slice(0, -2) || '0';
sech = cidHexClean.slice(-2);
cidHexCellmapper = enbh + ' ' + sech;
enb = parseInt(enbh, 16) || 0;
sec = parseInt(sech, 16) || 0;
cidDecCellmapper = enb + '/' + sec;
}
const fields = [
E('div', { 'class': 'cbi-value' }, [
E('label', { 'class': 'cbi-value-title' }, [ _('Cell ID (dec)') ]),
E('div', { 'class': 'cbi-value-field' },
E('input', { 'class': 'cbi-input-text', 'readonly': 'readonly', 'value': cidDec || '-' }, null)
)
]),
E('div', { 'class': 'cbi-value' }, [
E('label', { 'class': 'cbi-value-title' }, [ _('Cell ID (hex)') ]),
E('div', { 'class': 'cbi-value-field' },
E('input', { 'class': 'cbi-input-text', 'readonly': 'readonly', 'value': cidHex || '-' }, null)
)
]),
E('div', { 'class': 'cbi-value' }, [
E('label', { 'class': 'cbi-value-title' }, [ _('eNB ID (hex)') ]),
E('div', { 'class': 'cbi-value-field' },
E('input', { 'class': 'cbi-input-text', 'readonly': 'readonly', 'value': enbh || '-' }, null)
)
]),
E('div', { 'class': 'cbi-value' }, [
E('label', { 'class': 'cbi-value-title' }, [ _('Sector ID (hex)') ]),
E('div', { 'class': 'cbi-value-field' },
E('input', { 'class': 'cbi-input-text', 'readonly': 'readonly', 'value': sech || '-' }, null)
)
]),
E('div', { 'class': 'cbi-value' }, [
E('label', { 'class': 'cbi-value-title' }, [ _('eNB ID (dec)') ]),
E('div', { 'class': 'cbi-value-field' },
E('input', { 'class': 'cbi-input-text', 'readonly': 'readonly', 'value': enb.toString() || '-' }, null)
)
]),
E('div', { 'class': 'cbi-value' }, [
E('label', { 'class': 'cbi-value-title' }, [ _('Sector ID (dec)') ]),
E('div', { 'class': 'cbi-value-field' },
E('input', { 'class': 'cbi-input-text', 'readonly': 'readonly', 'value': sec.toString() || '-' }, null)
)
]),
E('div', { 'class': 'cbi-value' }, [
E('label', { 'class': 'cbi-value-title' }, [ _('Cell ID (hex) for CellMapper') ]),
E('div', { 'class': 'cbi-value-field' },
E('input', { 'class': 'cbi-input-text', 'readonly': 'readonly', 'value': cidHexCellmapper || '-' }, null)
)
]),
E('div', { 'class': 'cbi-value' }, [
E('label', { 'class': 'cbi-value-title' }, [ _('Cell ID (dec) for CellMapper') ]),
E('div', { 'class': 'cbi-value-field' },
E('input', { 'class': 'cbi-input-text', 'readonly': 'readonly', 'value': cidDecCellmapper || '-' }, null)
)
])
];
ui.showModal(_('Cell ID Information') + ' - ' + _('Modem') + ' #' + (modemIndex+1), [
E('div', { 'class': 'cbi-section' }, [
E('div', { 'class': 'cbi-section-descr' }, _('Detailed Cell ID information with formats for different tools and applications.')),
E('div', { 'class': 'cbi-section' },
E('div', {}, fields)
)
]),
E('div', { 'class': 'right' }, [
E('button', {
'class': 'btn',
'click': ui.createHandlerFn(this, function () {
ui.hideModal();
if (wasMainPollActive && refresh.interval > 0 && !poll.active()) {
refresh.lastSec = null;
poll.start();
}
})
}, _('Close'))
])
], 'cbi-modal');
} catch (err) {
ui.addNotification(null, E('p', {}, _('Error loading Cell ID data: ') + err.message), 'error');
if (wasMainPollActive && refresh.interval > 0 && !poll.active()) {
refresh.lastSec = null;
poll.start();
}
}
}
async function openBasicSignalsModal(modemIndex) {
if (typeof modemIndex !== 'number') return;
_mainPollWasActive = poll.active();
if (_mainPollWasActive) poll.stop();
_sigModalOpen = true;
_sigModalIndex = modemIndex;
const saved = await getSavedUpdateInterval();
const modalInterval = (saved > 0 ? saved : 5);
const refreshIntervalMs = modalInterval * 1000;
const signalValues = E('div', { 'class': 'cbi-section' }, [
_progressValueDiv('bs_csq', 'CSQ', _('(Signal Strength)'), 'bs_bar_csq', true),
_progressValueDiv('bs_rssi', 'RSSI', '(Received Signal Strength Indicator)', 'bs_bar_rssi', true),
_progressValueDiv('bs_rsrp', 'RSRP', '(Reference Signal Received Power)', 'bs_bar_rsrp', true),
_progressValueDiv('bs_rsrq', 'RSRQ', '(Reference Signal Received Quality)', 'bs_bar_rsrq', true),
_progressValueDiv('bs_sxx', 'SINR/SNR', '(Signal to Interference+Noise / Signal to Noise)', 'bs_bar_sxx', true),
_progressValueDiv('bs_w_rssi', 'RSSI', '(Received Signal Strength Indicator)', 'bs_bar_w_rssi', false),
_progressValueDiv('bs_rscp', 'RSCP', '(Received Signal Code Power)', 'bs_bar_rscp', false),
_progressValueDiv('bs_ecio', 'ECIO', '(Energy per Chip / Interference)', 'bs_bar_ecio', false)
]);
ui.showModal(_('Primary band signal levels') + ' - ' + _('Modem') + ' #' + (modemIndex+1), [
E('div', { 'class': 'cbi-section' }, [
E('div', { 'class': 'cbi-section-descr' },
_('Updating again in %s second(s).').format(modalInterval)
),
signalValues
]),
E('div', { 'class': 'right' }, [
E('button', {
'class': 'btn',
'click': ui.createHandlerFn(this, function () {
_sigModalOpen = false;
_sigModalIndex = null;
if (_sigModalTimer) { clearInterval(_sigModalTimer); _sigModalTimer = null; }
ui.hideModal();
if (_mainPollWasActive && refresh.interval > 0 && !poll.active()) {
refresh.lastSec = null;
poll.start();
}
})
}, _('Close'))
])
], 'cbi-modal');
setTimeout(_updateBasicSignalsModal, 100);
if (_sigModalTimer) { clearInterval(_sigModalTimer); }
_sigModalTimer = setInterval(_updateBasicSignalsModal, refreshIntervalMs);
}
/* BTS info download */
async function handleDownloadAction(evOrBtn) {
const LuCI = window.L;
let modemIndex = null;
const t = (evOrBtn && evOrBtn.target) ? evOrBtn.target : evOrBtn;
if (typeof evOrBtn === 'number') {
modemIndex = evOrBtn;
} else if (t) {
const dsIdx = t.getAttribute && t.getAttribute('data-modem-index');
if (dsIdx !== null && dsIdx !== '') modemIndex = parseInt(dsIdx, 10);
if (modemIndex == null) {
const id = t.id || '';
const m = id.match(/_(\d+)$/);
if (m) modemIndex = parseInt(m[1], 10);
}
}
if (modemIndex == null) return;
const cellElement = document.getElementById('cell_' + modemIndex);
if (!cellElement) return;
const cellValue = (cellElement.textContent || '').trim();
const cellDECNumeric = cellValue.split(/\s+/)[0] || '0';
if (parseInt(cellDECNumeric, 10) < 1) return;
/* --- API btsearch.pl --- */
try {
const result = await fs.exec_direct('/bin/uclient-fetch', [
'--header=Content-Type: application/json',
'--header=Accept: application/json',
'--header=Origin: https://btsearch.pl',
'--header=Referer: https://btsearch.pl/',
'--post-data={"query":"cid: ' + cellDECNumeric + ' ecid: ' + cellDECNumeric + '"}',
'-O', '-',
'https://btsearch.pl/api/v1/search?limit=100&sort=desc&sortBy=relevance'
]);
if (!result || !result.trim()) {
ui.addNotification(null, E('p', _('BTS search: no response from server.')), 'error');
return;
}
const api = JSON.parse(result.trim());
if (!api || !api.data || api.data.length === 0) {
ui.addNotification(null, E('p', _('BTS search: no results found.')), 'error');
return;
}
const station = api.data[0];
const loc = station.location || {};
const op = station.operator || {};
const cells = station.cells || [];
const matchedCell = cells.find(function(c) {
return c.details && (
c.details.ecid == parseInt(cellDECNumeric, 10) ||
c.details.cid == parseInt(cellDECNumeric, 10)
);
});
const det = matchedCell ? matchedCell.details : null;
const ratsSet = {};
cells.forEach(function(c) { if (c.rat) ratsSet[c.rat] = true; });
const rats = Object.keys(ratsSet).join(', ');
const btsLat = parseFloat(loc.latitude);
const btsLon = parseFloat(loc.longitude);
const hasCoords = !isNaN(btsLat) && !isNaN(btsLon);
function monthsAgo(dateStr) {
if (!dateStr) return '-';
const updated = new Date(dateStr);
const now = new Date();
let months = (now.getFullYear() - updated.getFullYear()) * 12
+ (now.getMonth() - updated.getMonth());
if (now.getDate() < updated.getDate()) months--;
if (months <= 0) return _('this month');
return months + ' ' + (months === 1 ? _('month ago') : _('months ago'));
}
function getTileConfig() {
if (document.documentElement.getAttribute('data-darkmode') === 'true') {
return {
url: 'https://{s}.basemaps.cartocdn.com/dark_all/{z}/{x}/{y}{r}.png',
attribution: '&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> &copy; <a href="https://carto.com/">CARTO</a>'
};
}
return {
url: 'https://tile.openstreetmap.org/{z}/{x}/{y}.png',
attribution: '&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a>'
};
}
const mapDivId = 'bts_modal_map_' + modemIndex + '_' + Date.now();
const mapDiv = E('div', {
'id': mapDivId,
'style': 'width: 100%; height: 260px; border-radius: 5px; margin-bottom: 10px; background: #e8e8e8; border: 1px solid var(--border-color-medium);'
});
function initBtsMap() {
const LL = window.LeafletL || window.L;
const container = document.getElementById(mapDivId);
if (!container) {
setTimeout(initBtsMap, 100);
return;
}
if (container.offsetWidth === 0 || container.offsetHeight === 0) {
setTimeout(initBtsMap, 100);
return;
}
try {
const btsMap = LL.map(mapDivId, {
zoomControl: true,
scrollWheelZoom: true
});
let tileLayer = null;
function applyTile() {
const tile = getTileConfig();
if (tileLayer) btsMap.removeLayer(tileLayer);
tileLayer = LL.tileLayer(tile.url, {
maxZoom: 19,
attribution: tile.attribution
}).addTo(btsMap);
}
applyTile();
const themeObserver = new MutationObserver(function() { applyTile(); });
themeObserver.observe(document.documentElement, { attributes: true, attributeFilter: ['data-darkmode'] });
const modalCleanup = new MutationObserver(function() {
if (!document.getElementById(mapDivId)) {
themeObserver.disconnect();
modalCleanup.disconnect();
}
});
modalCleanup.observe(document.body, { childList: true, subtree: true });
btsMap.setView([btsLat, btsLon], 16);
LL.marker([btsLat, btsLon])
.addTo(btsMap)
.bindPopup(
'<b>' + (op.name || '') + '</b><br>' +
(station.station_id || '') + '<br>' +
(loc.city || '') + (loc.address ? ', ' + loc.address : '')
)
.openPopup();
setTimeout(function() { btsMap.invalidateSize(); }, 300);
} catch(e) {
// map bugs
}
}
function loadLeafletAndInitMap() {
const leafletUrl = 'https://unpkg.com/leaflet@1.9.4/dist';
if (typeof window.LeafletLoaded !== 'undefined') {
requestAnimationFrame(function() { setTimeout(initBtsMap, 100); });
return;
}
const css = document.createElement('link');
css.rel = 'stylesheet';
css.type = 'text/css';
css.href = leafletUrl + '/leaflet.css';
document.getElementsByTagName('head')[0].appendChild(css);
const script = document.createElement('script');
script.type = 'text/javascript';
script.src = leafletUrl + '/leaflet.js';
script.onload = function() {
window.LeafletLoaded = true;
window.LeafletL = window.L;
window.L = LuCI;
requestAnimationFrame(function() { setTimeout(initBtsMap, 100); });
};
document.getElementsByTagName('head')[0].appendChild(script);
}
if (poll.active()) poll.stop();
ui.showModal(
E('span', {}, [
E('img', {
'src': LuCI.resource('icons/mybts.svg'),
'style': 'padding-left: 2px; height: 32px; width: auto; display: inline-block; vertical-align: middle;'
}),
' ',
_('BTS Information'),
E('hr')
]),
[
hasCoords ? E('div', { 'style': 'width: 100%; margin-bottom: 4px;' }, [ mapDiv ]) : E('span'),
E('div', { 'class': 'info-message' }, [
LuCI.itemlist(E('span'), [
_('Operator'), op.full_name || op.name || '-',
_('Station ID'), station.station_id || '-',
_('Location'), (loc.city || '-') + (loc.address ? ', ' + loc.address : ''),
_('Region'), (loc.region && loc.region.name) ? loc.region.name : '-',
_('GPS'), hasCoords ? btsLat.toFixed(6) + ', ' + btsLon.toFixed(6) : '-',
_('Standard'), rats || '-',
_('TAC/LAC'), det ? String(det.tac || det.lac || '-') : '-',
_('eNB ID'), det && det.enbid ? String(det.enbid) : '-',
_('Cell ID'), det && det.clid ? String(det.clid) : '-',
_('ECID'), det && det.ecid ? String(det.ecid) : '-',
_('EARFCN'), det && det.earfcn ? String(det.earfcn) : '-',
_('Confirmed'), station.is_confirmed ? _('Yes') : _('No'),
_('Updated'), station.updatedAt
? station.updatedAt.slice(0, 10) + ' (' + monthsAgo(station.updatedAt) + ')'
: '-'
])
]),
E('hr'),
E('div', { 'style': 'display: flex; justify-content: space-between; align-items: center;' }, [
hasCoords ? E('a', {
'href': 'https://btsearch.pl/#map=16.00/' + btsLat.toFixed(6) + '/' + btsLon.toFixed(6),
'target': '_blank',
'class': 'btn btn-primary',
'style': 'text-decoration: none;'
}, _('Show on map')) : E('span'),
E('button', {
'class': 'btn',
'click': ui.createHandlerFn(this, function() {
ui.hideModal();
if (!poll.active()) poll.start();
}),
}, _('Close')),
]),
]
);
if (hasCoords) loadLeafletAndInitMap();
} catch (err) {
ui.addNotification(null, E('p', {}, _('Error: ') + err.message));
if (!poll.active()) poll.start();
}
}
/* BTS info download */
function handleAction(evOrBtn) {
let modemIndex = null;
const t = (evOrBtn && evOrBtn.target) ? evOrBtn.target : evOrBtn;
if (typeof evOrBtn === 'number') {
modemIndex = evOrBtn;
} else if (t) {
const dsIdx = t.getAttribute && t.getAttribute('data-modem-index');
if (dsIdx !== null && dsIdx !== '') modemIndex = parseInt(dsIdx, 10);
if (modemIndex == null) {
const id = t.id || '';
const m = id.match(/_(\d+)$/);
if (m) modemIndex = parseInt(m[1], 10);
}
}
if (modemIndex == null) return;
return uci.load('modemdata').then(function() {
let bts_web = uci.get('modemdata', '@modemdata[0]', 'website');
let bts_action = uci.get('modemdata', '@modemdata[0]', 'btsaction');
if (bts_web && bts_web.indexOf('btsearch') >= 0) {
if (bts_action && bts_action.indexOf('open') >= 0) {
let cellElement = document.getElementById('cell_' + modemIndex);
let providerElement = document.getElementById('operator_' + modemIndex);
let providerValue = providerElement ? (providerElement.textContent || '').trim().toLowerCase() : '';
if (cellElement) {
let cellValue = (cellElement.textContent || '').trim();
let parts = cellValue.split(/\s+/);
let decPart = parts[0] ? parts[0].trim() : '';
let cellDECNumeric = decPart ? decPart.replace(/[()]/g, '') : '';
let searchsite = '';
switch (providerValue) {
case 't-mobile': searchsite = 'https://btsearch.pl/stations?mnc=26002&q=' + cellDECNumeric; break;
case 'orange': searchsite = 'https://btsearch.pl/stations?mnc=26003&q=' + cellDECNumeric; break;
case 'plus': searchsite = 'https://btsearch.pl/stations?mnc=26001&q=' + cellDECNumeric; break;
case 'play': searchsite = 'https://btsearch.pl/stations?mnc=26006&q=' + cellDECNumeric; break;
default: searchsite = 'https://btsearch.pl/stations?q=' + cellDECNumeric; break;
}
window.open(searchsite, '_blank');
}
} else {
handleDownloadAction(modemIndex);
}
}
if (bts_web && bts_web.indexOf('lteitaly') >= 0) {
let cellElement2 = document.getElementById('cell_' + modemIndex);
let mncElement = document.getElementById('mnc_' + modemIndex);
let mccElement = document.getElementById('mcc_' + modemIndex);
let cellValue2 = cellElement2 ? (cellElement2.textContent || '').trim() : '';
let mncValue = mncElement ? (mncElement.textContent || '').trim() : '';
let mccValue = mccElement ? (mccElement.textContent || '').trim() : '';
let cellNumeric2 = parseInt((cellValue2.split(/\s+/)[0] || '0'), 10);
let zzmnc = mncValue || '';
let first = zzmnc.slice(0, 1);
let second = zzmnc.slice(1, 2);
let zzcid = Math.round(cellNumeric2 / 256);
let cutmnc = zzmnc;
if (zzmnc.length == 3) {
if (first.indexOf('0') >= 0) cutmnc = zzmnc.slice(1, 3);
if (first.indexOf('0') >= 0 && second.indexOf('0') >= 0) cutmnc = zzmnc.slice(2, 3);
} else if (zzmnc.length == 2) {
first = zzmnc.slice(0, 1);
if (first.indexOf('0') >= 0) cutmnc = zzmnc.slice(1, 2);
else cutmnc = zzmnc;
} else if (zzmnc.length < 2 || (first.indexOf('0') < 0 && second.indexOf('0') < 0)) {
cutmnc = zzmnc;
}
window.open('https://lteitaly.it/internal/map.php#bts=' + mccValue + cutmnc + '.' + zzcid);
}
});
}
function updateTableToValues(ev, modemIndex) {
let table = document.getElementById('lteTable_' + modemIndex);
if (!table) return;
let headerCell = table.querySelector('tr:first-child th:last-child');
if (!headerCell) return;
let hasSnr = ev && (ev.snr !== undefined && ev.snr !== null && ev.snr !== '');
let hasSinr = ev && (ev.sinr !== undefined && ev.sinr !== null && ev.sinr !== '');
if (hasSinr) {
headerCell.textContent = _('SINR');
} else if (hasSnr) {
headerCell.textContent = _('SNR');
} else {
headerCell.textContent = _('SINR');
}
}
function handleSaveApply() { return null; }
function handleSave() { return null; }
function handleReset() { return null; }
function formatDuration(sec) {
if (sec === '-' || sec === '') return '-';
let d = Math.floor(sec / 86400),
h = Math.floor(sec / 3600) % 24,
m = Math.floor(sec / 60) % 60,
s = sec % 60;
let time = d > 0 ? d + 'd ' : '';
if (time !== '') time += h + 'h ';
else time = h > 0 ? h + 'h ' : '';
if (time !== '') time += m + 'm ';
else time = m > 0 ? m + 'm ' : '';
time += s + 's';
return time;
}
// See https://wiki.teltonika-networks.com/view/Mobile_Signal_Strength_Recommendations
function getSignalLabel(value, type) {
let signalValue = parseFloat(value);
if (isNaN(signalValue)) return { label: _('No data'), color: 'gray' };
switch (type) {
case 'RSSI':
if (signalValue > -65) return { label: _('Excellent'), color: 'green' };
if (signalValue >= -75) return { label: _('Good'), color: 'yellow' };
if (signalValue >= -85) return { label: _('Fair'), color: 'orange' };
return { label: _('Poor'), color: 'red' };
case 'RSSI_wcdma':
if (signalValue >= -70) return { label: _('Excellent'), color: 'green' };
if (signalValue >= -85) return { label: _('Good'), color: 'yellow' };
if (signalValue >= -100) return { label: _('Fair'), color: 'orange' };
return { label: _('Poor'), color: 'red' };
case 'RSRP':
if (signalValue >= -80) return { label: _('Excellent'), color: 'green' };
if (signalValue >= -90) return { label: _('Good'), color: 'yellow' };
if (signalValue >= -100) return { label: _('Fair'), color: 'orange' };
return { label: _('Poor'), color: 'red' };
case 'RSRQ':
if (signalValue >= -10) return { label: _('Excellent'), color: 'green' };
if (signalValue >= -15) return { label: _('Good'), color: 'yellow' };
if (signalValue >= -20) return { label: _('Fair'), color: 'orange' };
return { label: _('Poor'), color: 'red' };
case 'SINR':
if (signalValue > 15) return { label: _('Excellent'), color: 'green' };
if (signalValue >= 10) return { label: _('Good'), color: 'yellow' };
if (signalValue >= 5) return { label: _('Fair'), color: 'orange' };
return { label: _('Poor'), color: 'red' };
case 'SNR':
if (signalValue > 20) return { label: _('Excellent'), color: 'green' };
if (signalValue >= 13) return { label: _('Good'), color: 'yellow' };
if (signalValue >= 5) return { label: _('Fair'), color: 'orange' };
return { label: _('Poor'), color: 'red' };
case 'RSCP':
if (signalValue >= -75) return { label: _('Excellent'), color: 'green' };
if (signalValue >= -85) return { label: _('Good'), color: 'yellow' };
if (signalValue >= -95) return { label: _('Fair'), color: 'orange' };
return { label: _('Poor'), color: 'red' };
case 'ECIO':
if (signalValue >= -6 && signalValue <= 0) return { label: _('Excellent'), color: 'green' };
if (signalValue >= -10) return { label: _('Good'), color: 'yellow' };
if (signalValue >= -20) return { label: _('Fair'), color: 'orange' };
return { label: _('Poor'), color: 'red' };
default:
return { label: _('No data'), color: 'gray' };
}
}
function signalCell(value, label, statusColor) {
let cssClass = 'signal-badge';
switch (statusColor) {
case 'green': cssClass += ' signal-badge--excellent'; break;
case 'yellow': cssClass += ' signal-badge--good'; break;
case 'orange': cssClass += ' signal-badge--fair'; break;
case 'red': cssClass += ' signal-badge--poor'; break;
default: cssClass += ' signal-badge--nodata'; break;
}
return E('div', { 'class': 'signal-cell' }, [
E('span', { 'class': cssClass, 'title': String(value) }, value),
E('span', { 'class': 'signal-label', 'title': String(label) }, label)
]);
}
function createDataConnectionStateElement(stateId, status) {
let cssClass = 'connection-status';
let label = '';
if (status === 'CONNECTED') {
cssClass += ' connection-status--connected';
label = _('Connected');
} else {
cssClass += ' connection-status--disconnected';
label = _('Disconnected');
}
return E('div', { style: 'display:flex;font-size:12px;' }, [
E('span', {
id: stateId,
'class': cssClass
}, label)
]);
}
function formatDateTime(s) {
if (s.length == 14) return s.replace(/(\d{4})(\d{2})(\d{2})(\d{2})(\d{2})(\d{2})/, "$1-$2-$3 $4:$5:$6");
if (s.length == 12) return s.replace(/(\d{4})(\d{2})(\d{2})(\d{2})(\d{2})/, "$1-$2-$3 $4:$5");
if (s.length == 8) return s.replace(/(\d{4})(\d{2})(\d{2})/, "$1-$2-$3");
if (s.length == 6) return s.replace(/(\d{4})(\d{2})/, "$1-$2");
return s;
}
function checkOperatorName(t) {
let w = t.split(' ');
let f = {};
for (let i = 0; i < w.length; i++) {
let wo = w[i].toLowerCase();
if (!f.hasOwnProperty(wo)) f[wo] = i;
}
let u = Object.keys(f).map(function(wo){ return w[f[wo]]; });
return u.join(' ');
}
function CreateModemMultiverse(modemTabs, sectionsxt) {
return Promise.all(modemTabs.map(function(modem) {
return (function() {
return (function() {
if (modem.modemdata === 'serial' || modem.modemdata === 'ecm')
return L.resolveDefault(fs.exec_direct('/usr/bin/md_serial_ecm', [modem.comm_port, modem.network, modem.forced_plmn_op]));
else if (modem.modemdata === 'uqmi')
return L.resolveDefault(fs.exec_direct('/usr/bin/md_uqmi', [modem.comm_port, modem.network, modem.forced_plmn_op, modem.mbim_op]));
else if (modem.modemdata === 'mm') {
return L.resolveDefault(fs.exec_direct('/usr/bin/md_modemmanager', [modem.comm_port, modem.network, modem.forced_plmn_op]));
}
return Promise.resolve('');
})().then(function(res) {
if (!res) return;
let jsonraw = JSON.parse(res.replace(/[\u0000-\u001F\u007F-\u009F]/g, ""));
let json = Object.values(jsonraw);
if (!json || json.length < 3 || !json[0] || !json[1] || !json[2]) return;
if (json?.[2]?.error) {
const err = String(json[2].error);
if (err.includes('Device is busy')) {
if (poll.active()) poll.stop();
popTimeout(null, E('p', _('Waiting...') + ' ' + _('Device is busy')), 5000, 'info');
window.setTimeout(() => {
if (!poll.active()) poll.start();
}, 8000);
}
else if (err.includes('Device not found')) {
if (poll.active()) poll.stop();
ui.addNotification(null, E('p', _('Waiting...') + ' ' + _('Device not found')), 'error');
}
return;
}
let rowsWcdma = [];
let rowsLte = [];
let wcdmaTable = document.getElementById(modem.wcdmaTableId);
let lteTable = document.getElementById(modem.lteTableId);
const addonArr = Array.isArray(json?.[2]?.addon) ? json[2].addon : [];
const getAddon = (key) => {
const searchKeyLower = String(key).toLowerCase();
const found = addonArr.find(i => i && String(i['key']).toLowerCase() === searchKeyLower);
return found ? found['value'] : null;
};
let modeRaw = json?.[2]?.mode || '';
let modeLower = modeRaw.toLowerCase();
const isPrimaryBand = (key) => key && String(key).toLowerCase().includes('primary');
const bandsFiltered = addonArr.filter(item =>
item && (isPrimaryBand(item['key']) || /^\(S\d+\) band$/i.test(item['key']))
);
const cutBeforeAt = (str) => String(str ?? '').split('@')[0].trim() || '-';
const extractBwFromAt = (str) => (String(str ?? '').split('@')[1] || '').trim();
const isValidBw = (bw) => bw && !['', '-', 'MHz'].includes(String(bw).trim());
const isValidValue = (val) => val && val !== '-' && val !== '' && val !== null;
const withDbUnit = (val) => {
const s = String(val || '').trim();
if (s === '' || s === '-') return s;
return s.match(/dB/i) ? s : s + ' dB';
};
const makeBwCell = (ul, dl, fallback) => {
const haveUL = isValidBw(ul);
const haveDL = isValidBw(dl);
if (haveUL && haveDL)
return E('div', {}, [_('UL: ' + ul), E('br', {}), _('DL: ' + dl)]);
if (haveUL) return E('div', {}, [_('UL: ' + ul)]);
if (haveDL) return E('div', {}, [_('DL: ' + dl)]);
return typeof fallback === 'string' ? E('div', {}, _(fallback)) : (fallback ?? E('div', {}, _('-')));
};
const makeEarfcnCell = (ul, dl, fallback) => {
const haveUL = isValidValue(ul);
const haveDL = isValidValue(dl);
if (haveUL && haveDL)
return E('div', {}, [_('UL: ' + ul), E('br', {}), _('DL: ' + dl)]);
if (haveUL) return E('div', {}, [_('UL: ' + ul)]);
if (haveDL) return E('div', {}, [_('DL: ' + dl)]);
return typeof fallback === 'string' ? E('div', {}, _(fallback)) : (fallback ?? E('div', {}, _('-')));
};
if (!modeLower.match(/lte|5g/) && bandsFiltered.length === 0) {
// --- WCDMA (3G) ---
let bs = document.getElementById(modem.bandshowId);
if (bs) bs.style.display = 'block';
if (lteTable) lteTable.style.display = 'none';
if (wcdmaTable) wcdmaTable.style.display = 'table';
const uarfcn = getAddon('UARFCN') || '-';
const rssi = getAddon('RSSI') || '-';
const rscp = getAddon('RSCP') || '-';
const ecio = getAddon('ECIO') || '-';
rowsWcdma.push([
modeRaw || '-',
uarfcn,
!isNaN(parseInt(rssi, 10)) ? signalCell(rssi, getSignalLabel(rssi, 'RSSI_wcdma').label, getSignalLabel(rssi, 'RSSI_wcdma').color) : E('div', {}, _('')),
!isNaN(parseInt(rscp, 10)) ? signalCell(rscp, getSignalLabel(rscp, 'RSCP').label, getSignalLabel(rscp, 'RSCP').color) : E('div', {}, _('')),
!isNaN(parseInt(ecio, 10)) ? signalCell(ecio, getSignalLabel(ecio, 'ECIO').label, getSignalLabel(ecio, 'ECIO').color) : E('div', {}, _(''))
]);
if (wcdmaTable) cbi_update_table(wcdmaTable, rowsWcdma);
} else {
// --- LTE / 5G ---
let bs2 = document.getElementById(modem.bandshowId);
if (bs2) bs2.style.display = 'block';
if (lteTable) lteTable.style.display = 'table';
if (wcdmaTable) wcdmaTable.style.display = 'none';
let bands = bandsFiltered.slice();
const primaryBandFromAddon = addonArr.find(item => item && isPrimaryBand(item['key']));
const primaryBandValue = primaryBandFromAddon ? primaryBandFromAddon['value'] : null;
const extractBandFromMode = (mode) => {
const str = String(mode || '').trim();
if (!str) return null;
const m = str.match(/^\S*(?:LTE)\S*\s+(\S.*)$/i);
return m ? m[1].trim() : null;
};
if (bands.length === 0 && primaryBandValue)
bands.push({ 'key': 'Primary band', 'value': primaryBandValue });
if (bands.length === 0) {
const modeBand = extractBandFromMode(modeRaw);
bands.push({ 'key': 'Primary band', 'value': modeBand || _('(no data)') });
}
const getFirstValueWithUnit = function (key) {
let raw = getAddon(key) || '-';
let parts = String(raw).split('/');
let first = parts[0] ? parts[0].trim() : '-';
let second = parts[1] ? parts[1].trim() : '';
let unitMatch = second && second.match(/(dBm|dB)$/i);
return unitMatch ? (first + ' ' + unitMatch[1]) : first;
};
const getBandwidthForBand = (bandKey, rawBandValue) => {
let bwUL, bwDL, bwGeneric;
if (isPrimaryBand(bandKey)) {
bwUL = getAddon('Bandwidth UL');
bwDL = getAddon('Bandwidth DL');
bwGeneric = getAddon('Bandwidth');
} else {
const sccIndexM = bandKey.match(/\d+/);
if (sccIndexM) {
const n = sccIndexM[0];
bwUL = getAddon('(S' + n + ') Bandwidth UL');
bwDL = getAddon('(S' + n + ') Bandwidth DL');
bwGeneric = getAddon('(S' + n + ') Bandwidth');
}
}
if (isValidBw(bwUL) || isValidBw(bwDL)) return makeBwCell(bwUL, bwDL, '-');
else if (isValidBw(bwGeneric)) return E('div', {}, _(bwGeneric));
else {
const bwFromAt = extractBwFromAt(rawBandValue);
return bwFromAt ? E('div', {}, _(bwFromAt)) : E('div', {}, _('-'));
}
};
for (let i = 0; i < bands.length; i++) {
const bandKey = bands[i]['key'];
const bandLabel = isPrimaryBand(bandKey) ? 'PCC' : ('SCC' + (bandKey.match(/\d+/)?.[0] || ''));
const rawBandVal = bands[i]['value'] || '-';
const bandwidthForRow = getBandwidthForBand(bandKey, rawBandVal);
const bandValueForRow = cutBeforeAt(isPrimaryBand(bandKey) && primaryBandValue ? primaryBandValue : rawBandVal);
let row = [bandLabel + ' ' + bandValueForRow, bandwidthForRow];
const sccIndexM = bandKey.match(/\d+/);
if (sccIndexM) {
// SCC
const n = sccIndexM[0];
const getVal = (k) => getAddon(k) || '-';
const sinr = withDbUnit(getVal('(S' + n + ') SINR'));
const snr = withDbUnit(getVal('(S' + n + ') SNR'));
const earfcn = getAddon('(S' + n + ') EARFCN');
const earfcnDl = getAddon('(S' + n + ') EARFCN DL');
const earfcnUl = getAddon('(S' + n + ') EARFCN UL');
let earfcnCell;
if (isValidValue(earfcn)) {
earfcnCell = E('div', {}, _(earfcn));
} else {
earfcnCell = makeEarfcnCell(earfcnUl, earfcnDl, '-');
}
row.push(
getVal('(S' + n + ') PCI'),
earfcnCell,
!isNaN(parseInt(getVal('(S' + n + ') RSSI'), 10)) ? signalCell(getVal('(S' + n + ') RSSI'), getSignalLabel(getVal('(S' + n + ') RSSI'), 'RSSI').label, getSignalLabel(getVal('(S' + n + ') RSSI'), 'RSSI').color) : E('div', {}, _('')),
!isNaN(parseInt(getVal('(S' + n + ') RSRP'), 10)) ? signalCell(getVal('(S' + n + ') RSRP'), getSignalLabel(getVal('(S' + n + ') RSRP'), 'RSRP').label, getSignalLabel(getVal('(S' + n + ') RSRP'), 'RSRP').color) : E('div', {}, _('')),
!isNaN(parseInt(getVal('(S' + n + ') RSRQ'), 10)) ? signalCell(getVal('(S' + n + ') RSRQ'), getSignalLabel(getVal('(S' + n + ') RSRQ'), 'RSRQ').label, getSignalLabel(getVal('(S' + n + ') RSRQ'), 'RSRQ').color) : E('div', {}, _('')),
!isNaN(parseInt(sinr, 10)) ? signalCell(sinr, getSignalLabel(sinr, 'SINR').label, getSignalLabel(sinr, 'SINR').color)
: !isNaN(parseInt(snr, 10)) ? signalCell(snr, getSignalLabel(snr, 'SNR').label, getSignalLabel(snr, 'SNR').color)
: E('div', {}, _(''))
);
rowsLte.push(row);
} else {
// PCC
const pci = getAddon('PCI') || '-';
const earfcn = getAddon('EARFCN');
const earfcnDl = getAddon('EARFCN DL');
const earfcnUl = getAddon('EARFCN UL');
let earfcnCell;
if (isValidValue(earfcn)) {
earfcnCell = E('div', {}, _(earfcn));
} else {
earfcnCell = makeEarfcnCell(earfcnUl, earfcnDl, '-');
}
const sinr0 = withDbUnit(getAddon('SINR'));
const snr0 = withDbUnit(getAddon('SNR'));
const rssiFirst = getFirstValueWithUnit('RSSI');
const rsrpFirst = getFirstValueWithUnit('RSRP');
row.push(
pci,
earfcnCell,
!isNaN(parseFloat(rssiFirst)) ? signalCell(rssiFirst, getSignalLabel(rssiFirst, 'RSSI').label, getSignalLabel(rssiFirst, 'RSSI').color) : E('div', {}, _('')),
!isNaN(parseFloat(rsrpFirst)) ? signalCell(rsrpFirst, getSignalLabel(rsrpFirst, 'RSRP').label, getSignalLabel(rsrpFirst, 'RSRP').color) : E('div', {}, _('')),
!isNaN(parseInt(getAddon('RSRQ'), 10)) ? signalCell(getAddon('RSRQ'), getSignalLabel(getAddon('RSRQ'), 'RSRQ').label, getSignalLabel(getAddon('RSRQ'), 'RSRQ').color) : E('div', {}, _('')),
!isNaN(parseInt(sinr0, 10)) ? signalCell(sinr0, getSignalLabel(sinr0, 'SINR').label, getSignalLabel(sinr0, 'SINR').color)
: !isNaN(parseInt(snr0, 10)) ? signalCell(snr0, getSignalLabel(snr0, 'SNR').label, getSignalLabel(snr0, 'SNR').color)
: E('div', {}, _(''))
);
rowsLte.push(row);
}
}
if (lteTable) cbi_update_table(lteTable, rowsLte);
const tsnr = {
sinr: getAddon('SINR'),
snr: getAddon('SNR')
};
updateTableToValues(tsnr, modem.index);
}
// MOBILE SVG
let p = json[2].signal;
let icon;
switch (true) {
case (p <= 0): icon = L.resource('icons/mobile-signal-000-000.svg'); break;
case (p < 20): icon = L.resource('icons/mobile-signal-000-020.svg'); break;
case (p < 40): icon = L.resource('icons/mobile-signal-020-040.svg'); break;
case (p < 60): icon = L.resource('icons/mobile-signal-040-060.svg'); break;
case (p < 80): icon = L.resource('icons/mobile-signal-060-080.svg'); break;
default: icon = L.resource('icons/mobile-signal-080-100.svg'); break;
}
// Signal
let signalView = document.getElementById(modem.signalId);
if (signalView) {
signalView.innerHTML = '';
if (p === 0) {
if (lteTable) lteTable.style.display = 'none';
if (wcdmaTable) wcdmaTable.style.display = 'none';
let bs3 = document.getElementById(modem.bandshowId); if (bs3) bs3.style.display = 'none';
}
let title = _('Signal strength') + ': ' + p + '%';
signalView.appendChild(
E('div', { 'class': 'ifacebadge', 'style': 'display:inline-flex;align-items:center;white-space:nowrap;', 'title': title }, [
E('img', { 'src': icon, 'style': 'padding:0 auto;height:32px;width:auto;display:inline-block;margin:0 auto;flex-shrink:0;' }),
E('normal', {'style': 'margin-left:.2em;display:inline-block;white-space:nowrap;'}, p > 0 ? (p + '%') : '')
])
);
}
// Connection status
let stateView = document.getElementById(modem.stateId);
if (stateView) {
let status = json[1].status;
let el = createDataConnectionStateElement('dataStatus', status);
stateView.innerHTML = '';
stateView.appendChild(el);
}
// Connection statistics
let connstView = document.getElementById(modem.connstId);
if (connstView) {
if (json[1].conn_time_sec < 1) {
connstView.innerHTML = '';
if (lteTable) lteTable.style.display = 'none';
if (wcdmaTable) wcdmaTable.style.display = 'none';
let bs4 = document.getElementById(modem.bandshowId); if (bs4) bs4.style.display = 'none';
connstView.appendChild(E('div', {}, [ E('em', { 'class': 'spinning' }, _('Waiting for data...')) ]));
} else {
connstView.innerHTML = '';
let title2 = _('Time') + ': ' + formatDuration(json[1].conn_time_sec);
connstView.appendChild(
E('div', { 'class': 'ifacebadge', 'style': 'display:inline-flex;align-items:center;white-space:nowrap;', 'title': title2 }, [
E('img', { 'src': L.resource('icons/ctime_new.svg'), 'style': 'height:16px;width:auto;display:inline-block;flex-shrink:0;' }),
E('normal', { 'style': 'margin-left:.5em;display:inline-block;white-space:nowrap;' }, [ formatDuration(json[1].conn_time_sec) || '-' ])
])
);
}
}
let rxView = document.getElementById(modem.rxId);
if (rxView) rxView.textContent = json[1].rx.length > 1 ? json[1].rx : '0 B';
let txView = document.getElementById(modem.txId);
if (txView) txView.textContent = json[1].tx.length > 1 ? json[1].tx : '0 B';
// ON/OFF HIDE DATA
let hide_list = Array.isArray(sectionsxt[0].hide_data) ? sectionsxt[0].hide_data : [];
let hidedata = document.getElementById('hide-data') ? document.getElementById('hide-data').checked : false;
// Operator
let operatorView = document.getElementById(modem.operatorId);
if (operatorView) {
let opName = json[2].operator_name.length > 1 ? checkOperatorName(json[2].operator_name) : '-';
operatorView.textContent = (hidedata && hide_list.indexOf(_('Operator')) >= 0) ? opName.replace(/./g, '#') : opName;
}
// SIM status
let simView = document.getElementById(modem.simId);
if (simView) {
let reg = json[2].registration;
let simStatusValue = '-';
switch (reg) {
case '0': simStatusValue = _('Not registered'); break;
case '1': simStatusValue = _('Registered, home'); break;
case '2': simStatusValue = _('Searching..'); break;
case '3': simStatusValue = _('Registering denied'); break;
case '5': simStatusValue = _('Registered (roaming)'); break;
case '6': simStatusValue = _('Registered, only SMS'); break;
case '7': simStatusValue = _('Registered (roaming), only SMS'); break;
default: simStatusValue = '-';
}
simView.textContent = simStatusValue;
simView.title = simStatusValue;
}
// SIM slot
let slotView = document.getElementById(modem.slotId);
if (slotView) {
slotView.innerHTML = '';
let slotObj = (json[2].addon || []).find(function(i){ return i.key==='Slot'; });
let slot = slotObj ? ('SIM ' + slotObj.value) : _('No data');
if (slot && slot !== _('No data') && parseInt(slotObj.value, 10) >= 1) {
let slotDivElement = document.getElementById(modem.slotDivId);
if (slotDivElement) slotDivElement.style.display = 'flex';
slotView.appendChild(
E('div', { 'class': 'ifacebadge', 'title': _('Slot in use') + ': ' + slot }, [
E('img', { 'src': L.resource('icons/sim_new.svg'), 'style': 'height:32px;width:auto;display:inline-block;margin:0 auto;' }),
E('normal', { 'style': 'margin-left:.5em;' }, [ slot ])
])
);
}
}
// ICCID
let iccidView = document.getElementById(modem.iccidId);
if (iccidView) {
iccidView.innerHTML = (hidedata && hide_list.indexOf('ICCID') >= 0) ? (json[0].iccid || '').replace(/./g, '#') : (json[0].iccid || '');
}
// IMEI
let imeiView = document.getElementById(modem.imeiId);
if (imeiView) {
imeiView.innerHTML = (hidedata && hide_list.indexOf('IMEI') >= 0) ? (json[0].imei || '').replace(/./g, '#') : (json[0].imei || '');
}
// IMSI
let imsiView = document.getElementById(modem.imsiId);
if (imsiView) {
imsiView.innerHTML = (hidedata && hide_list.indexOf('IMSI') >= 0) ? (json[0].imsi || '').replace(/./g, '#') : (json[0].imsi || '');
}
// Country
let countryView = document.getElementById(modem.countryId);
if (countryView) {
let countryTxt = (json[2].country || '').replace('Poland', _('Poland'));
countryView.style.display = (countryTxt && countryTxt.length > 2) ? '' : 'none';
countryView.innerHTML = (hidedata && hide_list.indexOf(_('Country')) >= 0) ? countryTxt.replace(/./g, '#') : countryTxt;
}
// Modem type
let modemtypeView = document.getElementById(modem.modemtypeId);
if (modemtypeView) {
modemtypeView.textContent = (json[0].vendor || '-') + ' ' + (json[0].product || '-');
modemtypeView.title = modemtypeView.textContent;
}
// FW
let firmwareView = document.getElementById(modem.firmwareId);
if (firmwareView) {
firmwareView.textContent = json[0].revision || '-';
firmwareView.title = firmwareView.textContent;
}
// Temperature
let tempView = document.getElementById(modem.tempId);
if (tempView) {
let temperatureObj = (json[2].addon || []).find(function(i){ return i.key==='Temperature'; });
let temperature = temperatureObj ? temperatureObj.value : _('No data');
temperature = temperature.replace('&deg;', '°');
if (temperature && temperature !== _('No data') && temperature.length > 2) {
let tempDivElement = document.getElementById(modem.tempDivId);
if (tempDivElement) tempDivElement.style.display = 'flex';
tempView.innerHTML = '';
tempView.appendChild(
E('div', { 'class': 'ifacebadge', 'title': _('Chip Temperature') + ': ' + temperature }, [
E('img', { 'src': L.resource('icons/termometr.svg'), 'style': 'padding-left:2px;height:32px;width:auto;display:inline-block;' }),
E('normal', { 'style': 'margin-left:.1em;font-size:12px' }, [ temperature || '-' ])
])
);
}
}
// Cell ID
let cellView = document.getElementById(modem.cellId);
if (cellView) {
let celldata = (json[2].cid_dec + ' (' + json[2].cid_hex + ')');
if (hidedata && hide_list.indexOf(_('Cell ID')) >= 0) celldata = celldata.replace(/./g, '#');
if (parseInt(json[2].cid_dec || '0', 10) < 1) cellView.innerHTML = '-';
else {
cellView.textContent = celldata;
cellView.title = celldata;
}
}
// LAC
let lacView = document.getElementById(modem.lacId);
if (lacView) {
if (hidedata && hide_list.indexOf('LAC') >= 0) {
lacView.innerHTML = (json[2].lac_dec || '').replace(/./g, '#') + ' (' + (json[2].lac_hex || '').replace(/./g, '#') + ')';
} else {
if (!json[2].lac_dec || !json[2].lac_hex || json[2].lac_dec === '0' || json[2].lac_hex === '0')
lacView.innerHTML = '-';
else
lacView.innerHTML = json[2].lac_dec + ' (' + json[2].lac_hex + ')';
}
}
// TAC
let tacView = document.getElementById(modem.tacId);
if (tacView) {
let tacObj = (json[2].addon || []).find(function(i){ return i.key==='TAC'; });
let tac = tacObj ? tacObj.value : _('-');
tacView.innerHTML = (hidedata && hide_list.indexOf('TAC') >= 0) ? tac.replace(/./g, '#') : tac;
}
// MCC
let mccView = document.getElementById(modem.mccId);
if (mccView) {
mccView.innerHTML = (hidedata && hide_list.indexOf('MCC') >= 0) ? (json[2].operator_mcc || '').replace(/./g, '#') : (json[2].operator_mcc || '');
}
// MNC
let mncView = document.getElementById(modem.mncId);
if (mncView) {
mncView.innerHTML = (hidedata && hide_list.indexOf('MNC') >= 0) ? (json[2].operator_mnc || '').replace(/./g, '#') : (json[2].operator_mnc || '');
}
// Mode
let modeView = document.getElementById(modem.modeId);
if (modeView) {
if ((json[2].mode || '').length <= 1) {
modeView.textContent = '-';
if (lteTable) lteTable.style.display = 'none';
if (wcdmaTable) wcdmaTable.style.display = 'none';
let bs5 = document.getElementById(modem.bandshowId); if (bs5) bs5.style.display = 'none';
} else {
let modeRaw2 = json[2].mode || '';
let modeUp = modeRaw2.toUpperCase();
let modeDisplay;
if (modeUp.indexOf('LTE') >= 0 || modeUp.indexOf('5G') >= 0) {
let tech = '';
if (modeUp.indexOf('LTE') >= 0) tech = modeRaw2.split(' ')[0];
if (modeUp.indexOf('5G') >= 0) tech = modeRaw2.split(' ')[0] + (modeRaw2.split(' ')[1] ? (' ' + modeRaw2.split(' ')[1]) : '');
let count = (modeRaw2.match(/\//g) || []).length + 1;
modeDisplay = (count > 1) ? (tech + ' (' + count + 'CA)') : tech;
} else {
modeDisplay = modeRaw2.split(' ')[0];
}
modeDisplay = modeDisplay.replace('LTE_A', 'LTE-A');
modeView.textContent = modeDisplay;
}
}
});
})().catch(function(e) {
console.error('JSON parsing error', modem.comm_port, e);
});
})).then(function() {
return Promise.resolve();
});
}
return view.extend({
load: function() {
return Promise.all([
uci.load('defmodems'),
uci.load('modemdata')
]);
},
render: function (data) {
addDarkModeStyles();
let sections = uci.sections('defmodems', 'defmodems') || [];
let sectionsxt = uci.sections('modemdata', 'modemdata') || [];
loadUCInterval();
let info = _('For more information about the modemdata package, please visit: %shttps://github.com/obsy/modemdata%s.')
.format('<a href="https://github.com/obsy/modemdata" target="_blank">', '</a>');
if (!Array.isArray(sections) || sections.length === 0) {
let modemsModal = baseclass.extend({
__init__: function() {
this.title = _('No Modems Detected...');
this.description = _('Oops.. there are no modems in settings. You will be redirected to a tab where you can define the installed modem(s).');
this.countdown = 10;
this.timer = null;
},
startCountdown: function() {
let self = this;
let countdownLabel = document.getElementById('countdownLabel');
this.timer = setInterval(function() {
self.countdown--;
if (self.countdown > 0) {
countdownLabel.textContent = _('Redirecting in') +' '+ self.countdown +' '+ _('seconds...');
} else {
clearInterval(self.timer);
countdownLabel.textContent = _('Redirecting...');
let pkg = {
get modemdefURI() {
return 'admin/modem/modemdata/modemdefine';
}
};
window.location.href = L.url(pkg.modemdefURI);
}
}, 1000);
},
render: function() {
ui.showModal(this.title, [
E('div', { class: 'info-message' }, [
E('p', {}, this.description),
E('label', { id: 'countdownLabel' }, _('Redirecting in') +' '+ this.countdown +' '+ _('seconds...'))
]),
], 'cbi-modal');
this.startCountdown();
}
});
let modemDialog = new modemsModal();
modemDialog.render();
return E([
E('h2', { 'class': 'fade-in' }, _('Modemdata')),
E('div', { 'class': 'cbi-section-descr fade-in' },
_('Package allows the user to view the data read from the modem, to see the parameters of the connection to the mobile network.') + '<br />' + info)
]);
}
let globalToolbar = E('div', {
'class': 'right',
'style': 'width:100%; margin-bottom:8px; display:flex; align-items:flex-start; justify-content:space-between; gap:1rem; flex-wrap:wrap;'
}, [
E('div', { 'style': 'display:flex; flex-direction:column; align-items:flex-start; gap:.5rem; min-width:320px;' }, [
E('div', { 'style': 'display:flex; align-items:center; gap:.75rem; flex-wrap:wrap;' }, [
E('label', { 'for': 'selectInterval', 'style': 'text-align:left;' }, _('Auto update every:')),
E('select', { 'id': 'selectInterval', 'change': clickToSelectInterval }, [
E('option', { value: '-1' }, _('Disabled')),
E('option', { value: '5' }, _('5 seconds')),
E('option', { value: '10' }, _('10 seconds')),
E('option', { value: '30' }, _('30 seconds')),
E('option', { value: '45' }, _('45 seconds'))
])
]),
E('label', {
'id': 'countdown-label',
'style': 'font-size:.9em; opacity:.85; min-height:1.2em; margin-top:.25rem; text-align:left;'
},
E('em', { 'class': 'spinning' }, _('Please wait... data collection is in progress.'))
)
]),
E('div', { 'style': 'display:flex; align-items:center; gap:1rem; margin-left:auto; flex-wrap:wrap;' }, [
E('label', {
'class': 'cbi-checkbox',
'style': 'user-select:none;'
}, [
E('input', {
'id': 'hide-data',
'type': 'checkbox',
'name': 'showhistory',
'title': _('Hide selected data')
}),
' ',
E('label', { 'for': 'hide-data' }),
' ',
_('Hide data.')
]),
])
]);
let tabsContainer = E('div', { 'class': 'cbi-section-node cbi-section-node-tabbed' });
let modemTabs = [];
let wcdmaTableTitles = [
_('Name'),
_('UARFCN'),
_('RSSI'),
_('RSCP'),
_('EC/IO'),
];
let lteTableTitles = [
_('Band'),
_('Bandwidth'),
_('Physical cell ID'),
_('EARFCN'),
_('RSSI'),
_('RSRP'),
_('RSRQ'),
_('SINR'),
];
// TABS
for (let i = 0; i < sections.length; i++) {
let modem = sections[i];
let rmethod = modem.modemdata || '-';
let fplmn = modem.forced_plmn || '-';
let rmbim = modem.onproxy || '-';
let modemName = modem.modem || '-';
let desc = modem.user_desc || '-';
let net = modem.network || '-';
let mport = modem.comm_port || '-';
let tabTitle = modemName + (desc.length >= 2 ? (' (' + desc + ')') : '');
// Data_i
let signalId = 'signal_' + i;
let connstId = 'connst_' + i;
let stateId = 'state_' + i;
let operatorId = 'operator_' + i;
let countryId = 'country_' + i;
let simId = 'sim_' + i;
let rxId = 'rx_' + i;
let txId = 'tx_' + i;
let slotId = 'slot_' + i;
let slotDivId = 'slotDiv_' + i;
let tempDivId = 'tempDiv_' + i;
let iccidId = 'iccid_' + i;
let imeiId = 'imei_' + i;
let imsiId = 'imsi_' + i;
let modemtypeId = 'modemtype_' + i;
let firmwareId = 'firmware_' + i;
let tempId = 'temp_' + i;
let cellId = 'cell_' + i;
let lacId = 'lac_' + i;
let tacId = 'tac_' + i;
let mccId = 'mcc_' + i;
let mncId = 'mnc_' + i;
let modeId = 'mode_' + i;
let bandshowId = 'bandshow_' + i;
let wcdmaTableId = 'wcdmaTable_' + i;
let lteTableId = 'lteTable_' + i;
// WCDMA & LTE TABLE
let wcdmaTable = E('table', {
'class': 'table',
'id': wcdmaTableId,
'style': 'border:1px solid var(--border-color-medium)!important; table-layout:fixed; border-collapse:collapse; width:100%; display:none; font-size:12px;'
},
E('tr', { 'class': 'tr table-titles' }, [
E('th', { 'class': 'th left', 'style': 'min-width:80px; width:80px;' }, wcdmaTableTitles[0]),
E('th', { 'class': 'th left', 'style': 'min-width:80px; width:80px;;' }, wcdmaTableTitles[1]),
E('th', { 'class': 'th left', 'style': 'min-width:100px; width:100px;' }, wcdmaTableTitles[2]),
E('th', { 'class': 'th left', 'style': 'min-width:100px; width:100px;' }, wcdmaTableTitles[3]),
E('th', { 'class': 'th left', 'style': 'min-width:100px; width:100px;' }, wcdmaTableTitles[4])
])
);
let lteTable = E('table', {
'class': 'table',
'id': lteTableId,
'style': 'border:1px solid var(--border-color-medium)!important; table-layout:fixed; border-collapse:collapse; width:100%; display:none; font-size:12px;'
},
E('tr', { 'class': 'tr table-titles' }, [
E('th', { 'class': 'th left', 'style': 'min-width:75px; width:85px;' }, lteTableTitles[0]),
E('th', { 'class': 'th left', 'style': 'min-width:55px; width:60px;' }, lteTableTitles[1]),
E('th', { 'class': 'th left', 'style': 'min-width:50px; width:60px;' }, lteTableTitles[2]),
E('th', { 'class': 'th left', 'style': 'min-width:35px; width:45px;' }, lteTableTitles[3]),
E('th', { 'class': 'th left', 'style': 'min-width:90px; width:100px;' }, lteTableTitles[4]),
E('th', { 'class': 'th left', 'style': 'min-width:90px; width:100px;' }, lteTableTitles[5]),
E('th', { 'class': 'th left', 'style': 'min-width:90px; width:100px;' }, lteTableTitles[6]),
E('th', { 'class': 'th left', 'style': 'min-width:90px; width:100px;' }, lteTableTitles[7])
])
);
const perTabToolbar = E('div', {
'style': 'display:flex; align-items:center; gap:.5rem; margin-left:auto;'
}, [
E('span', {}, _('Primary band')),
E('button', {
'class': 'btn cbi-button-neutral',
'id': 'basicSignals_' + i,
'data-tooltip': _('Show signal levels'),
'click': ui.createHandlerFn(this, function(){ openBasicSignalsModal(i); })
}, _('☰')),
E('span', {}, _('Search BTS')),
E('button', {
'class': 'cbi-button cbi-button-action important',
'id': 'btsSearch_' + i,
'click': ui.createHandlerFn(this, function () { return handleAction(i); })
}, _('Search'))
]);
let modemTab = E('div', {
'data-tab': 'tab' + i,
'data-tab-title': tabTitle
}, [
E('div', { 'style': 'display:flex; align-items:center; justify-content:space-between; gap:1rem; margin-bottom:.25rem;' }, [
E('h4', {}, [ _('General Information') ]),
perTabToolbar
]),
E('div', { 'style': 'display:grid;grid-template-columns:repeat(auto-fit, minmax(200px, 1fr));margin-bottom:1em;gap:1em' }, [
// Connection
E('div', { 'class': 'ifacebox', 'style': 'margin:.25em;width:100%' }, [
E('div', { 'class': 'ifacebox-head port-label', 'style': 'font-weight:bold;padding:8px' }, [ _('Connection') ]),
E('div', { 'class': 'ifacebox-body', 'style': 'padding:8px' }, [
E('div', { 'style': 'display:flex;justify-content:space-between;align-items:center;gap:.5em;margin-bottom:8px;font-size:12px' }, [
E('span', {}, _('Signal') + ':'),
E('span', { 'style': 'font-weight:500;flex-shrink:0', 'id': signalId }, [ '-' ])
]),
E('div', { 'style': 'display:flex;justify-content:space-between;align-items:center;gap:.5em;margin-bottom:8px;font-size:12px' }, [
E('span', {
'style': 'min-width:0;'
}, _('Connection state') + ':'),
E('span', {
'style': 'font-weight:500;flex-shrink:0',
'id': stateId
}, [ '-' ])
]),
E('div', { 'style': 'display:flex;justify-content:space-between;margin-bottom:4px;font-size:12px' }, [
E('span', {}, _('Operator') + ':'),
E('span', { 'style': 'font-weight:500', 'id': operatorId }, [ '-' ])
]),
E('div', { 'style': 'display:flex;justify-content:space-between;margin-bottom:4px;font-size:12px' }, [
E('span', {}, _('Country') + ':'),
E('span', { 'style': 'font-weight:500', 'id': countryId }, [ '-' ])
]),
E('div', { 'style': 'display:flex;justify-content:space-between;margin-bottom:4px;font-size:12px' }, [
E('span', {}, _('Technology') + ':'),
E('span', { 'style': 'font-weight:500', 'id': modeId }, [ '-' ])
]),
E('div', { 'style': 'text-align:left;font-size:11px;border-top:1px solid var(--border-color-medium);padding-top:8px' }, [
E('div', { 'style': 'display:flex;justify-content:space-between;align-items:center;gap:.5em;margin-bottom:4px;font-size:12px' }, [
E('span', {
'style': 'min-width:0;'
}, _('Connection time') + ':'),
E('span', { 'style': 'font-weight:500;flex-shrink:0', 'id': connstId }, [ '-' ])
]),
E('div', { 'style': 'display:flex;justify-content:space-between;margin-bottom:2px' }, [
E('span', '▲ ' + _('Sent') + ':'),
E('span', { 'style': 'font-weight:500', 'id': txId }, [ '-' ])
]),
E('div', { 'style': 'display:flex;justify-content:space-between' }, [
E('span', '▼ ' + _('Received') + ':'),
E('span', { 'style': 'font-weight:500', 'id': rxId }, [ '-' ])
])
])
])
]),
// SIM Card
E('div', { 'class': 'ifacebox', 'style': 'margin:.25em;width:100%' }, [
E('div', { 'class': 'ifacebox-head port-label', 'style': 'font-weight:bold;padding:8px' }, [ _('SIM Card') ]),
E('div', { 'class': 'ifacebox-body', 'style': 'padding:8px' }, [
E('div', { 'id': slotDivId, 'style': 'display:none;justify-content:space-between;margin-bottom:4px;font-size:12px' }, [
E('span', {}, _('Slot in use') + ':'),
E('span', { 'style': 'font-weight:500', 'id': slotId }, [ '-' ])
]),
E('div', { 'style': 'display:flex;justify-content:space-between;margin-bottom:4px;font-size:12px' }, [
E('span', {}, _('SIM status') + ':'),
E('span', {
'style': 'font-weight:500; white-space:nowrap; overflow:hidden; text-overflow:ellipsis; max-width:75%; display:inline-block; cursor:pointer;',
'id': simId, 'title': '-'
}, [ '-' ])
]),
E('div', { 'style': 'text-align:left;font-size:11px;border-top:1px solid var(--border-color-medium);padding-top:8px' }, [
E('div', { 'style': 'display:flex;justify-content:space-between;margin-bottom:4px;font-size:12px' }, [
E('span', {}, _('IMSI') + ':'),
E('span', { 'style': 'font-weight:500', 'id': imsiId }, [ '-' ])
]),
E('div', { 'style': 'display:flex;justify-content:space-between;margin-bottom:4px;font-size:12px' }, [
E('span', {}, _('ICCID') + ':'),
E('span', { 'style': 'font-weight:500', 'id': iccidId }, [ '-' ])
])
])
])
]),
// Modem Info
E('div', { 'class': 'ifacebox', 'style': 'margin:.25em;width:100%' }, [
E('div', { 'class': 'ifacebox-head port-label', 'style': 'font-weight:bold;padding:8px' }, [ _('Modem Information') ]),
E('div', { 'class': 'ifacebox-body', 'style': 'padding:8px' }, [
E('div', { 'style': 'display:flex;justify-content:space-between;margin-bottom:4px;font-size:12px' }, [
E('span', {}, _('Modem type') + ':'),
E('span', {
'style': 'font-weight:500; white-space:nowrap; overflow:hidden; text-overflow:ellipsis; max-width:60%; display:inline-block; cursor:pointer;',
'id': modemtypeId, 'title': '-'
}, [ '-' ])
]),
E('div', { 'style': 'display:flex;justify-content:space-between;margin-bottom:4px;font-size:12px' }, [
E('span', {}, _('Revision / FW') + ':'),
E('span', {
'style': 'font-weight:500; white-space:nowrap; overflow:hidden; text-overflow:ellipsis; max-width:60%; display:inline-block; cursor:pointer;',
'id': firmwareId, 'title': '-'
}, [ '-' ])
]),
E('div', { 'style': 'display:flex;justify-content:space-between;margin-bottom:4px;font-size:12px' }, [
E('span', {}, _('Modem IMEI') + ':'),
E('span', { 'style': 'font-weight:500', 'id': imeiId }, [ '-' ])
]),
E('div', { 'id': tempDivId, 'style': 'display:none;justify-content:space-between;margin-bottom:4px;font-size:12px' }, [
E('span', {}, _('Chip Temperature') + ':'),
E('span', { 'style': 'font-weight:500', 'id': tempId }, [ '-' ])
])
])
]),
// Cell Info
E('div', { 'class': 'ifacebox', 'style': 'margin:.25em;width:100%' }, [
E('div', { 'class': 'ifacebox-head port-label', 'style': 'font-weight:bold;padding:8px' }, [ _('Cell Information') ]),
E('div', { 'class': 'ifacebox-body', 'style': 'padding:8px' }, [
E('div', { 'style': 'display:flex;justify-content:space-between;margin-bottom:4px;font-size:12px' }, [
E('span', {}, _('Cell ID') + ':'),
E('span', {
'style': 'font-weight:500; white-space:nowrap; overflow:hidden; text-overflow:ellipsis; max-width:42%; display:inline-block; cursor:pointer;',
'id': cellId,
'title': _('Click to view Cell ID details'),
'click': ui.createHandlerFn(this, function() { openCellIdModal(i); })
}, [ '-' ])
]),
E('div', { 'style': 'display:flex;justify-content:space-between;margin-bottom:4px;font-size:12px' }, [
E('span', {}, _('TAC') + ':'),
E('span', { 'style': 'font-weight:500', 'id': tacId }, [ '-' ])
]),
E('div', { 'style': 'display:flex;justify-content:space-between;margin-bottom:4px;font-size:12px' }, [
E('span', {}, _('LAC') + ':'),
E('span', { 'style': 'font-weight:500', 'id': lacId }, [ '-' ])
]),
E('div', { 'style': 'display:flex;justify-content:space-between;margin-bottom:4px;font-size:12px' }, [
E('span', {}, _('Mobile Country Code') + ':'),
E('span', { 'style': 'font-weight:500', 'id': mccId }, [ '-' ])
]),
E('div', { 'style': 'display:flex;justify-content:space-between;margin-bottom:4px;font-size:12px' }, [
E('span', {}, _('Mobile Network Code') + ':'),
E('span', { 'style': 'font-weight:500', 'id': mncId }, [ '-' ])
])
])
])
]),
E('h4', { id: bandshowId, style: 'display: none;' }, [ _('Bands') ]),
wcdmaTable,
lteTable
]);
tabsContainer.append(modemTab);
modemTabs.push({
index: i,
network: net,
comm_port: mport,
forced_plmn_op: fplmn,
mbim_op: rmbim,
modemdata: rmethod,
signalId: signalId,
connstId: connstId,
operatorId: operatorId,
countryId: countryId,
simId: simId,
rxId: rxId,
txId: txId,
slotId: slotId,
slotDivId: slotDivId,
tempDivId: tempDivId,
iccidId: iccidId,
imeiId: imeiId,
imsiId: imsiId,
modemtypeId: modemtypeId,
firmwareId: firmwareId,
tempId: tempId,
cellId: cellId,
lacId: lacId,
tacId: tacId,
mccId: mccId,
mncId: mncId,
stateId: stateId,
modeId: modeId,
bandshowId: bandshowId,
wcdmaTableId: wcdmaTableId,
lteTableId: lteTableId
});
}
setTimeout(function() {
refresh.labelEl = document.getElementById('countdown-label');
refresh.selectEl = document.getElementById('selectInterval');
if (refresh.selectEl) refresh.selectEl.value = String(refresh.interval);
if (!poll.active() || refresh.interval < 0) setUpdateMessage(refresh.labelEl, -1);
}, 0);
document.addEventListener('poll-start', function() {
if (refresh.selectEl) refresh.selectEl.value = String(refresh.interval);
});
document.addEventListener('poll-stop', function() {
if (refresh.selectEl) refresh.selectEl.value = '-1';
setUpdateMessage(refresh.labelEl, -1);
});
// Poll 1 sec
poll.add(function() {
return updateDataTick(function() {
return CreateModemMultiverse(modemTabs, sectionsxt);
});
}, 1);
// TABS
setTimeout(function() { ui.tabs.initTabGroup(tabsContainer.children); }, 0);
return E([
E('h2', { 'class': 'fade-in' }, _('Modemdata')),
E('div', { 'class': 'cbi-section-descr fade-in' },
_('Package allows the user to view the data read from the modem, to see the parameters of the connection to the mobile network.') + '<br />' + info),
E('hr'),
globalToolbar,
tabsContainer
]);
},
handleSaveApply: null,
handleSave : null,
handleReset : null
});