mirror of
https://github.com/kiddin9/op-packages.git
synced 2026-11-15 04:53:00 +08:00
🎉 Sync 2026-11-13 03:15:01
This commit is contained in:
1 parent
db03d04b97
commit
7f7e02b085
59 files changed
+1601
-1280
No files matched your search
@@ -0,0 +1,177 @@
|
||||
'use strict';
|
||||
'require baseclass';
|
||||
'require form';
|
||||
'require fs';
|
||||
'require view';
|
||||
'require uci';
|
||||
'require ui';
|
||||
'require tools.widgets as widgets';
|
||||
|
||||
/*
|
||||
|
||||
Copyright 2025-2026 Rafał Wabik - IceG - From eko.one.pl forum
|
||||
|
||||
MIT License
|
||||
|
||||
*/
|
||||
|
||||
let pkg = {
|
||||
get Name() { return 'modemdata'; },
|
||||
get URL() { return 'https://openwrt.org/packages/pkgdata/' + pkg.Name + '/'; },
|
||||
|
||||
// Package Manager
|
||||
get pkgMgrURINew() { return 'admin/system/package-manager'; },
|
||||
// opkg
|
||||
get pkgMgrURIOld() { return 'admin/system/opkg'; },
|
||||
|
||||
get modemdefURI() { return 'admin/modem/modemdata/modemdefine'; },
|
||||
|
||||
bestPkgMgrURI: function () {
|
||||
return L.resolveDefault(
|
||||
fs.stat('/www/luci-static/resources/view/system/package-manager.js'), null
|
||||
).then(st => {
|
||||
if (st && st.type === 'file')
|
||||
return pkg.pkgMgrURINew;
|
||||
|
||||
return L.resolveDefault(fs.stat('/usr/libexec/package-manager-call'), null)
|
||||
.then(st2 => (st2 ? pkg.pkgMgrURINew : pkg.pkgMgrURIOld));
|
||||
}).catch(() => pkg.pkgMgrURIOld);
|
||||
},
|
||||
|
||||
openInstallerSearch: function (query) {
|
||||
return pkg.bestPkgMgrURI().then(uri => {
|
||||
let q = query ? ('?query=' + encodeURIComponent(query)) : '';
|
||||
window.open(L.url(uri) + q, '_blank', 'noopener');
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return view.extend({
|
||||
|
||||
option_install_curl: function () {
|
||||
return pkg.openInstallerSearch('curl');
|
||||
},
|
||||
|
||||
checkPackages: function () {
|
||||
return fs.exec_direct('/usr/bin/opkg', ['list-installed'], 'text')
|
||||
.catch(function () {
|
||||
return fs.exec_direct('/usr/libexec/opkg-call', ['list-installed'], 'text')
|
||||
.catch(function () {
|
||||
return fs.exec_direct('/usr/libexec/package-manager-call', ['list-installed'], 'text')
|
||||
.catch(function () { return ''; });
|
||||
});
|
||||
})
|
||||
.then(function (data) {
|
||||
data = (data || '').trim();
|
||||
return data ? data.split('\n') : [];
|
||||
});
|
||||
},
|
||||
|
||||
load: function () {
|
||||
return Promise.all([
|
||||
this.checkPackages()
|
||||
]);
|
||||
},
|
||||
|
||||
render: function (data) {
|
||||
let installedList = Array.isArray(data[0]) ? data[0] : [];
|
||||
let curlInstalled = installedList.some(function (s) { return s && s.indexOf('curl') !== -1; });
|
||||
let m, s, o;
|
||||
|
||||
m = new form.Map('modemdata', _('Configuration'),
|
||||
_('Configuration panel for modemdata and gui application.')
|
||||
);
|
||||
|
||||
s = m.section(form.TypedSection, 'modemdata', '', null);
|
||||
s.anonymous = true;
|
||||
|
||||
o = s.option(form.ListValue, 'updtime', _('Refresh rate'),
|
||||
_('Set how often the data should be refreshed in the Modem(s) tab. \
|
||||
<br /><br /><b>Important</b> \
|
||||
<br />Not every modem / protocol can communicate quickly with scripts. Too high a polling rate can freeze the modem. \
|
||||
<br />ModemManager and uqmi need more time to respond.')
|
||||
);
|
||||
o.value('-1', _('Disabled'));
|
||||
o.value('5', _('5 seconds'));
|
||||
o.value('10', _('10 seconds'));
|
||||
o.value('30', _('30 seconds'));
|
||||
o.value('45', _('45 seconds'));
|
||||
o.default = '5';
|
||||
o.rmempty = false;
|
||||
|
||||
o = s.option(form.DynamicList, 'hide_data', _('Add data to hide'),
|
||||
_('Enter the data you would like to hide, e.g. <code>IMEI, IMSI</code>.')
|
||||
);
|
||||
o.rmempty = true;
|
||||
|
||||
s = m.section(form.TypedSection, 'modemdata', null);
|
||||
s.anonymous = true;
|
||||
s.addremove = false;
|
||||
|
||||
s.tab('bts1', _('BTS search settings'));
|
||||
s.anonymous = true;
|
||||
|
||||
o = s.taboption('bts1', form.DummyValue, '_dummy');
|
||||
o.rawhtml = true;
|
||||
o.default =
|
||||
'<div class="cbi-section-descr">' +
|
||||
_('Note: The package is fully compatible with the latest, third-generation version of the BTSearch website.') +
|
||||
/* --- Downloading data using curl
|
||||
' ' +
|
||||
_('To download data (via the API), you must have the curl package installed (to do this click on the button added below).') +
|
||||
*/
|
||||
'</div>';
|
||||
|
||||
o = s.taboption('bts1', form.ListValue, 'website',
|
||||
_('Website to search for BTS'),
|
||||
_('Select a website for searching.')
|
||||
);
|
||||
o.value('www.btsearch.pl', _('btsearch.pl'));
|
||||
o.value('lteitaly.it', _('lteitaly.it'));
|
||||
o.default = 'www.btsearch.pl';
|
||||
o.modalonly = true;
|
||||
|
||||
o = s.taboption('bts1', form.ListValue, 'btsaction', _('Action'),
|
||||
_('Select an action after clicking the search button.')
|
||||
);
|
||||
o.value('open', _('Open page'));
|
||||
o.default = 'open';
|
||||
o.modalonly = true;
|
||||
o.depends('website', 'www.btsearch.pl');
|
||||
o.value('download', _('Download data'));
|
||||
|
||||
/* --- Downloading data using curl
|
||||
|
||||
if (curlInstalled) {
|
||||
o.value('download', _('Download data'));
|
||||
}
|
||||
|
||||
let sCurl = m.section(form.NamedSection, 'kmods', 'kmods', _());
|
||||
sCurl.render = L.bind(function (view) {
|
||||
return form.NamedSection.prototype.render.apply(this, this.varargs(arguments, 1))
|
||||
.then(L.bind(function (node) {
|
||||
let btn = curlInstalled
|
||||
? E('button', { 'class': 'edit btn', 'disabled': true }, _('Installed'))
|
||||
: E('button', {
|
||||
'class': 'btn cbi-button-action',
|
||||
'click': ui.createHandlerFn(view, 'option_install_curl', this.map),
|
||||
'title': _('Install a package using package manager')
|
||||
}, [_('Install…')]);
|
||||
|
||||
node.appendChild(E('div', { 'class': 'cbi-value' }, [
|
||||
E('label', { 'class': 'cbi-value-title' }, _('Install curl')),
|
||||
E('div', { 'class': 'cbi-value-field', 'style': 'width:25vw' },
|
||||
E('div', { 'class': 'cbi-section-node' }, [ btn ])
|
||||
)
|
||||
]));
|
||||
node.appendChild(E('br'));
|
||||
node.appendChild(E('br'));
|
||||
return node;
|
||||
}, this));
|
||||
}, sCurl, this);
|
||||
|
||||
*/
|
||||
|
||||
return m.render();
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,656 @@
|
||||
'use strict';
|
||||
'require baseclass';
|
||||
'require form';
|
||||
'require fs';
|
||||
'require view';
|
||||
'require uci';
|
||||
'require ui';
|
||||
'require tools.widgets as widgets';
|
||||
|
||||
/*
|
||||
|
||||
Copyright 2025-2026 Rafał Wabik - IceG - From eko.one.pl forum
|
||||
|
||||
MIT License
|
||||
|
||||
*/
|
||||
|
||||
function usrdesc(section_id) {
|
||||
return E('span', (uci.get('defmodems', section_id, 'user_desc') || ''));
|
||||
}
|
||||
|
||||
function getmodem(section_id) {
|
||||
return E('span', (uci.get('defmodems', section_id, 'modem') || ''));
|
||||
}
|
||||
|
||||
function getmodemdata(section_id) {
|
||||
return E('span', '<code>' + (uci.get('defmodems', section_id, 'comm_port') + '</code>' || '<code>' + '' + '</code>'));
|
||||
}
|
||||
|
||||
return view.extend({
|
||||
modemPath: '/etc/modem/modemlist.json',
|
||||
|
||||
load: function () {
|
||||
return Promise.all([
|
||||
L.resolveDefault(fs.list('/dev'), null),
|
||||
L.resolveDefault(fs.read_direct('/sys/kernel/debug/usb/devices', ['-r'])),
|
||||
L.resolveDefault(fs.exec_direct('/usr/bin/mmcli', ['-L', '-J']), null),
|
||||
this.checkPackages(),
|
||||
uci.load('defmodems')
|
||||
]);
|
||||
},
|
||||
|
||||
pkg: {
|
||||
get Name() { return 'defmodems'; },
|
||||
get URL() { return 'https://openwrt.org/packages/pkgdata/' + this.Name + '/'; },
|
||||
get pkgMgrURINew() { return 'admin/system/package-manager'; },
|
||||
get pkgMgrURIOld() { return 'admin/system/opkg'; },
|
||||
|
||||
bestPkgMgrURI: function () {
|
||||
return L.resolveDefault(
|
||||
fs.stat('/www/luci-static/resources/view/system/package-manager.js'), null
|
||||
).then(function (st) {
|
||||
if (st && st.type === 'file')
|
||||
return 'admin/system/package-manager';
|
||||
return L.resolveDefault(fs.stat('/usr/libexec/package-manager-call'), null)
|
||||
.then(function (st2) {
|
||||
return st2 ? 'admin/system/package-manager' : 'admin/system/opkg';
|
||||
});
|
||||
}).catch(function () { return 'admin/system/opkg'; });
|
||||
},
|
||||
|
||||
openInstallerSearch: function (query) {
|
||||
let self = this;
|
||||
return self.bestPkgMgrURI().then(function (uri) {
|
||||
let q = query ? ('?query=' + encodeURIComponent(query)) : '';
|
||||
window.open(L.url(uri) + q, '_blank', 'noopener');
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
fileModemDialog: baseclass.extend({
|
||||
__init__: function (file, title, description, callback, fileExists) {
|
||||
if (fileExists === undefined) fileExists = false;
|
||||
this.file = file;
|
||||
this.title = title;
|
||||
this.description = description;
|
||||
this.callback = callback;
|
||||
this.fileExists = fileExists;
|
||||
},
|
||||
|
||||
load: function () {
|
||||
return L.resolveDefault(fs.read(this.file), '');
|
||||
},
|
||||
|
||||
handleSave: function () {
|
||||
let textarea = document.getElementById('widget.modal_content');
|
||||
let value = textarea.value.trim().replace(/\r\n/g, '\n') + '\n';
|
||||
|
||||
return fs.write(this.file, value)
|
||||
.then(function (rc) {
|
||||
textarea.value = value;
|
||||
return rc;
|
||||
})
|
||||
.catch(function (e) {
|
||||
ui.addNotification(
|
||||
null,
|
||||
E('p', _('Unable to save the contents') + ': %s'.format(e.message))
|
||||
);
|
||||
})
|
||||
.finally(function () {
|
||||
ui.hideModal();
|
||||
});
|
||||
},
|
||||
|
||||
error: function (e) {
|
||||
if (!this.fileExists && e instanceof Error && e.name === 'NotFoundError') {
|
||||
return this.render();
|
||||
} else {
|
||||
ui.showModal(
|
||||
this.title,
|
||||
[
|
||||
E('p', {}, _('Unable to read the contents') + ': %s'.format(e.message)),
|
||||
E('div', { 'class': 'right' }, [
|
||||
E('button', { 'class': 'btn', 'click': ui.hideModal }, _('Dismiss'))
|
||||
])
|
||||
],
|
||||
'cbi-modal'
|
||||
);
|
||||
}
|
||||
},
|
||||
|
||||
show: function () {
|
||||
let self = this;
|
||||
ui.showModal(null, E('p', { 'class': 'spinning' }, _('Loading')));
|
||||
this.load()
|
||||
.then(function (content) {
|
||||
ui.hideModal();
|
||||
return self.render(content);
|
||||
})
|
||||
.catch(function (e) {
|
||||
ui.hideModal();
|
||||
return self.error(e);
|
||||
});
|
||||
},
|
||||
|
||||
render: function (content) {
|
||||
ui.showModal(
|
||||
this.title,
|
||||
[
|
||||
E('p', this.description),
|
||||
E('textarea', {
|
||||
'id': 'widget.modal_content',
|
||||
'class': 'cbi-input-textarea',
|
||||
'style': 'width:100% !important; height: 60vh; min-height: 500px;',
|
||||
'wrap': 'off',
|
||||
'readonly': 'true',
|
||||
'spellcheck': 'false'
|
||||
}, content.trim()),
|
||||
E('div', { 'class': 'right' }, [
|
||||
E('button', { 'class': 'btn', 'click': ui.hideModal }, _('Dismiss'))
|
||||
])
|
||||
],
|
||||
'cbi-modal'
|
||||
);
|
||||
}
|
||||
}),
|
||||
|
||||
packageDialog: baseclass.extend({
|
||||
__init__: function (installedList, pkgHelper) {
|
||||
this.installed = Array.isArray(installedList) ? installedList : [];
|
||||
this.pkgHelper = pkgHelper;
|
||||
|
||||
this.sections = {
|
||||
serial: [
|
||||
{ name: 'sms-tool', label: 'sms-tool' },
|
||||
{ name: 'kmod-usb-serial', label: 'kmod-usb-serial' },
|
||||
{ name: 'kmod-usb-serial-option', label: 'kmod-usb-serial-option' }
|
||||
],
|
||||
ecm: [
|
||||
{ name: 'wget-ssl', label: 'wget-ssl' }
|
||||
],
|
||||
uqmi: [
|
||||
{ name: 'uqmi', label: 'uqmi' },
|
||||
{ name: 'kmod-usb-net-qmi-wwan', label: 'kmod-usb-net-qmi-wwan' },
|
||||
{ name: 'kmod-usb-net-cdc-mbim', label: 'kmod-usb-net-cdc-mbim' }
|
||||
],
|
||||
ModemManager: [
|
||||
{ name: 'modemmanager', label: 'modemmanager' }
|
||||
]
|
||||
};
|
||||
},
|
||||
|
||||
_isInstalled: function (name) {
|
||||
let list = this.installed || [];
|
||||
return list.find(function (s) { return s && s.indexOf(name) !== -1; }) ? true : false;
|
||||
},
|
||||
|
||||
_row: function (pkgName, installed) {
|
||||
let title = E('label', { 'class': 'cbi-value-title' }, pkgName);
|
||||
let btn = installed
|
||||
? E('button', { 'class': 'edit btn', 'disabled': true }, _('Installed'))
|
||||
: E('button', {
|
||||
'class': 'btn cbi-button-positive',
|
||||
'click': (function (name, ctx) { return function () { ctx.pkgHelper.openInstallerSearch(name); }; })(pkgName, this)
|
||||
}, _('Install…'));
|
||||
|
||||
let field = E('div', { 'class': 'cbi-value-field' }, [ btn ]);
|
||||
return E('div', { 'class': 'cbi-value' }, [ title, field ]);
|
||||
},
|
||||
|
||||
_sectionHeader: function (text) {
|
||||
return E('div', {}, [ E('h4', {}, text) ]);
|
||||
},
|
||||
|
||||
show: function () {
|
||||
ui.showModal(_('Required packages'), [
|
||||
E('p', { 'class': 'spinning' }, _('Loading package data…'))
|
||||
]);
|
||||
|
||||
let self = this;
|
||||
let loadInstalled = Promise.resolve().then(function () {
|
||||
if (self.installed && self.installed.length)
|
||||
return self.installed;
|
||||
if (typeof checkPackages === 'function')
|
||||
return checkPackages().then(function (list) { self.installed = list || []; return self.installed; });
|
||||
return [];
|
||||
});
|
||||
|
||||
loadInstalled.then(function () {
|
||||
let sectionOrder = ['serial', 'ecm', 'uqmi', 'ModemManager'];
|
||||
let node = [];
|
||||
|
||||
node.push(E('p', {}, _('Please select method for reading data from the modem and install the necessary packages.')));
|
||||
|
||||
for (let si = 0; si < sectionOrder.length; si++) {
|
||||
let key = sectionOrder[si];
|
||||
let pkgs = self.sections[key];
|
||||
if (!pkgs || pkgs.length === 0) continue;
|
||||
|
||||
node.push(self._sectionHeader(_(key)));
|
||||
|
||||
for (let i = 0; i < pkgs.length; i++) {
|
||||
let p = pkgs[i];
|
||||
let installed = self._isInstalled(p.name);
|
||||
node.push(self._row(p.label, installed));
|
||||
}
|
||||
}
|
||||
|
||||
let footer = E('div', { 'class': 'right' }, [
|
||||
E('div', { 'class': 'btn cbi-button-neutral', 'click': ui.hideModal }, _('Dismiss'))
|
||||
]);
|
||||
|
||||
let body = [
|
||||
E('div', {}, node),
|
||||
footer
|
||||
];
|
||||
|
||||
ui.showModal(_('Required packages'), body);
|
||||
}).catch(function (err) {
|
||||
ui.showModal(_('Required packages'), [
|
||||
E('p', {}, _('Failed to load package data.')),
|
||||
E('pre', {}, (err && err.toString) ? err.toString() : String(err)),
|
||||
E('div', { 'class': 'right' }, [
|
||||
E('div', { 'class': 'btn cbi-button-neutral', 'click': ui.hideModal }, _('Close'))
|
||||
])
|
||||
]);
|
||||
});
|
||||
}
|
||||
}),
|
||||
|
||||
checkPackages: function() {
|
||||
return fs.exec_direct('/usr/bin/opkg', ['list-installed'], 'text')
|
||||
.catch(function () {
|
||||
return fs.exec_direct('/usr/libexec/opkg-call', ['list-installed'], 'text')
|
||||
.catch(function () {
|
||||
return fs.exec_direct('/usr/libexec/package-manager-call', ['list-installed'], 'text')
|
||||
.catch(function () {
|
||||
return '';
|
||||
});
|
||||
});
|
||||
})
|
||||
.then(function (data) {
|
||||
data = (data || '').trim();
|
||||
return data ? data.split('\n') : [];
|
||||
});
|
||||
},
|
||||
|
||||
_isPackageInstalled: function(pkgName) {
|
||||
return this.checkPackages().then(function(installedPackages) {
|
||||
return installedPackages.some(function(pkg) {
|
||||
return pkg.includes(pkgName);
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
render: function (data) {
|
||||
let showModemDialog = new this.fileModemDialog(
|
||||
this.modemPath,
|
||||
_('Modems found'),
|
||||
_("List of found modems. Not all modems may be shown.")
|
||||
);
|
||||
|
||||
let installedList = Array.isArray(data[3]) ? data[3] : [];
|
||||
let showPackageDialog = new this.packageDialog(installedList, this.pkg);
|
||||
|
||||
// Modemz config info
|
||||
let dfmodems = uci.sections('defmodems', 'defmodems');
|
||||
if (!dfmodems || dfmodems.length === 0) {
|
||||
ui.addNotification(null, E('p', {}, [
|
||||
_('Proper Modem Configuration.'),
|
||||
E('br'), E('br'),
|
||||
E('b', _('User should remember:')),
|
||||
E('ul', {}, [
|
||||
E('li', _('1. For a specific modem, only one protocol should be set for communication with modem.')),
|
||||
E('li', _('2. Only one package can read data from modem (at the same time).')),
|
||||
E('li', _('3. Reading data from modem too often may cause it to freeze (MM and uqmi need more time).'))
|
||||
])
|
||||
]), 'info');
|
||||
}
|
||||
|
||||
fs.write('/etc/modem/modemlist.json', '');
|
||||
|
||||
let dlines = data[1].trim().split(/\n/).map(function (line) {
|
||||
return line.replace(/^<\d+>/, '');
|
||||
});
|
||||
let devslist = dlines.join('\n');
|
||||
let alldevs = devslist.split('\n\n');
|
||||
let results = [];
|
||||
|
||||
for (let ai = 0; ai < alldevs.length; ai++) {
|
||||
let modem = alldevs[ai];
|
||||
let lines = modem.split('\n');
|
||||
let vendor = '';
|
||||
let pid = '';
|
||||
let manufacturer = '';
|
||||
let product = '';
|
||||
let serialnumber = '';
|
||||
let driver = '';
|
||||
let bus = '';
|
||||
|
||||
for (let li = 0; li < lines.length; li++) {
|
||||
let line = lines[li];
|
||||
if (line.indexOf('Driver=hub') !== -1 || line.indexOf('Driver=usb-storage') !== -1 || line.indexOf('Driver=usblp') !== -1) {
|
||||
driver = line.split('Driver=')[1].trim();
|
||||
break;
|
||||
}
|
||||
if (line.indexOf('Spd=') !== -1) {
|
||||
let mSpd = line.match(/Spd=([^ ]+)/);
|
||||
if (mSpd && mSpd[1]) bus = mSpd[1].trim();
|
||||
}
|
||||
if (line.indexOf('Vendor=') !== -1) {
|
||||
let mVen = line.match(/Vendor=([^ ]+)/);
|
||||
if (mVen && mVen[1]) vendor = mVen[1].trim();
|
||||
}
|
||||
if (line.indexOf('ProdID=') !== -1) {
|
||||
let mPid = line.match(/ProdID=([^ ]+)/);
|
||||
if (mPid && mPid[1]) pid = mPid[1].trim();
|
||||
}
|
||||
if (line.indexOf('Manufacturer=') !== -1) {
|
||||
let mMan = line.match(/Manufacturer=(.*)/);
|
||||
if (mMan && mMan[1]) manufacturer = mMan[1].trim();
|
||||
}
|
||||
if (line.indexOf('Product=') !== -1) {
|
||||
let mPro = line.match(/Product=(.*)/);
|
||||
if (mPro && mPro[1]) product = mPro[1].trim();
|
||||
}
|
||||
if (line.indexOf('SerialNumber=') !== -1) {
|
||||
let mSn = line.match(/SerialNumber=(.*)/);
|
||||
if (mSn && mSn[1]) serialnumber = mSn[1].trim();
|
||||
}
|
||||
}
|
||||
|
||||
if (driver === '' && (manufacturer !== '' || product !== '')) {
|
||||
results.push({
|
||||
Manufacturer: manufacturer,
|
||||
Product: product,
|
||||
Vendor: vendor,
|
||||
ProdID: pid,
|
||||
Bus_speed: bus,
|
||||
Serial_Number: serialnumber
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
let outputJSON = JSON.stringify(results, null, 2);
|
||||
let countm = Object.keys(results).length;
|
||||
|
||||
fs.write('/etc/modem/modemlist.json', outputJSON + '\n');
|
||||
|
||||
let m, s, o;
|
||||
|
||||
m = new form.Map('defmodems', _('Defined modems'),
|
||||
_('Interface to define the available modems. The list of modems will make it easier for the user to switch between modems in LuCI.')
|
||||
);
|
||||
|
||||
s = m.section(form.TypedSection, 'general', _(''));
|
||||
s.anonymous = true;
|
||||
s.addremove = false;
|
||||
|
||||
o = s.option(form.Button, '_show_modem_btn', _('Show modems found'),
|
||||
_('Currently, only modems connected via USB are searched for.')
|
||||
);
|
||||
o.onclick = function () { showModemDialog.show(); };
|
||||
o.inputtitle = _('Show');
|
||||
o.inputstyle = 'edit btn';
|
||||
|
||||
o = s.option(form.Button, '_show_package_btn', _('Check required packages'),
|
||||
_('Before configuring the modem, check if all required packages are installed in the system.')
|
||||
);
|
||||
o.onclick = function () { showPackageDialog.show(); };
|
||||
o.inputtitle = _('Check');
|
||||
o.inputstyle = 'cbi-button cbi-button-action important';
|
||||
|
||||
s = m.section(form.GridSection, 'defmodems', _('Modem(s)'));
|
||||
s.anonymous = true;
|
||||
s.addremove = true;
|
||||
s.sortable = true;
|
||||
s.nodescriptions = true;
|
||||
s.addbtntitle = _('Add new modem settings...');
|
||||
|
||||
s.tab('general', _('Modem Settings'));
|
||||
|
||||
o = s.taboption('general', form.Value, 'modem', _('Manufacturer / Product'),
|
||||
_("Enter modem name manually if the suggested name is not obvious. \
|
||||
<br /><br /><b>Important</b> \
|
||||
<br />The modem name is only searched for modems connected via USB.")
|
||||
);
|
||||
|
||||
for (let i2 = 0; i2 < countm; i2++) {
|
||||
o.value(results[i2].Manufacturer + ' ' + results[i2].Product);
|
||||
}
|
||||
o.placeholder = _('Please select a modem');
|
||||
o.textvalue = getmodem.bind(o);
|
||||
o.rmempty = false;
|
||||
|
||||
o = s.taboption('general', form.Flag, 'skip_pkgcheck', _('Skip package verification'),
|
||||
_('Enable this option to skip checking for installed packages and allow selecting any method from the list below.')
|
||||
);
|
||||
o.rmempty = false;
|
||||
o.default = '0';
|
||||
o.modalonly = true;
|
||||
o.write = function (section_id, value) {
|
||||
uci.set('defmodems', section_id, 'skip_pkgcheck', value);
|
||||
return form.Flag.prototype.write.apply(this, [section_id, value]);
|
||||
};
|
||||
o.onchange = function (ev, section_id, value) {
|
||||
let selectEl = document.getElementById('widget.cbid.defmodems.' + section_id + '.modemdata');
|
||||
if (!selectEl) return;
|
||||
|
||||
let skip = (value == '1');
|
||||
let keys = skip ? modemdata_all_keys : modemdata_base_keylist;
|
||||
let labels = skip ? modemdata_all_labels : modemdata_base_vallist;
|
||||
let current = selectEl.value;
|
||||
|
||||
selectEl.innerHTML = '';
|
||||
for (let i = 0; i < keys.length; i++) {
|
||||
selectEl.appendChild(E('option', { 'value': keys[i] }, [ labels[i] ]));
|
||||
}
|
||||
|
||||
if (keys.indexOf(current) !== -1) {
|
||||
selectEl.value = current;
|
||||
} else if (keys.length) {
|
||||
selectEl.value = keys[0];
|
||||
}
|
||||
|
||||
selectEl.dispatchEvent(new Event('change', { bubbles: true }));
|
||||
};
|
||||
|
||||
o = s.taboption('general', form.ListValue, 'modemdata', _('Reading data via'),
|
||||
_('Select method for reading data from the modem. <br /> \
|
||||
<br />serial port: <br /> \
|
||||
Select one of the available ttyUSBX / ttyACMx / wwan0atX / mhi_DUN ports.<br /> \
|
||||
<br />ecm: <br /> \
|
||||
Enter the IP address 192.168.X.X under which the modem (Huawei) is available.<br /> \
|
||||
<br />uqmi: <br /> \
|
||||
Select one of the available cdc-wdmX ports.<br /> \
|
||||
<br />ModemManager: <br /> \
|
||||
Select one of the searched modem identifiers.')
|
||||
);
|
||||
|
||||
let have_smstool = installedList.find(function (s) { return s && s.indexOf('sms-tool') !== -1; }) ? true : false;
|
||||
let have_serial = installedList.find(function (s) { return s && s.indexOf('kmod-usb-serial') !== -1; }) ? true : false;
|
||||
let have_serialoption = installedList.find(function (s) { return s && s.indexOf('kmod-usb-serial-option') !== -1; }) ? true : false;
|
||||
|
||||
let have_ecm = installedList.find(function (s) { return s && s.indexOf('wget-ssl') !== -1; }) ? true : false;
|
||||
|
||||
let have_uqmi = installedList.find(function (s) { return s && s.indexOf('uqmi') !== -1; }) ? true : false;
|
||||
let have_uqmiqmi = installedList.find(function (s) { return s && s.indexOf('kmod-usb-net-qmi-wwan') !== -1; }) ? true : false;
|
||||
let have_uqmimbim = installedList.find(function (s) { return s && s.indexOf('kmod-usb-net-cdc-mbim') !== -1; }) ? true : false;
|
||||
|
||||
let have_mm = installedList.find(function (s) { return s && s.indexOf('modemmanager') !== -1; }) ? true : false;
|
||||
|
||||
if (have_serial && have_serialoption && have_smstool) {
|
||||
o.value('serial', _('serial'));
|
||||
}
|
||||
if (have_ecm) {
|
||||
o.value('ecm', _('ecm'));
|
||||
}
|
||||
if ((have_uqmi && have_uqmiqmi) || (have_uqmi && have_uqmimbim)) {
|
||||
o.value('uqmi', _('uqmi'));
|
||||
}
|
||||
if (have_mm) {
|
||||
o.value('mm', _('modemmanager'));
|
||||
}
|
||||
o.exclude = s.section;
|
||||
o.nocreate = true;
|
||||
o.rmempty = false;
|
||||
|
||||
let modemdata_base_keylist = (o.keylist || []).slice();
|
||||
let modemdata_base_vallist = (o.vallist || []).slice();
|
||||
let modemdata_all_keys = ['serial', 'ecm', 'uqmi', 'mm'];
|
||||
let modemdata_all_labels = [_('serial'), _('ecm'), _('uqmi'), _('modemmanager')];
|
||||
|
||||
o.renderWidget = function (section_id, option_index, cfgvalue) {
|
||||
let skip = (uci.get('defmodems', section_id, 'skip_pkgcheck') == '1');
|
||||
if (skip) {
|
||||
this.keylist = modemdata_all_keys.slice();
|
||||
this.vallist = modemdata_all_labels.slice();
|
||||
} else {
|
||||
this.keylist = modemdata_base_keylist.slice();
|
||||
this.vallist = modemdata_base_vallist.slice();
|
||||
}
|
||||
return form.ListValue.prototype.renderWidget.apply(this, arguments);
|
||||
};
|
||||
|
||||
o = s.taboption('general', form.Value, 'comm_port', _('Port / IP / Modem identifier'));
|
||||
o.rmempty = false;
|
||||
o.textvalue = getmodemdata.bind(o);
|
||||
o.modalonly = false;
|
||||
|
||||
o = s.taboption('general', form.Value, 'comm_serial', _('Port for communication'));
|
||||
o.depends('modemdata', 'serial');
|
||||
|
||||
data[0].sort(function (a, b) { return a.name > b.name; });
|
||||
data[0].forEach(function (dev) {
|
||||
if (dev.name.match(/^ttyUSB/) || dev.name.match(/^ttyACM/) || dev.name.match(/^mhi_/) || dev.name.match(/^wwan/)) {
|
||||
o.value('/dev/' + dev.name);
|
||||
}
|
||||
});
|
||||
o.placeholder = _('Please select a port');
|
||||
o.rmempty = false;
|
||||
o.modalonly = true;
|
||||
o.write = function (section_id, value) {
|
||||
uci.set('defmodems', section_id, 'comm_port', value);
|
||||
return form.Value.prototype.write.apply(this, [section_id, value]);
|
||||
};
|
||||
|
||||
o = s.taboption('general', form.Flag, 'forced_plmn_serial', _('Force PLMN'), _('Force reading PLMN from file.'));
|
||||
o.rmempty = false;
|
||||
o.modalonly = true;
|
||||
o.depends('modemdata', 'serial');
|
||||
o.write = function (section_id, value) {
|
||||
uci.set('defmodems', section_id, 'forced_plmn', value);
|
||||
return form.Value.prototype.write.apply(this, [section_id, value]);
|
||||
};
|
||||
|
||||
o = s.taboption('general', form.Value, 'comm_ecm', _('IP adress'));
|
||||
o.depends('modemdata', 'ecm');
|
||||
o.placeholder = _('Enter IP adress');
|
||||
o.rmempty = false;
|
||||
o.modalonly = true;
|
||||
o.write = function (section_id, value) {
|
||||
uci.set('defmodems', section_id, 'comm_port', value);
|
||||
return form.Value.prototype.write.apply(this, [section_id, value]);
|
||||
};
|
||||
|
||||
o.validate = function (section_id, value) {
|
||||
if (!/^[0-9.]+$/.test(value)) {
|
||||
return _('Only numbers and dots are allowed');
|
||||
}
|
||||
let mIp = value.match(/^192\.168\.(\d{1,3})\.(\d{1,3})$/);
|
||||
if (mIp) {
|
||||
let p3 = parseInt(mIp[1], 10);
|
||||
let p4 = parseInt(mIp[2], 10);
|
||||
if (p3 >= 0 && p3 <= 255 && p4 >= 0 && p4 <= 255) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return _('Enter a valid IP address in the format 192.168.X.X');
|
||||
};
|
||||
|
||||
o = s.taboption('general', form.Value, 'comm_uqmi', _('Port for communication'));
|
||||
o.depends('modemdata', 'uqmi');
|
||||
|
||||
data[0].sort(function (a, b) { return a.name > b.name; });
|
||||
data[0].forEach(function (dev) {
|
||||
if (dev.name.match(/^cdc-wdm/)) {
|
||||
o.value('/dev/' + dev.name);
|
||||
}
|
||||
});
|
||||
o.placeholder = _('Please select a QMI/MBIM port');
|
||||
o.rmempty = false;
|
||||
o.modalonly = true;
|
||||
o.write = function (section_id, value) {
|
||||
uci.set('defmodems', section_id, 'comm_port', value);
|
||||
return form.Value.prototype.write.apply(this, [section_id, value]);
|
||||
};
|
||||
|
||||
o = s.taboption('general', form.Flag, 'forced_plmn_uqmi', _('Force PLMN'), _('Force reading PLMN from file.'));
|
||||
o.rmempty = false;
|
||||
o.modalonly = true;
|
||||
o.depends('modemdata', 'uqmi');
|
||||
o.write = function (section_id, value) {
|
||||
uci.set('defmodems', section_id, 'forced_plmn', value);
|
||||
return form.Value.prototype.write.apply(this, [section_id, value]);
|
||||
};
|
||||
|
||||
o = s.taboption('general', form.Flag, 'onproxy', _('MBIM device'), _('Select if you want to read from MBIM.'));
|
||||
o.rmempty = false;
|
||||
o.modalonly = true;
|
||||
o.depends('modemdata', 'uqmi');
|
||||
o.write = function (section_id, value) {
|
||||
uci.set('defmodems', section_id, 'onproxy', value);
|
||||
return form.Value.prototype.write.apply(this, [section_id, value]);
|
||||
};
|
||||
|
||||
o = s.taboption('general', form.Value, 'comm_mm', _('Modem id'));
|
||||
o.depends('modemdata', 'mm');
|
||||
|
||||
data[0].sort(function (a, b) { return a.name > b.name; });
|
||||
try {
|
||||
let mmData = JSON.parse(data[2]);
|
||||
if (mmData['modem-list'] && Array.isArray(mmData['modem-list'])) {
|
||||
mmData['modem-list'].forEach(function (modem) {
|
||||
o.value(modem, _('modem ') + modem.split('/').pop());
|
||||
});
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('MMCLI scan error:', e);
|
||||
}
|
||||
|
||||
o.placeholder = _('Please select a modem id');
|
||||
o.rmempty = false;
|
||||
o.modalonly = true;
|
||||
o.write = function (section_id, value) {
|
||||
uci.set('defmodems', section_id, 'comm_port', value);
|
||||
return form.Value.prototype.write.apply(this, [section_id, value]);
|
||||
};
|
||||
|
||||
o = s.taboption('general', form.Flag, 'forced_plmn_mm', _('Force PLMN'), _('Force reading PLMN from file.'));
|
||||
o.rmempty = false;
|
||||
o.modalonly = true;
|
||||
o.depends('modemdata', 'mm');
|
||||
o.write = function (section_id, value) {
|
||||
uci.set('defmodems', section_id, 'forced_plmn', value);
|
||||
return form.Value.prototype.write.apply(this, [section_id, value]);
|
||||
};
|
||||
|
||||
o = s.taboption('general', widgets.NetworkSelect, 'network', _('Assigned interface'), _('Interface assigned to modem.'));
|
||||
o.rmempty = false;
|
||||
o.default = 'wan';
|
||||
|
||||
o = s.taboption('general', form.Value, 'user_desc', _('User description'));
|
||||
o.rmempty = true;
|
||||
o.modalonly = true;
|
||||
o.placeholder = _('Optional');
|
||||
|
||||
o = s.taboption('general', form.Value, 'user_desc', _('User description (optional)'));
|
||||
o.rawhtml = true;
|
||||
o.remove = function () {};
|
||||
o.modalonly = false;
|
||||
o.textvalue = usrdesc.bind(o);
|
||||
|
||||
return m.render();
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,537 @@
|
||||
'use strict';
|
||||
'require baseclass';
|
||||
'require form';
|
||||
'require fs';
|
||||
'require uci';
|
||||
'require ui';
|
||||
'require view';
|
||||
'require tools.widgets as widgets';
|
||||
|
||||
/*
|
||||
|
||||
Copyright 2025-2026 Rafał Wabik - IceG - From eko.one.pl forum
|
||||
|
||||
MIT License
|
||||
|
||||
*/
|
||||
|
||||
// 添加诊断页面样式
|
||||
function addDiagnosticsStyles() {
|
||||
if (document.getElementById('modemdata-diagnostics-styles')) return;
|
||||
|
||||
const style = document.createElement('style');
|
||||
style.id = 'modemdata-diagnostics-styles';
|
||||
style.type = 'text/css';
|
||||
style.textContent = `
|
||||
/* 强制重置 table 默认样式 - 使用更具体的选择器 */
|
||||
table.table,
|
||||
.cbi-map table.table {
|
||||
display: grid !important;
|
||||
grid-template-columns: repeat(auto-fill, minmax(320px, 1fr)) !important;
|
||||
gap: 16px !important;
|
||||
width: 100% !important;
|
||||
max-width: 100% !important;
|
||||
border: none !important;
|
||||
border-spacing: 0 !important;
|
||||
border-collapse: initial !important;
|
||||
table-layout: initial !important;
|
||||
margin-top: 1em !important;
|
||||
}
|
||||
|
||||
/* 让 tr 透传,使 td 直接成为 grid 子项 */
|
||||
table.table tr,
|
||||
table.table .tr,
|
||||
.cbi-map table.table tr {
|
||||
display: contents !important;
|
||||
}
|
||||
|
||||
/* 重置 td 的默认 table-cell 显示 */
|
||||
table.table td,
|
||||
table.table .td,
|
||||
.cbi-map table.table td {
|
||||
display: flex !important;
|
||||
}
|
||||
|
||||
/* 每个诊断项卡片样式 */
|
||||
.table .td.left {
|
||||
display: flex !important;
|
||||
flex-direction: column !important;
|
||||
align-items: flex-start !important;
|
||||
background: var(--modemdata-card-bg) !important;
|
||||
border: 1px solid var(--modemdata-card-border) !important;
|
||||
border-radius: 8px !important;
|
||||
padding: 16px !important;
|
||||
min-height: 150px !important;
|
||||
transition: all 0.2s ease !important;
|
||||
box-sizing: border-box !important;
|
||||
}
|
||||
|
||||
.table .td.left:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
|
||||
:root[data-darkmode="true"] .table .td.left:hover,
|
||||
body.dark .table .td.left:hover {
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.4);
|
||||
}
|
||||
|
||||
/* 隐藏空单元格 */
|
||||
.table .td.left:empty {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
/* 标题 - 左对齐 */
|
||||
.table .cbi-value-title {
|
||||
font-weight: 600 !important;
|
||||
font-size: 14px !important;
|
||||
margin: 0 0 10px 0 !important;
|
||||
padding: 0 !important;
|
||||
display: block !important;
|
||||
color: var(--modemdata-text-primary) !important;
|
||||
line-height: 1.4 !important;
|
||||
text-align: left !important;
|
||||
width: 100% !important;
|
||||
}
|
||||
|
||||
/* 命令区域 - 左对齐,单行显示 */
|
||||
.table .td.left p {
|
||||
margin: 0 0 12px 0 !important;
|
||||
padding: 0 !important;
|
||||
font-size: 11px !important;
|
||||
line-height: 1.4 !important;
|
||||
flex: 1 !important;
|
||||
text-align: left !important;
|
||||
width: 100% !important;
|
||||
overflow: hidden !important;
|
||||
}
|
||||
|
||||
/* code 标签 - 单行显示,可滚动 */
|
||||
.table .td.left p code {
|
||||
background-color: rgba(130, 130, 130, 0.3) !important;
|
||||
padding: 5px 10px !important;
|
||||
border-radius: 4px !important;
|
||||
font-size: 11px !important;
|
||||
font-family: 'Consolas', 'Monaco', 'Courier New', monospace !important;
|
||||
color: #0a0a0a !important;
|
||||
font-weight: 600 !important;
|
||||
border: 1px solid rgba(0, 0, 0, 0.2) !important;
|
||||
display: block !important;
|
||||
white-space: nowrap !important;
|
||||
overflow-x: auto !important;
|
||||
max-width: 100% !important;
|
||||
}
|
||||
|
||||
:root[data-darkmode="true"] .table .td.left p code,
|
||||
body.dark .table .td.left p code {
|
||||
background-color: rgba(210, 210, 210, 0.25) !important;
|
||||
color: #f5f5f5 !important;
|
||||
border-color: rgba(255, 255, 255, 0.25) !important;
|
||||
}
|
||||
|
||||
/* 按钮 - 左对齐,固定宽度 */
|
||||
.diag-action {
|
||||
display: flex !important;
|
||||
margin-top: auto !important;
|
||||
width: 100% !important;
|
||||
justify-content: flex-start !important;
|
||||
}
|
||||
|
||||
.diag-action .cbi-button {
|
||||
width: auto !important;
|
||||
min-width: 90px !important;
|
||||
padding: 6px 20px !important;
|
||||
display: inline-block !important;
|
||||
text-align: center !important;
|
||||
}
|
||||
|
||||
/* 修复下拉框文字被遮挡问题 */
|
||||
.cbi-input-select {
|
||||
line-height: 1.6 !important;
|
||||
padding: 8px 12px !important;
|
||||
min-height: 40px !important;
|
||||
font-size: 14px !important;
|
||||
}
|
||||
|
||||
.cbi-input-select option {
|
||||
line-height: 1.8 !important;
|
||||
padding: 8px 12px !important;
|
||||
min-height: 36px !important;
|
||||
font-size: 14px !important;
|
||||
}
|
||||
|
||||
/* 响应式断点优化 */
|
||||
@media (max-width: 669px) {
|
||||
table.table,
|
||||
.cbi-map table.table {
|
||||
grid-template-columns: 1fr !important;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 670px) {
|
||||
table.table,
|
||||
.cbi-map table.table {
|
||||
grid-template-columns: repeat(auto-fill, minmax(320px, 1fr)) !important;
|
||||
}
|
||||
}
|
||||
|
||||
/* 输出对话框样式 */
|
||||
#cmd_modal_output {
|
||||
font-family: 'Consolas', 'Monaco', 'Courier New', monospace;
|
||||
font-size: 12px;
|
||||
line-height: 1.5;
|
||||
background-color: #f5f5f5;
|
||||
border: 1px solid #ddd;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
:root[data-darkmode="true"] #cmd_modal_output,
|
||||
body.dark #cmd_modal_output {
|
||||
background-color: #1a1a1a;
|
||||
border-color: #444;
|
||||
color: #e0e0e0;
|
||||
}
|
||||
|
||||
/* 模态框按钮样式 */
|
||||
.cbi-modal .right .btn {
|
||||
margin-left: 8px;
|
||||
min-width: 80px;
|
||||
}
|
||||
|
||||
/* Modem 选择器样式 */
|
||||
#mselect {
|
||||
font-size: 13px;
|
||||
padding: 8px 12px;
|
||||
border-radius: 4px;
|
||||
border: 1px solid #ccc;
|
||||
transition: border-color 0.2s ease;
|
||||
}
|
||||
|
||||
#mselect:focus {
|
||||
border-color: #007bff;
|
||||
outline: none;
|
||||
box-shadow: 0 0 0 3px rgba(0, 123, 255, 0.1);
|
||||
}
|
||||
|
||||
:root[data-darkmode="true"] #mselect,
|
||||
body.dark #mselect {
|
||||
background-color: rgba(255, 255, 255, 0.05);
|
||||
border-color: rgba(255, 255, 255, 0.2);
|
||||
color: #e0e0e0;
|
||||
}
|
||||
`;
|
||||
document.head.appendChild(style);
|
||||
|
||||
// 强制清理 table 的 inline styles,确保 Grid 布局生效
|
||||
function cleanTableStyles() {
|
||||
document.querySelectorAll('table.table').forEach(function(table) {
|
||||
table.removeAttribute('style');
|
||||
table.style.cssText = 'display: grid !important; grid-template-columns: repeat(auto-fill, minmax(320px, 1fr)) !important; gap: 16px !important; width: 100% !important;';
|
||||
});
|
||||
document.querySelectorAll('table.table td, table.table .td').forEach(function(td) {
|
||||
td.removeAttribute('style');
|
||||
if (td.classList.contains('left')) {
|
||||
td.style.cssText = 'display: flex !important;';
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 多次尝试清理,确保生效
|
||||
setTimeout(cleanTableStyles, 50);
|
||||
setTimeout(cleanTableStyles, 200);
|
||||
setTimeout(cleanTableStyles, 500);
|
||||
}
|
||||
|
||||
let commandOutputDialog = baseclass.extend({
|
||||
__init__: function(title, content){ this.title = title; this.content = content; },
|
||||
|
||||
render: function(){
|
||||
let self = this;
|
||||
|
||||
ui.showModal(this.title, [
|
||||
E('p', _('Command result')),
|
||||
E('textarea', {
|
||||
'id': 'cmd_modal_output',
|
||||
'class': 'cbi-input-textarea',
|
||||
'style': 'width:100% !important; height:60vh; min-height:500px;',
|
||||
'readonly': true,
|
||||
'wrap': 'off',
|
||||
'spellcheck': 'false'
|
||||
}, this.content.trim()),
|
||||
|
||||
E('div', { 'class': 'right' }, [
|
||||
E('button', {
|
||||
'class': 'btn cbi-button-remove',
|
||||
'click': ui.createHandlerFn(this, function(){
|
||||
document.getElementById('cmd_modal_output').value = '';
|
||||
fs.write('/tmp/debug_result.txt', '')
|
||||
.catch(function(e){
|
||||
ui.addNotification(null, E('p', _('Unable to clear the file') + ': %s'.format(e.message)), 'error');
|
||||
});
|
||||
})
|
||||
}, _('Clear')), ' ',
|
||||
|
||||
E('button', {
|
||||
'class': 'cbi-button cbi-button-apply important',
|
||||
'click': ui.createHandlerFn(this, function(){
|
||||
let blob = new Blob([self.content], { type: 'text/plain' });
|
||||
let link = document.createElement('a');
|
||||
link.download = 'debug_result.txt';
|
||||
link.href = URL.createObjectURL(blob);
|
||||
link.click();
|
||||
URL.revokeObjectURL(link.href);
|
||||
})
|
||||
}, _('Download')), ' ',
|
||||
|
||||
E('button', {
|
||||
'class': 'btn',
|
||||
'click': ui.hideModal
|
||||
}, _('Close'))
|
||||
])
|
||||
], 'cbi-modal');
|
||||
},
|
||||
|
||||
show: function(){ this.render(); }
|
||||
});
|
||||
|
||||
return view.extend({
|
||||
handleCommand: function(exec, args){
|
||||
return fs.exec(exec, args).then((res)=>{
|
||||
let output = [res.stdout||'', res.stderr||''].join('\n');
|
||||
fs.write('/tmp/debug_result.txt', res.stdout||'');
|
||||
let dialog = new commandOutputDialog(_('Diagnostics'), output);
|
||||
dialog.show();
|
||||
}).catch((err)=>{ ui.addNotification(null, E('p', [err])); }).finally(()=>{});
|
||||
},
|
||||
|
||||
handleUSB: function(){ return this.handleCommand('/bin/cat', ['/sys/kernel/debug/usb/devices']); },
|
||||
handleTTY: function(){ return this.handleCommand('/bin/ls', ['/dev']); },
|
||||
|
||||
handleDBG: function(){
|
||||
let select = document.getElementById('mselect'); let value = select ? select.value : '';
|
||||
let valueParts = value.split('*'); let device = valueParts[1] || ''; let network = valueParts[2] || '';
|
||||
return this.handleCommand('/bin/sh', ['/usr/bin/md_serial_ecm', device, network]);
|
||||
},
|
||||
|
||||
handleproduct: function(){
|
||||
let select = document.getElementById('mselect'); let value = select ? select.value : '';
|
||||
let device = value.split('*')[1] || '';
|
||||
return this.handleCommand('/bin/sh', ['-x', '/usr/share/modemdata/product.sh', device]);
|
||||
},
|
||||
|
||||
handlenetwork: function(){
|
||||
let select = document.getElementById('mselect'); let value = select ? select.value : '';
|
||||
let network = value.split('*')[2] || '';
|
||||
return this.handleCommand('/bin/sh', ['-x', '/usr/share/modemdata/network.sh', network]);
|
||||
},
|
||||
|
||||
handleparams: function(){
|
||||
let select = document.getElementById('mselect'); let value = select ? select.value : '';
|
||||
let device = value.split('*')[1] || '';
|
||||
return this.handleCommand('/bin/sh', ['-x', '/usr/share/modemdata/params.sh', device]);
|
||||
},
|
||||
|
||||
handleuqmi: function(){
|
||||
let select = document.getElementById('mselect'); let value = select ? select.value : '';
|
||||
let valueParts = value.split('*'); let device = valueParts[1]||''; let network = valueParts[2]||''; let forced_plmn = valueParts[3]||''; let onproxy = (valueParts[4]||'').replace('undefined','0');
|
||||
return this.handleCommand('/bin/sh', ['/usr/bin/md_uqmi', device, network, forced_plmn, onproxy]);
|
||||
},
|
||||
|
||||
handleparuqmi: function(){
|
||||
let select = document.getElementById('mselect'); let value = select ? select.value : '';
|
||||
let valueParts = value.split('*'); let device = valueParts[1]||''; let forced_plmn = valueParts[3]||''; let onproxy = (valueParts[4]||'').replace('undefined','0');
|
||||
return this.handleCommand('/bin/sh', ['-x', '/usr/share/modemdata/params_qmi.sh', device, forced_plmn, onproxy]);
|
||||
},
|
||||
|
||||
handlemm: function(){
|
||||
let select = document.getElementById('mselect'); let value = select ? select.value : '';
|
||||
let valueParts = value.split('*'); let device = valueParts[1]||''; let network = valueParts[2]||''; let forced_plmn = valueParts[3]||'';
|
||||
return this.handleCommand('/bin/sh', ['/usr/bin/md_modemmanager', device, network, forced_plmn]);
|
||||
},
|
||||
|
||||
handleparamsmm: function(){
|
||||
let select = document.getElementById('mselect'); let value = select ? select.value : '';
|
||||
let valueParts = value.split('*'); let device = valueParts[1]||''; let forced_plmn = valueParts[3]||'';
|
||||
return this.handleCommand('/bin/sh', ['-x', '/usr/share/modemdata/params_modemmanager.sh', device, forced_plmn]);
|
||||
},
|
||||
|
||||
handleprodmm: function(){
|
||||
let select = document.getElementById('mselect'); let value = select ? select.value : '';
|
||||
let valueParts = value.split('*'); let device = valueParts[1]||'';
|
||||
return this.handleCommand('/bin/sh', ['-x', '/usr/share/modemdata/product_modemmanager.sh', device]);
|
||||
},
|
||||
|
||||
handleClear: function(){ fs.write('/tmp/debug_result.txt',''); },
|
||||
|
||||
handleDownload: function(){
|
||||
return L.resolveDefault(fs.read_direct('/tmp/debug_result.txt'), null).then((res)=>{
|
||||
if(res){ let blob = new Blob([res], { type:'text/plain' }); let link = document.createElement('a'); link.download = 'debug_result.txt'; link.href = URL.createObjectURL(blob); link.click(); URL.revokeObjectURL(link.href); }
|
||||
}).catch((err)=>{ ui.addNotification(null, E('p', {}, _('Download error') + ': ' + err.message)); });
|
||||
},
|
||||
|
||||
handleBlock: function(){
|
||||
let select = document.getElementById('mselect'); let value = select ? select.value : ''; let valueParts = value.split('_'); let modemdata = valueParts[0] || '';
|
||||
let buttons = document.querySelectorAll('.diag-action > .cbi-button'); for(let i=0;i<buttons.length;i++) buttons[i].removeAttribute('disabled');
|
||||
|
||||
if (modemdata.includes('uqmi')) {
|
||||
document.getElementById('s1').setAttribute('disabled','disabled');
|
||||
document.getElementById('s2').setAttribute('disabled','disabled');
|
||||
document.getElementById('s3').setAttribute('disabled','disabled');
|
||||
document.getElementById('m1').setAttribute('disabled','disabled');
|
||||
document.getElementById('m2').setAttribute('disabled','disabled');
|
||||
document.getElementById('m3').setAttribute('disabled','disabled');
|
||||
}
|
||||
if (modemdata.includes('serial')) {
|
||||
document.getElementById('u1').setAttribute('disabled','disabled');
|
||||
document.getElementById('u2').setAttribute('disabled','disabled');
|
||||
document.getElementById('m1').setAttribute('disabled','disabled');
|
||||
document.getElementById('m2').setAttribute('disabled','disabled');
|
||||
document.getElementById('m3').setAttribute('disabled','disabled');
|
||||
}
|
||||
if (modemdata.includes('mm')) {
|
||||
document.getElementById('s1').setAttribute('disabled','disabled');
|
||||
document.getElementById('s2').setAttribute('disabled','disabled');
|
||||
document.getElementById('s3').setAttribute('disabled','disabled');
|
||||
document.getElementById('u1').setAttribute('disabled','disabled');
|
||||
document.getElementById('u2').setAttribute('disabled','disabled');
|
||||
}
|
||||
},
|
||||
|
||||
load: function(){ return L.resolveDefault(uci.load('defmodems')); },
|
||||
|
||||
render: function(){
|
||||
// 初始化样式
|
||||
addDiagnosticsStyles();
|
||||
|
||||
let sections = uci.sections('defmodems','defmodems');
|
||||
let result = sections.map(s=>`${s.modemdata}*${s.comm_port}*${s.network}*${s.forced_plmn}*${s.onproxy}*#[ ${s.modemdata} ] ${s.comm_port} - ${s.modem} (${s.user_desc})`).join(';');
|
||||
result = result.replace("(undefined)","");
|
||||
|
||||
let off_s1,off_s2,off_s3,off_u1,off_u2,off_m1,off_m2,off_m3;
|
||||
let modemz = E('div', { class:'cbi-section' }, [
|
||||
E('div', { class:'cbi-section-descr' }, this.description),
|
||||
E('div', { class:'cbi-value' }, [
|
||||
E('label', { class:'cbi-value-title' }, [_('Modem')]),
|
||||
E('div', { class:'cbi-value-field' }, [
|
||||
E('select', { class:'cbi-input-select', id:'mselect', style:'width:100%;', change:ui.createHandlerFn(this,'handleBlock'), mousedown:ui.createHandlerFn(this,'handleBlock') },
|
||||
(result||'').trim().split(/;/).map(function(cmd){ let fields = cmd.split(/#/); let name = fields[1]; let code = fields[0]; return E('option', { value:code }, name); })
|
||||
)
|
||||
])
|
||||
])
|
||||
]);
|
||||
|
||||
let modemdata = result.split('_')[0] || '';
|
||||
if (modemdata.includes('uqmi')) {
|
||||
const ids=['s1','s2','s3','m1','m2','m3'];
|
||||
off_s1=ids.includes('s1')?true:false;
|
||||
off_s2=ids.includes('s2')?true:false;
|
||||
off_s3=ids.includes('s3')?true:false;
|
||||
off_m1=ids.includes('m1')?true:false;
|
||||
off_m2=ids.includes('m2')?true:false;
|
||||
off_m3=ids.includes('m3')?true:false;
|
||||
}
|
||||
if (modemdata.includes('serial')) {
|
||||
const ids=['u1','u2','m1','m2','m3'];
|
||||
off_u1=ids.includes('u1')?true:false;
|
||||
off_u2=ids.includes('u2')?true:false;
|
||||
off_m1=ids.includes('m1')?true:false;
|
||||
off_m2=ids.includes('m2')?true:false;
|
||||
off_m3=ids.includes('m3')?true:false;
|
||||
}
|
||||
if (modemdata.includes('mm')) {
|
||||
const ids=['s1','s2','s3','u1','u2'];
|
||||
off_s1=ids.includes('s1')?true:false;
|
||||
off_s2=ids.includes('s2')?true:false;
|
||||
off_s3=ids.includes('s3')?true:false;
|
||||
off_u1=ids.includes('u1')?true:false;
|
||||
off_u2=ids.includes('u2')?true:false;
|
||||
}
|
||||
|
||||
let table4 = E('table', { class:'table' }, [
|
||||
E('tr', {}, [
|
||||
E('td', { class:'td left' }, [
|
||||
E('label', { class:'cbi-value-title' }, _("USB debug information")),
|
||||
E('p', {}, _("<code>cat /sys/kernel/debug/usb/devices</code>")),
|
||||
E('span', { class:'diag-action' }, [ E('button', { class:'cbi-button', click:ui.createHandlerFn(this,'handleUSB') }, _('Run')) ])
|
||||
]),
|
||||
E('td', { class:'td left' }, [
|
||||
E('label', { class:'cbi-value-title' }, _("Check ttyX ports")),
|
||||
E('p', {}, _("<code>ls /dev</code>")),
|
||||
E('span', { class:'diag-action' }, [ E('button', { class:'cbi-button', click:ui.createHandlerFn(this,'handleTTY') }, _('Run')) ])
|
||||
]),
|
||||
E('td', { class:'td left' }, [
|
||||
E('label', { class:'cbi-value-title' }, _("Check") + ' ' + _("network.sh")),
|
||||
E('p', {}, _("<code>sh -x /usr/share/modemdata/network.sh</code>")),
|
||||
E('span', { class:'diag-action' }, [ E('button', { class:'cbi-button', click:ui.createHandlerFn(this,'handlenetwork') }, _('Run')) ])
|
||||
])
|
||||
])
|
||||
]);
|
||||
|
||||
let table = E('table', { class:'table' }, [
|
||||
E('tr', {}, [
|
||||
E('td', { class:'td left' }, [
|
||||
E('label', { class:'cbi-value-title' }, _("Check serial and ecm mode")),
|
||||
E('p', {}, _("<code>sh /usr/bin/md_serial_ecm</code>")),
|
||||
E('span', { class:'diag-action' }, [ E('button', { class:'cbi-button cbi-button-add', id:'s1', disabled:off_s1, click:ui.createHandlerFn(this,'handleDBG') }, _('Run')) ])
|
||||
]),
|
||||
E('td', { class:'td left' }, [
|
||||
E('label', { class:'cbi-value-title' }, _("Check") + ' ' + _("product.sh")),
|
||||
E('p', {}, _("<code>sh -x /usr/share/modemdata/product.sh</code>")),
|
||||
E('span', { class:'diag-action' }, [ E('button', { class:'cbi-button cbi-button-add', id:'s2', disabled:off_s2, click:ui.createHandlerFn(this,'handleproduct') }, _('Run')) ])
|
||||
]),
|
||||
E('td', { class:'td left' }, [
|
||||
E('label', { class:'cbi-value-title' }, _("Check") + ' ' + _("params.sh")),
|
||||
E('p', {}, _("<code>sh -x /usr/share/modemdata/params.sh</code>")),
|
||||
E('span', { class:'diag-action' }, [ E('button', { class:'cbi-button cbi-button-add', id:'s3', disabled:off_s3, click:ui.createHandlerFn(this,'handleparams') }, _('Run')) ])
|
||||
])
|
||||
])
|
||||
]);
|
||||
|
||||
let table2 = E('table', { class:'table' }, [
|
||||
E('tr', {}, [
|
||||
E('td', { class:'td left' }, [
|
||||
E('label', { class:'cbi-value-title' }, _("Check uqmi mode")),
|
||||
E('p', {}, _("<code>sh -x /usr/bin/md_uqmi</code>")),
|
||||
E('span', { class:'diag-action' }, [ E('button', { class:'cbi-button cbi-button-remove', id:'u1', disabled:off_u1, click:ui.createHandlerFn(this,'handleuqmi') }, _('Run')) ])
|
||||
]),
|
||||
E('td', { class:'td left' }, [
|
||||
E('label', { class:'cbi-value-title' }, _("Check") + ' ' + _("params_qmi.sh")),
|
||||
E('p', {}, _("<code>sh -x /usr/share/modemdata/params_qmi.sh</code>")),
|
||||
E('span', { class:'diag-action' }, [ E('button', { class:'cbi-button cbi-button-remove', id:'u2', disabled:off_u2, click:ui.createHandlerFn(this,'handleparuqmi') }, _('Run')) ])
|
||||
]),
|
||||
E('td', { class:'td left' }, [])
|
||||
])
|
||||
]);
|
||||
|
||||
let table3 = E('table', { class:'table' }, [
|
||||
E('tr', {}, [
|
||||
E('td', { class:'td left' }, [
|
||||
E('label', { class:'cbi-value-title' }, _("Check ModemManager mode")),
|
||||
E('p', {}, _("<code>sh -x /usr/bin/md_modemmanager</code>")),
|
||||
E('span', { class:'diag-action' }, [ E('button', { class:'cbi-button cbi-button-apply', id:'m1', disabled:off_m1, click:ui.createHandlerFn(this,'handlemm') }, _('Run')) ])
|
||||
]),
|
||||
E('td', { class:'td left' }, [
|
||||
E('label', { class:'cbi-value-title' }, _("Check") + ' ' + _("params_modemmanager.sh")),
|
||||
E('p', {}, _("<code>sh -x /usr/share/modemdata/params_modemmanager.sh</code>")),
|
||||
E('span', { class:'diag-action' }, [ E('button', { class:'cbi-button cbi-button-apply', id:'m2', disabled:off_m2, click:ui.createHandlerFn(this,'handleparamsmm') }, _('Run')) ])
|
||||
]),
|
||||
E('td', { class:'td left' }, [
|
||||
E('label', { class:'cbi-value-title' }, _("Check") + ' ' + _("product_modemmanager.sh")),
|
||||
E('p', {}, _("<code>sh -x /usr/share/modemdata/product_modemmanager.sh</code>")),
|
||||
E('span', { class:'diag-action' }, [ E('button', { class:'cbi-button cbi-button-apply', id:'m3', disabled:off_m3, click:ui.createHandlerFn(this,'handleprodmm') }, _('Run')) ])
|
||||
])
|
||||
])
|
||||
]);
|
||||
|
||||
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>');
|
||||
return E('div', { class:'cbi-map' }, [
|
||||
E('h2', {}, _('Diagnostics')),
|
||||
E('div', { class:'cbi-map-descr' }, _('Run various commands to verify data read from the modem and debug scripts.') + '<br>' + info),
|
||||
E('hr'), modemz, table4, table, table2, table3
|
||||
]);
|
||||
},
|
||||
|
||||
handleSaveApply: null,
|
||||
handleSave: null,
|
||||
handleReset: null
|
||||
});
|
||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,220 @@
|
||||
'use strict';
|
||||
'require form';
|
||||
'require fs';
|
||||
'require view';
|
||||
'require ui';
|
||||
'require uci';
|
||||
'require dom';
|
||||
'require tools.widgets as widgets';
|
||||
|
||||
/*
|
||||
|
||||
Copyright 2025-2026 Rafał Wabik - IceG - From eko.one.pl forum
|
||||
|
||||
MIT License
|
||||
|
||||
*/
|
||||
|
||||
function handleOpen(ev) {
|
||||
if (ev === 'ropenissues') { window.open('https://github.com/4IceG/luci-app-modemdata/issues'); return; }
|
||||
if (ev === 'copenissues') { window.open('https://github.com/obsy/modemdata/issues'); return; }
|
||||
if (ev === 'opendiscussion') { window.open('https://github.com/4IceG/luci-app-modemdata/discussions'); return; }
|
||||
if (ev === 'ropencoffee') { window.open('https://suppi.pl/rafalwabik'); return; }
|
||||
if (ev === 'copencoffee') { window.open('https://buycoffee.to/eko.one.pl'); return; }
|
||||
if (ev === 'opensupport') { window.open('https://github.com/sponsors/4IceG'); return; }
|
||||
if (ev === 'opentopic') { window.open('https://eko.one.pl/forum/viewtopic.php?id=24829'); return; }
|
||||
}
|
||||
|
||||
return view.extend({
|
||||
load: function () {
|
||||
return uci.load('modemdata');
|
||||
},
|
||||
|
||||
render: function (data) {
|
||||
let m, s, o;
|
||||
|
||||
let info = _('In the future tab will allow you to update the package from %sRafał (IceG) external repository%s.')
|
||||
.format('<a href="https://github.com/4IceG/Modem-extras-apk" target="_blank">', '</a>');
|
||||
|
||||
m = new form.Map('modemdata', _('Package update and support'), info);
|
||||
|
||||
s = m.section(form.TypedSection);
|
||||
s.anonymous = true;
|
||||
|
||||
s.tab('info', _('Modemdata Info'));
|
||||
|
||||
let packages = [
|
||||
{
|
||||
package: _('Modemdata'),
|
||||
author: 'Cezary Jackiewicz (obsy)',
|
||||
authorLink: 'https://github.com/obsy',
|
||||
buttons: [
|
||||
{
|
||||
class: 'cbi-button-action important',
|
||||
label: _('Buy a coffee'),
|
||||
tooltip: _('Buy a coffee if you want to support the development of the project and the author'),
|
||||
action: 'copencoffee',
|
||||
disabled: false
|
||||
},
|
||||
{
|
||||
class: 'cbi-button-remove',
|
||||
label: _('Report a bug'),
|
||||
tooltip: _('Report a bug on the package Github page'),
|
||||
action: 'copenissues',
|
||||
disabled: false
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
package: _('Luci-app-modemdata'),
|
||||
author: 'Rafał Wabik (IceG)',
|
||||
authorLink: 'https://github.com/4IceG',
|
||||
buttons: [
|
||||
{
|
||||
class: 'cbi-button-action important',
|
||||
label: _('Buy a coffee'),
|
||||
tooltip: _('Buy a coffee if you want to support the development of the project and the author'),
|
||||
action: 'ropencoffee',
|
||||
disabled: false
|
||||
},
|
||||
{
|
||||
class: 'cbi-button-action',
|
||||
label: _('Become a sponsor'),
|
||||
tooltip: _('Become a sponsor if you want to support the development of the project and the author'),
|
||||
action: 'opensupport',
|
||||
disabled: false
|
||||
},
|
||||
{
|
||||
class: 'cbi-button-add',
|
||||
label: _('Write on forum'),
|
||||
tooltip: _('Write in the topic of the package on the forum eko.one.pl'),
|
||||
action: 'opentopic',
|
||||
disabled: false
|
||||
},
|
||||
{
|
||||
class: 'cbi-button-neutral',
|
||||
label: _('Open discussion'),
|
||||
tooltip: _('Open a package discussion on Github'),
|
||||
action: 'opendiscussion',
|
||||
disabled: false
|
||||
},
|
||||
{
|
||||
class: 'cbi-button-remove',
|
||||
label: _('Report a bug'),
|
||||
tooltip: _('Report a bug on the package Github page'),
|
||||
action: 'ropenissues',
|
||||
disabled: false
|
||||
}
|
||||
]
|
||||
}
|
||||
];
|
||||
|
||||
let rows = [];
|
||||
let table = E('table', {
|
||||
'class': 'table',
|
||||
'style': 'border:1px solid var(--border-color-medium)!important; table-layout:fixed; border-collapse:collapse; width:100%;'
|
||||
}, [
|
||||
E('tr', { 'class': 'tr table-titles' }, [
|
||||
E('th', { 'class': 'th' }, _('Package name')),
|
||||
E('th', { 'class': 'th' }, _('Author (package maintainer)')),
|
||||
E('th', { 'class': 'th nowrap cbi-section-actions' })
|
||||
])
|
||||
]);
|
||||
|
||||
for (let i = 0; i < packages.length; i++) {
|
||||
let pkg = packages[i];
|
||||
let buttonElements = [];
|
||||
|
||||
for (let j = 0; j < pkg.buttons.length; j++) {
|
||||
let btn = pkg.buttons[j];
|
||||
let buttonAttrs = {
|
||||
'class': 'btn ' + btn.class,
|
||||
'data-tooltip': btn.tooltip
|
||||
};
|
||||
|
||||
if (btn.disabled) {
|
||||
buttonAttrs.disabled = true;
|
||||
} else if (btn.action) {
|
||||
buttonAttrs.click = function() { return handleOpen(btn.action); };
|
||||
}
|
||||
|
||||
buttonElements.push(E('button', buttonAttrs, btn.label));
|
||||
}
|
||||
|
||||
let authorCell = pkg.author ?
|
||||
E('a', { 'href': pkg.authorLink, 'target': '_blank', 'style': 'color:#37c' }, pkg.author) :
|
||||
'';
|
||||
|
||||
rows.push([
|
||||
pkg.package,
|
||||
authorCell,
|
||||
E('div', { 'style': 'display: flex; flex-wrap: wrap; gap: 0.5em;' }, buttonElements)
|
||||
]);
|
||||
}
|
||||
|
||||
cbi_update_table(table, rows);
|
||||
|
||||
o = s.taboption('info', form.DummyValue, '_packages_info');
|
||||
o.render = function() {
|
||||
return E('div', { 'class': 'cbi-section' }, [
|
||||
E('p', {}, _('Information about package authors and available support options.')),
|
||||
table
|
||||
]);
|
||||
};
|
||||
|
||||
s.tab('troubleshooting', _('Troubleshooting / Hints'));
|
||||
|
||||
let troubleshootingData = [
|
||||
// 1.
|
||||
{
|
||||
issue: _('If data in LuCI is not visible, and we are 100% sure that the modem has been defined/configured correctly.'),
|
||||
solution: _('Go to the Diagnostics tab and run a script check. Most often, the error is caused by a strange operator name. To fix this, select the Force PLMN from file option if it is available in the modem configuration options.')
|
||||
},
|
||||
// 2.
|
||||
{
|
||||
issue: _('Package does not show data for the Fibocom FM350-GL modem.'),
|
||||
solution: _('The modem is very sensitive to data read by the communication port. Selecting the wrong communication port may cause the modem to freeze or restart.') +
|
||||
' ' +
|
||||
'<a href="https://github.com/obsy/modemfeed/tree/master/luci/protocols/luci-proto-xmm" target="_blank" rel="noopener noreferrer">luci-proto-xmm</a>' +
|
||||
' ' + _('and') + ' ' +
|
||||
'<a href="https://github.com/obsy/modemfeed/tree/master/packages/net/xmm-modem" target="_blank" rel="noopener noreferrer">xmm-modem</a>' +
|
||||
' ' + _('packages are recommended for proper modem support.')
|
||||
}
|
||||
];
|
||||
|
||||
let troubleshootingRows = [];
|
||||
let troubleshootingTable = E('table', {
|
||||
'class': 'table',
|
||||
'style': 'border:1px solid var(--border-color-medium)!important; table-layout:fixed; border-collapse:collapse; width:100%;'
|
||||
}, [
|
||||
E('tr', { 'class': 'tr table-titles' }, [
|
||||
E('th', { 'class': 'th', 'style': 'width:40%' }, _('Problem')),
|
||||
E('th', { 'class': 'th' }, _('Suggested solution'))
|
||||
])
|
||||
]);
|
||||
|
||||
for (let i = 0; i < troubleshootingData.length; i++) {
|
||||
let item = troubleshootingData[i];
|
||||
troubleshootingRows.push([
|
||||
item.issue,
|
||||
E('em', {}, item.solution)
|
||||
]);
|
||||
}
|
||||
|
||||
cbi_update_table(troubleshootingTable, troubleshootingRows);
|
||||
|
||||
o = s.taboption('troubleshooting', form.DummyValue, '_troubleshooting_info');
|
||||
o.render = function() {
|
||||
return E('div', { 'class': 'cbi-section' }, [
|
||||
E('p', {}, _('Common problems that may arise when using the package, and ways to solve them.')),
|
||||
troubleshootingTable
|
||||
]);
|
||||
};
|
||||
|
||||
return m.render();
|
||||
},
|
||||
|
||||
handleSaveApply: null,
|
||||
handleSave: null,
|
||||
handleReset: null
|
||||
});
|
||||
Reference in new issue
Block a user