Various web UI fixes and improvements (#2422)
[ExpressLRS.git] / src / html / scan.js
blob13d9c67865e8a5bf64adf207149e122783c42b25
1 @@require(isTX)
3 /* eslint-disable comma-dangle */
4 /* eslint-disable max-len */
5 /* eslint-disable require-jsdoc */
7 document.addEventListener('DOMContentLoaded', init, false);
8 let colorTimer = undefined;
9 let colorUpdated  = false;
10 let storedModelId = 255;
11 let buttonActions = [];
12 let originalUID = undefined;
13 let originalUIDType = undefined;
15 function _(el) {
16   return document.getElementById(el);
19 function getPwmFormData() {
20   let ch = 0;
21   let inField;
22   const outData = [];
23   while (inField = _(`pwm_${ch}_ch`)) {
24     const inChannel = inField.value;
25     const mode = _(`pwm_${ch}_mode`).value;
26     const invert = _(`pwm_${ch}_inv`).checked ? 1 : 0;
27     const narrow = _(`pwm_${ch}_nar`).checked ? 1 : 0;
28     const failsafeField = _(`pwm_${ch}_fs`);
29     let failsafe = failsafeField.value;
30     if (failsafe > 2011) failsafe = 2011;
31     if (failsafe < 988) failsafe = 988;
32     failsafeField.value = failsafe;
34     const raw = (narrow << 19) | (mode << 15) | (invert << 14) | (inChannel << 10) | (failsafe - 988);
35     // console.log(`PWM ${ch} mode=${mode} input=${inChannel} fs=${failsafe} inv=${invert} nar=${narrow} raw=${raw}`);
36     outData.push(raw);
37     ++ch;
38   }
39   return outData;
42 function enumSelectGenerate(id, val, arOptions) {
43   // Generate a <select> item with every option in arOptions, and select the val element (0-based)
44   return `<div class="mui-select"><select id="${id}">` +
45       arOptions.map((item, idx) => {
46         if (item) return `<option value="${idx}"${(idx === val) ? ' selected' : ''} ${item === 'Disabled' ? 'disabled' : ''}>${item}</option>`;
47         return '';
48       }).join('') + '</select></div>';
51 @@if not isTX:
52 function updatePwmSettings(arPwm, allowDshot) {
53   if (arPwm === undefined) {
54     if (_('model_tab')) _('model_tab').style.display = 'none';
55     return;
56   }
57   let pin1Index = undefined;
58   let pin1SerialIndex = undefined;
59   let pin3Index = undefined;
60   let pin3SerialIndex = undefined;
61   // arPwm is an array of raw integers [49664,50688,51200]. 10 bits of failsafe position, 4 bits of input channel, 1 bit invert, 4 bits mode, 1 bit for narrow/750us
62   const htmlFields = ['<div class="mui-panel"><table class="pwmtbl mui-table"><tr><th class="mui--text-center">Output</th><th>Mode</th><th>Input</th><th class="mui--text-center">Invert?</th><th class="mui--text-center">750us?</th><th>Failsafe</th></tr>'];
63   arPwm.forEach((item, index) => {
64     const failsafe = (item.config & 1023) + 988; // 10 bits
65     const ch = (item.config >> 10) & 15; // 4 bits
66     const inv = (item.config >> 14) & 1;
67     const mode = (item.config >> 15) & 15; // 4 bits
68     const narrow = (item.config >> 19) & 1;
69     const pin = item.pin;
70     const modes = ['50Hz', '60Hz', '100Hz', '160Hz', '333Hz', '400Hz', '10KHzDuty', 'On/Off'];
71     // only ESP32 devices allow DShot
72     if (allowDshot === true) {
73       if (pin !== 0)
74         modes.push('DShot');
75       else
76         modes.push(undefined);
77     }
78     if (pin === 1) {
79       modes.push('Serial TX');
80       pin1Index = index;
81       pin1SerialIndex = modes.length-1;
82     }
83     if (pin === 3) {
84       modes.push('Serial RX');
85       pin3Index = index;
86       pin3SerialIndex = modes.length-1;
87     }
88     modes.push(undefined);  // true PWM
89     const modeSelect = enumSelectGenerate(`pwm_${index}_mode`, mode, modes);
90     const inputSelect = enumSelectGenerate(`pwm_${index}_ch`, ch,
91         ['ch1', 'ch2', 'ch3', 'ch4',
92           'ch5 (AUX1)', 'ch6 (AUX2)', 'ch7 (AUX3)', 'ch8 (AUX4)',
93           'ch9 (AUX5)', 'ch10 (AUX6)', 'ch11 (AUX7)', 'ch12 (AUX8)',
94           'ch13 (AUX9)', 'ch14 (AUX10)', 'ch15 (AUX11)', 'ch16 (AUX12)']);
95     htmlFields.push(`<tr><th class="mui--text-center">${index+1}</th>
96             <td>${modeSelect}</td>
97             <td>${inputSelect}</td>
98             <td><div class="mui-checkbox mui--text-center"><input type="checkbox" id="pwm_${index}_inv"${(inv) ? ' checked' : ''}></div></td>
99             <td><div class="mui-checkbox mui--text-center"><input type="checkbox" id="pwm_${index}_nar"${(narrow) ? ' checked' : ''}></div></td>
100             <td><div class="mui-textfield"><input id="pwm_${index}_fs" value="${failsafe}" size="6"/></div></td></tr>`);
101   });
102   htmlFields.push('</table></div>');
104   const grp = document.createElement('DIV');
105   grp.setAttribute('class', 'group');
106   grp.innerHTML = htmlFields.join('');
108   _('pwm').appendChild(grp);
110   let setDisabled = (index, onoff) => {
111     _(`pwm_${index}_ch`).disabled = onoff;
112     _(`pwm_${index}_inv`).disabled = onoff;
113     _(`pwm_${index}_nar`).disabled = onoff;
114     _(`pwm_${index}_fs`).disabled = onoff;
115   }
116   // put some constraints on pin1/3 mode selects
117   if (pin1Index !== undefined && pin3Index !== undefined) {
118     const pin1Mode = _(`pwm_${pin1Index}_mode`);
119     const pin3Mode = _(`pwm_${pin3Index}_mode`);
120     pin1Mode.onchange = () => {
121       if (Number(pin1Mode.value) === pin1SerialIndex) { // Serial
122         pin3Mode.value = pin3SerialIndex;
123         setDisabled(pin1Index, true);
124         setDisabled(pin3Index, true);
125         pin3Mode.disabled = true;
126         _('serial-config').style.display = _('is-airport').checked ? 'none' : 'block';
127         _('baud-config').style.display = 'block';
128       }
129       else {
130         pin3Mode.value = 0;
131         setDisabled(pin1Index, false);
132         setDisabled(pin3Index, false);
133         pin3Mode.disabled = false;
134         _('serial-config').style.display = 'none';
135         _('baud-config').style.display = 'none';
136       }
137     }
138     pin3Mode.onchange = () => {
139       if (Number(pin3Mode.value) === pin3SerialIndex) { // Serial
140         pin1Mode.value = pin1SerialIndex;
141         setDisabled(pin1Index, true);
142         setDisabled(pin3Index, true);
143         pin3Mode.disabled = true;
144         _('serial-config').style.display = _('is-airport').checked ? 'none' : 'block';
145         _('baud-config').style.display = 'block';
146       }
147     }
148     const pin3 = pin3Mode.value;
149     pin1Mode.onchange();
150     if(Number(pin1Mode.value) !== pin1SerialIndex) pin3Mode.value = pin3;
151   }
153 @@end
155 function init() {
156   // setup network radio button handling
157   _('nt0').onclick = () => _('credentials').style.display = 'block';
158   _('nt1').onclick = () => _('credentials').style.display = 'block';
159   _('nt2').onclick = () => _('credentials').style.display = 'none';
160   _('nt3').onclick = () => _('credentials').style.display = 'none';
161 @@if not isTX:
162   // setup model match checkbox handler
163   _('model-match').onclick = () => {
164     if (_('model-match').checked) {
165       _('modelid').style.display = 'block';
166       if (storedModelId === 255) {
167         _('modelid').value = '';
168       } else {
169         _('modelid').value = storedModelId;
170       }
171     } else {
172       _('modelid').style.display = 'none';
173       _('modelid').value = '255';
174     }
175   };
176 @@end
177   initOptions();
180 function updateUIDType(uidtype) {
181   let bg = '';
182   let fg = '';
183   let text = uidtype;
184   let desc = '';
185   if (!uidtype || uidtype === 'Not set') {
186     bg = '#D50000';  // default 'red' for 'Not set'
187     fg = 'white';
188     text = 'Not set';
189     desc = 'The default binding UID from the device address will be used';
190   }
191   if (uidtype === 'Flashed') {
192     bg = '#1976D2'; // blue/white
193     fg = 'white';
194     desc = 'The binding UID was generated from a binding phrase set at flash time';
195   }
196   if (uidtype === 'Overridden') {
197     bg = '#689F38'; // green
198     fg = 'black';
199     desc = 'The binding UID has been generated from a bind-phrase previously entered into the "binding phrase" field above';
200   }
201   if (uidtype === 'Traditional') {
202     bg = '#D50000'; // red
203     fg = 'white';
204     desc = 'The binding UID has been set using traditional binding method i.e. button or 3-times power cycle and bound via the Lua script';
205   }
206   if (uidtype === 'Modified') {
207     bg = '#7c00d5'; // purple
208     fg = 'white';
209     desc = 'The binding UID has been modified, but not yet saved';
210   }
211   if (uidtype === 'On loan') {
212     bg = '#FFA000'; // amber
213     fg = 'black';
214     desc = 'The binding UID has been set using the model-loan feature';
215   }
216   _('uid-type').style.backgroundColor = bg;
217   _('uid-type').style.color = fg;
218   _('uid-type').textContent = text;
219   _('uid-text').textContent = desc;
222 function updateConfig(data, options) {
223   if (data.product_name) _('product_name').textContent = data.product_name;
224   if (data.reg_domain) _('reg_domain').textContent = data.reg_domain;
225   if (data.uid) {
226     _('uid').value = data.uid.toString();
227     originalUID = data.uid;
228   }
229   originalUIDType = data.uidtype;
230   updateUIDType(data.uidtype);
232   if (data.mode==='STA') {
233     _('stamode').style.display = 'block';
234     _('ssid').textContent = data.ssid;
235   } else {
236     _('apmode').style.display = 'block';
237   }
238 @@if not isTX:
239   if (data.hasOwnProperty('modelid') && data.modelid !== 255) {
240     _('modelid').style.display = 'block';
241     _('model-match').checked = true;
242     storedModelId = data.modelid;
243   } else {
244     _('modelid').style.display = 'none';
245     _('model-match').checked = false;
246     storedModelId = 255;
247   }
248   _('modelid').value = storedModelId;
249   _('force-tlm').checked = data.hasOwnProperty('force-tlm') && data['force-tlm'];
250   _('serial-protocol').onchange = () => {
251     const proto = Number(_('serial-protocol').value);
252     if (_('is-airport').checked) {
253       _('rcvr-uart-baud').disabled = false;
254       _('rcvr-uart-baud').value = options['rcvr-uart-baud'];
255       _('serial-config').style.display = 'none';
256       _('sbus-config').style.display = 'none';
257       return;
258     }
259     _('serial-config').style.display = 'block';
260     if (proto === 0 || proto === 1) { // Airport or CRSF
261       _('rcvr-uart-baud').disabled = false;
262       _('rcvr-uart-baud').value = options['rcvr-uart-baud'];
263       _('sbus-config').style.display = 'none';
264     }
265     else if (proto === 2 || proto === 3 || proto === 5) { // SBUS (and inverted) or DJI-RS Pro
266       _('rcvr-uart-baud').disabled = true;
267       _('rcvr-uart-baud').value = '100000';
268       _('sbus-config').style.display = 'block';
269       _('sbus-failsafe').value = data['sbus-failsafe'];
270     }
271     else if (proto === 4) { // SUMD
272       _('rcvr-uart-baud').disabled = true;
273       _('rcvr-uart-baud').value = '115200';
274       _('sbus-config').style.display = 'none';
275     }
276     else if (proto === 6) { // HoTT
277       _('rcvr-uart-baud').disabled = true;
278       _('rcvr-uart-baud').value = '19200';
279       _('sbus-config').style.display = 'none';
280     }
281   }
282   updatePwmSettings(data.pwm, data['allow-dshot']);
283   _('serial-protocol').value = data['serial-protocol'];
284   _('serial-protocol').onchange();
285   _('is-airport').onchange = _('serial-protocol').onchange;
286 @@end
287 @@if isTX:
288   if (data.hasOwnProperty['button-colors']) {
289     if (_('button1-color')) _('button1-color').oninput = changeCurrentColors;
290     if (data['button-colors'][0] === -1) _('button1-color-div').style.display = 'none';
291     else _('button1-color').value = color(data['button-colors'][0]);
293     if (_('button2-color')) _('button2-color').oninput = changeCurrentColors;
294     if (data['button-colors'][1] === -1) _('button2-color-div').style.display = 'none';
295     else _('button2-color').value = color(data['button-colors'][1]);
296   }
297   if (data.hasOwnProperty('button-actions')) {
298     updateButtons(data['button-actions']);
299   } else {
300     _('button-tab').style.display = 'none';
301   }
302   if (data['has-highpower'] === true) _('has-highpower').style.display = 'block';
303 @@end
306 function initOptions() {
307   const xmlhttp = new XMLHttpRequest();
308   xmlhttp.onreadystatechange = function() {
309     if (this.readyState === 4 && this.status === 200) {
310       const data = JSON.parse(this.responseText);
311       updateOptions(data['options']);
312       updateConfig(data['config'], data['options']);
313       initBindingPhraseGen();
314     }
315   };
316   xmlhttp.open('GET', '/config', true);
317   xmlhttp.send();
320 function getNetworks() {
321   const xmlhttp = new XMLHttpRequest();
322   xmlhttp.onload = function() {
323     if (this.status === 204) {
324       setTimeout(getNetworks, 2000);
325     } else {
326       const data = JSON.parse(this.responseText);
327       if (data.length > 0) {
328         _('loader').style.display = 'none';
329         autocomplete(_('network'), data);
330       }
331     }
332   };
333   xmlhttp.onerror = function() {
334     setTimeout(getNetworks, 2000);
335   };
336   xmlhttp.open('GET', 'networks.json', true);
337   xmlhttp.send();
340 _('network-tab').addEventListener('mui.tabs.showstart', getNetworks);
342 // =========================================================
344 function uploadFile() {
345   _('upload_btn').disabled = true
346   try {
347     const file = _('firmware_file').files[0];
348     const formdata = new FormData();
349     formdata.append('upload', file, file.name);
350     const ajax = new XMLHttpRequest();
351     ajax.upload.addEventListener('progress', progressHandler, false);
352     ajax.addEventListener('load', completeHandler, false);
353     ajax.addEventListener('error', errorHandler, false);
354     ajax.addEventListener('abort', abortHandler, false);
355     ajax.open('POST', '/update');
356     ajax.setRequestHeader('X-FileSize', file.size);
357     ajax.send(formdata);
358   }
359   catch (e) {
360     _('upload_btn').disabled = false
361   }
364 function progressHandler(event) {
365   // _("loaded_n_total").innerHTML = "Uploaded " + event.loaded + " bytes of " + event.total;
366   const percent = Math.round((event.loaded / event.total) * 100);
367   _('progressBar').value = percent;
368   _('status').innerHTML = percent + '% uploaded... please wait';
371 function completeHandler(event) {
372   _('status').innerHTML = '';
373   _('progressBar').value = 0;
374   _('upload_btn').disabled = false
375   const data = JSON.parse(event.target.responseText);
376   if (data.status === 'ok') {
377     function showMessage() {
378       cuteAlert({
379         type: 'success',
380         title: 'Update Succeeded',
381         message: data.msg
382       });
383     }
384     // This is basically a delayed display of the success dialog with a fake progress
385     let percent = 0;
386     const interval = setInterval(()=>{
387       percent = percent + 2;
388       _('progressBar').value = percent;
389       _('status').innerHTML = percent + '% flashed... please wait';
390       if (percent === 100) {
391         clearInterval(interval);
392         _('status').innerHTML = '';
393         _('progressBar').value = 0;
394         showMessage();
395       }
396     }, 100);
397   } else if (data.status === 'mismatch') {
398     cuteAlert({
399       type: 'question',
400       title: 'Targets Mismatch',
401       message: data.msg,
402       confirmText: 'Flash anyway',
403       cancelText: 'Cancel'
404     }).then((e)=>{
405       const xmlhttp = new XMLHttpRequest();
406       xmlhttp.onreadystatechange = function() {
407         if (this.readyState === 4) {
408           _('status').innerHTML = '';
409           _('progressBar').value = 0;
410           if (this.status === 200) {
411             const data = JSON.parse(this.responseText);
412             cuteAlert({
413               type: 'info',
414               title: 'Force Update',
415               message: data.msg
416             });
417           } else {
418             cuteAlert({
419               type: 'error',
420               title: 'Force Update',
421               message: 'An error occurred trying to force the update'
422             });
423           }
424         }
425       };
426       xmlhttp.open('POST', '/forceupdate', true);
427       const data = new FormData();
428       data.append('action', e);
429       xmlhttp.send(data);
430     });
431   } else {
432     cuteAlert({
433       type: 'error',
434       title: 'Update Failed',
435       message: data.msg
436     });
437   }
440 function errorHandler(event) {
441   _('status').innerHTML = '';
442   _('progressBar').value = 0;
443   _('upload_btn').disabled = false
444   cuteAlert({
445     type: 'error',
446     title: 'Update Failed',
447     message: event.target.responseText
448   });
451 function abortHandler(event) {
452   _('status').innerHTML = '';
453   _('progressBar').value = 0;
454   _('upload_btn').disabled = false
455   cuteAlert({
456     type: 'info',
457     title: 'Update Aborted',
458     message: event.target.responseText
459   });
462 _('firmware_file').addEventListener('change', (e) => {
463   e.preventDefault();
464   uploadFile();
467 _('fileselect').addEventListener('change', (e) => {
468   const files = e.target.files || e.dataTransfer.files;
469   const reader = new FileReader();
470   reader.onload = function(x) {
471     xmlhttp = new XMLHttpRequest();
472     xmlhttp.onreadystatechange = function() {
473       _('fileselect').value = '';
474       if (this.readyState === 4) {
475         if (this.status === 200) {
476           cuteAlert({
477             type: 'info',
478             title: 'Upload Model Configuration',
479             message: this.responseText
480           });
481         } else {
482           cuteAlert({
483             type: 'error',
484             title: 'Upload Model Configuration',
485             message: 'An error occurred while uploading model configuration file'
486           });
487         }
488       }
489     };
490     xmlhttp.open('POST', '/import', true);
491     xmlhttp.setRequestHeader('Content-Type', 'application/json');
492     xmlhttp.send(x.target.result);
493   }
494   reader.readAsText(files[0]);
495 }, false);
497 // =========================================================
499 function callback(title, msg, url, getdata, success) {
500   return function(e) {
501     e.stopPropagation();
502     e.preventDefault();
503     xmlhttp = new XMLHttpRequest();
504     xmlhttp.onreadystatechange = function() {
505       if (this.readyState === 4) {
506         if (this.status === 200) {
507           if (success) success();
508           cuteAlert({
509             type: 'info',
510             title: title,
511             message: this.responseText
512           });
513         } else {
514           cuteAlert({
515             type: 'error',
516             title: title,
517             message: msg
518           });
519         }
520       }
521     };
522     xmlhttp.open('POST', url, true);
523     if (getdata) data = getdata(xmlhttp);
524     else data = null;
525     xmlhttp.send(data);
526   };
529 function setupNetwork(event) {
530   if (_('nt0').checked) {
531     callback('Set Home Network', 'An error occurred setting the home network', '/sethome?save', function() {
532       return new FormData(_('sethome'));
533     }, function() {
534       _('wifi-ssid').value = _('network').value;
535       _('wifi-password').value = _('password').value;
536     })(event);
537   }
538   if (_('nt1').checked) {
539     callback('Connect To Network', 'An error occurred connecting to the network', '/sethome', function() {
540       return new FormData(_('sethome'));
541     })(event);
542   }
543   if (_('nt2').checked) {
544     callback('Start Access Point', 'An error occurred starting the Access Point', '/access', null)(event);
545   }
546   if (_('nt3').checked) {
547     callback('Forget Home Network', 'An error occurred forgetting the home network', '/forget', null)(event);
548   }
551 @@if not isTX:
552 _('reset-model').addEventListener('click', callback('Reset Model Settings', 'An error occurred reseting model settings', '/reset?model', null));
553 @@end
554 _('reset-options').addEventListener('click', callback('Reset Runtime Options', 'An error occurred reseting runtime options', '/reset?options', null));
556 _('sethome').addEventListener('submit', setupNetwork);
557 _('connect').addEventListener('click', callback('Connect to Home Network', 'An error occurred connecting to the Home network', '/connect', null));
558 if (_('config')) {
559   _('config').addEventListener('submit', callback('Set Configuration', 'An error occurred updating the configuration', '/config',
560       (xmlhttp) => {
561         xmlhttp.setRequestHeader('Content-Type', 'application/json');
562         return JSON.stringify({
563           "pwm": getPwmFormData(),
564           "serial-protocol": +_('serial-protocol').value,
565           "sbus-failsafe": +_('sbus-failsafe').value,
566           "modelid": +_('modelid').value,
567           "force-tlm": +_('force-tlm').checked
568         });
569       }));
572 function submitOptions(e) {
573   e.stopPropagation();
574   e.preventDefault();
575   const xhr = new XMLHttpRequest();
576   xhr.open('POST', '/options.json');
577   xhr.setRequestHeader('Content-Type', 'application/json');
578   // Convert the DOM element into a JSON object containing the form elements
579   const formElem = _('upload_options');
580   const formObject = Object.fromEntries(new FormData(formElem));
581   // Add in all the unchecked checkboxes which will be absent from a FormData object
582   formElem.querySelectorAll('input[type=checkbox]:not(:checked)').forEach((k) => formObject[k.name] = false);
583   // Force customised to true as this is now customising it
584   formObject['customised'] = true;
586   // Serialize and send the formObject
587   xhr.send(JSON.stringify(formObject, function(k, v) {
588     if (v === '') return undefined;
589     if (_(k)) {
590       if (_(k).type === 'color') return undefined;
591       if (_(k).type === 'checkbox') return v === 'on';
592       if (_(k).classList.contains('datatype-boolean')) return v === 'true';
593       if (_(k).classList.contains('array')) {
594         const arr = v.split(',').map((element) => {
595           return Number(element);
596         });
597         return arr.length === 0 ? undefined : arr;
598       }
599     }
600     if (typeof v === 'boolean') return v;
601     if (v === 'true') return true;
602     if (v === 'false') return false;
603     return isNaN(v) ? v : +v;
604   }));
606   xhr.onreadystatechange = function() {
607     if (this.readyState === 4) {
608       if (this.status === 200) {
609         cuteAlert({
610           type: 'question',
611           title: 'Upload Succeeded',
612           message: 'Reboot to take effect',
613           confirmText: 'Reboot',
614           cancelText: 'Close'
615         }).then((e) => {
616           originalUID = _('uid').value;
617           originalUIDType = 'Flashed';
618           _('phrase').value = '';
619           updateUIDType(originalUIDType);
620           if (e === 'confirm') {
621             const xhr = new XMLHttpRequest();
622             xhr.open('POST', '/reboot');
623             xhr.setRequestHeader('Content-Type', 'application/json');
624             xhr.onreadystatechange = function() {};
625             xhr.send();
626           }
627         });
628       } else {
629         cuteAlert({
630           type: 'error',
631           title: 'Upload Failed',
632           message: this.responseText
633         });
634       }
635     }
636   };
639 _('submit-options').addEventListener('click', submitOptions);
641 @@if isTX:
642 function submitButtonActions(e) {
643   e.stopPropagation();
644   e.preventDefault();
645   const xhr = new XMLHttpRequest();
646   xhr.open('POST', '/config');
647   xhr.setRequestHeader('Content-Type', 'application/json');
648   // put in the colors
649   if (buttonActions[0]) buttonActions[0].color = to8bit(_(`button1-color`).value)
650   if (buttonActions[1]) buttonActions[1].color = to8bit(_(`button2-color`).value)
651   xhr.send(JSON.stringify({'button-actions': buttonActions}));
653   xhr.onreadystatechange = function() {
654     if (this.readyState === 4) {
655       if (this.status === 200) {
656         cuteAlert({
657           type: 'info',
658           title: 'Success',
659           message: 'Button actions have been saved'
660         });
661       } else {
662         cuteAlert({
663           type: 'error',
664           title: 'Failed',
665           message: 'An error occurred while saving button configuration'
666         });
667       }
668     }
669   }
671 _('submit-actions').addEventListener('click', submitButtonActions);
672 @@end
674 function updateOptions(data) {
675   for (const [key, value] of Object.entries(data)) {
676     if (key ==='wifi-on-interval' && value === -1) continue;
677     if (_(key)) {
678       if (_(key).type === 'checkbox') {
679         _(key).checked = value;
680       } else {
681         if (Array.isArray(value)) _(key).value = value.toString();
682         else _(key).value = value;
683       }
684       if(_(key).onchange) _(key).onchange();
685     }
686   }
687   if (data['wifi-ssid']) _('homenet').textContent = data['wifi-ssid'];
688   else _('connect').style.display = 'none';
689   if (data['customised']) _('reset-options').style.display = 'block';
690   _('submit-options').disabled = false;
693 @@if isTX:
694 function toRGB(c)
696   r = c & 0xE0 ;
697   r = ((r << 16) + (r << 13) + (r << 10)) & 0xFF0000;
698   g = c & 0x1C;
699   g = ((g<< 11) + (g << 8) + (g << 5)) & 0xFF00;
700   b = ((c & 0x3) << 1) + ((c & 0x3) >> 1);
701   b = (b << 5) + (b << 2) + (b >> 1);
702   s = (r+g+b).toString(16);
703   return '#' + "000000".substring(0, 6-s.length) + s;
706 function updateButtons(data) {
707   buttonActions = data;
708   for (const [b, _v] of Object.entries(data)) {
709     for (const [p, v] of Object.entries(_v['action'])) {
710       appendRow(parseInt(b), parseInt(p), v);
711     }
712     _(`button${parseInt(b)+1}-color-div`).style.display = 'block';
713     _(`button${parseInt(b)+1}-color`).value = toRGB(_v['color']);
714   }
715   _('button1-color').oninput = changeCurrentColors;
716   _('button2-color').oninput = changeCurrentColors;
719 function changeCurrentColors() {
720   if (colorTimer === undefined) {
721     sendCurrentColors();
722     colorTimer = setInterval(timeoutCurrentColors, 50);
723   } else {
724     colorUpdated = true;
725   }
728 function to8bit(v)
730   v = parseInt(v.substring(1), 16)
731   return ((v >> 16) & 0xE0) + ((v >> (8+3)) & 0x1C) + ((v >> 6) & 0x3)
734 function sendCurrentColors() {
735   const formData = new FormData(_('button_actions'));
736   const data = Object.fromEntries(formData);
737   colors = [];
738   for (const [k, v] of Object.entries(data)) {
739     if (_(k) && _(k).type === 'color') {
740       const index = parseInt(k.substring('6')) - 1;
741       if (_(k + '-div').style.display === 'none') colors[index] = -1;
742       else colors[index] = to8bit(v);
743     }
744   }
745   const xmlhttp = new XMLHttpRequest();
746   xmlhttp.open('POST', '/buttons', true);
747   xmlhttp.setRequestHeader('Content-type', 'application/json');
748   xmlhttp.send(JSON.stringify(colors));
749   colorUpdated = false;
752 function timeoutCurrentColors() {
753   if (colorUpdated) {
754     sendCurrentColors();
755   } else {
756     clearInterval(colorTimer);
757     colorTimer = undefined;
758   }
761 function checkEnableButtonActionSave() {
762   let disable = false;
763   for (const [b, _v] of Object.entries(buttonActions)) {
764     for (const [p, v] of Object.entries(_v['action'])) {
765       if (v['action'] !== 0 && (_(`select-press-${b}-${p}`).value === '' || _(`select-long-${b}-${p}`).value === '' || _(`select-short-${b}-${p}`).value === '')) {
766         disable = true;
767       }
768     }
769   }
770   _('submit-actions').disabled = disable;
773 function changeAction(b, p, value) {
774   buttonActions[b]['action'][p]['action'] = value;
775   if (value === 0) {
776     _(`select-press-${b}-${p}`).value = '';
777     _(`select-long-${b}-${p}`).value = '';
778     _(`select-short-${b}-${p}`).value = '';
779   }
780   checkEnableButtonActionSave();
783 function changePress(b, p, value) {
784   buttonActions[b]['action'][p]['is-long-press'] = (value==='true');
785   _(`mui-long-${b}-${p}`).style.display = value==='true' ? 'block' : 'none';
786   _(`mui-short-${b}-${p}`).style.display = value==='true' ? 'none' : 'block';
787   checkEnableButtonActionSave();
790 function changeCount(b, p, value) {
791   buttonActions[b]['action'][p]['count'] = parseInt(value);
792   _(`select-long-${b}-${p}`).value = value;
793   _(`select-short-${b}-${p}`).value = value;
794   checkEnableButtonActionSave();
797 function appendRow(b,p,v) {
798   const row = _('button-actions').insertRow();
799   row.innerHTML = `
800 <td>
801   Button ${parseInt(b)+1}
802 </td>
803 <td>
804   <div class="mui-select">
805     <select onchange="changeAction(${b}, ${p}, parseInt(this.value));">
806       <option value='0' ${v['action']===0 ? 'selected' : ''}>Unused</option>
807       <option value='1' ${v['action']===1 ? 'selected' : ''}>Increase Power</option>
808       <option value='2' ${v['action']===2 ? 'selected' : ''}>Go to VTX Band Menu</option>
809       <option value='3' ${v['action']===3 ? 'selected' : ''}>Go to VTX Channel Menu</option>
810       <option value='4' ${v['action']===4 ? 'selected' : ''}>Send VTX Settings</option>
811       <option value='5' ${v['action']===5 ? 'selected' : ''}>Start WiFi</option>
812       <option value='6' ${v['action']===6 ? 'selected' : ''}>Enter Binding Mode</option>
813     </select>
814     <label>Action</label>
815   </div>
816 </td>
817 <td>
818   <div class="mui-select">
819     <select id="select-press-${b}-${p}" onchange="changePress(${b}, ${p}, this.value);">
820       <option value='' disabled hidden ${v['action']===0 ? 'selected' : ''}></option>
821       <option value='false' ${v['is-long-press']===false ? 'selected' : ''}>Short press (click)</option>
822       <option value='true' ${v['is-long-press']===true ? 'selected' : ''}>Long press (hold)</option>
823     </select>
824     <label>Press</label>
825   </div>
826 </td>
827 <td>
828   <div class="mui-select" id="mui-long-${b}-${p}" style="display:${buttonActions[b]['action'][p]['is-long-press'] ? "block": "none"};">
829     <select id="select-long-${b}-${p}" onchange="changeCount(${b}, ${p}, this.value);">
830       <option value='' disabled hidden ${v['action']===0 ? 'selected' : ''}></option>
831       <option value='0' ${v['count']===0 ? 'selected' : ''}>for 0.5 seconds</option>
832       <option value='1' ${v['count']===1 ? 'selected' : ''}>for 1 second</option>
833       <option value='2' ${v['count']===2 ? 'selected' : ''}>for 1.5 seconds</option>
834       <option value='3' ${v['count']===3 ? 'selected' : ''}>for 2 seconds</option>
835       <option value='4' ${v['count']===4 ? 'selected' : ''}>for 2.5 seconds</option>
836       <option value='5' ${v['count']===5 ? 'selected' : ''}>for 3 seconds</option>
837       <option value='6' ${v['count']===6 ? 'selected' : ''}>for 3.5 seconds</option>
838       <option value='7' ${v['count']===7 ? 'selected' : ''}>for 4 seconds</option>
839     </select>
840     <label>Count</label>
841   </div>
842   <div class="mui-select" id="mui-short-${b}-${p}" style="display:${buttonActions[b]['action'][p]['is-long-press'] ? "none": "block"};">
843     <select id="select-short-${b}-${p}" onchange="changeCount(${b}, ${p}, this.value);">
844       <option value='' disabled hidden ${v['action']===0 ? 'selected' : ''}></option>
845       <option value='0' ${v['count']===0 ? 'selected' : ''}>1 time</option>
846       <option value='1' ${v['count']===1 ? 'selected' : ''}>2 times</option>
847       <option value='2' ${v['count']===2 ? 'selected' : ''}>3 times</option>
848       <option value='3' ${v['count']===3 ? 'selected' : ''}>4 times</option>
849       <option value='4' ${v['count']===4 ? 'selected' : ''}>5 times</option>
850       <option value='5' ${v['count']===5 ? 'selected' : ''}>6 times</option>
851       <option value='6' ${v['count']===6 ? 'selected' : ''}>7 times</option>
852       <option value='7' ${v['count']===7 ? 'selected' : ''}>8 times</option>
853     </select>
854     <label>Count</label>
855   </div>
856 </td>
859 @@end
861 md5 = function() {
862   const k = [];
863   let i = 0;
865   for (; i < 64;) {
866     k[i] = 0 | (Math.abs(Math.sin(++i)) * 4294967296);
867   }
869   function calcMD5(str) {
870     let b; let c; let d; let j;
871     const x = [];
872     const str2 = unescape(encodeURI(str));
873     let a = str2.length;
874     const h = [b = 1732584193, c = -271733879, ~b, ~c];
875     let i = 0;
877     for (; i <= a;) x[i >> 2] |= (str2.charCodeAt(i) || 128) << 8 * (i++ % 4);
879     x[str = (a + 8 >> 6) * 16 + 14] = a * 8;
880     i = 0;
882     for (; i < str; i += 16) {
883       a = h; j = 0;
884       for (; j < 64;) {
885         a = [
886           d = a[3],
887           ((b = a[1] | 0) +
888             ((d = (
889               (a[0] +
890                 [
891                   b & (c = a[2]) | ~b & d,
892                   d & b | ~d & c,
893                   b ^ c ^ d,
894                   c ^ (b | ~d)
895                 ][a = j >> 4]
896               ) +
897               (k[j] +
898                 (x[[
899                   j,
900                   5 * j + 1,
901                   3 * j + 5,
902                   7 * j
903                 ][a] % 16 + i] | 0)
904               )
905             )) << (a = [
906               7, 12, 17, 22,
907               5, 9, 14, 20,
908               4, 11, 16, 23,
909               6, 10, 15, 21
910             ][4 * a + j++ % 4]) | d >>> 32 - a)
911           ),
912           b,
913           c
914         ];
915       }
916       for (j = 4; j;) h[--j] = h[j] + a[j];
917     }
919     str = [];
920     for (; j < 32;) str.push(((h[j >> 3] >> ((1 ^ j++ & 7) * 4)) & 15) * 16 + ((h[j >> 3] >> ((1 ^ j++ & 7) * 4)) & 15));
922     return new Uint8Array(str);
923   }
924   return calcMD5;
925 }();
927 function uidBytesFromText(text) {
928   const bindingPhraseFull = `-DMY_BINDING_PHRASE="${text}"`;
929   const bindingPhraseHashed = md5(bindingPhraseFull);
930   return bindingPhraseHashed.subarray(0, 6);
933 function initBindingPhraseGen() {
934   const uid = _('uid');
936   function setOutput(text) {
937     if (text.length === 0) {
938       uid.value = originalUID.toString();
939       updateUIDType(originalUIDType);
940     }
941     else {
942       uid.value = uidBytesFromText(text);
943       updateUIDType('Modified');
944     }
945   }
947   function updateValue(e) {
948     setOutput(e.target.value);
949   }
951   _('phrase').addEventListener('input', updateValue);
952   setOutput('');
955 @@include("libs.js")