mirror of
https://github.com/kiddin9/op-packages.git
synced 2026-11-14 20:48:17 +08:00
2292 lines
89 KiB
JavaScript
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: '© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> © <a href="https://carto.com/">CARTO</a>'
|
|
};
|
|
}
|
|
return {
|
|
url: 'https://tile.openstreetmap.org/{z}/{x}/{y}.png',
|
|
attribution: '© <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('°', '°');
|
|
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
|
|
});
|