custom.js 65 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648164916501651165216531654165516561657165816591660166116621663166416651666166716681669167016711672167316741675167616771678167916801681168216831684168516861687168816891690169116921693169416951696169716981699170017011702170317041705170617071708170917101711171217131714171517161717171817191720172117221723172417251726172717281729173017311732173317341735173617371738173917401741174217431744174517461747174817491750175117521753175417551756175717581759176017611762176317641765176617671768176917701771177217731774177517761777177817791780178117821783178417851786178717881789179017911792179317941795179617971798179918001801180218031804180518061807180818091810181118121813181418151816181718181819182018211822182318241825182618271828182918301831183218331834183518361837183818391840184118421843184418451846184718481849185018511852185318541855185618571858185918601861186218631864186518661867186818691870187118721873187418751876187718781879188018811882188318841885188618871888188918901891189218931894189518961897189818991900190119021903190419051906190719081909191019111912191319141915191619171918191919201921192219231924192519261927192819291930193119321933193419351936193719381939194019411942194319441945194619471948194919501951195219531954195519561957195819591960196119621963196419651966196719681969197019711972197319741975197619771978197919801981198219831984198519861987198819891990199119921993199419951996199719981999200020012002200320042005200620072008200920102011201220132014201520162017201820192020202120222023202420252026202720282029203020312032203320342035203620372038203920402041204220432044204520462047204820492050205120522053205420552056205720582059206020612062206320642065206620672068206920702071207220732074207520762077207820792080208120822083208420852086208720882089209020912092209320942095209620972098209921002101210221032104210521062107210821092110211121122113211421152116211721182119212021212122212321242125212621272128212921302131213221332134213521362137213821392140
  1. var he = require('he');
  2. var Promise = require('es6-promise').Promise;
  3. window.bootstrap = require('bootstrap');
  4. import Cookies from 'js-cookie';
  5. if (!String.prototype.format) {
  6. Object.assign(String.prototype, {
  7. format() {
  8. const args = arguments;
  9. return this.replace(/{(\d+)}/g, function (match, number) {
  10. return typeof args[number] !== 'undefined' ? args[number] : match;
  11. });
  12. },
  13. });
  14. }
  15. if (!String.prototype.encodeHTML) {
  16. Object.assign(String.prototype, {
  17. encodeHTML() {
  18. return he.encode(this).replace(/\n/g, '<br />');
  19. },
  20. });
  21. }
  22. Object.assign(Date.prototype, {
  23. toLocalShort() {
  24. const opt = { dateStyle: 'short', timeStyle: 'short' };
  25. return this.toLocaleString(undefined, opt);
  26. },
  27. });
  28. function handleNVSVisible(){
  29. let nvs_previous_checked = isEnabled(Cookies.get("show-nvs"));
  30. $('input#show-nvs')[0].checked = nvs_previous_checked ;
  31. if ($('input#show-nvs')[0].checked || recovery) {
  32. $('*[href*="-nvs"]').show();
  33. } else {
  34. $('*[href*="-nvs"]').hide();
  35. }
  36. }
  37. function isEnabled(val) {
  38. return val!=undefined && typeof val === 'string' && val.match("[Yy1]");
  39. }
  40. const nvsTypes = {
  41. NVS_TYPE_U8: 0x01,
  42. /*! < Type uint8_t */
  43. NVS_TYPE_I8: 0x11,
  44. /*! < Type int8_t */
  45. NVS_TYPE_U16: 0x02,
  46. /*! < Type uint16_t */
  47. NVS_TYPE_I16: 0x12,
  48. /*! < Type int16_t */
  49. NVS_TYPE_U32: 0x04,
  50. /*! < Type uint32_t */
  51. NVS_TYPE_I32: 0x14,
  52. /*! < Type int32_t */
  53. NVS_TYPE_U64: 0x08,
  54. /*! < Type uint64_t */
  55. NVS_TYPE_I64: 0x18,
  56. /*! < Type int64_t */
  57. NVS_TYPE_STR: 0x21,
  58. /*! < Type string */
  59. NVS_TYPE_BLOB: 0x42,
  60. /*! < Type blob */
  61. NVS_TYPE_ANY: 0xff /*! < Must be last */,
  62. };
  63. const btIcons = {
  64. bt_playing: {'label':'','icon': 'media_bluetooth_on'},
  65. bt_disconnected: {'label':'','icon': 'media_bluetooth_off'},
  66. bt_neutral: {'label':'','icon': 'bluetooth'},
  67. bt_connecting: {'label':'','icon': 'bluetooth_searching'},
  68. bt_connected: {'label':'','icon': 'bluetooth_connected'},
  69. bt_disabled: {'label':'','icon': 'bluetooth_disabled'},
  70. play_arrow: {'label':'','icon': 'play_circle_filled'},
  71. pause: {'label':'','icon': 'pause_circle'},
  72. stop: {'label':'','icon': 'stop_circle'},
  73. '': {'label':'','icon':''}
  74. };
  75. const batIcons = [
  76. { icon: "battery_0_bar", label:'▪', ranges: [{ f: 5.8, t: 6.8 }, { f: 8.8, t: 10.2 }] },
  77. { icon: "battery_2_bar", label:'▪▪', ranges: [{ f: 6.8, t: 7.4 }, { f: 10.2, t: 11.1 }] },
  78. { icon: "battery_3_bar", label:'▪▪▪', ranges: [{ f: 7.4, t: 7.5 }, { f: 11.1, t: 11.25 }] },
  79. { icon: "battery_4_bar", label:'▪▪▪▪', ranges: [{ f: 7.5, t: 7.8 }, { f: 11.25, t: 11.7 }] }
  80. ];
  81. const btStateIcons = [
  82. { desc: 'Idle', sub: ['bt_neutral'] },
  83. { desc: 'Discovering', sub: ['bt_connecting'] },
  84. { desc: 'Discovered', sub: ['bt_connecting'] },
  85. { desc: 'Unconnected', sub: ['bt_disconnected'] },
  86. { desc: 'Connecting', sub: ['bt_connecting'] },
  87. {
  88. desc: 'Connected',
  89. sub: ['bt_connected', 'play_arrow', 'bt_playing', 'pause', 'stop'],
  90. },
  91. { desc: 'Disconnecting', sub: ['bt_disconnected'] },
  92. ];
  93. const pillcolors = {
  94. MESSAGING_INFO: 'badge-success',
  95. MESSAGING_WARNING: 'badge-warning',
  96. MESSAGING_ERROR: 'badge-danger',
  97. };
  98. const connectReturnCode = {
  99. OK: 0,
  100. FAIL: 1,
  101. DISC: 2,
  102. LOST: 3,
  103. RESTORE: 4,
  104. ETH: 5
  105. }
  106. const taskStates = {
  107. 0: 'eRunning',
  108. /*! < A task is querying the state of itself, so must be running. */
  109. 1: 'eReady',
  110. /*! < The task being queried is in a read or pending ready list. */
  111. 2: 'eBlocked',
  112. /*! < The task being queried is in the Blocked state. */
  113. 3: 'eSuspended',
  114. /*! < The task being queried is in the Suspended state, or is in the Blocked state with an infinite time out. */
  115. 4: 'eDeleted',
  116. };
  117. let flashState = {
  118. NONE: 0,
  119. REBOOT_TO_RECOVERY: 2,
  120. SET_FWURL: 5,
  121. FLASHING: 6,
  122. DONE: 7,
  123. UPLOADING: 8,
  124. ERROR: 9,
  125. UPLOADCOMPLETE: 10,
  126. _state: -1,
  127. olderRecovery: false,
  128. statusText: '',
  129. flashURL: '',
  130. flashFileName: '',
  131. statusPercent: 0,
  132. Completed: false,
  133. recovery: false,
  134. prevRecovery: false,
  135. updateModal: new bootstrap.Modal(document.getElementById('otadiv'), {}),
  136. reset: function () {
  137. this.olderRecovery = false;
  138. this.statusText = '';
  139. this.statusPercent = -1;
  140. this.flashURL = '';
  141. this.flashFileName = undefined;
  142. this.UpdateProgress();
  143. $('#rTable tr.release').removeClass('table-success table-warning');
  144. $('.flact').prop('disabled', false);
  145. $('#flashfilename').value = null;
  146. $('#fw-url-input').value = null;
  147. if(!this.isStateError()){
  148. $('span#flash-status').html('');
  149. $('#fwProgressLabel').parent().removeClass('bg-danger');
  150. }
  151. this._state = this.NONE
  152. return this;
  153. },
  154. isStateUploadComplete: function () {
  155. return this._state == this.UPLOADCOMPLETE;
  156. },
  157. isStateError: function () {
  158. return this._state == this.ERROR;
  159. },
  160. isStateNone: function () {
  161. return this._state == this.NONE;
  162. },
  163. isStateRebootRecovery: function () {
  164. return this._state == this.REBOOT_TO_RECOVERY;
  165. },
  166. isStateSetUrl: function () {
  167. return this._state == this.SET_FWURL;
  168. },
  169. isStateFlashing: function () {
  170. return this._state == this.FLASHING;
  171. },
  172. isStateDone: function () {
  173. return this._state == this.DONE;
  174. },
  175. isStateUploading: function () {
  176. return this._state == this.UPLOADING;
  177. },
  178. init: function () {
  179. this._state = this.NONE;
  180. return this;
  181. },
  182. SetStateError: function () {
  183. this._state = this.ERROR;
  184. $('#fwProgressLabel').parent().addClass('bg-danger');
  185. return this;
  186. },
  187. SetStateNone: function () {
  188. this._state = this.NONE;
  189. return this;
  190. },
  191. SetStateRebootRecovery: function () {
  192. this._state = this.REBOOT_TO_RECOVERY;
  193. // Reboot system to recovery mode
  194. this.SetStatusText('Starting recovery mode.')
  195. $.ajax({
  196. url: '/recovery.json',
  197. context: this,
  198. dataType: 'text',
  199. method: 'POST',
  200. cache: false,
  201. contentType: 'application/json; charset=utf-8',
  202. data: JSON.stringify({
  203. timestamp: Date.now(),
  204. }),
  205. error: function (xhr, _ajaxOptions, thrownError) {
  206. this.setOTAError(`Unexpected error while trying to restart to recovery. (status=${xhr.status ?? ''}, error=${thrownError ?? ''} ) `);
  207. },
  208. complete: function (response) {
  209. this.SetStatusText('Waiting for system to boot.')
  210. },
  211. });
  212. return this;
  213. },
  214. SetStateSetUrl: function () {
  215. this._state = this.SET_FWURL;
  216. this.statusText = 'Sending firmware download location.';
  217. let confData = {
  218. fwurl: {
  219. value: this.flashURL,
  220. type: 33,
  221. }
  222. };
  223. post_config(confData);
  224. return this;
  225. },
  226. SetStateFlashing: function () {
  227. this._state = this.FLASHING;
  228. return this;
  229. },
  230. SetStateDone: function () {
  231. this._state = this.DONE;
  232. this.reset();
  233. return this;
  234. },
  235. SetStateUploading: function () {
  236. this._state = this.UPLOADING;
  237. return this.SetStatusText('Sending file to device.');
  238. },
  239. SetStateUploadComplete: function () {
  240. this._state = this.UPLOADCOMPLETE;
  241. return this;
  242. },
  243. isFlashExecuting: function () {
  244. return true === (this._state != this.UPLOADING && (this.statusText !== '' || this.statusPercent >= 0));
  245. },
  246. toString: function () {
  247. let keys = Object.keys(this);
  248. return keys.find(x => this[x] === this._state);
  249. },
  250. setOTATargets: function () {
  251. this.flashURL = '';
  252. this.flashFileName = '';
  253. this.flashURL = $('#fw-url-input').val();
  254. let fileInput = $('#flashfilename')[0].files;
  255. if (fileInput.length > 0) {
  256. this.flashFileName = fileInput[0];
  257. }
  258. if (this.flashFileName.length == 0 && this.flashURL.length == 0) {
  259. this.setOTAError('Invalid url or file. Cannot start OTA');
  260. }
  261. return this;
  262. },
  263. setOTAError: function (message) {
  264. this.SetStateError().SetStatusPercent(0).SetStatusText(message).reset();
  265. return this;
  266. },
  267. ShowDialog: function () {
  268. if (!this.isStateNone()) {
  269. this.updateModal.show();
  270. $('.flact').prop('disabled', true);
  271. }
  272. return this;
  273. },
  274. SetStatusPercent: function (pct) {
  275. var pctChanged = (this.statusPercent != pct);
  276. this.statusPercent = pct;
  277. if (pctChanged) {
  278. if (!this.isStateUploading() && !this.isStateFlashing()) {
  279. this.SetStateFlashing();
  280. }
  281. if (pct == 100) {
  282. if (this.isStateFlashing()) {
  283. this.SetStateDone();
  284. }
  285. else if (this.isStateUploading()) {
  286. this.statusPercent = 0;
  287. this.SetStateFlashing();
  288. }
  289. }
  290. this.UpdateProgress().ShowDialog();
  291. }
  292. return this;
  293. },
  294. SetStatusText: function (txt) {
  295. var changed = (this.statusText != txt);
  296. this.statusText = txt;
  297. if (changed) {
  298. $('span#flash-status').html(this.statusText);
  299. this.ShowDialog();
  300. }
  301. return this;
  302. },
  303. UpdateProgress: function () {
  304. $('.progress-bar')
  305. .css('width', this.statusPercent + '%')
  306. .attr('aria-valuenow', this.statusPercent)
  307. .text(this.statusPercent + '%')
  308. $('.progress-bar').html((this.isStateDone() ? 100 : this.statusPercent) + '%');
  309. return this;
  310. },
  311. StartOTA: function () {
  312. this.logEvent(this.StartOTA.name);
  313. $('#fwProgressLabel').parent().removeClass('bg-danger');
  314. this.setOTATargets();
  315. if (this.isStateError()) {
  316. return this;
  317. }
  318. if (!recovery) {
  319. this.SetStateRebootRecovery();
  320. }
  321. else {
  322. this.SetStateFlashing().TargetReadyStartOTA();
  323. }
  324. return this;
  325. },
  326. UploadLocalFile: function () {
  327. this.SetStateUploading();
  328. const xhttp = new XMLHttpRequest();
  329. xhttp.context = this;
  330. var boundHandleUploadProgressEvent = this.HandleUploadProgressEvent.bind(this);
  331. var boundsetOTAError=this.setOTAError.bind(this);
  332. xhttp.upload.addEventListener("progress",boundHandleUploadProgressEvent, false);
  333. xhttp.onreadystatechange = function () {
  334. if (xhttp.readyState === 4) {
  335. if (xhttp.status === 0 || xhttp.status === 404) {
  336. boundsetOTAError(`Upload Failed. Recovery version might not support uploading. Please use web update instead.`);
  337. }
  338. }
  339. };
  340. xhttp.open('POST', '/flash.json', true);
  341. xhttp.send(this.flashFileName);
  342. },
  343. TargetReadyStartOTA: function () {
  344. if (recovery && this.prevRecovery && !this.isStateRebootRecovery() && !this.isStateFlashing()) {
  345. // this should only execute once, while being in a valid state
  346. return this;
  347. }
  348. this.logEvent(this.TargetReadyStartOTA.name);
  349. if (!recovery) {
  350. console.error('Event TargetReadyStartOTA fired in the wrong mode ');
  351. return this;
  352. }
  353. this.prevRecovery = true;
  354. if (this.flashFileName !== '') {
  355. this.UploadLocalFile();
  356. }
  357. else if (this.flashURL != '') {
  358. this.SetStateSetUrl();
  359. }
  360. else {
  361. this.setOTAError('Invalid URL or file name while trying to start the OTa process')
  362. }
  363. },
  364. HandleUploadProgressEvent: function (data) {
  365. this.logEvent(this.HandleUploadProgressEvent.name);
  366. this.SetStateUploading().SetStatusPercent(Math.round(data.loaded / data.total * 100)).SetStatusText('Uploading file to device');
  367. },
  368. EventTargetStatus: function (data) {
  369. if (!this.isStateNone()) {
  370. this.logEvent(this.EventTargetStatus.name);
  371. }
  372. if (data.ota_pct ?? -1 >= 0) {
  373. this.olderRecovery = true;
  374. this.SetStatusPercent(data.ota_pct);
  375. }
  376. if ((data.ota_dsc ?? '') != '') {
  377. this.olderRecovery = true;
  378. this.SetStatusText(data.ota_dsc);
  379. }
  380. if (data.recovery != undefined) {
  381. this.recovery = data.recovery === 1 ? true : false;
  382. }
  383. if (this.isStateRebootRecovery() && this.recovery) {
  384. this.TargetReadyStartOTA();
  385. }
  386. },
  387. EventOTAMessageClass: function (data) {
  388. this.logEvent(this.EventOTAMessageClass.name);
  389. var otaData = JSON.parse(data);
  390. this.SetStatusPercent(otaData.ota_pct).SetStatusText(otaData.ota_dsc);
  391. },
  392. logEvent: function (fun) {
  393. console.log(`${fun}, flash state ${this.toString()}, recovery: ${this.recovery}, ota pct: ${this.statusPercent}, ota desc: ${this.statusText}`);
  394. }
  395. };
  396. window.hideSurrounding = function (obj) {
  397. $(obj).parent().parent().hide();
  398. }
  399. let presetsloaded = false;
  400. let is_i2c_locked = false;
  401. let statusInterval = 2000;
  402. let messageInterval = 2500;
  403. function post_config(data) {
  404. let confPayload = {
  405. timestamp: Date.now(),
  406. config: data
  407. };
  408. $.ajax({
  409. url: '/config.json',
  410. dataType: 'text',
  411. method: 'POST',
  412. cache: false,
  413. contentType: 'application/json; charset=utf-8',
  414. data: JSON.stringify(confPayload),
  415. error: handleExceptionResponse,
  416. });
  417. }
  418. window.hFlash = function () {
  419. // reset file upload selection if any;
  420. $('#flashfilename').value = null
  421. flashState.StartOTA();
  422. }
  423. window.handleReboot = function (link) {
  424. if (link == 'reboot_ota') {
  425. $('#reboot_ota_nav').removeClass('active').prop("disabled", true); delayReboot(500, '', 'reboot_ota');
  426. }
  427. else {
  428. $('#reboot_nav').removeClass('active'); delayReboot(500, '', link);
  429. }
  430. }
  431. function isConnected(){
  432. return ConnectedTo.ip && ConnectedTo.ip!='0.0.0.0';
  433. }
  434. function getIcon(icons){
  435. return isConnected()?icons.icon:icons.label;
  436. }
  437. function handlebtstate(data) {
  438. let icon = '';
  439. let tt = '';
  440. if (data.bt_status !== undefined && data.bt_sub_status !== undefined) {
  441. const iconindex = btStateIcons[data.bt_status].sub[data.bt_sub_status];
  442. if (iconindex) {
  443. icon = btIcons[iconindex];
  444. tt = btStateIcons[data.bt_status].desc;
  445. } else {
  446. icon = btIcons.bt_connected;
  447. tt = 'Output status';
  448. }
  449. }
  450. $('#o_type').attr('title', tt);
  451. $('#o_bt').html(isConnected()?icon.label:icon.text);
  452. }
  453. function handleTemplateTypeRadio(outtype) {
  454. $('#o_type').children('span').css({ display: 'none' });
  455. if (outtype === 'bt') {
  456. output = 'bt';
  457. } else if (outtype === 'spdif') {
  458. output = 'spdif';
  459. } else {
  460. output = 'i2s';
  461. }
  462. $('#' + output).prop('checked', true);
  463. $('#o_' + output).css({ display: 'inline' });
  464. }
  465. function handleExceptionResponse(xhr, _ajaxOptions, thrownError) {
  466. console.log(xhr.status);
  467. console.log(thrownError);
  468. if (thrownError !== '') {
  469. showLocalMessage(thrownError, 'MESSAGING_ERROR');
  470. }
  471. }
  472. function HideCmdMessage(cmdname) {
  473. $('#toast_' + cmdname)
  474. .removeClass('table-success')
  475. .removeClass('table-warning')
  476. .removeClass('table-danger')
  477. .addClass('table-success')
  478. .removeClass('show');
  479. $('#msg_' + cmdname).html('');
  480. }
  481. function showCmdMessage(cmdname, msgtype, msgtext, append = false) {
  482. let color = 'table-success';
  483. if (msgtype === 'MESSAGING_WARNING') {
  484. color = 'table-warning';
  485. } else if (msgtype === 'MESSAGING_ERROR') {
  486. color = 'table-danger';
  487. }
  488. $('#toast_' + cmdname)
  489. .removeClass('table-success')
  490. .removeClass('table-warning')
  491. .removeClass('table-danger')
  492. .addClass(color)
  493. .addClass('show');
  494. let escapedtext = msgtext
  495. .substring(0, msgtext.length - 1)
  496. .encodeHTML()
  497. .replace(/\n/g, '<br />');
  498. escapedtext =
  499. ($('#msg_' + cmdname).html().length > 0 && append
  500. ? $('#msg_' + cmdname).html() + '<br/>'
  501. : '') + escapedtext;
  502. $('#msg_' + cmdname).html(escapedtext);
  503. }
  504. let releaseURL =
  505. 'https://api.github.com/repos/sle118/squeezelite-esp32/releases';
  506. let recovery = false;
  507. let messagesHeld = false;
  508. const commandHeader = 'squeezelite -b 500:2000 -d all=info -C 30 -W';
  509. //let blockFlashButton = false;
  510. let apList = null;
  511. //let selectedSSID = '';
  512. //let checkStatusInterval = null;
  513. let messagecount = 0;
  514. let messageseverity = 'MESSAGING_INFO';
  515. let SystemConfig = {};
  516. let LastCommandsState = null;
  517. var output = '';
  518. let hostName = '';
  519. let versionName = 'Squeezelite-ESP32';
  520. let prevmessage = '';
  521. let project_name = versionName;
  522. let board_model = '';
  523. let platform_name = versionName;
  524. let preset_name = '';
  525. let btSinkNamesOptSel = '#cfg-audio-bt_source-sink_name';
  526. let ConnectedTo = {};
  527. let ConnectingToSSID = {};
  528. let lmsBaseUrl;
  529. let prevLMSIP = '';
  530. const ConnectingToActions = {
  531. 'CONN': 0, 'MAN': 1, 'STS': 2,
  532. }
  533. Promise.prototype.delay = function (duration) {
  534. return this.then(
  535. function (value) {
  536. return new Promise(function (resolve) {
  537. setTimeout(function () {
  538. resolve(value);
  539. }, duration);
  540. });
  541. },
  542. function (reason) {
  543. return new Promise(function (_resolve, reject) {
  544. setTimeout(function () {
  545. reject(reason);
  546. }, duration);
  547. });
  548. }
  549. );
  550. };
  551. function getConfigJson(slimMode) {
  552. const config = {};
  553. $('input.nvs').each(function (_index, entry) {
  554. if (!slimMode) {
  555. const nvsType = parseInt(entry.attributes.nvs_type.value, 10);
  556. if (entry.id !== '') {
  557. config[entry.id] = {};
  558. if (
  559. nvsType === nvsTypes.NVS_TYPE_U8 ||
  560. nvsType === nvsTypes.NVS_TYPE_I8 ||
  561. nvsType === nvsTypes.NVS_TYPE_U16 ||
  562. nvsType === nvsTypes.NVS_TYPE_I16 ||
  563. nvsType === nvsTypes.NVS_TYPE_U32 ||
  564. nvsType === nvsTypes.NVS_TYPE_I32 ||
  565. nvsType === nvsTypes.NVS_TYPE_U64 ||
  566. nvsType === nvsTypes.NVS_TYPE_I64
  567. ) {
  568. config[entry.id].value = parseInt(entry.value);
  569. } else {
  570. config[entry.id].value = entry.value;
  571. }
  572. config[entry.id].type = nvsType;
  573. }
  574. } else {
  575. config[entry.id] = entry.value;
  576. }
  577. });
  578. const key = $('#nvs-new-key').val();
  579. const val = $('#nvs-new-value').val();
  580. if (key !== '') {
  581. if (!slimMode) {
  582. config[key] = {};
  583. config[key].value = val;
  584. config[key].type = 33;
  585. } else {
  586. config[key] = val;
  587. }
  588. }
  589. return config;
  590. }
  591. function handleHWPreset(allfields, reboot) {
  592. const selJson = JSON.parse(allfields[0].value);
  593. var cmd = allfields[0].attributes.cmdname.value;
  594. console.log(`selected model: ${selJson.name}`);
  595. let confPayload = {
  596. timestamp: Date.now(),
  597. config: { model_config: { value: selJson.name, type: 33 } }
  598. };
  599. for (const [name, value] of Object.entries(selJson.config)) {
  600. const storedval = (typeof value === 'string' || value instanceof String) ? value : JSON.stringify(value);
  601. confPayload.config[name] = {
  602. value: storedval,
  603. type: 33,
  604. }
  605. showCmdMessage(
  606. cmd,
  607. 'MESSAGING_INFO',
  608. `Setting ${name}=${storedval} `,
  609. true
  610. );
  611. }
  612. showCmdMessage(
  613. cmd,
  614. 'MESSAGING_INFO',
  615. `Committing `,
  616. true
  617. );
  618. $.ajax({
  619. url: '/config.json',
  620. dataType: 'text',
  621. method: 'POST',
  622. cache: false,
  623. contentType: 'application/json; charset=utf-8',
  624. data: JSON.stringify(confPayload),
  625. error: function (xhr, _ajaxOptions, thrownError) {
  626. handleExceptionResponse(xhr, _ajaxOptions, thrownError);
  627. showCmdMessage(
  628. cmd,
  629. 'MESSAGING_ERROR',
  630. `Unexpected error ${(thrownError !== '') ? thrownError : 'with return status = ' + xhr.status} `,
  631. true
  632. );
  633. },
  634. success: function (response) {
  635. showCmdMessage(
  636. cmd,
  637. 'MESSAGING_INFO',
  638. `Saving complete `,
  639. true
  640. );
  641. console.log(response);
  642. if (reboot) {
  643. delayReboot(2500, cmd);
  644. }
  645. },
  646. });
  647. }
  648. // pull json file from https://gist.githubusercontent.com/sle118/dae585e157b733a639c12dc70f0910c5/raw/b462691f69e2ad31ac95c547af6ec97afb0f53db/squeezelite-esp32-presets.json and
  649. function loadPresets() {
  650. if ($("#cfg-hw-preset-model_config").length == 0) return;
  651. if (presetsloaded) return;
  652. presetsloaded = true;
  653. $('#cfg-hw-preset-model_config').html('<option>--</option>');
  654. $.getJSON(
  655. 'https://gist.githubusercontent.com/sle118/dae585e157b733a639c12dc70f0910c5/raw/',
  656. { _: new Date().getTime() },
  657. function (data) {
  658. $.each(data, function (key, val) {
  659. $('#cfg-hw-preset-model_config').append(`<option value='${JSON.stringify(val).replace(/"/g, '\"').replace(/\'/g, '\"')}'>${val.name}</option>`);
  660. if (preset_name !== '' && preset_name == val.name) {
  661. $('#cfg-hw-preset-model_config').val(preset_name);
  662. }
  663. });
  664. if (preset_name !== '') {
  665. ('#prev_preset').show().val(preset_name);
  666. }
  667. }
  668. ).fail(function (jqxhr, textStatus, error) {
  669. const err = textStatus + ', ' + error;
  670. console.log('Request Failed: ' + err);
  671. }
  672. );
  673. }
  674. function delayReboot(duration, cmdname, ota = 'reboot') {
  675. const url = '/' + ota + '.json';
  676. $('tbody#tasks').empty();
  677. $('#tasks_sect').css('visibility', 'collapse');
  678. Promise.resolve({ cmdname: cmdname, url: url })
  679. .delay(duration)
  680. .then(function (data) {
  681. if (data.cmdname.length > 0) {
  682. showCmdMessage(
  683. data.cmdname,
  684. 'MESSAGING_WARNING',
  685. 'System is rebooting.\n',
  686. true
  687. );
  688. } else {
  689. showLocalMessage('System is rebooting.\n', 'MESSAGING_WARNING');
  690. }
  691. console.log('now triggering reboot');
  692. $("button[onclick*='handleReboot']").addClass('rebooting');
  693. $.ajax({
  694. url: data.url,
  695. dataType: 'text',
  696. method: 'POST',
  697. cache: false,
  698. contentType: 'application/json; charset=utf-8',
  699. data: JSON.stringify({
  700. timestamp: Date.now(),
  701. }),
  702. error: handleExceptionResponse,
  703. complete: function () {
  704. console.log('reboot call completed');
  705. Promise.resolve(data)
  706. .delay(6000)
  707. .then(function (rdata) {
  708. if (rdata.cmdname.length > 0) {
  709. HideCmdMessage(rdata.cmdname);
  710. }
  711. getCommands();
  712. getConfig();
  713. });
  714. },
  715. });
  716. });
  717. }
  718. // eslint-disable-next-line no-unused-vars
  719. window.saveAutoexec1 = function (apply) {
  720. showCmdMessage('cfg-audio-tmpl', 'MESSAGING_INFO', 'Saving.\n', false);
  721. let commandLine = commandHeader + ' -n "' + $('#player').val() + '"';
  722. if (output === 'bt') {
  723. commandLine += ' -o "BT" -R -Z 192000';
  724. showCmdMessage(
  725. 'cfg-audio-tmpl',
  726. 'MESSAGING_INFO',
  727. 'Remember to configure the Bluetooth audio device name.\n',
  728. true
  729. );
  730. } else if (output === 'spdif') {
  731. commandLine += ' -o SPDIF -Z 192000';
  732. } else {
  733. commandLine += ' -o I2S';
  734. }
  735. if ($('#optional').val() !== '') {
  736. commandLine += ' ' + $('#optional').val();
  737. }
  738. const data = {
  739. timestamp: Date.now(),
  740. };
  741. data.config = {
  742. autoexec1: { value: commandLine, type: 33 },
  743. autoexec: {
  744. value: $('#disable-squeezelite').prop('checked') ? '0' : '1',
  745. type: 33,
  746. },
  747. };
  748. $.ajax({
  749. url: '/config.json',
  750. dataType: 'text',
  751. method: 'POST',
  752. cache: false,
  753. contentType: 'application/json; charset=utf-8',
  754. data: JSON.stringify(data),
  755. error: handleExceptionResponse,
  756. complete: function (response) {
  757. if (
  758. response.responseText &&
  759. JSON.parse(response.responseText).result === 'OK'
  760. ) {
  761. showCmdMessage('cfg-audio-tmpl', 'MESSAGING_INFO', 'Done.\n', true);
  762. if (apply) {
  763. delayReboot(1500, 'cfg-audio-tmpl');
  764. }
  765. } else if (JSON.parse(response.responseText).result) {
  766. showCmdMessage(
  767. 'cfg-audio-tmpl',
  768. 'MESSAGING_WARNING',
  769. JSON.parse(response.responseText).Result + '\n',
  770. true
  771. );
  772. } else {
  773. showCmdMessage(
  774. 'cfg-audio-tmpl',
  775. 'MESSAGING_ERROR',
  776. response.statusText + '\n'
  777. );
  778. }
  779. console.log(response.responseText);
  780. },
  781. });
  782. console.log('sent data:', JSON.stringify(data));
  783. }
  784. window.handleDisconnect = function () {
  785. $.ajax({
  786. url: '/connect.json',
  787. dataType: 'text',
  788. method: 'DELETE',
  789. cache: false,
  790. contentType: 'application/json; charset=utf-8',
  791. data: JSON.stringify({
  792. timestamp: Date.now(),
  793. }),
  794. });
  795. }
  796. function setPlatformFilter(val) {
  797. if ($('.upf').filter(function () { return $(this).text().toUpperCase() === val.toUpperCase() }).length > 0) {
  798. $('#splf').val(val).trigger('input');
  799. return true;
  800. }
  801. return false;
  802. }
  803. window.handleConnect = function () {
  804. ConnectingToSSID.ssid = $('#manual_ssid').val();
  805. ConnectingToSSID.pwd = $('#manual_pwd').val();
  806. ConnectingToSSID.dhcpname = $('#dhcp-name2').val();
  807. $("*[class*='connecting']").hide();
  808. $('#ssid-wait').text(ConnectingToSSID.ssid);
  809. $('.connecting').show();
  810. $.ajax({
  811. url: '/connect.json',
  812. dataType: 'text',
  813. method: 'POST',
  814. cache: false,
  815. contentType: 'application/json; charset=utf-8',
  816. data: JSON.stringify({
  817. timestamp: Date.now(),
  818. ssid: ConnectingToSSID.ssid,
  819. pwd: ConnectingToSSID.pwd
  820. }),
  821. error: handleExceptionResponse,
  822. });
  823. // now we can re-set the intervals regardless of result
  824. }
  825. $(document).ready(function () {
  826. $('.material-icons').each(function (_index, entry) {
  827. entry.attributes['icon']=entry.textContent;
  828. });
  829. setIcons(true);
  830. handleNVSVisible();
  831. flashState.init();
  832. $('#fw-url-input').on('input', function () {
  833. if ($(this).val().length > 8 && ($(this).val().startsWith('http://') || $(this).val().startsWith('https://'))) {
  834. $('#start-flash').show();
  835. }
  836. else {
  837. $('#start-flash').hide();
  838. }
  839. });
  840. $('.upSrch').on('input', function () {
  841. const val = this.value;
  842. $("#rTable tr").removeClass(this.id + '_hide');
  843. if (val.length > 0) {
  844. $(`#rTable td:nth-child(${$(this).parent().index() + 1})`).filter(function () {
  845. return !$(this).text().toUpperCase().includes(val.toUpperCase());
  846. }).parent().addClass(this.id + '_hide');
  847. }
  848. $('[class*="_hide"]').hide();
  849. $('#rTable tr').not('[class*="_hide"]').show()
  850. });
  851. setTimeout(refreshAP, 1500);
  852. $('#WifiConnectDialog')[0].addEventListener('shown.bs.modal', function (event) {
  853. $("*[class*='connecting']").hide();
  854. if (event?.relatedTarget) {
  855. ConnectingToSSID.Action = ConnectingToActions.CONN;
  856. if ($(event.relatedTarget).children('td:eq(1)').text() == ConnectedTo.ssid) {
  857. ConnectingToSSID.Action = ConnectingToActions.STS;
  858. }
  859. else {
  860. if (!$(event.relatedTarget).is(':last-child')) {
  861. ConnectingToSSID.ssid = $(event.relatedTarget).children('td:eq(1)').text();
  862. $('#manual_ssid').val(ConnectingToSSID.ssid);
  863. }
  864. else {
  865. ConnectingToSSID.Action = ConnectingToActions.MAN;
  866. ConnectingToSSID.ssid = '';
  867. $('#manual_ssid').val(ConnectingToSSID.ssid);
  868. }
  869. }
  870. }
  871. if (ConnectingToSSID.Action !== ConnectingToActions.STS) {
  872. $('.connecting-init').show();
  873. $('#manual_ssid').trigger('focus');
  874. }
  875. else {
  876. handleWifiDialog();
  877. }
  878. });
  879. $('#WifiConnectDialog')[0].addEventListener('hidden.bs.modal', function () {
  880. $('#WifiConnectDialog input').val('');
  881. });
  882. $('#uCnfrm')[0].addEventListener('shown.bs.modal', function () {
  883. $('#selectedFWURL').text($('#fw-url-input').val());
  884. });
  885. $('input#show-commands')[0].checked = LastCommandsState === 1;
  886. $('a[href^="#tab-commands"]').hide();
  887. $('#load-nvs').on('click', function () {
  888. $('#nvsfilename').trigger('click');
  889. });
  890. $('#nvsfilename').on('change', function () {
  891. if (typeof window.FileReader !== 'function') {
  892. throw "The file API isn't supported on this browser.";
  893. }
  894. if (!this.files) {
  895. throw 'This browser does not support the `files` property of the file input.';
  896. }
  897. if (!this.files[0]) {
  898. return undefined;
  899. }
  900. const file = this.files[0];
  901. let fr = new FileReader();
  902. fr.onload = function (e) {
  903. let data = {};
  904. try {
  905. data = JSON.parse(e.target.result);
  906. } catch (ex) {
  907. alert('Parsing failed!\r\n ' + ex);
  908. }
  909. $('input.nvs').each(function (_index, entry) {
  910. $(this).parent().removeClass('bg-warning').removeClass('bg-success');
  911. if (data[entry.id]) {
  912. if (data[entry.id] !== entry.value) {
  913. console.log(
  914. 'Changed ' + entry.id + ' ' + entry.value + '==>' + data[entry.id]
  915. );
  916. $(this).parent().addClass('bg-warning');
  917. $(this).val(data[entry.id]);
  918. }
  919. else {
  920. $(this).parent().addClass('bg-success');
  921. }
  922. }
  923. });
  924. var changed = $("input.nvs").children('.bg-warning');
  925. if (changed) {
  926. alert('Highlighted values were changed. Press Commit to change on the device');
  927. }
  928. }
  929. fr.readAsText(file);
  930. this.value = null;
  931. }
  932. );
  933. $('#clear-syslog').on('click', function () {
  934. messagecount = 0;
  935. messageseverity = 'MESSAGING_INFO';
  936. $('#msgcnt').text('');
  937. $('#syslogTable').html('');
  938. });
  939. $('#ok-credits').on('click', function () {
  940. $('#credits').slideUp('fast', function () { });
  941. $('#app').slideDown('fast', function () { });
  942. });
  943. $('#acredits').on('click', function (event) {
  944. event.preventDefault();
  945. $('#app').slideUp('fast', function () { });
  946. $('#credits').slideDown('fast', function () { });
  947. });
  948. $('input#show-commands').on('click', function () {
  949. this.checked = this.checked ? 1 : 0;
  950. if (this.checked) {
  951. $('a[href^="#tab-commands"]').show();
  952. LastCommandsState = 1;
  953. } else {
  954. LastCommandsState = 0;
  955. $('a[href^="#tab-commands"]').hide();
  956. }
  957. });
  958. $('input#show-nvs').on('click', function () {
  959. this.checked = this.checked ? 1 : 0;
  960. Cookies.set("show-nvs", this.checked?'Y':'N');
  961. handleNVSVisible();
  962. });
  963. $('#btn_reboot_recovery').on('click', function () {
  964. handleReboot('recovery');
  965. });
  966. $('#btn_reboot').on('click', function () {
  967. handleReboot('reboot');
  968. });
  969. $('#btn_flash').on('click', function () {
  970. hFlash();
  971. });
  972. $('#save-autoexec1').on('click', function () {
  973. saveAutoexec1(false);
  974. });
  975. $('#commit-autoexec1').on('click', function () {
  976. saveAutoexec1(true);
  977. });
  978. $('#btn_disconnect').on('click', function () {
  979. ConnectedTo={};
  980. refreshAPHTML2();
  981. $.ajax({
  982. url: '/connect.json',
  983. dataType: 'text',
  984. method: 'DELETE',
  985. cache: false,
  986. contentType: 'application/json; charset=utf-8',
  987. data: JSON.stringify({
  988. timestamp: Date.now(),
  989. }),
  990. });
  991. });
  992. $('#btnJoin').on('click', function () {
  993. handleConnect();
  994. });
  995. $('#reboot_nav').on('click', function () {
  996. handleReboot('reboot');
  997. });
  998. $('#reboot_ota_nav').on('click', function () {
  999. handleReboot('reboot_ota');
  1000. });
  1001. $('#save-as-nvs').on('click', function () {
  1002. const config = getConfigJson(true);
  1003. const a = document.createElement('a');
  1004. a.href = URL.createObjectURL(
  1005. new Blob([JSON.stringify(config, null, 2)], {
  1006. type: 'text/plain',
  1007. })
  1008. );
  1009. a.setAttribute(
  1010. 'download',
  1011. 'nvs_config_' + hostName + '_' + Date.now() + 'json'
  1012. );
  1013. document.body.appendChild(a);
  1014. a.click();
  1015. document.body.removeChild(a);
  1016. });
  1017. $('#save-nvs').on('click', function () {
  1018. post_config(getConfigJson(false));
  1019. });
  1020. $('#fwUpload').on('click', function () {
  1021. const fileInput = document.getElementById('flashfilename').files;
  1022. if (fileInput.length === 0) {
  1023. alert('No file selected!');
  1024. } else {
  1025. $('#fw-url-input').value = null;
  1026. flashState.StartOTA();
  1027. }
  1028. });
  1029. $('[name=output-tmpl]').on('click', function () {
  1030. handleTemplateTypeRadio(this.id);
  1031. });
  1032. $('#chkUpdates').on('click', function () {
  1033. $('#rTable').html('');
  1034. $.getJSON(releaseURL, function (data) {
  1035. let i = 0;
  1036. const branches = [];
  1037. data.forEach(function (release) {
  1038. const namecomponents = release.name.split('#');
  1039. const branch = namecomponents[3];
  1040. if (!branches.includes(branch)) {
  1041. branches.push(branch);
  1042. }
  1043. });
  1044. let fwb = '';
  1045. branches.forEach(function (branch) {
  1046. fwb += '<option value="' + branch + '">' + branch + '</option>';
  1047. });
  1048. $('#fwbranch').append(fwb);
  1049. data.forEach(function (release) {
  1050. let url = '';
  1051. release.assets.forEach(function (asset) {
  1052. if (asset.name.match(/\.bin$/)) {
  1053. url = asset.browser_download_url;
  1054. }
  1055. });
  1056. const namecomponents = release.name.split('#');
  1057. const ver = namecomponents[0];
  1058. const cfg = namecomponents[2];
  1059. const branch = namecomponents[3];
  1060. var bits = ver.substr(ver.lastIndexOf('-') + 1);
  1061. bits = (bits == '32' || bits == '16') ? bits : '';
  1062. let body = release.body;
  1063. body = body.replace(/'/gi, '"');
  1064. body = body.replace(
  1065. /[\s\S]+(### Revision Log[\s\S]+)### ESP-IDF Version Used[\s\S]+/,
  1066. '$1'
  1067. );
  1068. body = body.replace(/- \(.+?\) /g, '- ').encodeHTML();
  1069. $('#rTable').append(`<tr class='release ' fwurl='${url}'>
  1070. <td data-bs-toggle='tooltip' title='${body}'>${ver}</td><td>${new Date(release.created_at).toLocalShort()}
  1071. </td><td class='upf'>${cfg}</td><td>${branch}</td><td>${bits}</td></tr>`
  1072. );
  1073. });
  1074. if (i > 7) {
  1075. $('#releaseTable').append(
  1076. "<tr id='showall'>" +
  1077. "<td colspan='6'>" +
  1078. "<input type='button' id='showallbutton' class='btn btn-info' value='Show older releases' />" +
  1079. '</td>' +
  1080. '</tr>'
  1081. );
  1082. $('#showallbutton').on('click', function () {
  1083. $('tr.hide').removeClass('hide');
  1084. $('tr#showall').addClass('hide');
  1085. });
  1086. }
  1087. $('#searchfw').css('display', 'inline');
  1088. if (!setPlatformFilter(platform_name)) {
  1089. setPlatformFilter(project_name)
  1090. }
  1091. $('#rTable tr.release').on('click', function () {
  1092. var url = this.attributes['fwurl'].value;
  1093. if (lmsBaseUrl) {
  1094. url = url.replace(/.*\/download\//, lmsBaseUrl + '/plugins/SqueezeESP32/firmware/');
  1095. }
  1096. $('#fw-url-input').val(url);
  1097. $('#start-flash').show();
  1098. $('#rTable tr.release').removeClass('table-success table-warning');
  1099. $(this).addClass('table-success table-warning');
  1100. });
  1101. }).fail(function () {
  1102. alert('failed to fetch release history!');
  1103. });
  1104. });
  1105. $('#fwcheck').on('click', function () {
  1106. $('#releaseTable').html('');
  1107. $('#fwbranch').empty();
  1108. $.getJSON(releaseURL, function (data) {
  1109. let i = 0;
  1110. const branches = [];
  1111. data.forEach(function (release) {
  1112. const namecomponents = release.name.split('#');
  1113. const branch = namecomponents[3];
  1114. if (!branches.includes(branch)) {
  1115. branches.push(branch);
  1116. }
  1117. });
  1118. let fwb;
  1119. branches.forEach(function (branch) {
  1120. fwb += '<option value="' + branch + '">' + branch + '</option>';
  1121. });
  1122. $('#fwbranch').append(fwb);
  1123. data.forEach(function (release) {
  1124. let url = '';
  1125. release.assets.forEach(function (asset) {
  1126. if (asset.name.match(/\.bin$/)) {
  1127. url = asset.browser_download_url;
  1128. }
  1129. });
  1130. const namecomponents = release.name.split('#');
  1131. const ver = namecomponents[0];
  1132. const idf = namecomponents[1];
  1133. const cfg = namecomponents[2];
  1134. const branch = namecomponents[3];
  1135. let body = release.body;
  1136. body = body.replace(/'/gi, '"');
  1137. body = body.replace(
  1138. /[\s\S]+(### Revision Log[\s\S]+)### ESP-IDF Version Used[\s\S]+/,
  1139. '$1'
  1140. );
  1141. body = body.replace(/- \(.+?\) /g, '- ');
  1142. const trclass = i++ > 6 ? ' hide' : '';
  1143. $('#releaseTable').append(
  1144. "<tr class='release" +
  1145. trclass +
  1146. "'>" +
  1147. "<td data-bs-toggle='tooltip' title='" +
  1148. body +
  1149. "'>" +
  1150. ver +
  1151. '</td>' +
  1152. '<td>' +
  1153. new Date(release.created_at).toLocalShort() +
  1154. '</td>' +
  1155. '<td>' +
  1156. cfg +
  1157. '</td>' +
  1158. '<td>' +
  1159. idf +
  1160. '</td>' +
  1161. '<td>' +
  1162. branch +
  1163. '</td>' +
  1164. "<td><input type='button' class='btn btn-success' value='Select' data-bs-url='" +
  1165. url +
  1166. "' onclick='setURL(this);' /></td>" +
  1167. '</tr>'
  1168. );
  1169. });
  1170. if (i > 7) {
  1171. $('#releaseTable').append(
  1172. "<tr id='showall'>" +
  1173. "<td colspan='6'>" +
  1174. "<input type='button' id='showallbutton' class='btn btn-info' value='Show older releases' />" +
  1175. '</td>' +
  1176. '</tr>'
  1177. );
  1178. $('#showallbutton').on('click', function () {
  1179. $('tr.hide').removeClass('hide');
  1180. $('tr#showall').addClass('hide');
  1181. });
  1182. }
  1183. $('#searchfw').css('display', 'inline');
  1184. }).fail(function () {
  1185. alert('failed to fetch release history!');
  1186. });
  1187. });
  1188. $('#updateAP').on('click', function () {
  1189. refreshAP();
  1190. console.log('refresh AP');
  1191. });
  1192. // first time the page loads: attempt to get the connection status and start the wifi scan
  1193. getConfig();
  1194. getCommands();
  1195. getMessages();
  1196. checkStatus();
  1197. });
  1198. // eslint-disable-next-line no-unused-vars
  1199. window.setURL = function (button) {
  1200. let url = button.dataset.url;
  1201. $('[data-bs-url^="http"]')
  1202. .addClass('btn-success')
  1203. .removeClass('btn-danger');
  1204. $('[data-bs-url="' + url + '"]')
  1205. .addClass('btn-danger')
  1206. .removeClass('btn-success');
  1207. // if user can proxy download through LMS, modify the URL
  1208. if (lmsBaseUrl) {
  1209. url = url.replace(/.*\/download\//, lmsBaseUrl + '/plugins/SqueezeESP32/firmware/');
  1210. }
  1211. $('#fwurl').val(url);
  1212. }
  1213. function rssiToIcon(rssi) {
  1214. if (rssi >= -55) {
  1215. return {'label':'****','icon':`signal_wifi_statusbar_4_bar`};
  1216. } else if (rssi >= -60) {
  1217. return {'label':'***','icon':`network_wifi_3_bar`};
  1218. } else if (rssi >= -65) {
  1219. return {'label':'**','icon':`network_wifi_2_bar`};
  1220. } else if (rssi >= -70) {
  1221. return {'label':'*','icon':`network_wifi_1_bar`};
  1222. } else {
  1223. return {'label':'.','icon':`signal_wifi_statusbar_null`};
  1224. }
  1225. }
  1226. function refreshAP() {
  1227. if (ConnectedTo?.urc === connectReturnCode.ETH) return;
  1228. $.ajaxSetup({
  1229. timeout: 3000 //Time in milliseconds
  1230. });
  1231. $.getJSON('/scan.json', async function () {
  1232. await sleep(2000);
  1233. $.getJSON('/ap.json', function (data) {
  1234. if (data.length > 0) {
  1235. // sort by signal strength
  1236. data.sort(function (a, b) {
  1237. const x = a.rssi;
  1238. const y = b.rssi;
  1239. // eslint-disable-next-line no-nested-ternary
  1240. return x < y ? 1 : x > y ? -1 : 0;
  1241. });
  1242. apList = data;
  1243. refreshAPHTML2(apList);
  1244. }
  1245. });
  1246. });
  1247. }
  1248. function formatAP(ssid, rssi, auth) {
  1249. const rssi_icon=rssiToIcon(rssi);
  1250. const auth_icon={label:auth == 0 ? '🔓' : '🔒',icon:auth == 0 ? 'no_encryption' : 'lock'};
  1251. return `<tr data-bs-toggle="modal" data-bs-target="#WifiConnectDialog"><td></td><td>${ssid}</td><td>
  1252. <span class="material-icons" style="fill:white; display: inline" aria-label="${rssi_icon.label}" icon="${rssi_icon.icon}" >${getIcon(rssi_icon)}</span>
  1253. </td><td>
  1254. <span class="material-icons" aria-label="${auth_icon.label}" icon="${auth_icon.icon}">${getIcon(auth_icon)}</span>
  1255. </td></tr>`;
  1256. }
  1257. function refreshAPHTML2(data) {
  1258. let h = '';
  1259. $('#wifiTable tr td:first-of-type').text('');
  1260. $('#wifiTable tr').removeClass('table-success table-warning');
  1261. if (data) {
  1262. data.forEach(function (e) {
  1263. h += formatAP(e.ssid, e.rssi, e.auth);
  1264. });
  1265. $('#wifiTable').html(h);
  1266. }
  1267. if ($('.manual_add').length == 0) {
  1268. $('#wifiTable').append(formatAP('Manual add', 0, 0));
  1269. $('#wifiTable tr:last').addClass('table-light text-dark').addClass('manual_add');
  1270. }
  1271. if (ConnectedTo.ssid && (ConnectedTo.urc === connectReturnCode.OK || ConnectedTo.urc === connectReturnCode.RESTORE)) {
  1272. const wifiSelector = `#wifiTable td:contains("${ConnectedTo.ssid}")`;
  1273. if ($(wifiSelector).filter(function () { return $(this).text() === ConnectedTo.ssid; }).length == 0) {
  1274. $('#wifiTable').prepend(`${formatAP(ConnectedTo.ssid, ConnectedTo.rssi ?? 0, 0)}`);
  1275. }
  1276. $(wifiSelector).filter(function () { return $(this).text() === ConnectedTo.ssid; }).siblings().first().html('&check;').parent().addClass((ConnectedTo.urc === connectReturnCode.OK ? 'table-success' : 'table-warning'));
  1277. $('span#foot-if').html(`SSID: <strong>${ConnectedTo.ssid}</strong>, IP: <strong>${ConnectedTo.ip}</strong>`);
  1278. $('#wifiStsIcon').html(rssiToIcon(ConnectedTo.rssi));
  1279. }
  1280. else if (ConnectedTo?.urc !== connectReturnCode.ETH) {
  1281. $('span#foot-if').html('');
  1282. }
  1283. }
  1284. function refreshETH() {
  1285. if (ConnectedTo.urc === connectReturnCode.ETH) {
  1286. $('span#foot-if').html(`Network: Ethernet, IP: <strong>${ConnectedTo.ip}</strong>`);
  1287. }
  1288. }
  1289. function showTask(task) {
  1290. console.debug(
  1291. this.toLocaleString() +
  1292. '\t' +
  1293. task.nme +
  1294. '\t' +
  1295. task.cpu +
  1296. '\t' +
  1297. taskStates[task.st] +
  1298. '\t' +
  1299. task.minstk +
  1300. '\t' +
  1301. task.bprio +
  1302. '\t' +
  1303. task.cprio +
  1304. '\t' +
  1305. task.num
  1306. );
  1307. $('tbody#tasks').append(
  1308. '<tr class="table-primary"><th scope="row">' +
  1309. task.num +
  1310. '</th><td>' +
  1311. task.nme +
  1312. '</td><td>' +
  1313. task.cpu +
  1314. '</td><td>' +
  1315. taskStates[task.st] +
  1316. '</td><td>' +
  1317. task.minstk +
  1318. '</td><td>' +
  1319. task.bprio +
  1320. '</td><td>' +
  1321. task.cprio +
  1322. '</td></tr>'
  1323. );
  1324. }
  1325. function btExists(name) {
  1326. return getBTSinkOpt(name).length > 0;
  1327. }
  1328. function getBTSinkOpt(name) {
  1329. return $(`${btSinkNamesOptSel} option:contains('${name}')`);
  1330. }
  1331. function getMessages() {
  1332. $.ajaxSetup({
  1333. timeout: messageInterval //Time in milliseconds
  1334. });
  1335. $.getJSON('/messages.json', async function (data) {
  1336. for (const msg of data) {
  1337. const msgAge = msg.current_time - msg.sent_time;
  1338. var msgTime = new Date();
  1339. msgTime.setTime(msgTime.getTime() - msgAge);
  1340. switch (msg.class) {
  1341. case 'MESSAGING_CLASS_OTA':
  1342. flashState.EventOTAMessageClass(msg.message);
  1343. break;
  1344. case 'MESSAGING_CLASS_STATS':
  1345. // for task states, check structure : task_state_t
  1346. var statsData = JSON.parse(msg.message);
  1347. console.debug(
  1348. msgTime.toLocalShort() +
  1349. ' - Number of running tasks: ' +
  1350. statsData.ntasks
  1351. );
  1352. console.debug(
  1353. msgTime.toLocalShort() +
  1354. '\tname' +
  1355. '\tcpu' +
  1356. '\tstate' +
  1357. '\tminstk' +
  1358. '\tbprio' +
  1359. '\tcprio' +
  1360. '\tnum'
  1361. );
  1362. if (statsData.tasks) {
  1363. if ($('#tasks_sect').css('visibility') === 'collapse') {
  1364. $('#tasks_sect').css('visibility', 'visible');
  1365. }
  1366. $('tbody#tasks').html('');
  1367. statsData.tasks
  1368. .sort(function (a, b) {
  1369. return b.cpu - a.cpu;
  1370. })
  1371. .forEach(showTask, msgTime);
  1372. } else if ($('#tasks_sect').css('visibility') === 'visible') {
  1373. $('tbody#tasks').empty();
  1374. $('#tasks_sect').css('visibility', 'collapse');
  1375. }
  1376. break;
  1377. case 'MESSAGING_CLASS_SYSTEM':
  1378. showMessage(msg, msgTime);
  1379. break;
  1380. case 'MESSAGING_CLASS_CFGCMD':
  1381. var msgparts = msg.message.split(/([^\n]*)\n(.*)/gs);
  1382. showCmdMessage(msgparts[1], msg.type, msgparts[2], true);
  1383. break;
  1384. case 'MESSAGING_CLASS_BT':
  1385. if ($("#cfg-audio-bt_source-sink_name").is('input')) {
  1386. var attr = $("#cfg-audio-bt_source-sink_name")[0].attributes;
  1387. var attrs = '';
  1388. for (var j = 0; j < attr.length; j++) {
  1389. if (attr.item(j).name != "type") {
  1390. attrs += `${attr.item(j).name} = "${attr.item(j).value}" `;
  1391. }
  1392. }
  1393. var curOpt = $("#cfg-audio-bt_source-sink_name")[0].value;
  1394. $("#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> `);
  1395. }
  1396. JSON.parse(msg.message).forEach(function (btEntry) {
  1397. //<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">
  1398. //<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>
  1399. if (!btExists(btEntry.name)) {
  1400. $("#cfg-audio-bt_source-sink_name").append(`<option>${btEntry.name}</option>`);
  1401. showMessage({ type: msg.type, message: `BT Audio device found: ${btEntry.name} RSSI: ${btEntry.rssi} ` }, msgTime);
  1402. }
  1403. getBTSinkOpt(btEntry.name).attr('data-bs-description', `${btEntry.name} (${btEntry.rssi}dB)`)
  1404. .attr('rssi', btEntry.rssi)
  1405. .attr('value', btEntry.name)
  1406. .text(`${btEntry.name} [${btEntry.rssi}dB]`).trigger('change');
  1407. });
  1408. $(btSinkNamesOptSel).append($(`${btSinkNamesOptSel} option`).remove().sort(function (a, b) {
  1409. console.log(`${parseInt($(a).attr('rssi'))} < ${parseInt($(b).attr('rssi'))} ? `);
  1410. return parseInt($(a).attr('rssi')) < parseInt($(b).attr('rssi')) ? 1 : -1;
  1411. }));
  1412. break;
  1413. default:
  1414. break;
  1415. }
  1416. }
  1417. setTimeout(getMessages,messageInterval);
  1418. }).fail(function (xhr, ajaxOptions, thrownError) {
  1419. if (xhr.status == 404) {
  1420. $('.orec').hide(); // system commands won't be available either
  1421. messagesHeld = true;
  1422. }
  1423. else {
  1424. handleExceptionResponse(xhr, ajaxOptions, thrownError);
  1425. }
  1426. if(xhr.status == 0 && xhr.readyState ==0){
  1427. // probably a timeout. Target is rebooting?
  1428. setTimeout(getMessages,messageInterval*2); // increase duration if a failure happens
  1429. }
  1430. else if(!messagesHeld){
  1431. // 404 here means we rebooted to an old recovery
  1432. setTimeout(getMessages,messageInterval); // increase duration if a failure happens
  1433. }
  1434. }
  1435. );
  1436. /*
  1437. Minstk is minimum stack space left
  1438. Bprio is base priority
  1439. cprio is current priority
  1440. nme is name
  1441. st is task state. I provided a "typedef" that you can use to convert to text
  1442. cpu is cpu percent used
  1443. */
  1444. }
  1445. function handleRecoveryMode(data) {
  1446. const locRecovery = data.recovery ?? 0;
  1447. if (locRecovery === 1) {
  1448. recovery = true;
  1449. $('.recovery_element').show();
  1450. $('.ota_element').hide();
  1451. $('#boot-button').html('Reboot');
  1452. $('#boot-form').attr('action', '/reboot_ota.json');
  1453. } else {
  1454. if(!recovery && messagesHeld){
  1455. messagesHeld=false;
  1456. setTimeout(getMessages,messageInterval); // increase duration if a failure happens
  1457. }
  1458. recovery = false;
  1459. $('.recovery_element').hide();
  1460. $('.ota_element').show();
  1461. $('#boot-button').html('Recovery');
  1462. $('#boot-form').attr('action', '/recovery.json');
  1463. }
  1464. }
  1465. function hasConnectionChanged(data) {
  1466. // gw: "192.168.10.1"
  1467. // ip: "192.168.10.225"
  1468. // netmask: "255.255.255.0"
  1469. // ssid: "MyTestSSID"
  1470. return (data.urc !== ConnectedTo.urc ||
  1471. data.ssid !== ConnectedTo.ssid ||
  1472. data.gw !== ConnectedTo.gw ||
  1473. data.netmask !== ConnectedTo.netmask ||
  1474. data.ip !== ConnectedTo.ip || data.rssi !== ConnectedTo.rssi)
  1475. }
  1476. function handleWifiDialog(data) {
  1477. if ($('#WifiConnectDialog').is(':visible')) {
  1478. if (ConnectedTo.ip) {
  1479. $('#ipAddress').text(ConnectedTo.ip);
  1480. }
  1481. if (ConnectedTo.ssid) {
  1482. $('#connectedToSSID').text(ConnectedTo.ssid);
  1483. }
  1484. if (ConnectedTo.gw) {
  1485. $('#gateway').text(ConnectedTo.gw);
  1486. }
  1487. if (ConnectedTo.netmask) {
  1488. $('#netmask').text(ConnectedTo.netmask);
  1489. }
  1490. if (ConnectingToSSID.Action === undefined || (ConnectingToSSID.Action && ConnectingToSSID.Action == ConnectingToActions.STS)) {
  1491. $("*[class*='connecting']").hide();
  1492. $('.connecting-status').show();
  1493. }
  1494. if (SystemConfig.ap_ssid) {
  1495. $('#apName').text(SystemConfig.ap_ssid.value);
  1496. }
  1497. if (SystemConfig.ap_pwd) {
  1498. $('#apPass').text(SystemConfig.ap_pwd.value);
  1499. }
  1500. if (!data) {
  1501. return;
  1502. }
  1503. else {
  1504. switch (data.urc) {
  1505. case connectReturnCode.OK:
  1506. if (data.ssid && data.ssid === ConnectingToSSID.ssid) {
  1507. $("*[class*='connecting']").hide();
  1508. $('.connecting-success').show();
  1509. ConnectingToSSID.Action = ConnectingToActions.STS;
  1510. }
  1511. break;
  1512. case connectReturnCode.FAIL:
  1513. //
  1514. if (ConnectingToSSID.Action != ConnectingToActions.STS && ConnectingToSSID.ssid == data.ssid) {
  1515. $("*[class*='connecting']").hide();
  1516. $('.connecting-fail').show();
  1517. }
  1518. break;
  1519. case connectReturnCode.LOST:
  1520. break;
  1521. case connectReturnCode.RESTORE:
  1522. if (ConnectingToSSID.Action != ConnectingToActions.STS && ConnectingToSSID.ssid != data.ssid) {
  1523. $("*[class*='connecting']").hide();
  1524. $('.connecting-fail').show();
  1525. }
  1526. break;
  1527. case connectReturnCode.DISC:
  1528. // that's a manual disconnect
  1529. // if ($('#wifi-status').is(':visible')) {
  1530. // $('#wifi-status').slideUp('fast', function() {});
  1531. // $('span#foot-wifi').html('');
  1532. // }
  1533. break;
  1534. default:
  1535. break;
  1536. }
  1537. }
  1538. }
  1539. }
  1540. function setIcons(offline){
  1541. $('.material-icons').each(function (_index, entry) {
  1542. entry.textContent = entry.attributes[offline?'aria-label':'icon'].value;
  1543. });
  1544. }
  1545. function handleNetworkStatus(data) {
  1546. setIcons(data.ssid==='');
  1547. if (hasConnectionChanged(data) || !data.urc) {
  1548. ConnectedTo = data;
  1549. $(".if_eth").hide();
  1550. $('.if_wifi').hide();
  1551. if (!data.urc || ConnectedTo.urc != connectReturnCode.ETH) {
  1552. $('.if_wifi').show();
  1553. refreshAPHTML2();
  1554. }
  1555. else {
  1556. $(".if_eth").show();
  1557. refreshETH();
  1558. }
  1559. }
  1560. handleWifiDialog(data);
  1561. }
  1562. function batteryToIcon(voltage) {
  1563. /* Assuming Li-ion 18650s as a power source, 3.9V per cell, or above is treated
  1564. as full charge (>75% of capacity). 3.4V is empty. The gauge is loosely
  1565. following the graph here:
  1566. https://learn.adafruit.com/li-ion-and-lipoly-batteries/voltages
  1567. using the 0.2C discharge profile for the rest of the values.
  1568. */
  1569. for (const iconEntry of batIcons) {
  1570. for (const entryRanges of iconEntry.ranges) {
  1571. if (inRange(voltage, entryRanges.f, entryRanges.t)) {
  1572. return { label: iconEntry.label, icon:iconEntry.icon};
  1573. }
  1574. }
  1575. }
  1576. return {label:'▪▪▪▪',icon:"battery_full"};
  1577. }
  1578. function checkStatus() {
  1579. $.ajaxSetup({
  1580. timeout: statusInterval //Time in milliseconds
  1581. });
  1582. $.getJSON('/status.json', function (data) {
  1583. handleRecoveryMode(data);
  1584. handleNVSVisible();
  1585. handleNetworkStatus(data);
  1586. handlebtstate(data);
  1587. flashState.EventTargetStatus(data);
  1588. if (data.project_name && data.project_name !== '') {
  1589. project_name = data.project_name;
  1590. }
  1591. if (data.platform_name && data.platform_name !== '') {
  1592. platform_name = data.platform_name;
  1593. }
  1594. if (board_model === '') board_model = project_name;
  1595. if (board_model === '') board_model = 'Squeezelite-ESP32';
  1596. if (data.version && data.version !== '') {
  1597. versionName = data.version;
  1598. $("#navtitle").html(`${board_model}${recovery ? '<br>[recovery]' : ''}`);
  1599. $('span#foot-fw').html(`fw: <strong>${versionName}</strong>, mode: <strong>${recovery ? "Recovery" : project_name}</strong>`);
  1600. } else {
  1601. $('span#flash-status').html('');
  1602. }
  1603. if (data.Voltage) {
  1604. const bat_icon=batteryToIcon(data.Voltage);
  1605. $('#battery').html(`${getIcon(bat_icon)}`);
  1606. $('#battery').attr("aria-label",bat_icon.label);
  1607. $('#battery').attr("icon",bat_icon.icon);
  1608. $('#battery').show();
  1609. } else {
  1610. $('#battery').hide();
  1611. }
  1612. if ((data.message ?? '') != '' && prevmessage != data.message) {
  1613. // supporting older recovery firmwares - messages will come from the status.json structure
  1614. prevmessage = data.message;
  1615. showLocalMessage(data.message, 'MESSAGING_INFO')
  1616. }
  1617. is_i2c_locked = data.is_i2c_locked;
  1618. if (is_i2c_locked) {
  1619. $('flds-cfg-hw-preset').hide();
  1620. }
  1621. else {
  1622. $('flds-cfg-hw-preset').show();
  1623. }
  1624. $("button[onclick*='handleReboot']").removeClass('rebooting');
  1625. if (typeof lmsBaseUrl == "undefined" || data.lms_ip != prevLMSIP && data.lms_ip && data.lms_port) {
  1626. const baseUrl = 'http://' + data.lms_ip + ':' + data.lms_port;
  1627. prevLMSIP = data.lms_ip;
  1628. $.ajax({
  1629. url: baseUrl + '/plugins/SqueezeESP32/firmware/-check.bin',
  1630. type: 'HEAD',
  1631. dataType: 'text',
  1632. cache: false,
  1633. error: function () {
  1634. // define the value, so we don't check it any more.
  1635. lmsBaseUrl = '';
  1636. },
  1637. success: function () {
  1638. lmsBaseUrl = baseUrl;
  1639. }
  1640. });
  1641. }
  1642. $('#o_jack').css({ display: Number(data.Jack) ? 'inline' : 'none' });
  1643. setTimeout(checkStatus,statusInterval);
  1644. }).fail(function (xhr, ajaxOptions, thrownError) {
  1645. handleExceptionResponse(xhr, ajaxOptions, thrownError);
  1646. if(xhr.status == 0 && xhr.readyState ==0){
  1647. // probably a timeout. Target is rebooting?
  1648. setTimeout(checkStatus,messageInterval*2); // increase duration if a failure happens
  1649. }
  1650. else {
  1651. setTimeout(checkStatus,messageInterval); // increase duration if a failure happens
  1652. }
  1653. });
  1654. }
  1655. // eslint-disable-next-line no-unused-vars
  1656. window.runCommand = function (button, reboot) {
  1657. let cmdstring = button.attributes.cmdname.value;
  1658. showCmdMessage(
  1659. button.attributes.cmdname.value,
  1660. 'MESSAGING_INFO',
  1661. 'Executing.',
  1662. false
  1663. );
  1664. const fields = document.getElementById('flds-' + cmdstring);
  1665. const allfields = fields?.querySelectorAll('select,input');
  1666. if (cmdstring === 'cfg-hw-preset') return handleHWPreset(allfields, reboot);
  1667. cmdstring += ' ';
  1668. if (fields) {
  1669. for (const field of allfields) {
  1670. let qts = '';
  1671. let opt = '';
  1672. let attr = field.attributes;
  1673. let isSelect = $(field).is('select');
  1674. const hasValue = attr?.hasvalue?.value === 'true';
  1675. const validVal = (isSelect && field.value !== '--') || (!isSelect && field.value !== '');
  1676. if (!hasValue || hasValue && validVal) {
  1677. if (attr?.longopts?.value !== 'undefined') {
  1678. opt += '--' + attr?.longopts?.value;
  1679. } else if (attr?.shortopts?.value !== 'undefined') {
  1680. opt = '-' + attr.shortopts.value;
  1681. }
  1682. if (attr?.hasvalue?.value === 'true') {
  1683. if (attr?.value !== '') {
  1684. qts = /\s/.test(field.value) ? '"' : '';
  1685. cmdstring += opt + ' ' + qts + field.value + qts + ' ';
  1686. }
  1687. } else {
  1688. // this is a checkbox
  1689. if (field?.checked) {
  1690. cmdstring += opt + ' ';
  1691. }
  1692. }
  1693. }
  1694. }
  1695. }
  1696. console.log(cmdstring);
  1697. const data = {
  1698. timestamp: Date.now(),
  1699. };
  1700. data.command = cmdstring;
  1701. $.ajax({
  1702. url: '/commands.json',
  1703. dataType: 'text',
  1704. method: 'POST',
  1705. cache: false,
  1706. contentType: 'application/json; charset=utf-8',
  1707. data: JSON.stringify(data),
  1708. error: function (xhr, _ajaxOptions, thrownError) {
  1709. var cmd = JSON.parse(this.data).command;
  1710. if (xhr.status == 404) {
  1711. showCmdMessage(
  1712. cmd.substr(0, cmd.indexOf(' ')),
  1713. 'MESSAGING_ERROR',
  1714. `${recovery ? 'Limited recovery mode active. Unsupported action ' : 'Unexpected error while processing command'}`,
  1715. true
  1716. );
  1717. }
  1718. else {
  1719. handleExceptionResponse(xhr, _ajaxOptions, thrownError);
  1720. showCmdMessage(
  1721. cmd.substr(0, cmd.indexOf(' ') - 1),
  1722. 'MESSAGING_ERROR',
  1723. `Unexpected error ${(thrownError !== '') ? thrownError : 'with return status = ' + xhr.status}`,
  1724. true
  1725. );
  1726. }
  1727. },
  1728. success: function (response) {
  1729. $('.orec').show();
  1730. console.log(response);
  1731. if (
  1732. JSON.parse(response).Result === 'Success' &&
  1733. reboot
  1734. ) {
  1735. delayReboot(2500, button.attributes.cmdname.value);
  1736. }
  1737. },
  1738. });
  1739. }
  1740. function getLongOps(data, name, longopts) {
  1741. return data.values[name] !== undefined ? data.values[name][longopts] : "";
  1742. }
  1743. function getCommands() {
  1744. $.ajaxSetup({
  1745. timeout: 7000 //Time in milliseconds
  1746. });
  1747. $.getJSON('/commands.json', function (data) {
  1748. console.log(data);
  1749. $('.orec').show();
  1750. data.commands.forEach(function (command) {
  1751. if ($('#flds-' + command.name).length === 0) {
  1752. const cmdParts = command.name.split('-');
  1753. const isConfig = cmdParts[0] === 'cfg';
  1754. const targetDiv = '#tab-' + cmdParts[0] + '-' + cmdParts[1];
  1755. let innerhtml = '';
  1756. 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}">`;
  1757. if (command.argtable) {
  1758. command.argtable.forEach(function (arg) {
  1759. let placeholder = arg.datatype || '';
  1760. const ctrlname = command.name + '-' + arg.longopts;
  1761. const curvalue = getLongOps(data, command.name, arg.longopts);
  1762. let attributes = 'hasvalue=' + arg.hasvalue + ' ';
  1763. attributes += 'longopts="' + arg.longopts + '" ';
  1764. attributes += 'shortopts="' + arg.shortopts + '" ';
  1765. attributes += 'checkbox=' + arg.checkbox + ' ';
  1766. attributes += 'cmdname="' + command.name + '" ';
  1767. attributes +=
  1768. 'id="' +
  1769. ctrlname +
  1770. '" name="' +
  1771. ctrlname +
  1772. '" hasvalue="' +
  1773. arg.hasvalue +
  1774. '" ';
  1775. let extraclass = arg.mincount > 0 ? 'bg-success' : '';
  1776. if (arg.glossary === 'hidden') {
  1777. attributes += ' style="visibility: hidden;"';
  1778. }
  1779. if (arg.checkbox) {
  1780. innerhtml += `<div class="form-check"><label class="form-check-label"><input type="checkbox" ${attributes} class="form-check-input ${extraclass}" value="" >${arg.glossary.encodeHTML()}</label>`;
  1781. } else {
  1782. innerhtml += `<div class="form-group" ><label for="${ctrlname}">${arg.glossary.encodeHTML()}</label>`;
  1783. if (placeholder.includes('|')) {
  1784. extraclass = placeholder.startsWith('+') ? ' multiple ' : '';
  1785. placeholder = placeholder
  1786. .replace('<', '')
  1787. .replace('=', '')
  1788. .replace('>', '');
  1789. innerhtml += `<select ${attributes} class="form-control ${extraclass}" >`;
  1790. placeholder = '--|' + placeholder;
  1791. placeholder.split('|').forEach(function (choice) {
  1792. innerhtml += '<option >' + choice + '</option>';
  1793. });
  1794. innerhtml += '</select>';
  1795. } else {
  1796. innerhtml += `<input type="text" class="form-control ${extraclass}" placeholder="${placeholder}" ${attributes}>`;
  1797. }
  1798. }
  1799. innerhtml += `${arg.checkbox ? '</div>' : ''}<small class="form-text text-muted">Previous value: ${arg.checkbox ? (curvalue ? 'Checked' : 'Unchecked') : (curvalue || '')}</small>${arg.checkbox ? '' : '</div>'}`;
  1800. });
  1801. }
  1802. innerhtml += `<div style="margin-top: 16px;">
  1803. <div class="toast hide" role="alert" aria-live="assertive" aria-atomic="true" id="toast_${command.name}">
  1804. <div class="toast-header">
  1805. <strong class="mr-auto">Result</strong
  1806. <button type="button" class="btn-close" data-bs-dismiss="toast" aria-label="Close"></button>
  1807. </div>
  1808. <div class="toast-body" id="msg_${command.name}"></div>
  1809. </div>`;
  1810. if (isConfig) {
  1811. innerhtml +=
  1812. `<button type="submit" class="btn btn-info sclk" id="btn-save-${command.name}" cmdname="${command.name}">Save</button>
  1813. <button type="submit" class="btn btn-warning cclk" id="btn-commit-${command.name}" cmdname="${command.name}">Apply</button>`;
  1814. } else {
  1815. innerhtml += `<button type="submit" class="btn btn-success sclk" id="btn-run-${command.name}" cmdname="${command.name}">Execute</button>`;
  1816. }
  1817. innerhtml += '</div></fieldset></div></div>';
  1818. if (isConfig) {
  1819. $(targetDiv).append(innerhtml);
  1820. } else {
  1821. $('#commands-list').append(innerhtml);
  1822. }
  1823. }
  1824. });
  1825. $(".sclk").off('click').on('click', function () { runCommand(this, false); });
  1826. $(".cclk").off('click').on('click', function () { runCommand(this, true); });
  1827. data.commands.forEach(function (command) {
  1828. $('[cmdname=' + command.name + ']:input').val('');
  1829. $('[cmdname=' + command.name + ']:checkbox').prop('checked', false);
  1830. if (command.argtable) {
  1831. command.argtable.forEach(function (arg) {
  1832. const ctrlselector = '#' + command.name + '-' + arg.longopts;
  1833. const ctrlValue = getLongOps(data, command.name, arg.longopts);
  1834. if (arg.checkbox) {
  1835. $(ctrlselector)[0].checked = ctrlValue;
  1836. } else {
  1837. if (ctrlValue !== undefined) {
  1838. $(ctrlselector)
  1839. .val(ctrlValue)
  1840. .trigger('change');
  1841. }
  1842. if (
  1843. $(ctrlselector)[0].value.length === 0 &&
  1844. (arg.datatype || '').includes('|')
  1845. ) {
  1846. $(ctrlselector)[0].value = '--';
  1847. }
  1848. }
  1849. });
  1850. }
  1851. });
  1852. loadPresets();
  1853. }).fail(function (xhr, ajaxOptions, thrownError) {
  1854. if (xhr.status == 404) {
  1855. $('.orec').hide();
  1856. }
  1857. else {
  1858. handleExceptionResponse(xhr, ajaxOptions, thrownError);
  1859. }
  1860. $('#commands-list').empty();
  1861. });
  1862. }
  1863. function getConfig() {
  1864. $.ajaxSetup({
  1865. timeout: 7000 //Time in milliseconds
  1866. });
  1867. $.getJSON('/config.json', function (entries) {
  1868. $('#nvsTable tr').remove();
  1869. const data = (entries.config ? entries.config : entries);
  1870. SystemConfig = data;
  1871. Object.keys(data)
  1872. .sort()
  1873. .forEach(function (key) {
  1874. let val = data[key].value;
  1875. if (key === 'autoexec') {
  1876. if (data.autoexec.value === '0') {
  1877. $('#disable-squeezelite')[0].checked = true;
  1878. } else {
  1879. $('#disable-squeezelite')[0].checked = false;
  1880. }
  1881. } else if (key === 'autoexec1') {
  1882. const re = /-o\s?(["][^"]*["]|[^-]+)/g;
  1883. const m = re.exec(val);
  1884. if (m[1].toUpperCase().startsWith('I2S')) {
  1885. handleTemplateTypeRadio('i2s');
  1886. } else if (m[1].toUpperCase().startsWith('SPDIF')) {
  1887. handleTemplateTypeRadio('spdif');
  1888. } else if (m[1].toUpperCase().startsWith('"BT')) {
  1889. handleTemplateTypeRadio('bt');
  1890. }
  1891. } else if (key === 'host_name') {
  1892. val = val.replaceAll('"', '');
  1893. $('input#dhcp-name1').val(val);
  1894. $('input#dhcp-name2').val(val);
  1895. $('#player').val(val);
  1896. document.title = val;
  1897. hostName = val;
  1898. } else if (key === 'rel_api') {
  1899. releaseURL = val;
  1900. }
  1901. else if (key === 'enable_airplay') {
  1902. $("#s_airplay").css({ display: isEnabled(val) ? 'inline' : 'none' })
  1903. }
  1904. else if (key === 'enable_cspot') {
  1905. $("#s_cspot").css({ display: isEnabled(val) ? 'inline' : 'none' })
  1906. }
  1907. else if (key == 'preset_name') {
  1908. preset_name = val;
  1909. }
  1910. else if (key == 'board_model') {
  1911. board_model = val;
  1912. }
  1913. $('tbody#nvsTable').append(
  1914. '<tr>' +
  1915. '<td>' +
  1916. key +
  1917. '</td>' +
  1918. "<td class='value'>" +
  1919. "<input type='text' class='form-control nvs' id='" +
  1920. key +
  1921. "' nvs_type=" +
  1922. data[key].type +
  1923. ' >' +
  1924. '</td>' +
  1925. '</tr>'
  1926. );
  1927. $('input#' + key).val(data[key].value);
  1928. });
  1929. $('tbody#nvsTable').append(
  1930. "<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>"
  1931. );
  1932. if (entries.gpio) {
  1933. $('#pins').show();
  1934. $('tbody#gpiotable tr').remove();
  1935. entries.gpio.forEach(function (gpioEntry) {
  1936. $('tbody#gpiotable').append(
  1937. '<tr class=' +
  1938. (gpioEntry.fixed ? 'table-secondary' : 'table-primary') +
  1939. '><th scope="row">' +
  1940. gpioEntry.group +
  1941. '</th><td>' +
  1942. gpioEntry.name +
  1943. '</td><td>' +
  1944. gpioEntry.gpio +
  1945. '</td><td>' +
  1946. (gpioEntry.fixed ? 'Fixed' : 'Configuration') +
  1947. '</td></tr>'
  1948. );
  1949. });
  1950. }
  1951. else {
  1952. $('#pins').hide();
  1953. }
  1954. }).fail(function (xhr, ajaxOptions, thrownError) {
  1955. handleExceptionResponse(xhr, ajaxOptions, thrownError);
  1956. });
  1957. }
  1958. function showLocalMessage(message, severity) {
  1959. const msg = {
  1960. message: message,
  1961. type: severity,
  1962. };
  1963. showMessage(msg, new Date());
  1964. }
  1965. function showMessage(msg, msgTime) {
  1966. let color = 'table-success';
  1967. if (msg.type === 'MESSAGING_WARNING') {
  1968. color = 'table-warning';
  1969. if (messageseverity === 'MESSAGING_INFO') {
  1970. messageseverity = 'MESSAGING_WARNING';
  1971. }
  1972. } else if (msg.type === 'MESSAGING_ERROR') {
  1973. if (
  1974. messageseverity === 'MESSAGING_INFO' ||
  1975. messageseverity === 'MESSAGING_WARNING'
  1976. ) {
  1977. messageseverity = 'MESSAGING_ERROR';
  1978. }
  1979. color = 'table-danger';
  1980. }
  1981. if (++messagecount > 0) {
  1982. $('#msgcnt').removeClass('badge-success');
  1983. $('#msgcnt').removeClass('badge-warning');
  1984. $('#msgcnt').removeClass('badge-danger');
  1985. $('#msgcnt').addClass(pillcolors[messageseverity]);
  1986. $('#msgcnt').text(messagecount);
  1987. }
  1988. $('#syslogTable').append(
  1989. "<tr class='" +
  1990. color +
  1991. "'>" +
  1992. '<td>' +
  1993. msgTime.toLocalShort() +
  1994. '</td>' +
  1995. '<td>' +
  1996. msg.message.encodeHTML() +
  1997. '</td>' +
  1998. '</tr>'
  1999. );
  2000. }
  2001. function inRange(x, min, max) {
  2002. return (x - min) * (x - max) <= 0;
  2003. }
  2004. function sleep(ms) {
  2005. return new Promise(resolve => setTimeout(resolve, ms));
  2006. }