This commit is contained in:
Miguel Fuertes committed 2025-10-07 21:01:46 +02:00
1 parent cfe0ee5ddd
commit 50774bfbf7
2 files changed
+171 -159

No files matched your search

@@ -2,7 +2,6 @@
'require view';
'require form';
'require uci';
'require ui';
'require fs';
return view.extend({
@@ -19,153 +18,150 @@ return view.extend({
m = new form.Map('usbgadget',
_('USB Gadget Configuration'),
_('Configure USB gadget functions to make OpenWrt appear as various USB devices when connected to a host computer.'));
_('Configure USB gadget functions. Changes take effect after saving and restarting the service.'));
// Device Configuration Section
// =================================================================
// Device Settings
// =================================================================
s = m.section(form.TypedSection, 'device', _('Device Settings'));
s.anonymous = true;
s.addremove = false;
o = s.option(form.Flag, 'enabled', _('Enable USB Gadget'),
_('Enable USB gadget mode. Disable to use USB port in host mode (for USB storage, etc.)'));
_('Disable to use USB port in host mode'));
o.rmempty = false;
o.default = '1';
o = s.option(form.Value, 'gadget_name', _('Gadget Name'),
_('Internal gadget identifier. Can be any string.'));
o = s.option(form.Value, 'gadget_name', _('Gadget Name'));
o.placeholder = 'g1';
o.default = 'g1';
o = s.option(form.Value, 'manufacturer', _('Manufacturer String'),
_('USB manufacturer string shown to the host computer'));
o = s.option(form.Value, 'manufacturer', _('Manufacturer'));
o.placeholder = 'OpenWrt';
o.default = 'OpenWrt';
o = s.option(form.Value, 'product', _('Product String'),
_('USB product string shown to the host computer'));
o = s.option(form.Value, 'product', _('Product'));
o.placeholder = 'USB Gadget';
o.default = 'USB Gadget';
o = s.option(form.Value, 'vendor_id', _('Vendor ID'),
_('USB Vendor ID in hexadecimal format (0x1d6b = Linux Foundation)'));
o = s.option(form.Value, 'vendor_id', _('Vendor ID'));
o.placeholder = '0x1d6b';
o.default = '0x1d6b';
o.validate = function(section_id, value) {
if (!/^0x[0-9a-fA-F]{4}$/.test(value))
return _('Must be a 4-digit hexadecimal number (e.g., 0x1d6b)');
if (value && !/^0x[0-9a-fA-F]{4}$/.test(value))
return _('Must be hex format: 0x1234');
return true;
};
o = s.option(form.Value, 'product_id', _('Product ID'),
_('USB Product ID in hexadecimal format'));
o = s.option(form.Value, 'product_id', _('Product ID'));
o.placeholder = '0x0104';
o.default = '0x0104';
o.validate = function(section_id, value) {
if (!/^0x[0-9a-fA-F]{4}$/.test(value))
return _('Must be a 4-digit hexadecimal number (e.g., 0x0104)');
if (value && !/^0x[0-9a-fA-F]{4}$/.test(value))
return _('Must be hex format: 0x1234');
return true;
};
o = s.option(form.Value, 'device_version', _('Device Version'),
_('USB device version in BCD format'));
o.placeholder = '0x0100';
o.default = '0x0100';
if (udc_devices.length > 0) {
o = s.option(form.ListValue, 'udc_device', _('UDC Device'),
_('USB Device Controller. Leave empty for auto-detection.'));
o = s.option(form.ListValue, 'udc_device', _('UDC Device'));
o.value('', _('Auto-detect'));
udc_devices.forEach(function(dev) {
o.value(dev.name, dev.name);
});
}
// USB Functions Section
s = m.section(form.TypedSection, 'function', _('USB Functions'),
_('Enable one or more USB functions. Only one network function (RNDIS/ECM/NCM) should be enabled at a time.'));
s.anonymous = false;
s.addremove = false;
s.tab('network', _('Network Functions'));
s.tab('serial', _('Serial Console'));
s.tab('storage', _('Mass Storage'));
// =================================================================
// RNDIS
o = s.taboption('network', form.Flag, 'enabled', _('Enable'));
o.modalonly = false;
o.depends('.name', 'rndis');
// =================================================================
s = m.section(form.NamedSection, 'rndis', 'function',
_('RNDIS (Windows Ethernet)'),
_('Windows-compatible USB ethernet. Works on all Windows versions.'));
o = s.option(form.Flag, 'enabled', _('Enable RNDIS'));
o.rmempty = false;
o = s.taboption('network', form.DummyValue, '_rndis_desc', _('Description'));
o.depends('.name', 'rndis');
o = s.option(form.DummyValue, '_desc', _('Description'));
o.rawhtml = true;
o.cfgvalue = function() {
return _('RNDIS - Windows compatible USB ethernet (plug-and-play on all Windows versions)');
return _('Plug-and-play on Windows. Automatically adds to LAN bridge.');
};
// =================================================================
// ECM
o = s.taboption('network', form.Flag, 'enabled', _('Enable'));
o.depends('.name', 'ecm');
// =================================================================
s = m.section(form.NamedSection, 'ecm', 'function',
_('ECM (macOS/Linux Ethernet)'),
_('For macOS ≤10.14 and Linux. Do not enable with NCM.'));
o = s.option(form.Flag, 'enabled', _('Enable ECM'));
o.rmempty = false;
o = s.taboption('network', form.DummyValue, '_ecm_desc', _('Description'));
o.depends('.name', 'ecm');
o = s.option(form.DummyValue, '_desc', _('Description'));
o.rawhtml = true;
o.cfgvalue = function() {
return _('ECM - CDC Ethernet for macOS (≤10.14) and Linux. Do not enable with NCM.');
return _('CDC Ethernet for macOS and Linux. Disable RNDIS when using.');
};
// =================================================================
// NCM
o = s.taboption('network', form.Flag, 'enabled', _('Enable'));
o.depends('.name', 'ncm');
// =================================================================
s = m.section(form.NamedSection, 'ncm', 'function',
_('NCM (Modern Ethernet)'),
_('Best performance. For Windows 11+, macOS ≥10.15, Linux.'));
o = s.option(form.Flag, 'enabled', _('Enable NCM'));
o.rmempty = false;
o = s.taboption('network', form.DummyValue, '_ncm_desc', _('Description'));
o.depends('.name', 'ncm');
o = s.option(form.DummyValue, '_desc', _('Description'));
o.rawhtml = true;
o.cfgvalue = function() {
return _('NCM - Modern high-performance USB ethernet for Windows 11+, macOS ≥10.15, Linux. Best performance.');
return _('Modern high-speed USB ethernet. Best choice for new systems.');
};
// =================================================================
// ACM Serial
o = s.taboption('serial', form.Flag, 'enabled', _('Enable Serial Console'));
o.depends('.name', 'acm');
// =================================================================
s = m.section(form.NamedSection, 'acm', 'function',
_('ACM (Serial Console)'),
_('Provides /dev/ttyACM0 on host. Access: screen /dev/ttyACM0 115200'));
o = s.option(form.Flag, 'enabled', _('Enable Serial Console'));
o.rmempty = false;
o = s.taboption('serial', form.Flag, 'shell', _('Enable Login Shell'),
_('Provide login shell on serial console. Disable for raw TTY mode.'));
o.depends('.name', 'acm');
o = s.option(form.Flag, 'shell', _('Enable Login Shell'),
_('Provide shell access. Disable for raw TTY.'));
o.default = '1';
o = s.taboption('serial', form.DummyValue, '_acm_info', _('Connection Info'));
o.depends('.name', 'acm');
o = s.option(form.DummyValue, '_info', _('Connection Info'));
o.rawhtml = true;
o.cfgvalue = function() {
return _('Access via /dev/ttyACM0 on Linux/macOS or COMx on Windows. Use: screen /dev/ttyACM0 115200');
return '<strong>Linux/macOS:</strong> screen /dev/ttyACM0 115200<br>' +
'<strong>Windows:</strong> Use PuTTY on COM port';
};
// =================================================================
// Mass Storage
o = s.taboption('storage', form.Flag, 'enabled', _('Enable Mass Storage'));
o.depends('.name', 'ums');
// =================================================================
s = m.section(form.NamedSection, 'ums', 'function',
_('Mass Storage'),
_('Expose storage image as USB drive.'));
o = s.option(form.Flag, 'enabled', _('Enable Mass Storage'));
o.rmempty = false;
o = s.taboption('storage', form.Value, 'image_path', _('Image File Path'),
_('Path to storage image file. Created automatically if it doesn\'t exist.'));
o.depends('.name', 'ums');
o = s.option(form.Value, 'image_path', _('Image Path'),
_('File will be created automatically if it doesn\'t exist.'));
o.default = '/var/lib/usb-gadget/storage.img';
o.rmempty = false;
o = s.taboption('storage', form.Value, 'image_size', _('Image Size'),
_('Size of image file when created (e.g., 512M, 1G, 2G)'));
o.depends('.name', 'ums');
o.default = '512M';
o = s.option(form.Value, 'image_size', _('Image Size'),
_('Used when creating (e.g., 512M, 1G, 2G)'));
o.placeholder = '512M';
o.default = '512M';
o = s.taboption('storage', form.Flag, 'readonly', _('Read-Only Mode'),
_('Mount storage as read-only on the host'));
o.depends('.name', 'ums');
o = s.option(form.Flag, 'readonly', _('Read-Only'),
_('Mount as read-only on the host computer'));
o.default = '0';
return m.render();
},
handleSaveApply: function(ev, mode) {
return this.super('handleSaveApply', [ev, mode]).then(function() {
return fs.exec('/etc/init.d/usb-gadget', ['restart']);
});
}
});
@@ -1,26 +1,16 @@
'use strict';
'require view';
'require form';
'require fs';
'require ui';
'require uci';
'require rpc';
'require poll';
var callServiceList = rpc.declare({
object: 'service',
method: 'list',
params: ['name'],
expect: { '': {} }
});
return view.extend({
load: function() {
return Promise.all([
uci.load('usbgadget'),
L.resolveDefault(fs.exec('/etc/init.d/usb-gadget', ['status']), null),
L.resolveDefault(fs.list('/sys/class/udc'), []),
L.resolveDefault(callServiceList('usb-gadget'), {})
L.resolveDefault(fs.list('/sys/class/udc'), [])
]);
},
@@ -28,43 +18,11 @@ return view.extend({
var status_result = data[1];
var udc_devices = data[2];
var enabled = uci.get('usbgadget', 'usb', 'enabled') == '1';
var enabled = uci.get('usbgadget', 'usb', 'enabled');
var gadget_name = uci.get('usbgadget', 'usb', 'gadget_name') || 'g1';
var manufacturer = uci.get('usbgadget', 'usb', 'manufacturer') || 'OpenWrt';
var m, s, o;
// Crear un mapa simple para mostrar información
m = new form.JSONMap({},
_('USB Gadget Status'),
_('View current USB gadget configuration and status'));
s = m.section(form.NamedSection, '_status', 'status');
// Status Overview
o = s.option(form.DummyValue, '_status', _('Gadget Status'));
o.rawhtml = true;
o.cfgvalue = function() {
return enabled ?
'<span style="color:green">&#9679;</span> ' + _('Active (Device Mode)') :
'<span style="color:red">&#9679;</span> ' + _('Disabled (Host Mode)');
};
o = s.option(form.DummyValue, '_gadget_name', _('Gadget Name'));
o.cfgvalue = function() { return gadget_name; };
// UDC Controller
if (udc_devices.length > 0) {
o = s.option(form.DummyValue, '_udc', _('USB Device Controller'));
o.cfgvalue = function() {
return udc_devices.map(function(dev) {
return dev.name;
}).join(', ');
};
}
// Enabled Functions
o = s.option(form.DummyValue, '_functions', _('Enabled Functions'));
o.cfgvalue = function() {
// Obtener funciones habilitadas
var functions = [];
['rndis', 'ecm', 'ncm', 'acm', 'ums'].forEach(function(func) {
if (uci.get('usbgadget', func, 'enabled') == '1') {
@@ -72,59 +30,117 @@ return view.extend({
functions.push(desc);
}
});
return functions.length > 0 ? functions.join(', ') : _('None');
};
// Service Controls
o = s.option(form.Button, '_restart', _('Service Control'));
o.inputtitle = _('Restart Service');
o.inputstyle = 'apply';
o.onclick = function() {
return fs.exec('/etc/init.d/usb-gadget', ['restart'])
// Construir array de filas de la tabla
var tableRows = [
E('tr', { 'class': 'tr' }, [
E('td', { 'class': 'td left', 'width': '33%' }, E('strong', {}, _('Gadget Status'))),
E('td', { 'class': 'td left', 'id': 'status_value' },
enabled == '1' ?
E('span', { 'style': 'color:green' }, '● ' + _('Active (Device Mode)')) :
E('span', { 'style': 'color:red' }, '● ' + _('Disabled (Host Mode)'))
)
]),
E('tr', { 'class': 'tr' }, [
E('td', { 'class': 'td left' }, E('strong', {}, _('Gadget Name'))),
E('td', { 'class': 'td left' }, gadget_name)
]),
E('tr', { 'class': 'tr' }, [
E('td', { 'class': 'td left' }, E('strong', {}, _('Manufacturer'))),
E('td', { 'class': 'td left' }, manufacturer)
]),
E('tr', { 'class': 'tr' }, [
E('td', { 'class': 'td left' }, E('strong', {}, _('Enabled Functions'))),
E('td', { 'class': 'td left' },
functions.length > 0 ? functions.join(', ') : E('em', {}, _('None')))
])
];
// Añadir fila UDC solo si hay dispositivos
if (udc_devices.length > 0) {
tableRows.splice(3, 0, E('tr', { 'class': 'tr' }, [
E('td', { 'class': 'td left' }, E('strong', {}, _('USB Controller'))),
E('td', { 'class': 'td left' }, udc_devices.map(function(d) { return d.name; }).join(', '))
]));
}
var viewElements = [
E('h2', {}, _('USB Gadget Status')),
E('div', { 'class': 'cbi-map-descr' },
_('View current USB gadget configuration and status')),
E('div', { 'class': 'cbi-section' }, [
E('h3', {}, _('Current Status')),
E('table', { 'class': 'table' }, tableRows)
]),
E('div', { 'class': 'cbi-section' }, [
E('div', { 'class': 'cbi-section-node' }, [
E('div', { 'class': 'cbi-value' }, [
E('label', { 'class': 'cbi-value-title' }, _('Service Control')),
E('div', { 'class': 'cbi-value-field' }, [
E('button', {
'class': 'cbi-button cbi-button-apply',
'click': function() {
ui.showModal(_('Restarting service...'), [
E('p', { 'class': 'spinning' }, _('Please wait...'))
]);
fs.exec('/etc/init.d/usb-gadget', ['restart'])
.then(function() {
ui.hideModal();
ui.addNotification(null,
E('p', _('USB Gadget service restarted')),
E('p', _('USB Gadget service restarted successfully')),
'info');
})
.catch(function(e) {
ui.hideModal();
ui.addNotification(null,
E('p', _('Failed to restart service: %s').format(e.message)),
E('p', _('Failed to restart: %s').format(e.message)),
'error');
});
};
}
}, _('Restart Service'))
])
])
])
])
];
// Detailed Status Output
// Añadir output detallado solo si existe
if (status_result && status_result.stdout) {
s = m.section(form.NamedSection, '_details', 'details',
_('Detailed Status'));
o = s.option(form.DummyValue, '_output', _('Status Output'));
o.rawhtml = true;
o.cfgvalue = function() {
return '<pre style="background:#f0f0f0; padding:10px; overflow:auto; max-height:400px">' +
status_result.stdout + '</pre>';
};
viewElements.push(
E('div', { 'class': 'cbi-section' }, [
E('h3', {}, _('Detailed Output')),
E('pre', {
'style': 'background:#f9f9f9; padding:10px; overflow:auto; max-height:400px; border:1px solid #ddd'
}, status_result.stdout)
])
);
}
return m.render().then(function(mapNode) {
var view = E('div', { 'class': 'cbi-map' }, viewElements);
// Polling cada 5 segundos
poll.add(function() {
return Promise.all([
uci.load('usbgadget'),
L.resolveDefault(fs.exec('/etc/init.d/usb-gadget', ['status']), null)
]).then(function(pollData) {
var newStatus = pollData[1];
var statusNode = mapNode.querySelector('[data-name="_status"] .td.right');
if (statusNode) {
var nowEnabled = uci.get('usbgadget', 'usb', 'enabled') == '1';
statusNode.innerHTML = nowEnabled ?
'<span style="color:green">&#9679;</span> ' + _('Active (Device Mode)') :
'<span style="color:red">&#9679;</span> ' + _('Disabled (Host Mode)');
return uci.load('usbgadget').then(function() {
var newEnabled = uci.get('usbgadget', 'usb', 'enabled');
var statusEl = document.getElementById('status_value');
if (statusEl) {
statusEl.innerHTML = '';
statusEl.appendChild(
newEnabled == '1' ?
E('span', { 'style': 'color:green' }, '● ' + _('Active (Device Mode)')) :
E('span', { 'style': 'color:red' }, '● ' + _('Disabled (Host Mode)'))
);
}
});
}, 5);
return mapNode;
});
return view;
},
handleSaveApply: null,