mirror of
https://github.com/sle118/squeezelite-esp32.git
synced 2025-12-11 05:57:05 +03:00
2373 lines
72 KiB
JavaScript
2373 lines
72 KiB
JavaScript
var he = require('he');
|
|
var Promise = require('es6-promise').Promise;
|
|
window.bootstrap = require('bootstrap');
|
|
import Cookies from 'js-cookie';
|
|
|
|
|
|
|
|
if (!String.prototype.format) {
|
|
Object.assign(String.prototype, {
|
|
format() {
|
|
const args = arguments;
|
|
return this.replace(/{(\d+)}/g, function (match, number) {
|
|
return typeof args[number] !== 'undefined' ? args[number] : match;
|
|
});
|
|
},
|
|
});
|
|
}
|
|
if (!String.prototype.encodeHTML) {
|
|
Object.assign(String.prototype, {
|
|
encodeHTML() {
|
|
return he.encode(this).replace(/\n/g, '<br />');
|
|
},
|
|
});
|
|
}
|
|
Object.assign(Date.prototype, {
|
|
toLocalShort() {
|
|
const opt = { dateStyle: 'short', timeStyle: 'short' };
|
|
return this.toLocaleString(undefined, opt);
|
|
},
|
|
});
|
|
function get_control_option_value(obj) {
|
|
let ctrl,id,val,opt;
|
|
let radio = false;
|
|
let checked = false;
|
|
if (typeof (obj) === 'string') {
|
|
id = obj;
|
|
ctrl = $(`#${id}`);
|
|
} else {
|
|
id = $(obj).attr('id');
|
|
ctrl = $(obj);
|
|
}
|
|
if(ctrl.attr('type') === 'checkbox'){
|
|
opt = $(obj).checked?id.replace('cmd_opt_', ''):'';
|
|
val = true;
|
|
}
|
|
else {
|
|
opt = id.replace('cmd_opt_', '');
|
|
val = $(obj).val();
|
|
val = `${val.includes(" ") ? '"' : ''}${val}${val.includes(" ") ? '"' : ''}`;
|
|
}
|
|
|
|
return { opt, val };
|
|
}
|
|
function handleNVSVisible() {
|
|
let nvs_previous_checked = isEnabled(Cookies.get("show-nvs"));
|
|
$('input#show-nvs')[0].checked = nvs_previous_checked;
|
|
if ($('input#show-nvs')[0].checked || recovery) {
|
|
$('*[href*="-nvs"]').show();
|
|
} else {
|
|
$('*[href*="-nvs"]').hide();
|
|
}
|
|
}
|
|
function concatenateOptions(options) {
|
|
let commandLine = ' ';
|
|
for (const [option, value] of Object.entries(options)) {
|
|
if (option !== 'n' && option !== 'o') {
|
|
commandLine += `-${option} `;
|
|
if (value !== true) {
|
|
commandLine += `${value} `;
|
|
}
|
|
}
|
|
}
|
|
return commandLine;
|
|
}
|
|
|
|
function isEnabled(val) {
|
|
return val != undefined && typeof val === 'string' && val.match("[Yy1]");
|
|
}
|
|
|
|
const nvsTypes = {
|
|
NVS_TYPE_U8: 0x01,
|
|
/*! < Type uint8_t */
|
|
NVS_TYPE_I8: 0x11,
|
|
/*! < Type int8_t */
|
|
NVS_TYPE_U16: 0x02,
|
|
/*! < Type uint16_t */
|
|
NVS_TYPE_I16: 0x12,
|
|
/*! < Type int16_t */
|
|
NVS_TYPE_U32: 0x04,
|
|
/*! < Type uint32_t */
|
|
NVS_TYPE_I32: 0x14,
|
|
/*! < Type int32_t */
|
|
NVS_TYPE_U64: 0x08,
|
|
/*! < Type uint64_t */
|
|
NVS_TYPE_I64: 0x18,
|
|
/*! < Type int64_t */
|
|
NVS_TYPE_STR: 0x21,
|
|
/*! < Type string */
|
|
NVS_TYPE_BLOB: 0x42,
|
|
/*! < Type blob */
|
|
NVS_TYPE_ANY: 0xff /*! < Must be last */,
|
|
};
|
|
const btIcons = {
|
|
bt_playing: { 'label': '', 'icon': 'media_bluetooth_on' },
|
|
bt_disconnected: { 'label': '', 'icon': 'media_bluetooth_off' },
|
|
bt_neutral: { 'label': '', 'icon': 'bluetooth' },
|
|
bt_connecting: { 'label': '', 'icon': 'bluetooth_searching' },
|
|
bt_connected: { 'label': '', 'icon': 'bluetooth_connected' },
|
|
bt_disabled: { 'label': '', 'icon': 'bluetooth_disabled' },
|
|
play_arrow: { 'label': '', 'icon': 'play_circle_filled' },
|
|
pause: { 'label': '', 'icon': 'pause_circle' },
|
|
stop: { 'label': '', 'icon': 'stop_circle' },
|
|
'': { 'label': '', 'icon': '' }
|
|
};
|
|
const batIcons = [
|
|
{ icon: "battery_0_bar", label: '▪', ranges: [{ f: 5.8, t: 6.8 }, { f: 8.8, t: 10.2 }] },
|
|
{ icon: "battery_2_bar", label: '▪▪', ranges: [{ f: 6.8, t: 7.4 }, { f: 10.2, t: 11.1 }] },
|
|
{ icon: "battery_3_bar", label: '▪▪▪', ranges: [{ f: 7.4, t: 7.5 }, { f: 11.1, t: 11.25 }] },
|
|
{ icon: "battery_4_bar", label: '▪▪▪▪', ranges: [{ f: 7.5, t: 7.8 }, { f: 11.25, t: 11.7 }] }
|
|
];
|
|
const btStateIcons = [
|
|
{ desc: 'Idle', sub: ['bt_neutral'] },
|
|
{ desc: 'Discovering', sub: ['bt_connecting'] },
|
|
{ desc: 'Discovered', sub: ['bt_connecting'] },
|
|
{ desc: 'Unconnected', sub: ['bt_disconnected'] },
|
|
{ desc: 'Connecting', sub: ['bt_connecting'] },
|
|
{
|
|
desc: 'Connected',
|
|
sub: ['bt_connected', 'play_arrow', 'bt_playing', 'pause', 'stop'],
|
|
},
|
|
{ desc: 'Disconnecting', sub: ['bt_disconnected'] },
|
|
];
|
|
|
|
const pillcolors = {
|
|
MESSAGING_INFO: 'badge-success',
|
|
MESSAGING_WARNING: 'badge-warning',
|
|
MESSAGING_ERROR: 'badge-danger',
|
|
};
|
|
const connectReturnCode = {
|
|
OK: 0,
|
|
FAIL: 1,
|
|
DISC: 2,
|
|
LOST: 3,
|
|
RESTORE: 4,
|
|
ETH: 5
|
|
}
|
|
const taskStates = {
|
|
0: 'eRunning',
|
|
/*! < A task is querying the state of itself, so must be running. */
|
|
1: 'eReady',
|
|
/*! < The task being queried is in a read or pending ready list. */
|
|
2: 'eBlocked',
|
|
/*! < The task being queried is in the Blocked state. */
|
|
3: 'eSuspended',
|
|
/*! < The task being queried is in the Suspended state, or is in the Blocked state with an infinite time out. */
|
|
4: 'eDeleted',
|
|
};
|
|
let flashState = {
|
|
NONE: 0,
|
|
REBOOT_TO_RECOVERY: 2,
|
|
SET_FWURL: 5,
|
|
FLASHING: 6,
|
|
DONE: 7,
|
|
UPLOADING: 8,
|
|
ERROR: 9,
|
|
UPLOADCOMPLETE: 10,
|
|
_state: -1,
|
|
olderRecovery: false,
|
|
statusText: '',
|
|
flashURL: '',
|
|
flashFileName: '',
|
|
statusPercent: 0,
|
|
Completed: false,
|
|
recovery: false,
|
|
prevRecovery: false,
|
|
updateModal: new bootstrap.Modal(document.getElementById('otadiv'), {}),
|
|
reset: function () {
|
|
|
|
this.olderRecovery = false;
|
|
this.statusText = '';
|
|
this.statusPercent = -1;
|
|
this.flashURL = '';
|
|
this.flashFileName = undefined;
|
|
this.UpdateProgress();
|
|
$('#rTable tr.release').removeClass('table-success table-warning');
|
|
$('.flact').prop('disabled', false);
|
|
$('#flashfilename').value = null;
|
|
$('#fw-url-input').value = null;
|
|
if (!this.isStateError()) {
|
|
$('span#flash-status').html('');
|
|
$('#fwProgressLabel').parent().removeClass('bg-danger');
|
|
}
|
|
this._state = this.NONE
|
|
return this;
|
|
},
|
|
isStateUploadComplete: function () {
|
|
return this._state == this.UPLOADCOMPLETE;
|
|
},
|
|
isStateError: function () {
|
|
return this._state == this.ERROR;
|
|
},
|
|
isStateNone: function () {
|
|
return this._state == this.NONE;
|
|
},
|
|
isStateRebootRecovery: function () {
|
|
return this._state == this.REBOOT_TO_RECOVERY;
|
|
},
|
|
isStateSetUrl: function () {
|
|
return this._state == this.SET_FWURL;
|
|
},
|
|
isStateFlashing: function () {
|
|
return this._state == this.FLASHING;
|
|
},
|
|
isStateDone: function () {
|
|
return this._state == this.DONE;
|
|
},
|
|
isStateUploading: function () {
|
|
return this._state == this.UPLOADING;
|
|
},
|
|
init: function () {
|
|
this._state = this.NONE;
|
|
return this;
|
|
},
|
|
|
|
SetStateError: function () {
|
|
this._state = this.ERROR;
|
|
$('#fwProgressLabel').parent().addClass('bg-danger');
|
|
return this;
|
|
},
|
|
SetStateNone: function () {
|
|
this._state = this.NONE;
|
|
return this;
|
|
},
|
|
SetStateRebootRecovery: function () {
|
|
this._state = this.REBOOT_TO_RECOVERY;
|
|
// Reboot system to recovery mode
|
|
this.SetStatusText('Starting recovery mode.')
|
|
$.ajax({
|
|
url: '/recovery.json',
|
|
context: this,
|
|
dataType: 'text',
|
|
method: 'POST',
|
|
cache: false,
|
|
contentType: 'application/json; charset=utf-8',
|
|
data: JSON.stringify({
|
|
timestamp: Date.now(),
|
|
}),
|
|
error: function (xhr, _ajaxOptions, thrownError) {
|
|
this.setOTAError(`Unexpected error while trying to restart to recovery. (status=${xhr.status ?? ''}, error=${thrownError ?? ''} ) `);
|
|
},
|
|
complete: function (response) {
|
|
this.SetStatusText('Waiting for system to boot.')
|
|
},
|
|
});
|
|
return this;
|
|
},
|
|
SetStateSetUrl: function () {
|
|
this._state = this.SET_FWURL;
|
|
this.statusText = 'Sending firmware download location.';
|
|
let confData = {
|
|
fwurl: {
|
|
value: this.flashURL,
|
|
type: 33,
|
|
}
|
|
};
|
|
post_config(confData);
|
|
return this;
|
|
},
|
|
SetStateFlashing: function () {
|
|
this._state = this.FLASHING;
|
|
return this;
|
|
},
|
|
SetStateDone: function () {
|
|
this._state = this.DONE;
|
|
this.reset();
|
|
return this;
|
|
},
|
|
SetStateUploading: function () {
|
|
this._state = this.UPLOADING;
|
|
return this.SetStatusText('Sending file to device.');
|
|
},
|
|
SetStateUploadComplete: function () {
|
|
this._state = this.UPLOADCOMPLETE;
|
|
return this;
|
|
},
|
|
|
|
isFlashExecuting: function () {
|
|
return true === (this._state != this.UPLOADING && (this.statusText !== '' || this.statusPercent >= 0));
|
|
},
|
|
|
|
|
|
|
|
toString: function () {
|
|
let keys = Object.keys(this);
|
|
return keys.find(x => this[x] === this._state);
|
|
},
|
|
|
|
setOTATargets: function () {
|
|
this.flashURL = '';
|
|
this.flashFileName = '';
|
|
this.flashURL = $('#fw-url-input').val();
|
|
let fileInput = $('#flashfilename')[0].files;
|
|
if (fileInput.length > 0) {
|
|
this.flashFileName = fileInput[0];
|
|
}
|
|
if (this.flashFileName.length == 0 && this.flashURL.length == 0) {
|
|
this.setOTAError('Invalid url or file. Cannot start OTA');
|
|
}
|
|
return this;
|
|
},
|
|
|
|
setOTAError: function (message) {
|
|
this.SetStateError().SetStatusPercent(0).SetStatusText(message).reset();
|
|
return this;
|
|
},
|
|
|
|
ShowDialog: function () {
|
|
if (!this.isStateNone()) {
|
|
this.updateModal.show();
|
|
$('.flact').prop('disabled', true);
|
|
}
|
|
return this;
|
|
},
|
|
|
|
SetStatusPercent: function (pct) {
|
|
var pctChanged = (this.statusPercent != pct);
|
|
this.statusPercent = pct;
|
|
if (pctChanged) {
|
|
if (!this.isStateUploading() && !this.isStateFlashing()) {
|
|
this.SetStateFlashing();
|
|
}
|
|
if (pct == 100) {
|
|
if (this.isStateFlashing()) {
|
|
this.SetStateDone();
|
|
}
|
|
else if (this.isStateUploading()) {
|
|
this.statusPercent = 0;
|
|
this.SetStateFlashing();
|
|
}
|
|
}
|
|
this.UpdateProgress().ShowDialog();
|
|
}
|
|
return this;
|
|
},
|
|
SetStatusText: function (txt) {
|
|
var changed = (this.statusText != txt);
|
|
this.statusText = txt;
|
|
if (changed) {
|
|
$('span#flash-status').html(this.statusText);
|
|
this.ShowDialog();
|
|
}
|
|
|
|
return this;
|
|
},
|
|
UpdateProgress: function () {
|
|
$('.progress-bar')
|
|
.css('width', this.statusPercent + '%')
|
|
.attr('aria-valuenow', this.statusPercent)
|
|
.text(this.statusPercent + '%')
|
|
$('.progress-bar').html((this.isStateDone() ? 100 : this.statusPercent) + '%');
|
|
return this;
|
|
},
|
|
StartOTA: function () {
|
|
this.logEvent(this.StartOTA.name);
|
|
$('#fwProgressLabel').parent().removeClass('bg-danger');
|
|
this.setOTATargets();
|
|
if (this.isStateError()) {
|
|
return this;
|
|
}
|
|
if (!recovery) {
|
|
this.SetStateRebootRecovery();
|
|
}
|
|
else {
|
|
this.SetStateFlashing().TargetReadyStartOTA();
|
|
}
|
|
|
|
return this;
|
|
},
|
|
UploadLocalFile: function () {
|
|
this.SetStateUploading();
|
|
const xhttp = new XMLHttpRequest();
|
|
xhttp.context = this;
|
|
var boundHandleUploadProgressEvent = this.HandleUploadProgressEvent.bind(this);
|
|
var boundsetOTAError = this.setOTAError.bind(this);
|
|
xhttp.upload.addEventListener("progress", boundHandleUploadProgressEvent, false);
|
|
xhttp.onreadystatechange = function () {
|
|
if (xhttp.readyState === 4) {
|
|
if (xhttp.status === 0 || xhttp.status === 404) {
|
|
boundsetOTAError(`Upload Failed. Recovery version might not support uploading. Please use web update instead.`);
|
|
}
|
|
}
|
|
};
|
|
xhttp.open('POST', '/flash.json', true);
|
|
xhttp.send(this.flashFileName);
|
|
},
|
|
TargetReadyStartOTA: function () {
|
|
if (recovery && this.prevRecovery && !this.isStateRebootRecovery() && !this.isStateFlashing()) {
|
|
// this should only execute once, while being in a valid state
|
|
return this;
|
|
}
|
|
|
|
this.logEvent(this.TargetReadyStartOTA.name);
|
|
if (!recovery) {
|
|
console.error('Event TargetReadyStartOTA fired in the wrong mode ');
|
|
return this;
|
|
}
|
|
this.prevRecovery = true;
|
|
|
|
if (this.flashFileName !== '') {
|
|
this.UploadLocalFile();
|
|
}
|
|
else if (this.flashURL != '') {
|
|
this.SetStateSetUrl();
|
|
}
|
|
else {
|
|
this.setOTAError('Invalid URL or file name while trying to start the OTa process')
|
|
}
|
|
},
|
|
HandleUploadProgressEvent: function (data) {
|
|
this.logEvent(this.HandleUploadProgressEvent.name);
|
|
this.SetStateUploading().SetStatusPercent(Math.round(data.loaded / data.total * 100)).SetStatusText('Uploading file to device');
|
|
},
|
|
EventTargetStatus: function (data) {
|
|
if (!this.isStateNone()) {
|
|
this.logEvent(this.EventTargetStatus.name);
|
|
}
|
|
if (data.ota_pct ?? -1 >= 0) {
|
|
this.olderRecovery = true;
|
|
this.SetStatusPercent(data.ota_pct);
|
|
}
|
|
if ((data.ota_dsc ?? '') != '') {
|
|
this.olderRecovery = true;
|
|
this.SetStatusText(data.ota_dsc);
|
|
}
|
|
|
|
if (data.recovery != undefined) {
|
|
this.recovery = data.recovery === 1 ? true : false;
|
|
}
|
|
if (this.isStateRebootRecovery() && this.recovery) {
|
|
this.TargetReadyStartOTA();
|
|
}
|
|
},
|
|
EventOTAMessageClass: function (data) {
|
|
this.logEvent(this.EventOTAMessageClass.name);
|
|
var otaData = JSON.parse(data);
|
|
this.SetStatusPercent(otaData.ota_pct).SetStatusText(otaData.ota_dsc);
|
|
},
|
|
logEvent: function (fun) {
|
|
console.log(`${fun}, flash state ${this.toString()}, recovery: ${this.recovery}, ota pct: ${this.statusPercent}, ota desc: ${this.statusText}`);
|
|
}
|
|
|
|
};
|
|
window.hideSurrounding = function (obj) {
|
|
$(obj).parent().parent().hide();
|
|
}
|
|
|
|
let presetsloaded = false;
|
|
let is_i2c_locked = false;
|
|
let statusInterval = 2000;
|
|
let messageInterval = 2500;
|
|
function post_config(data) {
|
|
let confPayload = {
|
|
timestamp: Date.now(),
|
|
config: data
|
|
};
|
|
$.ajax({
|
|
url: '/config.json',
|
|
dataType: 'text',
|
|
method: 'POST',
|
|
cache: false,
|
|
contentType: 'application/json; charset=utf-8',
|
|
data: JSON.stringify(confPayload),
|
|
error: handleExceptionResponse,
|
|
});
|
|
}
|
|
|
|
|
|
window.hFlash = function () {
|
|
// reset file upload selection if any;
|
|
$('#flashfilename').value = null
|
|
flashState.StartOTA();
|
|
}
|
|
window.handleReboot = function (link) {
|
|
if (link == 'reboot_ota') {
|
|
$('#reboot_ota_nav').removeClass('active').prop("disabled", true); delayReboot(500, '', 'reboot_ota');
|
|
}
|
|
else {
|
|
$('#reboot_nav').removeClass('active'); delayReboot(500, '', link);
|
|
}
|
|
}
|
|
|
|
function parseSqueezeliteCommandLine(commandLine) {
|
|
const options = {};
|
|
let output, name;
|
|
let otherValues = '';
|
|
|
|
const argRegex = /("[^"]+"|'[^']+'|\S+)/g;
|
|
const args = commandLine.match(argRegex);
|
|
|
|
let i = 0;
|
|
|
|
while (i < args.length) {
|
|
const arg = args[i];
|
|
|
|
if (arg.startsWith('-')) {
|
|
const option = arg.slice(1);
|
|
|
|
if (option === '') {
|
|
otherValues += args.slice(i).join(' ');
|
|
break;
|
|
}
|
|
|
|
let value = true;
|
|
|
|
if (i + 1 < args.length && !args[i + 1].startsWith('-')) {
|
|
value = args[i + 1].replace(/"/g, '').replace(/'/g, '');
|
|
i++;
|
|
}
|
|
|
|
options[option] = value;
|
|
} else {
|
|
otherValues += arg + ' ';
|
|
}
|
|
|
|
i++;
|
|
}
|
|
|
|
otherValues = otherValues.trim();
|
|
output = getOutput(options);
|
|
name = getName(options);
|
|
let otherOptions={btname:null,n:null};
|
|
// assign o and n options to otheroptions if present
|
|
if (options.o && output.toUpperCase() === 'BT') {
|
|
let temp = parseSqueezeliteCommandLine(options.o);
|
|
if(temp.name) {
|
|
otherOptions.btname = temp.name;
|
|
}
|
|
delete options.o;
|
|
}
|
|
if (options.n) {
|
|
otherOptions['n'] = options.n;
|
|
delete options.n;
|
|
}
|
|
return { name, output, options, otherValues,otherOptions };
|
|
}
|
|
|
|
function getOutput(options) {
|
|
let output;
|
|
if (options.o){
|
|
output = options.o.replace(/"/g, '').replace(/'/g, '');
|
|
/* set output as the first alphanumerical word in the command line */
|
|
if (output.indexOf(' ') > 0) {
|
|
output = output.substring(0, output.indexOf(' '));
|
|
}
|
|
}
|
|
return output;
|
|
}
|
|
|
|
function getName(options) {
|
|
let name;
|
|
/* if n option present, assign to name variable */
|
|
if (options.n){
|
|
name = options.n.replace(/"/g, '').replace(/'/g, '');
|
|
}
|
|
return name;
|
|
}
|
|
|
|
|
|
function isConnected() {
|
|
return ConnectedTo.hasOwnProperty('ip') && ConnectedTo.ip != '0.0.0.0' && ConnectedTo.ip != '';
|
|
}
|
|
function getIcon(icons) {
|
|
return isConnected() ? icons.icon : icons.label;
|
|
}
|
|
function handlebtstate(data) {
|
|
let icon = '';
|
|
let tt = '';
|
|
if (data.bt_status !== undefined && data.bt_sub_status !== undefined) {
|
|
const iconindex = btStateIcons[data.bt_status].sub[data.bt_sub_status];
|
|
if (iconindex) {
|
|
icon = btIcons[iconindex];
|
|
tt = btStateIcons[data.bt_status].desc;
|
|
} else {
|
|
icon = btIcons.bt_connected;
|
|
tt = 'Output status';
|
|
}
|
|
}
|
|
|
|
$('#o_type').attr('title', tt);
|
|
$('#o_bt').html(isConnected() ? icon.label : icon.text);
|
|
}
|
|
function handleTemplateTypeRadio(outtype) {
|
|
$('#o_type').children('span').css({ display: 'none' });
|
|
let changed = false;
|
|
if (outtype === 'bt') {
|
|
changed = output !== 'bt' && output !== '';
|
|
output = 'bt';
|
|
} else if (outtype === 'spdif') {
|
|
changed = output !== 'spdif' && output !== '';
|
|
output = 'spdif';
|
|
} else {
|
|
changed = output !== 'i2s' && output !== '';
|
|
output = 'i2s';
|
|
}
|
|
$('#' + output).prop('checked', true);
|
|
$('#o_' + output).css({ display: 'inline' });
|
|
if (changed) {
|
|
Object.keys(commandDefaults[output]).forEach(function (key) {
|
|
$(`#cmd_opt_${key}`).val(commandDefaults[output][key]);
|
|
});
|
|
}
|
|
}
|
|
|
|
function handleExceptionResponse(xhr, _ajaxOptions, thrownError) {
|
|
console.log(xhr.status);
|
|
console.log(thrownError);
|
|
if (thrownError !== '') {
|
|
showLocalMessage(thrownError, 'MESSAGING_ERROR');
|
|
}
|
|
}
|
|
function HideCmdMessage(cmdname) {
|
|
$('#toast_' + cmdname)
|
|
.removeClass('table-success')
|
|
.removeClass('table-warning')
|
|
.removeClass('table-danger')
|
|
.addClass('table-success')
|
|
.removeClass('show');
|
|
$('#msg_' + cmdname).html('');
|
|
}
|
|
function showCmdMessage(cmdname, msgtype, msgtext, append = false) {
|
|
let color = 'table-success';
|
|
if (msgtype === 'MESSAGING_WARNING') {
|
|
color = 'table-warning';
|
|
} else if (msgtype === 'MESSAGING_ERROR') {
|
|
color = 'table-danger';
|
|
}
|
|
$('#toast_' + cmdname)
|
|
.removeClass('table-success')
|
|
.removeClass('table-warning')
|
|
.removeClass('table-danger')
|
|
.addClass(color)
|
|
.addClass('show');
|
|
let escapedtext = msgtext
|
|
.substring(0, msgtext.length - 1)
|
|
.encodeHTML()
|
|
.replace(/\n/g, '<br />');
|
|
escapedtext =
|
|
($('#msg_' + cmdname).html().length > 0 && append
|
|
? $('#msg_' + cmdname).html() + '<br/>'
|
|
: '') + escapedtext;
|
|
$('#msg_' + cmdname).html(escapedtext);
|
|
}
|
|
|
|
let releaseURL =
|
|
'https://api.github.com/repos/sle118/squeezelite-esp32/releases';
|
|
|
|
let recovery = false;
|
|
let messagesHeld = false;
|
|
let commandBTSinkName = '';
|
|
const commandHeader = 'squeezelite ';
|
|
const commandDefaults = {
|
|
i2s: { b: "500:2000", C: "30", W: "", Z: "96000", o: "I2S" },
|
|
spdif: { b: "500:2000", C: "30", W: "", Z: "48000", o: "SPDIF" },
|
|
bt: { b: "500:2000", C: "30", W: "", Z: "44100", o: "BT" },
|
|
};
|
|
let validOptions = {
|
|
codecs: ['flac', 'pcm', 'mp3', 'ogg', 'aac', 'wma', 'alac', 'dsd', 'mad', 'mpg']
|
|
};
|
|
|
|
//let blockFlashButton = false;
|
|
let apList = null;
|
|
//let selectedSSID = '';
|
|
//let checkStatusInterval = null;
|
|
let messagecount = 0;
|
|
let messageseverity = 'MESSAGING_INFO';
|
|
let SystemConfig = {};
|
|
let LastCommandsState = null;
|
|
var output = '';
|
|
let hostName = '';
|
|
let versionName = 'Squeezelite-ESP32';
|
|
let prevmessage = '';
|
|
let project_name = versionName;
|
|
let depth = 16;
|
|
let board_model = '';
|
|
let platform_name = versionName;
|
|
let preset_name = '';
|
|
let btSinkNamesOptSel = '#cfg-audio-bt_source-sink_name';
|
|
let ConnectedTo = {};
|
|
let ConnectingToSSID = {};
|
|
let lmsBaseUrl;
|
|
let prevLMSIP = '';
|
|
const ConnectingToActions = {
|
|
'CONN': 0, 'MAN': 1, 'STS': 2,
|
|
}
|
|
|
|
Promise.prototype.delay = function (duration) {
|
|
return this.then(
|
|
function (value) {
|
|
return new Promise(function (resolve) {
|
|
setTimeout(function () {
|
|
resolve(value);
|
|
}, duration);
|
|
});
|
|
},
|
|
function (reason) {
|
|
return new Promise(function (_resolve, reject) {
|
|
setTimeout(function () {
|
|
reject(reason);
|
|
}, duration);
|
|
});
|
|
}
|
|
);
|
|
};
|
|
|
|
function getConfigJson(slimMode) {
|
|
const config = {};
|
|
$('input.nvs').each(function (_index, entry) {
|
|
if (!slimMode) {
|
|
const nvsType = parseInt(entry.attributes.nvs_type.value, 10);
|
|
if (entry.id !== '') {
|
|
config[entry.id] = {};
|
|
if (
|
|
nvsType === nvsTypes.NVS_TYPE_U8 ||
|
|
nvsType === nvsTypes.NVS_TYPE_I8 ||
|
|
nvsType === nvsTypes.NVS_TYPE_U16 ||
|
|
nvsType === nvsTypes.NVS_TYPE_I16 ||
|
|
nvsType === nvsTypes.NVS_TYPE_U32 ||
|
|
nvsType === nvsTypes.NVS_TYPE_I32 ||
|
|
nvsType === nvsTypes.NVS_TYPE_U64 ||
|
|
nvsType === nvsTypes.NVS_TYPE_I64
|
|
) {
|
|
config[entry.id].value = parseInt(entry.value);
|
|
} else {
|
|
config[entry.id].value = entry.value;
|
|
}
|
|
config[entry.id].type = nvsType;
|
|
}
|
|
} else {
|
|
config[entry.id] = entry.value;
|
|
}
|
|
});
|
|
const key = $('#nvs-new-key').val();
|
|
const val = $('#nvs-new-value').val();
|
|
if (key !== '') {
|
|
if (!slimMode) {
|
|
config[key] = {};
|
|
config[key].value = val;
|
|
config[key].type = 33;
|
|
} else {
|
|
config[key] = val;
|
|
}
|
|
}
|
|
return config;
|
|
}
|
|
|
|
function handleHWPreset(allfields, reboot) {
|
|
|
|
const selJson = JSON.parse(allfields[0].value);
|
|
var cmd = allfields[0].attributes.cmdname.value;
|
|
|
|
console.log(`selected model: ${selJson.name}`);
|
|
let confPayload = {
|
|
timestamp: Date.now(),
|
|
config: { model_config: { value: selJson.name, type: 33 } }
|
|
};
|
|
for (const [name, value] of Object.entries(selJson.config)) {
|
|
const storedval = (typeof value === 'string' || value instanceof String) ? value : JSON.stringify(value);
|
|
confPayload.config[name] = {
|
|
value: storedval,
|
|
type: 33,
|
|
}
|
|
showCmdMessage(
|
|
cmd,
|
|
'MESSAGING_INFO',
|
|
`Setting ${name}=${storedval} `,
|
|
true
|
|
);
|
|
}
|
|
|
|
showCmdMessage(
|
|
cmd,
|
|
'MESSAGING_INFO',
|
|
`Committing `,
|
|
true
|
|
);
|
|
$.ajax({
|
|
url: '/config.json',
|
|
dataType: 'text',
|
|
method: 'POST',
|
|
cache: false,
|
|
contentType: 'application/json; charset=utf-8',
|
|
data: JSON.stringify(confPayload),
|
|
error: function (xhr, _ajaxOptions, thrownError) {
|
|
handleExceptionResponse(xhr, _ajaxOptions, thrownError);
|
|
showCmdMessage(
|
|
cmd,
|
|
'MESSAGING_ERROR',
|
|
`Unexpected error ${(thrownError !== '') ? thrownError : 'with return status = ' + xhr.status} `,
|
|
true
|
|
);
|
|
},
|
|
success: function (response) {
|
|
showCmdMessage(
|
|
cmd,
|
|
'MESSAGING_INFO',
|
|
`Saving complete `,
|
|
true
|
|
);
|
|
console.log(response);
|
|
if (reboot) {
|
|
delayReboot(2500, cmd);
|
|
}
|
|
},
|
|
});
|
|
}
|
|
|
|
|
|
// pull json file from https://gist.githubusercontent.com/sle118/dae585e157b733a639c12dc70f0910c5/raw/b462691f69e2ad31ac95c547af6ec97afb0f53db/squeezelite-esp32-presets.json and
|
|
function loadPresets() {
|
|
if ($("#cfg-hw-preset-model_config").length == 0) return;
|
|
if (presetsloaded) return;
|
|
presetsloaded = true;
|
|
$('#cfg-hw-preset-model_config').html('<option>--</option>');
|
|
$.getJSON(
|
|
'https://gist.githubusercontent.com/sle118/dae585e157b733a639c12dc70f0910c5/raw/',
|
|
{ _: new Date().getTime() },
|
|
function (data) {
|
|
$.each(data, function (key, val) {
|
|
$('#cfg-hw-preset-model_config').append(`<option value='${JSON.stringify(val).replace(/"/g, '\"').replace(/\'/g, '\"')}'>${val.name}</option>`);
|
|
if (preset_name !== '' && preset_name == val.name) {
|
|
$('#cfg-hw-preset-model_config').val(preset_name);
|
|
}
|
|
});
|
|
if (preset_name !== '') {
|
|
('#prev_preset').show().val(preset_name);
|
|
}
|
|
}
|
|
|
|
).fail(function (jqxhr, textStatus, error) {
|
|
const err = textStatus + ', ' + error;
|
|
console.log('Request Failed: ' + err);
|
|
}
|
|
);
|
|
}
|
|
|
|
function delayReboot(duration, cmdname, ota = 'reboot') {
|
|
const url = '/' + ota + '.json';
|
|
$('tbody#tasks').empty();
|
|
$('#tasks_sect').css('visibility', 'collapse');
|
|
Promise.resolve({ cmdname: cmdname, url: url })
|
|
.delay(duration)
|
|
.then(function (data) {
|
|
if (data.cmdname.length > 0) {
|
|
showCmdMessage(
|
|
data.cmdname,
|
|
'MESSAGING_WARNING',
|
|
'System is rebooting.\n',
|
|
true
|
|
);
|
|
} else {
|
|
showLocalMessage('System is rebooting.\n', 'MESSAGING_WARNING');
|
|
}
|
|
console.log('now triggering reboot');
|
|
$("button[onclick*='handleReboot']").addClass('rebooting');
|
|
$.ajax({
|
|
url: data.url,
|
|
dataType: 'text',
|
|
method: 'POST',
|
|
cache: false,
|
|
contentType: 'application/json; charset=utf-8',
|
|
data: JSON.stringify({
|
|
timestamp: Date.now(),
|
|
}),
|
|
error: handleExceptionResponse,
|
|
complete: function () {
|
|
console.log('reboot call completed');
|
|
Promise.resolve(data)
|
|
.delay(6000)
|
|
.then(function (rdata) {
|
|
if (rdata.cmdname.length > 0) {
|
|
HideCmdMessage(rdata.cmdname);
|
|
}
|
|
getCommands();
|
|
getConfig();
|
|
});
|
|
},
|
|
});
|
|
});
|
|
}
|
|
// eslint-disable-next-line no-unused-vars
|
|
window.saveAutoexec1 = function (apply) {
|
|
showCmdMessage('cfg-audio-tmpl', 'MESSAGING_INFO', 'Saving.\n', false);
|
|
let commandLine = `${commandHeader} -o ${output} `;
|
|
$('.sqcmd').each(function () {
|
|
let { opt, val } = get_control_option_value($(this));
|
|
if ((opt && opt.length>0 ) && typeof(val) == 'boolean' || val.length > 0) {
|
|
const optStr=opt===':'?opt:(` -${opt} `);
|
|
val = typeof(val) == 'boolean'?'':val;
|
|
commandLine += `${optStr} ${val}`;
|
|
}
|
|
});
|
|
const resample=$('#cmd_opt_R input[name=resample]:checked');
|
|
if (resample.length>0 && resample.attr('suffix')!=='') {
|
|
commandLine += resample.attr('suffix');
|
|
// now check resample_i option and if checked, add suffix to command line
|
|
if ($('#resample_i').is(":checked") && resample.attr('aint') =='true') {
|
|
commandLine += $('#resample_i').attr('suffix');
|
|
}
|
|
}
|
|
|
|
|
|
if (output === 'bt') {
|
|
showCmdMessage(
|
|
'cfg-audio-tmpl',
|
|
'MESSAGING_INFO',
|
|
'Remember to configure the Bluetooth audio device name.\n',
|
|
true
|
|
);
|
|
}
|
|
commandLine += concatenateOptions(options);
|
|
const data = {
|
|
timestamp: Date.now(),
|
|
};
|
|
data.config = {
|
|
autoexec1: { value: commandLine, type: 33 },
|
|
autoexec: {
|
|
value: $('#disable-squeezelite').prop('checked') ? '0' : '1',
|
|
type: 33,
|
|
},
|
|
};
|
|
|
|
$.ajax({
|
|
url: '/config.json',
|
|
dataType: 'text',
|
|
method: 'POST',
|
|
cache: false,
|
|
contentType: 'application/json; charset=utf-8',
|
|
data: JSON.stringify(data),
|
|
error: handleExceptionResponse,
|
|
complete: function (response) {
|
|
if (
|
|
response.responseText &&
|
|
JSON.parse(response.responseText).result === 'OK'
|
|
) {
|
|
showCmdMessage('cfg-audio-tmpl', 'MESSAGING_INFO', 'Done.\n', true);
|
|
if (apply) {
|
|
delayReboot(1500, 'cfg-audio-tmpl');
|
|
}
|
|
} else if (JSON.parse(response.responseText).result) {
|
|
showCmdMessage(
|
|
'cfg-audio-tmpl',
|
|
'MESSAGING_WARNING',
|
|
JSON.parse(response.responseText).Result + '\n',
|
|
true
|
|
);
|
|
} else {
|
|
showCmdMessage(
|
|
'cfg-audio-tmpl',
|
|
'MESSAGING_ERROR',
|
|
response.statusText + '\n'
|
|
);
|
|
}
|
|
console.log(response.responseText);
|
|
},
|
|
});
|
|
console.log('sent data:', JSON.stringify(data));
|
|
}
|
|
window.handleDisconnect = function () {
|
|
$.ajax({
|
|
url: '/connect.json',
|
|
dataType: 'text',
|
|
method: 'DELETE',
|
|
cache: false,
|
|
contentType: 'application/json; charset=utf-8',
|
|
data: JSON.stringify({
|
|
timestamp: Date.now(),
|
|
}),
|
|
});
|
|
}
|
|
function setPlatformFilter(val) {
|
|
if ($('.upf').filter(function () { return $(this).text().toUpperCase() === val.toUpperCase() }).length > 0) {
|
|
$('#splf').val(val).trigger('input');
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
window.handleConnect = function () {
|
|
ConnectingToSSID.ssid = $('#manual_ssid').val();
|
|
ConnectingToSSID.pwd = $('#manual_pwd').val();
|
|
ConnectingToSSID.dhcpname = $('#dhcp-name2').val();
|
|
$("*[class*='connecting']").hide();
|
|
$('#ssid-wait').text(ConnectingToSSID.ssid);
|
|
$('.connecting').show();
|
|
$.ajax({
|
|
url: '/connect.json',
|
|
dataType: 'text',
|
|
method: 'POST',
|
|
cache: false,
|
|
contentType: 'application/json; charset=utf-8',
|
|
data: JSON.stringify({
|
|
timestamp: Date.now(),
|
|
ssid: ConnectingToSSID.ssid,
|
|
pwd: ConnectingToSSID.pwd
|
|
}),
|
|
error: handleExceptionResponse,
|
|
});
|
|
|
|
// now we can re-set the intervals regardless of result
|
|
|
|
}
|
|
function renderError(opt,error){
|
|
const fieldname = `cmd_opt_${opt}`;
|
|
let errorFieldName=`${fieldname}-error`;
|
|
let errorField=$(`#${errorFieldName}`);
|
|
let field=$(`#${fieldname}`);
|
|
|
|
if (!errorField || errorField.length ==0) {
|
|
field.after(`<div id="${errorFieldName}" class="invalid-feedback"></div>`);
|
|
errorField=$(`#${errorFieldName}`);
|
|
}
|
|
if(error.length ==0){
|
|
errorField.hide();
|
|
field.removeClass('is-invalid');
|
|
field.addClass('is-valid');
|
|
errorField.text('');
|
|
}
|
|
else {
|
|
errorField.show();
|
|
errorField.text(error);
|
|
field.removeClass('is-valid');
|
|
field.addClass('is-invalid');
|
|
}
|
|
return errorField;
|
|
}
|
|
$(document).ready(function () {
|
|
$('.material-icons').each(function (_index, entry) {
|
|
entry.attributes['icon'] = entry.textContent;
|
|
});
|
|
setIcons(true);
|
|
handleNVSVisible();
|
|
flashState.init();
|
|
$('#fw-url-input').on('input', function () {
|
|
if ($(this).val().length > 8 && ($(this).val().startsWith('http://') || $(this).val().startsWith('https://'))) {
|
|
$('#start-flash').show();
|
|
}
|
|
else {
|
|
$('#start-flash').hide();
|
|
}
|
|
});
|
|
$('.upSrch').on('input', function () {
|
|
const val = this.value;
|
|
$("#rTable tr").removeClass(this.id + '_hide');
|
|
if (val.length > 0) {
|
|
$(`#rTable td:nth-child(${$(this).parent().index() + 1})`).filter(function () {
|
|
return !$(this).text().toUpperCase().includes(val.toUpperCase());
|
|
}).parent().addClass(this.id + '_hide');
|
|
}
|
|
$('[class*="_hide"]').hide();
|
|
$('#rTable tr').not('[class*="_hide"]').show()
|
|
|
|
});
|
|
setTimeout(refreshAP, 1500);
|
|
/* add validation for cmd_opt_c, which accepts a comma separated list.
|
|
getting known codecs from validOptions.codecs array
|
|
use bootstrap classes to highlight the error with an overlay message */
|
|
$('#options input').on('input', function () {
|
|
const { opt, val } = get_control_option_value(this);
|
|
if (opt === 'c' || opt === 'e') {
|
|
const fieldname = `cmd_opt_${opt}_codec-error`;
|
|
|
|
const values = val.split(',').map(function (item) {
|
|
return item.trim();
|
|
});
|
|
/* get a list of invalid codecs */
|
|
const invalid = values.filter(function (item) {
|
|
return !validOptions.codecs.includes(item);
|
|
});
|
|
renderError(opt,invalid.length > 0 ? `Invalid codec(s) ${invalid.join(', ')}` : '');
|
|
}
|
|
/* add validation for cmd_opt_m, which accepts a mac_address */
|
|
if (opt === 'm') {
|
|
const mac_regex = /^([0-9A-Fa-f]{2}[:-]){5}([0-9A-Fa-f]{2})$/;
|
|
renderError(opt,mac_regex.test(val) ? '' : 'Invalid MAC address');
|
|
}
|
|
if (opt === 'r') {
|
|
const rateRegex = /^(\d+\.?\d*|\.\d+)-(\d+\.?\d*|\.\d+)$|^(\d+\.?\d*)$|^(\d+\.?\d*,)+\d+\.?\d*$/;
|
|
renderError(opt,rateRegex.test(val)?'':`Invalid rate(s) ${val}. Acceptable format: <maxrate>|<minrate>-<maxrate>|<rate1>,<rate2>,<rate3>`);
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
$('#WifiConnectDialog')[0].addEventListener('shown.bs.modal', function (event) {
|
|
$("*[class*='connecting']").hide();
|
|
|
|
if (event?.relatedTarget) {
|
|
ConnectingToSSID.Action = ConnectingToActions.CONN;
|
|
if ($(event.relatedTarget).children('td:eq(1)').text() == ConnectedTo.ssid) {
|
|
ConnectingToSSID.Action = ConnectingToActions.STS;
|
|
}
|
|
else {
|
|
if (!$(event.relatedTarget).is(':last-child')) {
|
|
ConnectingToSSID.ssid = $(event.relatedTarget).children('td:eq(1)').text();
|
|
$('#manual_ssid').val(ConnectingToSSID.ssid);
|
|
}
|
|
else {
|
|
ConnectingToSSID.Action = ConnectingToActions.MAN;
|
|
ConnectingToSSID.ssid = '';
|
|
$('#manual_ssid').val(ConnectingToSSID.ssid);
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
if (ConnectingToSSID.Action !== ConnectingToActions.STS) {
|
|
$('.connecting-init').show();
|
|
$('#manual_ssid').trigger('focus');
|
|
}
|
|
else {
|
|
handleWifiDialog();
|
|
}
|
|
});
|
|
|
|
$('#WifiConnectDialog')[0].addEventListener('hidden.bs.modal', function () {
|
|
$('#WifiConnectDialog input').val('');
|
|
});
|
|
|
|
$('#uCnfrm')[0].addEventListener('shown.bs.modal', function () {
|
|
$('#selectedFWURL').text($('#fw-url-input').val());
|
|
});
|
|
|
|
$('input#show-commands')[0].checked = LastCommandsState === 1;
|
|
$('a[href^="#tab-commands"]').hide();
|
|
$('#load-nvs').on('click', function () {
|
|
$('#nvsfilename').trigger('click');
|
|
});
|
|
$('#nvsfilename').on('change', function () {
|
|
if (typeof window.FileReader !== 'function') {
|
|
throw "The file API isn't supported on this browser.";
|
|
}
|
|
if (!this.files) {
|
|
throw 'This browser does not support the `files` property of the file input.';
|
|
}
|
|
if (!this.files[0]) {
|
|
return undefined;
|
|
}
|
|
|
|
const file = this.files[0];
|
|
let fr = new FileReader();
|
|
fr.onload = function (e) {
|
|
let data = {};
|
|
try {
|
|
data = JSON.parse(e.target.result);
|
|
} catch (ex) {
|
|
alert('Parsing failed!\r\n ' + ex);
|
|
}
|
|
$('input.nvs').each(function (_index, entry) {
|
|
$(this).parent().removeClass('bg-warning').removeClass('bg-success');
|
|
if (data[entry.id]) {
|
|
if (data[entry.id] !== entry.value) {
|
|
console.log(
|
|
'Changed ' + entry.id + ' ' + entry.value + '==>' + data[entry.id]
|
|
);
|
|
$(this).parent().addClass('bg-warning');
|
|
$(this).val(data[entry.id]);
|
|
}
|
|
else {
|
|
$(this).parent().addClass('bg-success');
|
|
}
|
|
}
|
|
});
|
|
var changed = $("input.nvs").children('.bg-warning');
|
|
if (changed) {
|
|
alert('Highlighted values were changed. Press Commit to change on the device');
|
|
}
|
|
}
|
|
fr.readAsText(file);
|
|
this.value = null;
|
|
|
|
}
|
|
);
|
|
$('#clear-syslog').on('click', function () {
|
|
messagecount = 0;
|
|
messageseverity = 'MESSAGING_INFO';
|
|
$('#msgcnt').text('');
|
|
$('#syslogTable').html('');
|
|
});
|
|
|
|
$('#ok-credits').on('click', function () {
|
|
$('#credits').slideUp('fast', function () { });
|
|
$('#app').slideDown('fast', function () { });
|
|
});
|
|
|
|
$('#acredits').on('click', function (event) {
|
|
event.preventDefault();
|
|
$('#app').slideUp('fast', function () { });
|
|
$('#credits').slideDown('fast', function () { });
|
|
});
|
|
|
|
$('input#show-commands').on('click', function () {
|
|
this.checked = this.checked ? 1 : 0;
|
|
if (this.checked) {
|
|
$('a[href^="#tab-commands"]').show();
|
|
LastCommandsState = 1;
|
|
} else {
|
|
LastCommandsState = 0;
|
|
$('a[href^="#tab-commands"]').hide();
|
|
}
|
|
});
|
|
|
|
$('input#show-nvs').on('click', function () {
|
|
this.checked = this.checked ? 1 : 0;
|
|
Cookies.set("show-nvs", this.checked ? 'Y' : 'N');
|
|
handleNVSVisible();
|
|
});
|
|
$('#btn_reboot_recovery').on('click', function () {
|
|
handleReboot('recovery');
|
|
});
|
|
$('#btn_reboot').on('click', function () {
|
|
handleReboot('reboot');
|
|
});
|
|
$('#btn_flash').on('click', function () {
|
|
hFlash();
|
|
});
|
|
$('#save-autoexec1').on('click', function () {
|
|
saveAutoexec1(false);
|
|
});
|
|
$('#commit-autoexec1').on('click', function () {
|
|
saveAutoexec1(true);
|
|
});
|
|
$('#btn_disconnect').on('click', function () {
|
|
ConnectedTo = {};
|
|
refreshAPHTML2();
|
|
$.ajax({
|
|
url: '/connect.json',
|
|
dataType: 'text',
|
|
method: 'DELETE',
|
|
cache: false,
|
|
contentType: 'application/json; charset=utf-8',
|
|
data: JSON.stringify({
|
|
timestamp: Date.now(),
|
|
}),
|
|
});
|
|
});
|
|
$('#btnJoin').on('click', function () {
|
|
handleConnect();
|
|
});
|
|
$('#reboot_nav').on('click', function () {
|
|
handleReboot('reboot');
|
|
});
|
|
$('#reboot_ota_nav').on('click', function () {
|
|
handleReboot('reboot_ota');
|
|
});
|
|
|
|
$('#save-as-nvs').on('click', function () {
|
|
const config = getConfigJson(true);
|
|
const a = document.createElement('a');
|
|
a.href = URL.createObjectURL(
|
|
new Blob([JSON.stringify(config, null, 2)], {
|
|
type: 'text/plain',
|
|
})
|
|
);
|
|
a.setAttribute(
|
|
'download',
|
|
'nvs_config_' + hostName + '_' + Date.now() + 'json'
|
|
);
|
|
document.body.appendChild(a);
|
|
a.click();
|
|
document.body.removeChild(a);
|
|
});
|
|
|
|
$('#save-nvs').on('click', function () {
|
|
post_config(getConfigJson(false));
|
|
});
|
|
|
|
$('#fwUpload').on('click', function () {
|
|
const fileInput = document.getElementById('flashfilename').files;
|
|
if (fileInput.length === 0) {
|
|
alert('No file selected!');
|
|
} else {
|
|
$('#fw-url-input').value = null;
|
|
flashState.StartOTA();
|
|
}
|
|
|
|
});
|
|
$('[name=output-tmpl]').on('click', function () {
|
|
handleTemplateTypeRadio(this.id);
|
|
});
|
|
|
|
$('#chkUpdates').on('click', function () {
|
|
$('#rTable').html('');
|
|
$.getJSON(releaseURL, function (data) {
|
|
let i = 0;
|
|
const branches = [];
|
|
data.forEach(function (release) {
|
|
const namecomponents = release.name.split('#');
|
|
const branch = namecomponents[3];
|
|
if (!branches.includes(branch)) {
|
|
branches.push(branch);
|
|
}
|
|
});
|
|
let fwb = '';
|
|
branches.forEach(function (branch) {
|
|
fwb += '<option value="' + branch + '">' + branch + '</option>';
|
|
});
|
|
$('#fwbranch').append(fwb);
|
|
|
|
data.forEach(function (release) {
|
|
let url = '';
|
|
release.assets.forEach(function (asset) {
|
|
if (asset.name.match(/\.bin$/)) {
|
|
url = asset.browser_download_url;
|
|
}
|
|
});
|
|
const namecomponents = release.name.split('#');
|
|
const ver = namecomponents[0];
|
|
const cfg = namecomponents[2];
|
|
const branch = namecomponents[3];
|
|
var bits = ver.substr(ver.lastIndexOf('-') + 1);
|
|
bits = (bits == '32' || bits == '16') ? bits : '';
|
|
|
|
let body = release.body;
|
|
body = body.replace(/'/gi, '"');
|
|
body = body.replace(
|
|
/[\s\S]+(### Revision Log[\s\S]+)### ESP-IDF Version Used[\s\S]+/,
|
|
'$1'
|
|
);
|
|
body = body.replace(/- \(.+?\) /g, '- ').encodeHTML();
|
|
$('#rTable').append(`<tr class='release ' fwurl='${url}'>
|
|
<td data-bs-toggle='tooltip' title='${body}'>${ver}</td><td>${new Date(release.created_at).toLocalShort()}
|
|
</td><td class='upf'>${cfg}</td><td>${branch}</td><td>${bits}</td></tr>`
|
|
);
|
|
});
|
|
if (i > 7) {
|
|
$('#releaseTable').append(
|
|
"<tr id='showall'>" +
|
|
"<td colspan='6'>" +
|
|
"<input type='button' id='showallbutton' class='btn btn-info' value='Show older releases' />" +
|
|
'</td>' +
|
|
'</tr>'
|
|
);
|
|
$('#showallbutton').on('click', function () {
|
|
$('tr.hide').removeClass('hide');
|
|
$('tr#showall').addClass('hide');
|
|
});
|
|
}
|
|
$('#searchfw').css('display', 'inline');
|
|
if (!setPlatformFilter(platform_name)) {
|
|
setPlatformFilter(project_name)
|
|
}
|
|
$('#rTable tr.release').on('click', function () {
|
|
var url = this.attributes['fwurl'].value;
|
|
if (lmsBaseUrl) {
|
|
url = url.replace(/.*\/download\//, lmsBaseUrl + '/plugins/SqueezeESP32/firmware/');
|
|
}
|
|
$('#fw-url-input').val(url);
|
|
$('#start-flash').show();
|
|
$('#rTable tr.release').removeClass('table-success table-warning');
|
|
$(this).addClass('table-success table-warning');
|
|
});
|
|
|
|
}).fail(function () {
|
|
alert('failed to fetch release history!');
|
|
});
|
|
});
|
|
$('#fwcheck').on('click', function () {
|
|
$('#releaseTable').html('');
|
|
$('#fwbranch').empty();
|
|
$.getJSON(releaseURL, function (data) {
|
|
let i = 0;
|
|
const branches = [];
|
|
data.forEach(function (release) {
|
|
const namecomponents = release.name.split('#');
|
|
const branch = namecomponents[3];
|
|
if (!branches.includes(branch)) {
|
|
branches.push(branch);
|
|
}
|
|
});
|
|
let fwb;
|
|
branches.forEach(function (branch) {
|
|
fwb += '<option value="' + branch + '">' + branch + '</option>';
|
|
});
|
|
$('#fwbranch').append(fwb);
|
|
|
|
data.forEach(function (release) {
|
|
let url = '';
|
|
release.assets.forEach(function (asset) {
|
|
if (asset.name.match(/\.bin$/)) {
|
|
url = asset.browser_download_url;
|
|
}
|
|
});
|
|
const namecomponents = release.name.split('#');
|
|
const ver = namecomponents[0];
|
|
const idf = namecomponents[1];
|
|
const cfg = namecomponents[2];
|
|
const branch = namecomponents[3];
|
|
|
|
let body = release.body;
|
|
body = body.replace(/'/gi, '"');
|
|
body = body.replace(
|
|
/[\s\S]+(### Revision Log[\s\S]+)### ESP-IDF Version Used[\s\S]+/,
|
|
'$1'
|
|
);
|
|
body = body.replace(/- \(.+?\) /g, '- ');
|
|
const trclass = i++ > 6 ? ' hide' : '';
|
|
$('#releaseTable').append(
|
|
"<tr class='release" +
|
|
trclass +
|
|
"'>" +
|
|
"<td data-bs-toggle='tooltip' title='" +
|
|
body +
|
|
"'>" +
|
|
ver +
|
|
'</td>' +
|
|
'<td>' +
|
|
new Date(release.created_at).toLocalShort() +
|
|
'</td>' +
|
|
'<td>' +
|
|
cfg +
|
|
'</td>' +
|
|
'<td>' +
|
|
idf +
|
|
'</td>' +
|
|
'<td>' +
|
|
branch +
|
|
'</td>' +
|
|
"<td><input type='button' class='btn btn-success' value='Select' data-bs-url='" +
|
|
url +
|
|
"' onclick='setURL(this);' /></td>" +
|
|
'</tr>'
|
|
);
|
|
});
|
|
if (i > 7) {
|
|
$('#releaseTable').append(
|
|
"<tr id='showall'>" +
|
|
"<td colspan='6'>" +
|
|
"<input type='button' id='showallbutton' class='btn btn-info' value='Show older releases' />" +
|
|
'</td>' +
|
|
'</tr>'
|
|
);
|
|
$('#showallbutton').on('click', function () {
|
|
$('tr.hide').removeClass('hide');
|
|
$('tr#showall').addClass('hide');
|
|
});
|
|
}
|
|
$('#searchfw').css('display', 'inline');
|
|
}).fail(function () {
|
|
alert('failed to fetch release history!');
|
|
});
|
|
});
|
|
|
|
$('#updateAP').on('click', function () {
|
|
refreshAP();
|
|
console.log('refresh AP');
|
|
});
|
|
|
|
// first time the page loads: attempt to get the connection status and start the wifi scan
|
|
getConfig();
|
|
getCommands();
|
|
getMessages();
|
|
checkStatus();
|
|
|
|
});
|
|
|
|
// eslint-disable-next-line no-unused-vars
|
|
window.setURL = function (button) {
|
|
let url = button.dataset.url;
|
|
|
|
$('[data-bs-url^="http"]')
|
|
.addClass('btn-success')
|
|
.removeClass('btn-danger');
|
|
$('[data-bs-url="' + url + '"]')
|
|
.addClass('btn-danger')
|
|
.removeClass('btn-success');
|
|
|
|
// if user can proxy download through LMS, modify the URL
|
|
if (lmsBaseUrl) {
|
|
url = url.replace(/.*\/download\//, lmsBaseUrl + '/plugins/SqueezeESP32/firmware/');
|
|
}
|
|
|
|
$('#fwurl').val(url);
|
|
}
|
|
|
|
|
|
function rssiToIcon(rssi) {
|
|
if (rssi >= -55) {
|
|
return { 'label': '****', 'icon': `signal_wifi_statusbar_4_bar` };
|
|
} else if (rssi >= -60) {
|
|
return { 'label': '***', 'icon': `network_wifi_3_bar` };
|
|
} else if (rssi >= -65) {
|
|
return { 'label': '**', 'icon': `network_wifi_2_bar` };
|
|
} else if (rssi >= -70) {
|
|
return { 'label': '*', 'icon': `network_wifi_1_bar` };
|
|
} else {
|
|
return { 'label': '.', 'icon': `signal_wifi_statusbar_null` };
|
|
}
|
|
}
|
|
|
|
function refreshAP() {
|
|
if (ConnectedTo?.urc === connectReturnCode.ETH) return;
|
|
$.ajaxSetup({
|
|
timeout: 3000 //Time in milliseconds
|
|
});
|
|
$.getJSON('/scan.json', async function () {
|
|
await sleep(2000);
|
|
$.getJSON('/ap.json', function (data) {
|
|
if (data.length > 0) {
|
|
// sort by signal strength
|
|
data.sort(function (a, b) {
|
|
const x = a.rssi;
|
|
const y = b.rssi;
|
|
// eslint-disable-next-line no-nested-ternary
|
|
return x < y ? 1 : x > y ? -1 : 0;
|
|
});
|
|
apList = data;
|
|
refreshAPHTML2(apList);
|
|
|
|
}
|
|
});
|
|
});
|
|
}
|
|
function formatAP(ssid, rssi, auth) {
|
|
const rssi_icon = rssiToIcon(rssi);
|
|
const auth_icon = { label: auth == 0 ? '🔓' : '🔒', icon: auth == 0 ? 'no_encryption' : 'lock' };
|
|
|
|
return `<tr data-bs-toggle="modal" data-bs-target="#WifiConnectDialog"><td></td><td>${ssid}</td><td>
|
|
<span class="material-icons" style="fill:white; display: inline" aria-label="${rssi_icon.label}" icon="${rssi_icon.icon}" >${getIcon(rssi_icon)}</span>
|
|
</td><td>
|
|
<span class="material-icons" aria-label="${auth_icon.label}" icon="${auth_icon.icon}">${getIcon(auth_icon)}</span>
|
|
</td></tr>`;
|
|
}
|
|
function refreshAPHTML2(data) {
|
|
let h = '';
|
|
$('#wifiTable tr td:first-of-type').text('');
|
|
$('#wifiTable tr').removeClass('table-success table-warning');
|
|
if (data) {
|
|
data.forEach(function (e) {
|
|
h += formatAP(e.ssid, e.rssi, e.auth);
|
|
});
|
|
$('#wifiTable').html(h);
|
|
}
|
|
if ($('.manual_add').length == 0) {
|
|
$('#wifiTable').append(formatAP('Manual add', 0, 0));
|
|
$('#wifiTable tr:last').addClass('table-light text-dark').addClass('manual_add');
|
|
}
|
|
if (ConnectedTo.ssid && (ConnectedTo.urc === connectReturnCode.OK || ConnectedTo.urc === connectReturnCode.RESTORE)) {
|
|
const wifiSelector = `#wifiTable td:contains("${ConnectedTo.ssid}")`;
|
|
if ($(wifiSelector).filter(function () { return $(this).text() === ConnectedTo.ssid; }).length == 0) {
|
|
$('#wifiTable').prepend(`${formatAP(ConnectedTo.ssid, ConnectedTo.rssi ?? 0, 0)}`);
|
|
}
|
|
$(wifiSelector).filter(function () { return $(this).text() === ConnectedTo.ssid; }).siblings().first().html('✓').parent().addClass((ConnectedTo.urc === connectReturnCode.OK ? 'table-success' : 'table-warning'));
|
|
$('span#foot-if').html(`SSID: <strong>${ConnectedTo.ssid}</strong>, IP: <strong>${ConnectedTo.ip}</strong>`);
|
|
$('#wifiStsIcon').html(rssiToIcon(ConnectedTo.rssi));
|
|
|
|
}
|
|
else if (ConnectedTo?.urc !== connectReturnCode.ETH) {
|
|
$('span#foot-if').html('');
|
|
}
|
|
|
|
}
|
|
function refreshETH() {
|
|
|
|
if (ConnectedTo.urc === connectReturnCode.ETH) {
|
|
$('span#foot-if').html(`Network: Ethernet, IP: <strong>${ConnectedTo.ip}</strong>`);
|
|
}
|
|
}
|
|
function showTask(task) {
|
|
console.debug(
|
|
this.toLocaleString() +
|
|
'\t' +
|
|
task.nme +
|
|
'\t' +
|
|
task.cpu +
|
|
'\t' +
|
|
taskStates[task.st] +
|
|
'\t' +
|
|
task.minstk +
|
|
'\t' +
|
|
task.bprio +
|
|
'\t' +
|
|
task.cprio +
|
|
'\t' +
|
|
task.num
|
|
);
|
|
$('tbody#tasks').append(
|
|
'<tr class="table-primary"><th scope="row">' +
|
|
task.num +
|
|
'</th><td>' +
|
|
task.nme +
|
|
'</td><td>' +
|
|
task.cpu +
|
|
'</td><td>' +
|
|
taskStates[task.st] +
|
|
'</td><td>' +
|
|
task.minstk +
|
|
'</td><td>' +
|
|
task.bprio +
|
|
'</td><td>' +
|
|
task.cprio +
|
|
'</td></tr>'
|
|
);
|
|
}
|
|
function btExists(name) {
|
|
return getBTSinkOpt(name).length > 0;
|
|
}
|
|
function getBTSinkOpt(name) {
|
|
return $(`${btSinkNamesOptSel} option:contains('${name}')`);
|
|
}
|
|
function getMessages() {
|
|
$.ajaxSetup({
|
|
timeout: messageInterval //Time in milliseconds
|
|
});
|
|
$.getJSON('/messages.json', async function (data) {
|
|
for (const msg of data) {
|
|
const msgAge = msg.current_time - msg.sent_time;
|
|
var msgTime = new Date();
|
|
msgTime.setTime(msgTime.getTime() - msgAge);
|
|
switch (msg.class) {
|
|
case 'MESSAGING_CLASS_OTA':
|
|
flashState.EventOTAMessageClass(msg.message);
|
|
break;
|
|
case 'MESSAGING_CLASS_STATS':
|
|
// for task states, check structure : task_state_t
|
|
var statsData = JSON.parse(msg.message);
|
|
console.debug(
|
|
msgTime.toLocalShort() +
|
|
' - Number of running tasks: ' +
|
|
statsData.ntasks
|
|
);
|
|
console.debug(
|
|
msgTime.toLocalShort() +
|
|
'\tname' +
|
|
'\tcpu' +
|
|
'\tstate' +
|
|
'\tminstk' +
|
|
'\tbprio' +
|
|
'\tcprio' +
|
|
'\tnum'
|
|
);
|
|
if (statsData.tasks) {
|
|
if ($('#tasks_sect').css('visibility') === 'collapse') {
|
|
$('#tasks_sect').css('visibility', 'visible');
|
|
}
|
|
$('tbody#tasks').html('');
|
|
statsData.tasks
|
|
.sort(function (a, b) {
|
|
return b.cpu - a.cpu;
|
|
})
|
|
.forEach(showTask, msgTime);
|
|
} else if ($('#tasks_sect').css('visibility') === 'visible') {
|
|
$('tbody#tasks').empty();
|
|
$('#tasks_sect').css('visibility', 'collapse');
|
|
}
|
|
break;
|
|
case 'MESSAGING_CLASS_SYSTEM':
|
|
showMessage(msg, msgTime);
|
|
break;
|
|
case 'MESSAGING_CLASS_CFGCMD':
|
|
var msgparts = msg.message.split(/([^\n]*)\n(.*)/gs);
|
|
showCmdMessage(msgparts[1], msg.type, msgparts[2], true);
|
|
break;
|
|
case 'MESSAGING_CLASS_BT':
|
|
if ($("#cfg-audio-bt_source-sink_name").is('input')) {
|
|
var attr = $("#cfg-audio-bt_source-sink_name")[0].attributes;
|
|
var attrs = '';
|
|
for (var j = 0; j < attr.length; j++) {
|
|
if (attr.item(j).name != "type") {
|
|
attrs += `${attr.item(j).name} = "${attr.item(j).value}" `;
|
|
}
|
|
}
|
|
var curOpt = $("#cfg-audio-bt_source-sink_name")[0].value;
|
|
$("#cfg-audio-bt_source-sink_name").replaceWith(`<select id="cfg-audio-bt_source-sink_name" ${attrs}><option value="${curOpt}" data-bs-description="${curOpt}">${curOpt}</option></select> `);
|
|
}
|
|
JSON.parse(msg.message).forEach(function (btEntry) {
|
|
//<input type="text" class="form-control bg-success" placeholder="name" hasvalue="true" longopts="sink_name" shortopts="n" checkbox="false" cmdname="cfg-audio-bt_source" id="cfg-audio-bt_source-sink_name" name="cfg-audio-bt_source-sink_name">
|
|
//<select hasvalue="true" longopts="jack_behavior" shortopts="j" checkbox="false" cmdname="cfg-audio-general" id="cfg-audio-general-jack_behavior" name="cfg-audio-general-jack_behavior" class="form-control "><option>--</option><option>Headphones</option><option>Subwoofer</option></select>
|
|
if (!btExists(btEntry.name)) {
|
|
$("#cfg-audio-bt_source-sink_name").append(`<option>${btEntry.name}</option>`);
|
|
showMessage({ type: msg.type, message: `BT Audio device found: ${btEntry.name} RSSI: ${btEntry.rssi} ` }, msgTime);
|
|
}
|
|
getBTSinkOpt(btEntry.name).attr('data-bs-description', `${btEntry.name} (${btEntry.rssi}dB)`)
|
|
.attr('rssi', btEntry.rssi)
|
|
.attr('value', btEntry.name)
|
|
.text(`${btEntry.name} [${btEntry.rssi}dB]`).trigger('change');
|
|
|
|
});
|
|
$(btSinkNamesOptSel).append($(`${btSinkNamesOptSel} option`).remove().sort(function (a, b) {
|
|
console.log(`${parseInt($(a).attr('rssi'))} < ${parseInt($(b).attr('rssi'))} ? `);
|
|
return parseInt($(a).attr('rssi')) < parseInt($(b).attr('rssi')) ? 1 : -1;
|
|
}));
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
}
|
|
setTimeout(getMessages, messageInterval);
|
|
}).fail(function (xhr, ajaxOptions, thrownError) {
|
|
|
|
if (xhr.status == 404) {
|
|
$('.orec').hide(); // system commands won't be available either
|
|
messagesHeld = true;
|
|
}
|
|
else {
|
|
handleExceptionResponse(xhr, ajaxOptions, thrownError);
|
|
}
|
|
if (xhr.status == 0 && xhr.readyState == 0) {
|
|
// probably a timeout. Target is rebooting?
|
|
setTimeout(getMessages, messageInterval * 2); // increase duration if a failure happens
|
|
}
|
|
else if (!messagesHeld) {
|
|
// 404 here means we rebooted to an old recovery
|
|
setTimeout(getMessages, messageInterval); // increase duration if a failure happens
|
|
}
|
|
|
|
}
|
|
);
|
|
|
|
/*
|
|
Minstk is minimum stack space left
|
|
Bprio is base priority
|
|
cprio is current priority
|
|
nme is name
|
|
st is task state. I provided a "typedef" that you can use to convert to text
|
|
cpu is cpu percent used
|
|
*/
|
|
}
|
|
function handleRecoveryMode(data) {
|
|
const locRecovery = data.recovery ?? 0;
|
|
if (locRecovery === 1) {
|
|
recovery = true;
|
|
$('.recovery_element').show();
|
|
$('.ota_element').hide();
|
|
$('#boot-button').html('Reboot');
|
|
$('#boot-form').attr('action', '/reboot_ota.json');
|
|
} else {
|
|
if (!recovery && messagesHeld) {
|
|
messagesHeld = false;
|
|
setTimeout(getMessages, messageInterval); // increase duration if a failure happens
|
|
}
|
|
recovery = false;
|
|
|
|
$('.recovery_element').hide();
|
|
$('.ota_element').show();
|
|
$('#boot-button').html('Recovery');
|
|
$('#boot-form').attr('action', '/recovery.json');
|
|
}
|
|
|
|
}
|
|
|
|
function hasConnectionChanged(data) {
|
|
// gw: "192.168.10.1"
|
|
// ip: "192.168.10.225"
|
|
// netmask: "255.255.255.0"
|
|
// ssid: "MyTestSSID"
|
|
|
|
return (data.urc !== ConnectedTo.urc ||
|
|
data.ssid !== ConnectedTo.ssid ||
|
|
data.gw !== ConnectedTo.gw ||
|
|
data.netmask !== ConnectedTo.netmask ||
|
|
data.ip !== ConnectedTo.ip || data.rssi !== ConnectedTo.rssi)
|
|
}
|
|
function handleWifiDialog(data) {
|
|
if ($('#WifiConnectDialog').is(':visible')) {
|
|
if (ConnectedTo.ip) {
|
|
$('#ipAddress').text(ConnectedTo.ip);
|
|
}
|
|
if (ConnectedTo.ssid) {
|
|
$('#connectedToSSID').text(ConnectedTo.ssid);
|
|
}
|
|
if (ConnectedTo.gw) {
|
|
$('#gateway').text(ConnectedTo.gw);
|
|
}
|
|
if (ConnectedTo.netmask) {
|
|
$('#netmask').text(ConnectedTo.netmask);
|
|
}
|
|
if (ConnectingToSSID.Action === undefined || (ConnectingToSSID.Action && ConnectingToSSID.Action == ConnectingToActions.STS)) {
|
|
$("*[class*='connecting']").hide();
|
|
$('.connecting-status').show();
|
|
}
|
|
if (SystemConfig.ap_ssid) {
|
|
$('#apName').text(SystemConfig.ap_ssid.value);
|
|
}
|
|
if (SystemConfig.ap_pwd) {
|
|
$('#apPass').text(SystemConfig.ap_pwd.value);
|
|
}
|
|
if (!data) {
|
|
return;
|
|
}
|
|
else {
|
|
switch (data.urc) {
|
|
case connectReturnCode.OK:
|
|
if (data.ssid && data.ssid === ConnectingToSSID.ssid) {
|
|
$("*[class*='connecting']").hide();
|
|
$('.connecting-success').show();
|
|
ConnectingToSSID.Action = ConnectingToActions.STS;
|
|
}
|
|
break;
|
|
case connectReturnCode.FAIL:
|
|
//
|
|
if (ConnectingToSSID.Action != ConnectingToActions.STS && ConnectingToSSID.ssid == data.ssid) {
|
|
$("*[class*='connecting']").hide();
|
|
$('.connecting-fail').show();
|
|
}
|
|
break;
|
|
case connectReturnCode.LOST:
|
|
|
|
break;
|
|
case connectReturnCode.RESTORE:
|
|
if (ConnectingToSSID.Action != ConnectingToActions.STS && ConnectingToSSID.ssid != data.ssid) {
|
|
$("*[class*='connecting']").hide();
|
|
$('.connecting-fail').show();
|
|
}
|
|
break;
|
|
case connectReturnCode.DISC:
|
|
// that's a manual disconnect
|
|
// if ($('#wifi-status').is(':visible')) {
|
|
// $('#wifi-status').slideUp('fast', function() {});
|
|
// $('span#foot-wifi').html('');
|
|
|
|
// }
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
}
|
|
|
|
}
|
|
}
|
|
function setIcons(offline) {
|
|
$('.material-icons').each(function (_index, entry) {
|
|
entry.textContent = entry.attributes[offline ? 'aria-label' : 'icon'].value;
|
|
});
|
|
}
|
|
function handleNetworkStatus(data) {
|
|
setIcons(!isConnected());
|
|
if (hasConnectionChanged(data) || !data.urc) {
|
|
ConnectedTo = data;
|
|
$(".if_eth").hide();
|
|
$('.if_wifi').hide();
|
|
if (!data.urc || ConnectedTo.urc != connectReturnCode.ETH) {
|
|
$('.if_wifi').show();
|
|
refreshAPHTML2();
|
|
}
|
|
else {
|
|
$(".if_eth").show();
|
|
refreshETH();
|
|
}
|
|
|
|
}
|
|
handleWifiDialog(data);
|
|
}
|
|
|
|
|
|
|
|
function batteryToIcon(voltage) {
|
|
/* Assuming Li-ion 18650s as a power source, 3.9V per cell, or above is treated
|
|
as full charge (>75% of capacity). 3.4V is empty. The gauge is loosely
|
|
following the graph here:
|
|
https://learn.adafruit.com/li-ion-and-lipoly-batteries/voltages
|
|
using the 0.2C discharge profile for the rest of the values.
|
|
*/
|
|
|
|
for (const iconEntry of batIcons) {
|
|
for (const entryRanges of iconEntry.ranges) {
|
|
if (inRange(voltage, entryRanges.f, entryRanges.t)) {
|
|
return { label: iconEntry.label, icon: iconEntry.icon };
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
return { label: '▪▪▪▪', icon: "battery_full" };
|
|
}
|
|
function checkStatus() {
|
|
$.ajaxSetup({
|
|
timeout: statusInterval //Time in milliseconds
|
|
});
|
|
$.getJSON('/status.json', function (data) {
|
|
handleRecoveryMode(data);
|
|
handleNVSVisible();
|
|
handleNetworkStatus(data);
|
|
handlebtstate(data);
|
|
flashState.EventTargetStatus(data);
|
|
if(data.depth) {
|
|
depth = data.depth;
|
|
if(depth==16){
|
|
$('#cmd_opt_R').show();
|
|
}
|
|
else{
|
|
$('#cmd_opt_R').hide();
|
|
}
|
|
}
|
|
|
|
|
|
if (data.project_name && data.project_name !== '') {
|
|
project_name = data.project_name;
|
|
}
|
|
if (data.platform_name && data.platform_name !== '') {
|
|
platform_name = data.platform_name;
|
|
}
|
|
if (board_model === '') board_model = project_name;
|
|
if (board_model === '') board_model = 'Squeezelite-ESP32';
|
|
if (data.version && data.version !== '') {
|
|
versionName = data.version;
|
|
$("#navtitle").html(`${board_model}${recovery ? '<br>[recovery]' : ''}`);
|
|
$('span#foot-fw').html(`fw: <strong>${versionName}</strong>, mode: <strong>${recovery ? "Recovery" : project_name}</strong>`);
|
|
} else {
|
|
$('span#flash-status').html('');
|
|
}
|
|
if (data.Voltage) {
|
|
const bat_icon = batteryToIcon(data.Voltage);
|
|
$('#battery').html(`${getIcon(bat_icon)}`);
|
|
$('#battery').attr("aria-label", bat_icon.label);
|
|
$('#battery').attr("icon", bat_icon.icon);
|
|
$('#battery').show();
|
|
} else {
|
|
$('#battery').hide();
|
|
}
|
|
if ((data.message ?? '') != '' && prevmessage != data.message) {
|
|
// supporting older recovery firmwares - messages will come from the status.json structure
|
|
prevmessage = data.message;
|
|
showLocalMessage(data.message, 'MESSAGING_INFO')
|
|
}
|
|
is_i2c_locked = data.is_i2c_locked;
|
|
if (is_i2c_locked) {
|
|
$('flds-cfg-hw-preset').hide();
|
|
}
|
|
else {
|
|
$('flds-cfg-hw-preset').show();
|
|
}
|
|
$("button[onclick*='handleReboot']").removeClass('rebooting');
|
|
|
|
if (typeof lmsBaseUrl == "undefined" || data.lms_ip != prevLMSIP && data.lms_ip && data.lms_port) {
|
|
const baseUrl = 'http://' + data.lms_ip + ':' + data.lms_port;
|
|
prevLMSIP = data.lms_ip;
|
|
$.ajax({
|
|
url: baseUrl + '/plugins/SqueezeESP32/firmware/-check.bin',
|
|
type: 'HEAD',
|
|
dataType: 'text',
|
|
cache: false,
|
|
error: function () {
|
|
// define the value, so we don't check it any more.
|
|
lmsBaseUrl = '';
|
|
},
|
|
success: function () {
|
|
lmsBaseUrl = baseUrl;
|
|
}
|
|
});
|
|
}
|
|
$('#o_jack').css({ display: Number(data.Jack) ? 'inline' : 'none' });
|
|
setTimeout(checkStatus, statusInterval);
|
|
}).fail(function (xhr, ajaxOptions, thrownError) {
|
|
handleExceptionResponse(xhr, ajaxOptions, thrownError);
|
|
if (xhr.status == 0 && xhr.readyState == 0) {
|
|
// probably a timeout. Target is rebooting?
|
|
setTimeout(checkStatus, messageInterval * 2); // increase duration if a failure happens
|
|
}
|
|
else {
|
|
setTimeout(checkStatus, messageInterval); // increase duration if a failure happens
|
|
}
|
|
});
|
|
}
|
|
// eslint-disable-next-line no-unused-vars
|
|
window.runCommand = function (button, reboot) {
|
|
let cmdstring = button.attributes.cmdname.value;
|
|
showCmdMessage(
|
|
button.attributes.cmdname.value,
|
|
'MESSAGING_INFO',
|
|
'Executing.',
|
|
false
|
|
);
|
|
const fields = document.getElementById('flds-' + cmdstring);
|
|
const allfields = fields?.querySelectorAll('select,input');
|
|
if (cmdstring === 'cfg-hw-preset') return handleHWPreset(allfields, reboot);
|
|
cmdstring += ' ';
|
|
if (fields) {
|
|
|
|
for (const field of allfields) {
|
|
let qts = '';
|
|
let opt = '';
|
|
let attr = field.attributes;
|
|
let isSelect = $(field).is('select');
|
|
const hasValue = attr?.hasvalue?.value === 'true';
|
|
const validVal = (isSelect && field.value !== '--') || (!isSelect && field.value !== '');
|
|
|
|
if (!hasValue || hasValue && validVal) {
|
|
if (attr?.longopts?.value !== 'undefined') {
|
|
opt += '--' + attr?.longopts?.value;
|
|
} else if (attr?.shortopts?.value !== 'undefined') {
|
|
opt = '-' + attr.shortopts.value;
|
|
}
|
|
|
|
if (attr?.hasvalue?.value === 'true') {
|
|
if (attr?.value !== '') {
|
|
qts = /\s/.test(field.value) ? '"' : '';
|
|
cmdstring += opt + ' ' + qts + field.value + qts + ' ';
|
|
}
|
|
} else {
|
|
// this is a checkbox
|
|
if (field?.checked) {
|
|
cmdstring += opt + ' ';
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
console.log(cmdstring);
|
|
|
|
const data = {
|
|
timestamp: Date.now(),
|
|
};
|
|
data.command = cmdstring;
|
|
|
|
$.ajax({
|
|
url: '/commands.json',
|
|
dataType: 'text',
|
|
method: 'POST',
|
|
cache: false,
|
|
contentType: 'application/json; charset=utf-8',
|
|
data: JSON.stringify(data),
|
|
error: function (xhr, _ajaxOptions, thrownError) {
|
|
var cmd = JSON.parse(this.data).command;
|
|
if (xhr.status == 404) {
|
|
showCmdMessage(
|
|
cmd.substr(0, cmd.indexOf(' ')),
|
|
'MESSAGING_ERROR',
|
|
`${recovery ? 'Limited recovery mode active. Unsupported action ' : 'Unexpected error while processing command'}`,
|
|
true
|
|
);
|
|
}
|
|
else {
|
|
handleExceptionResponse(xhr, _ajaxOptions, thrownError);
|
|
showCmdMessage(
|
|
cmd.substr(0, cmd.indexOf(' ') - 1),
|
|
'MESSAGING_ERROR',
|
|
`Unexpected error ${(thrownError !== '') ? thrownError : 'with return status = ' + xhr.status}`,
|
|
true
|
|
);
|
|
}
|
|
},
|
|
success: function (response) {
|
|
$('.orec').show();
|
|
console.log(response);
|
|
if (
|
|
JSON.parse(response).Result === 'Success' &&
|
|
reboot
|
|
) {
|
|
delayReboot(2500, button.attributes.cmdname.value);
|
|
}
|
|
},
|
|
});
|
|
}
|
|
function getLongOps(data, name, longopts) {
|
|
return data.values[name] !== undefined ? data.values[name][longopts] : "";
|
|
}
|
|
function getCommands() {
|
|
$.ajaxSetup({
|
|
timeout: 7000 //Time in milliseconds
|
|
});
|
|
$.getJSON('/commands.json', function (data) {
|
|
console.log(data);
|
|
$('.orec').show();
|
|
data.commands.forEach(function (command) {
|
|
if ($('#flds-' + command.name).length === 0) {
|
|
const cmdParts = command.name.split('-');
|
|
const isConfig = cmdParts[0] === 'cfg';
|
|
const targetDiv = '#tab-' + cmdParts[0] + '-' + cmdParts[1];
|
|
let innerhtml = '';
|
|
innerhtml += `<div class="card text-white mb-3"><div class="card-header">${command.help.encodeHTML().replace(/\n/g, '<br />')}</div><div class="card-body"><fieldset id="flds-${command.name}">`;
|
|
if (command.argtable) {
|
|
command.argtable.forEach(function (arg) {
|
|
let placeholder = arg.datatype || '';
|
|
const ctrlname = command.name + '-' + arg.longopts;
|
|
const curvalue = getLongOps(data, command.name, arg.longopts);
|
|
|
|
let attributes = 'hasvalue=' + arg.hasvalue + ' ';
|
|
attributes += 'longopts="' + arg.longopts + '" ';
|
|
attributes += 'shortopts="' + arg.shortopts + '" ';
|
|
attributes += 'checkbox=' + arg.checkbox + ' ';
|
|
attributes += 'cmdname="' + command.name + '" ';
|
|
attributes +=
|
|
'id="' +
|
|
ctrlname +
|
|
'" name="' +
|
|
ctrlname +
|
|
'" hasvalue="' +
|
|
arg.hasvalue +
|
|
'" ';
|
|
let extraclass = arg.mincount > 0 ? 'bg-success' : '';
|
|
if (arg.glossary === 'hidden') {
|
|
attributes += ' style="visibility: hidden;"';
|
|
}
|
|
if (arg.checkbox) {
|
|
innerhtml += `<div class="form-check"><label class="form-check-label"><input type="checkbox" ${attributes} class="form-check-input ${extraclass}" value="" >${arg.glossary.encodeHTML()}</label>`;
|
|
} else {
|
|
innerhtml += `<div class="form-group" ><label for="${ctrlname}">${arg.glossary.encodeHTML()}</label>`;
|
|
if (placeholder.includes('|')) {
|
|
extraclass = placeholder.startsWith('+') ? ' multiple ' : '';
|
|
placeholder = placeholder
|
|
.replace('<', '')
|
|
.replace('=', '')
|
|
.replace('>', '');
|
|
innerhtml += `<select ${attributes} class="form-control ${extraclass}" >`;
|
|
placeholder = '--|' + placeholder;
|
|
placeholder.split('|').forEach(function (choice) {
|
|
innerhtml += '<option >' + choice + '</option>';
|
|
});
|
|
innerhtml += '</select>';
|
|
} else {
|
|
innerhtml += `<input type="text" class="form-control ${extraclass}" placeholder="${placeholder}" ${attributes}>`;
|
|
}
|
|
}
|
|
|
|
innerhtml += `${arg.checkbox ? '</div>' : ''}<small class="form-text text-muted">Previous value: ${arg.checkbox ? (curvalue ? 'Checked' : 'Unchecked') : (curvalue || '')}</small>${arg.checkbox ? '' : '</div>'}`;
|
|
});
|
|
}
|
|
innerhtml += `<div style="margin-top: 16px;">
|
|
<div class="toast hide" role="alert" aria-live="assertive" aria-atomic="true" id="toast_${command.name}">
|
|
<div class="toast-header">
|
|
<strong class="mr-auto">Result</strong
|
|
<button type="button" class="btn-close" data-bs-dismiss="toast" aria-label="Close"></button>
|
|
</div>
|
|
<div class="toast-body" id="msg_${command.name}"></div>
|
|
</div>`;
|
|
if (isConfig) {
|
|
innerhtml +=
|
|
`<button type="submit" class="btn btn-info sclk" id="btn-save-${command.name}" cmdname="${command.name}">Save</button>
|
|
<button type="submit" class="btn btn-warning cclk" id="btn-commit-${command.name}" cmdname="${command.name}">Apply</button>`;
|
|
} else {
|
|
innerhtml += `<button type="submit" class="btn btn-success sclk" id="btn-run-${command.name}" cmdname="${command.name}">Execute</button>`;
|
|
}
|
|
innerhtml += '</div></fieldset></div></div>';
|
|
if (isConfig) {
|
|
$(targetDiv).append(innerhtml);
|
|
} else {
|
|
$('#commands-list').append(innerhtml);
|
|
}
|
|
}
|
|
});
|
|
$(".sclk").off('click').on('click', function () { runCommand(this, false); });
|
|
$(".cclk").off('click').on('click', function () { runCommand(this, true); });
|
|
data.commands.forEach(function (command) {
|
|
$('[cmdname=' + command.name + ']:input').val('');
|
|
$('[cmdname=' + command.name + ']:checkbox').prop('checked', false);
|
|
if (command.argtable) {
|
|
command.argtable.forEach(function (arg) {
|
|
const ctrlselector = '#' + command.name + '-' + arg.longopts;
|
|
const ctrlValue = getLongOps(data, command.name, arg.longopts);
|
|
if (arg.checkbox) {
|
|
$(ctrlselector)[0].checked = ctrlValue;
|
|
} else {
|
|
if (ctrlValue !== undefined) {
|
|
$(ctrlselector)
|
|
.val(ctrlValue)
|
|
.trigger('change');
|
|
}
|
|
if (
|
|
$(ctrlselector)[0].value.length === 0 &&
|
|
(arg.datatype || '').includes('|')
|
|
) {
|
|
$(ctrlselector)[0].value = '--';
|
|
}
|
|
}
|
|
});
|
|
}
|
|
});
|
|
loadPresets();
|
|
}).fail(function (xhr, ajaxOptions, thrownError) {
|
|
if (xhr.status == 404) {
|
|
$('.orec').hide();
|
|
}
|
|
else {
|
|
handleExceptionResponse(xhr, ajaxOptions, thrownError);
|
|
}
|
|
$('#commands-list').empty();
|
|
|
|
});
|
|
}
|
|
|
|
function getConfig() {
|
|
$.ajaxSetup({
|
|
timeout: 7000 //Time in milliseconds
|
|
});
|
|
$.getJSON('/config.json', function (entries) {
|
|
$('#nvsTable tr').remove();
|
|
const data = (entries.config ? entries.config : entries);
|
|
SystemConfig = data;
|
|
commandBTSinkName = '';
|
|
Object.keys(data)
|
|
.sort()
|
|
.forEach(function (key) {
|
|
let val = data[key].value;
|
|
if (key === 'autoexec') {
|
|
if (data.autoexec.value === '0') {
|
|
$('#disable-squeezelite')[0].checked = true;
|
|
} else {
|
|
$('#disable-squeezelite')[0].checked = false;
|
|
}
|
|
} else if (key === 'autoexec1') {
|
|
/* call new function to parse the squeezelite options */
|
|
processSqueezeliteCommandLine(val);
|
|
} else if (key === 'host_name') {
|
|
val = val.replaceAll('"', '');
|
|
$('input#dhcp-name1').val(val);
|
|
$('input#dhcp-name2').val(val);
|
|
if ($('#cmd_opt_n').length == 0) {
|
|
$('#cmd_opt_n').val(val);
|
|
}
|
|
document.title = val;
|
|
hostName = val;
|
|
} else if (key === 'rel_api') {
|
|
releaseURL = val;
|
|
}
|
|
else if (key === 'enable_airplay') {
|
|
$("#s_airplay").css({ display: isEnabled(val) ? 'inline' : 'none' })
|
|
}
|
|
else if (key === 'enable_cspot') {
|
|
$("#s_cspot").css({ display: isEnabled(val) ? 'inline' : 'none' })
|
|
}
|
|
else if (key == 'preset_name') {
|
|
preset_name = val;
|
|
}
|
|
else if (key == 'board_model') {
|
|
board_model = val;
|
|
}
|
|
|
|
$('tbody#nvsTable').append(
|
|
'<tr>' +
|
|
'<td>' +
|
|
key +
|
|
'</td>' +
|
|
"<td class='value'>" +
|
|
"<input type='text' class='form-control nvs' id='" +
|
|
key +
|
|
"' nvs_type=" +
|
|
data[key].type +
|
|
' >' +
|
|
'</td>' +
|
|
'</tr>'
|
|
);
|
|
$('input#' + key).val(data[key].value);
|
|
});
|
|
if(commandBTSinkName.length > 0) {
|
|
// persist the sink name found in the autoexec1 command line
|
|
$('#cfg-audio-bt_source-sink_name').val(commandBTSinkName);
|
|
}
|
|
$('tbody#nvsTable').append(
|
|
"<tr><td><input type='text' class='form-control' id='nvs-new-key' placeholder='new key'></td><td><input type='text' class='form-control' id='nvs-new-value' placeholder='new value' nvs_type=33 ></td></tr>"
|
|
);
|
|
if (entries.gpio) {
|
|
$('#pins').show();
|
|
$('tbody#gpiotable tr').remove();
|
|
entries.gpio.forEach(function (gpioEntry) {
|
|
$('tbody#gpiotable').append(
|
|
'<tr class=' +
|
|
(gpioEntry.fixed ? 'table-secondary' : 'table-primary') +
|
|
'><th scope="row">' +
|
|
gpioEntry.group +
|
|
'</th><td>' +
|
|
gpioEntry.name +
|
|
'</td><td>' +
|
|
gpioEntry.gpio +
|
|
'</td><td>' +
|
|
(gpioEntry.fixed ? 'Fixed' : 'Configuration') +
|
|
'</td></tr>'
|
|
);
|
|
});
|
|
}
|
|
else {
|
|
$('#pins').hide();
|
|
}
|
|
}).fail(function (xhr, ajaxOptions, thrownError) {
|
|
handleExceptionResponse(xhr, ajaxOptions, thrownError);
|
|
});
|
|
}
|
|
|
|
function processSqueezeliteCommandLine(val) {
|
|
const parsed = parseSqueezeliteCommandLine(val);
|
|
if (parsed.output.toUpperCase().startsWith('I2S')) {
|
|
handleTemplateTypeRadio('i2s');
|
|
} else if (parsed.output.toUpperCase().startsWith('SPDIF')) {
|
|
handleTemplateTypeRadio('spdif');
|
|
} else if (parsed.output.toUpperCase().startsWith('BT')) {
|
|
if(parsed.otherOptions.btname){
|
|
commandBTSinkName= parsed.otherOptions.btname;
|
|
}
|
|
handleTemplateTypeRadio('bt');
|
|
}
|
|
Object.keys(parsed.options).forEach(function (key) {
|
|
const option = parsed.options[key];
|
|
if (!$(`#cmd_opt_${key}`).hasOwnProperty('checked')) {
|
|
$(`#cmd_opt_${key}`).val(option);
|
|
} else {
|
|
$(`#cmd_opt_${key}`)[0].checked = option;
|
|
}
|
|
});
|
|
if (parsed.options.hasOwnProperty('u')) {
|
|
// parse -u v[:i] and check the appropriate radio button with id #resample_v
|
|
const [resampleValue, resampleInterpolation] = parsed.options.u.split(':');
|
|
$(`#resample_${resampleValue}`).prop('checked', true);
|
|
// if resampleinterpolation is set, check resample_i checkbox
|
|
if (resampleInterpolation) {
|
|
$('#resample_i').prop('checked', true);
|
|
}
|
|
}
|
|
|
|
|
|
}
|
|
|
|
function showLocalMessage(message, severity) {
|
|
const msg = {
|
|
message: message,
|
|
type: severity,
|
|
};
|
|
showMessage(msg, new Date());
|
|
}
|
|
|
|
function showMessage(msg, msgTime) {
|
|
let color = 'table-success';
|
|
|
|
if (msg.type === 'MESSAGING_WARNING') {
|
|
color = 'table-warning';
|
|
if (messageseverity === 'MESSAGING_INFO') {
|
|
messageseverity = 'MESSAGING_WARNING';
|
|
}
|
|
} else if (msg.type === 'MESSAGING_ERROR') {
|
|
if (
|
|
messageseverity === 'MESSAGING_INFO' ||
|
|
messageseverity === 'MESSAGING_WARNING'
|
|
) {
|
|
messageseverity = 'MESSAGING_ERROR';
|
|
}
|
|
color = 'table-danger';
|
|
}
|
|
if (++messagecount > 0) {
|
|
$('#msgcnt').removeClass('badge-success');
|
|
$('#msgcnt').removeClass('badge-warning');
|
|
$('#msgcnt').removeClass('badge-danger');
|
|
$('#msgcnt').addClass(pillcolors[messageseverity]);
|
|
$('#msgcnt').text(messagecount);
|
|
}
|
|
|
|
$('#syslogTable').append(
|
|
"<tr class='" +
|
|
color +
|
|
"'>" +
|
|
'<td>' +
|
|
msgTime.toLocalShort() +
|
|
'</td>' +
|
|
'<td>' +
|
|
msg.message.encodeHTML() +
|
|
'</td>' +
|
|
'</tr>'
|
|
);
|
|
}
|
|
|
|
function inRange(x, min, max) {
|
|
return (x - min) * (x - max) <= 0;
|
|
}
|
|
|
|
function sleep(ms) {
|
|
return new Promise(resolve => setTimeout(resolve, ms));
|
|
}
|