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;
16 return document.getElementById(el);
19 function getPwmFormData() {
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}`);
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>`;
48 }).join('') + '</select></div>';
52 function updatePwmSettings(arPwm, allowDshot) {
53 if (arPwm === undefined) {
54 if (_('model_tab')) _('model_tab').style.display = 'none';
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;
70 const modes = ['50Hz', '60Hz', '100Hz', '160Hz', '333Hz', '400Hz', '10KHzDuty', 'On/Off'];
71 // only ESP32 devices allow DShot
72 if (allowDshot === true) {
76 modes.push(undefined);
79 modes.push('Serial TX');
81 pin1SerialIndex = modes.length-1;
84 modes.push('Serial RX');
86 pin3SerialIndex = modes.length-1;
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>`);
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;
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';
131 setDisabled(pin1Index, false);
132 setDisabled(pin3Index, false);
133 pin3Mode.disabled = false;
134 _('serial-config').style.display = 'none';
135 _('baud-config').style.display = 'none';
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';
148 const pin3 = pin3Mode.value;
150 if(Number(pin1Mode.value) !== pin1SerialIndex) pin3Mode.value = pin3;
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';
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 = '';
169 _('modelid').value = storedModelId;
172 _('modelid').style.display = 'none';
173 _('modelid').value = '255';
180 function updateUIDType(uidtype) {
185 if (!uidtype || uidtype === 'Not set') {
186 bg = '#D50000'; // default 'red' for 'Not set'
189 desc = 'The default binding UID from the device address will be used';
191 if (uidtype === 'Flashed') {
192 bg = '#1976D2'; // blue/white
194 desc = 'The binding UID was generated from a binding phrase set at flash time';
196 if (uidtype === 'Overridden') {
197 bg = '#689F38'; // green
199 desc = 'The binding UID has been generated from a bind-phrase previously entered into the "binding phrase" field above';
201 if (uidtype === 'Traditional') {
202 bg = '#D50000'; // red
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';
206 if (uidtype === 'Modified') {
207 bg = '#7c00d5'; // purple
209 desc = 'The binding UID has been modified, but not yet saved';
211 if (uidtype === 'On loan') {
212 bg = '#FFA000'; // amber
214 desc = 'The binding UID has been set using the model-loan feature';
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;
226 _('uid').value = data.uid.toString();
227 originalUID = data.uid;
229 originalUIDType = data.uidtype;
230 updateUIDType(data.uidtype);
232 if (data.mode==='STA') {
233 _('stamode').style.display = 'block';
234 _('ssid').textContent = data.ssid;
236 _('apmode').style.display = 'block';
239 if (data.hasOwnProperty('modelid') && data.modelid !== 255) {
240 _('modelid').style.display = 'block';
241 _('model-match').checked = true;
242 storedModelId = data.modelid;
244 _('modelid').style.display = 'none';
245 _('model-match').checked = false;
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';
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';
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'];
271 else if (proto === 4) { // SUMD
272 _('rcvr-uart-baud').disabled = true;
273 _('rcvr-uart-baud').value = '115200';
274 _('sbus-config').style.display = 'none';
276 else if (proto === 6) { // HoTT
277 _('rcvr-uart-baud').disabled = true;
278 _('rcvr-uart-baud').value = '19200';
279 _('sbus-config').style.display = 'none';
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;
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]);
297 if (data.hasOwnProperty('button-actions')) {
298 updateButtons(data['button-actions']);
300 _('button-tab').style.display = 'none';
302 if (data['has-highpower'] === true) _('has-highpower').style.display = 'block';
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();
316 xmlhttp.open('GET', '/config', true);
320 function getNetworks() {
321 const xmlhttp = new XMLHttpRequest();
322 xmlhttp.onload = function() {
323 if (this.status === 204) {
324 setTimeout(getNetworks, 2000);
326 const data = JSON.parse(this.responseText);
327 if (data.length > 0) {
328 _('loader').style.display = 'none';
329 autocomplete(_('network'), data);
333 xmlhttp.onerror = function() {
334 setTimeout(getNetworks, 2000);
336 xmlhttp.open('GET', 'networks.json', true);
340 _('network-tab').addEventListener('mui.tabs.showstart', getNetworks);
342 // =========================================================
344 function uploadFile() {
345 _('upload_btn').disabled = true
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);
360 _('upload_btn').disabled = false
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() {
380 title: 'Update Succeeded',
384 // This is basically a delayed display of the success dialog with a fake progress
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;
397 } else if (data.status === 'mismatch') {
400 title: 'Targets Mismatch',
402 confirmText: 'Flash anyway',
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);
414 title: 'Force Update',
420 title: 'Force Update',
421 message: 'An error occurred trying to force the update'
426 xmlhttp.open('POST', '/forceupdate', true);
427 const data = new FormData();
428 data.append('action', e);
434 title: 'Update Failed',
440 function errorHandler(event) {
441 _('status').innerHTML = '';
442 _('progressBar').value = 0;
443 _('upload_btn').disabled = false
446 title: 'Update Failed',
447 message: event.target.responseText
451 function abortHandler(event) {
452 _('status').innerHTML = '';
453 _('progressBar').value = 0;
454 _('upload_btn').disabled = false
457 title: 'Update Aborted',
458 message: event.target.responseText
462 _('firmware_file').addEventListener('change', (e) => {
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) {
478 title: 'Upload Model Configuration',
479 message: this.responseText
484 title: 'Upload Model Configuration',
485 message: 'An error occurred while uploading model configuration file'
490 xmlhttp.open('POST', '/import', true);
491 xmlhttp.setRequestHeader('Content-Type', 'application/json');
492 xmlhttp.send(x.target.result);
494 reader.readAsText(files[0]);
497 // =========================================================
499 function callback(title, msg, url, getdata, success) {
503 xmlhttp = new XMLHttpRequest();
504 xmlhttp.onreadystatechange = function() {
505 if (this.readyState === 4) {
506 if (this.status === 200) {
507 if (success) success();
511 message: this.responseText
522 xmlhttp.open('POST', url, true);
523 if (getdata) data = getdata(xmlhttp);
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'));
534 _('wifi-ssid').value = _('network').value;
535 _('wifi-password').value = _('password').value;
538 if (_('nt1').checked) {
539 callback('Connect To Network', 'An error occurred connecting to the network', '/sethome', function() {
540 return new FormData(_('sethome'));
543 if (_('nt2').checked) {
544 callback('Start Access Point', 'An error occurred starting the Access Point', '/access', null)(event);
546 if (_('nt3').checked) {
547 callback('Forget Home Network', 'An error occurred forgetting the home network', '/forget', null)(event);
552 _('reset-model').addEventListener('click', callback('Reset Model Settings', 'An error occurred reseting model settings', '/reset?model', null));
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));
559 _('config').addEventListener('submit', callback('Set Configuration', 'An error occurred updating the configuration', '/config',
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
572 function submitOptions(e) {
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;
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);
597 return arr.length === 0 ? undefined : arr;
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;
606 xhr.onreadystatechange = function() {
607 if (this.readyState === 4) {
608 if (this.status === 200) {
611 title: 'Upload Succeeded',
612 message: 'Reboot to take effect',
613 confirmText: 'Reboot',
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() {};
631 title: 'Upload Failed',
632 message: this.responseText
639 _('submit-options').addEventListener('click', submitOptions);
642 function submitButtonActions(e) {
645 const xhr = new XMLHttpRequest();
646 xhr.open('POST', '/config');
647 xhr.setRequestHeader('Content-Type', 'application/json');
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) {
659 message: 'Button actions have been saved'
665 message: 'An error occurred while saving button configuration'
671 _('submit-actions').addEventListener('click', submitButtonActions);
674 function updateOptions(data) {
675 for (const [key, value] of Object.entries(data)) {
676 if (key ==='wifi-on-interval' && value === -1) continue;
678 if (_(key).type === 'checkbox') {
679 _(key).checked = value;
681 if (Array.isArray(value)) _(key).value = value.toString();
682 else _(key).value = value;
684 if(_(key).onchange) _(key).onchange();
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;
697 r = ((r << 16) + (r << 13) + (r << 10)) & 0xFF0000;
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);
712 _(`button${parseInt(b)+1}-color-div`).style.display = 'block';
713 _(`button${parseInt(b)+1}-color`).value = toRGB(_v['color']);
715 _('button1-color').oninput = changeCurrentColors;
716 _('button2-color').oninput = changeCurrentColors;
719 function changeCurrentColors() {
720 if (colorTimer === undefined) {
722 colorTimer = setInterval(timeoutCurrentColors, 50);
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);
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);
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() {
756 clearInterval(colorTimer);
757 colorTimer = undefined;
761 function checkEnableButtonActionSave() {
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 === '')) {
770 _('submit-actions').disabled = disable;
773 function changeAction(b, p, value) {
774 buttonActions[b]['action'][p]['action'] = value;
776 _(`select-press-${b}-${p}`).value = '';
777 _(`select-long-${b}-${p}`).value = '';
778 _(`select-short-${b}-${p}`).value = '';
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();
801 Button ${parseInt(b)+1}
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>
814 <label>Action</label>
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>
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>
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>
866 k[i] = 0 | (Math.abs(Math.sin(++i)) * 4294967296);
869 function calcMD5(str) {
870 let b; let c; let d; let j;
872 const str2 = unescape(encodeURI(str));
874 const h = [b = 1732584193, c = -271733879, ~b, ~c];
877 for (; i <= a;) x[i >> 2] |= (str2.charCodeAt(i) || 128) << 8 * (i++ % 4);
879 x[str = (a + 8 >> 6) * 16 + 14] = a * 8;
882 for (; i < str; i += 16) {
891 b & (c = a[2]) | ~b & d,
910 ][4 * a + j++ % 4]) | d >>> 32 - a)
916 for (j = 4; j;) h[--j] = h[j] + a[j];
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);
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);
942 uid.value = uidBytesFromText(text);
943 updateUIDType('Modified');
947 function updateValue(e) {
948 setOutput(e.target.value);
951 _('phrase').addEventListener('input', updateValue);