Communicate Rx available antenna mode to the Tx (#3039)
[ExpressLRS.git] / src / html / scan.js
blob14d834b00fc042171278d05d9c2134cd448caf1c
1 @@require(PLATFORM, isTX, is8285)
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 modeSelectionInit = true;
13 let originalUID = undefined;
14 let originalUIDType = undefined;
16 function _(el) {
17 return document.getElementById(el);
20 function getPwmFormData() {
21 let ch = 0;
22 let inField;
23 const outData = [];
24 while (inField = _(`pwm_${ch}_ch`)) {
25 const inChannel = inField.value;
26 const mode = _(`pwm_${ch}_mode`).value;
27 const invert = _(`pwm_${ch}_inv`).checked ? 1 : 0;
28 const narrow = _(`pwm_${ch}_nar`).checked ? 1 : 0;
29 const failsafeField = _(`pwm_${ch}_fs`);
30 const failsafeModeField = _(`pwm_${ch}_fsmode`);
31 let failsafe = failsafeField.value;
32 if (failsafe > 2011) failsafe = 2011;
33 if (failsafe < 988) failsafe = 988;
34 failsafeField.value = failsafe;
35 let failsafeMode = failsafeModeField.value;
37 const raw = (narrow << 19) | (mode << 15) | (invert << 14) | (inChannel << 10) | (failsafeMode << 20) | (failsafe - 988);
38 // console.log(`PWM ${ch} mode=${mode} input=${inChannel} fs=${failsafe} fsmode=${failsafeMode} inv=${invert} nar=${narrow} raw=${raw}`);
39 outData.push(raw);
40 ++ch;
42 return outData;
45 function enumSelectGenerate(id, val, arOptions) {
46 // Generate a <select> item with every option in arOptions, and select the val element (0-based)
47 const retVal = `<div class="mui-select compact"><select id="${id}" class="pwmitm">` +
48 arOptions.map((item, idx) => {
49 if (item) return `<option value="${idx}"${(idx === val) ? ' selected' : ''} ${item === 'Disabled' ? 'disabled' : ''}>${item}</option>`;
50 return '';
51 }).join('') + '</select></div>';
52 return retVal;
55 function generateFeatureBadges(features) {
56 let str = '';
57 if (!!(features & 1)) str += `<span style="color: #696969; background-color: #a8dcfa" class="badge">TX</span>`;
58 else if (!!(features & 2)) str += `<span style="color: #696969; background-color: #d2faa8" class="badge">RX</span>`;
59 if ((features & 12) === 12) str += `<span style="color: #696969; background-color: #fab4a8" class="badge">I2C</span>`;
60 else if (!!(features & 4)) str += `<span style="color: #696969; background-color: #fab4a8" class="badge">SCL</span>`;
61 else if (!!(features & 8)) str += `<span style="color: #696969; background-color: #fab4a8" class="badge">SDA</span>`;
63 // Serial2
64 if ((features & 96) === 96) str += `<span style="color: #696969; background-color: #36b5ff" class="badge">Serial2</span>`;
65 else if (!!(features & 32)) str += `<span style="color: #696969; background-color: #36b5ff" class="badge">RX2</span>`;
66 else if (!!(features & 64)) str += `<span style="color: #696969; background-color: #36b5ff" class="badge">TX2</span>`;
68 return str;
71 @@if not isTX:
72 function updatePwmSettings(arPwm) {
73 if (arPwm === undefined) {
74 if (_('model_tab')) _('model_tab').style.display = 'none';
75 return;
77 var pinRxIndex = undefined;
78 var pinTxIndex = undefined;
79 var pinModes = []
80 // 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
81 const htmlFields = ['<div class="mui-panel pwmpnl"><table class="pwmtbl mui-table"><tr><th class="fixed-column">Output</th><th class="mui--text-center fixed-column">Features</th><th>Mode</th><th>Input</th><th class="mui--text-center fixed-column">Invert?</th><th class="mui--text-center fixed-column">750us?</th><th class="mui--text-center fixed-column pwmitm">Failsafe Mode</th><th class="mui--text-center fixed-column pwmitm">Failsafe Pos</th></tr>'];
82 arPwm.forEach((item, index) => {
83 const failsafe = (item.config & 1023) + 988; // 10 bits
84 const failsafeMode = (item.config >> 20) & 3; // 2 bits
85 const ch = (item.config >> 10) & 15; // 4 bits
86 const inv = (item.config >> 14) & 1;
87 const mode = (item.config >> 15) & 15; // 4 bits
88 const narrow = (item.config >> 19) & 1;
89 const features = item.features;
90 const modes = ['50Hz', '60Hz', '100Hz', '160Hz', '333Hz', '400Hz', '10KHzDuty', 'On/Off'];
91 if (features & 16) {
92 modes.push('DShot');
93 } else {
94 modes.push(undefined);
96 if (features & 1) {
97 modes.push('Serial TX');
98 modes.push(undefined); // SCL
99 modes.push(undefined); // SDA
100 modes.push(undefined); // true PWM
101 pinRxIndex = index;
102 } else if (features & 2) {
103 modes.push('Serial RX');
104 modes.push(undefined); // SCL
105 modes.push(undefined); // SDA
106 modes.push(undefined); // true PWM
107 pinTxIndex = index;
108 } else {
109 modes.push(undefined); // Serial
110 if (features & 4) {
111 modes.push('I2C SCL');
112 } else {
113 modes.push(undefined);
115 if (features & 8) {
116 modes.push('I2C SDA');
117 } else {
118 modes.push(undefined);
120 modes.push(undefined); // true PWM
123 if (features & 32) {
124 modes.push('Serial2 RX');
125 } else {
126 modes.push(undefined);
128 if (features & 64) {
129 modes.push('Serial2 TX');
130 } else {
131 modes.push(undefined);
134 const modeSelect = enumSelectGenerate(`pwm_${index}_mode`, mode, modes);
135 const inputSelect = enumSelectGenerate(`pwm_${index}_ch`, ch,
136 ['ch1', 'ch2', 'ch3', 'ch4',
137 'ch5 (AUX1)', 'ch6 (AUX2)', 'ch7 (AUX3)', 'ch8 (AUX4)',
138 'ch9 (AUX5)', 'ch10 (AUX6)', 'ch11 (AUX7)', 'ch12 (AUX8)',
139 'ch13 (AUX9)', 'ch14 (AUX10)', 'ch15 (AUX11)', 'ch16 (AUX12)']);
140 const failsafeModeSelect = enumSelectGenerate(`pwm_${index}_fsmode`, failsafeMode,
141 ['Set Position', 'No Pulses', 'Last Position']); // match eServoOutputFailsafeMode
142 htmlFields.push(`<tr><td class="mui--text-center mui--text-title">${index + 1}</td>
143 <td>${generateFeatureBadges(features)}</td>
144 <td>${modeSelect}</td>
145 <td>${inputSelect}</td>
146 <td><div class="mui-checkbox mui--text-center"><input type="checkbox" id="pwm_${index}_inv"${(inv) ? ' checked' : ''}></div></td>
147 <td><div class="mui-checkbox mui--text-center"><input type="checkbox" id="pwm_${index}_nar"${(narrow) ? ' checked' : ''}></div></td>
148 <td>${failsafeModeSelect}</td>
149 <td><div class="mui-textfield compact"><input id="pwm_${index}_fs" value="${failsafe}" size="6" class="pwmitm" /></div></td></tr>`);
150 pinModes[index] = mode;
152 htmlFields.push('</table></div>');
154 const grp = document.createElement('DIV');
155 grp.setAttribute('class', 'group');
156 grp.innerHTML = htmlFields.join('');
158 _('pwm').appendChild(grp);
160 const setDisabled = (index, onoff) => {
161 _(`pwm_${index}_ch`).disabled = onoff;
162 _(`pwm_${index}_inv`).disabled = onoff;
163 _(`pwm_${index}_nar`).disabled = onoff;
164 _(`pwm_${index}_fs`).disabled = onoff;
165 _(`pwm_${index}_fsmode`).disabled = onoff;
167 arPwm.forEach((item,index)=>{
168 const pinMode = _(`pwm_${index}_mode`)
169 pinMode.onchange = () => {
170 setDisabled(index, pinMode.value > 9);
171 const updateOthers = (value, enable) => {
172 if (value > 9) { // disable others
173 arPwm.forEach((item, other) => {
174 if (other != index) {
175 document.querySelectorAll(`#pwm_${other}_mode option`).forEach(opt => {
176 if (opt.value == value) {
177 if (modeSelectionInit)
178 opt.disabled = true;
179 else
180 opt.disabled = enable;
187 updateOthers(pinMode.value, true); // disable others
188 updateOthers(pinModes[index], false); // enable others
189 pinModes[index] = pinMode.value;
191 // show Serial2 protocol selection only if Serial2 TX is assigned
192 _('serial1-config').style.display = 'none';
193 if (pinMode.value == 14) // Serial2 TX
194 _('serial1-config').style.display = 'block';
196 pinMode.onchange();
198 // disable and hide the failsafe position field if not using the set-position failsafe mode
199 const failsafeMode = _(`pwm_${index}_fsmode`);
200 failsafeMode.onchange = () => {
201 const failsafeField = _(`pwm_${index}_fs`);
202 if (failsafeMode.value == 0) {
203 failsafeField.disabled = false;
204 failsafeField.style.display = 'block';
206 else {
207 failsafeField.disabled = true;
208 failsafeField.style.display = 'none';
211 failsafeMode.onchange();
214 modeSelectionInit = false;
216 // put some constraints on pinRx/Tx mode selects
217 if (pinRxIndex !== undefined && pinTxIndex !== undefined) {
218 const pinRxMode = _(`pwm_${pinRxIndex}_mode`);
219 const pinTxMode = _(`pwm_${pinTxIndex}_mode`);
220 pinRxMode.onchange = () => {
221 if (pinRxMode.value == 9) { // Serial
222 pinTxMode.value = 9;
223 setDisabled(pinRxIndex, true);
224 setDisabled(pinTxIndex, true);
225 pinTxMode.disabled = true;
226 _('serial-config').style.display = 'block';
227 _('baud-config').style.display = 'block';
229 else {
230 pinTxMode.value = 0;
231 setDisabled(pinRxIndex, false);
232 setDisabled(pinTxIndex, false);
233 pinTxMode.disabled = false;
234 _('serial-config').style.display = 'none';
235 _('baud-config').style.display = 'none';
238 pinTxMode.onchange = () => {
239 if (pinTxMode.value == 9) { // Serial
240 pinRxMode.value = 9;
241 setDisabled(pinRxIndex, true);
242 setDisabled(pinTxIndex, true);
243 pinTxMode.disabled = true;
244 _('serial-config').style.display = 'block';
245 _('baud-config').style.display = 'block';
248 const pinTx = pinTxMode.value;
249 pinRxMode.onchange();
250 if (pinRxMode.value != 9) pinTxMode.value = pinTx;
253 @@end
255 function init() {
256 // setup network radio button handling
257 _('nt0').onclick = () => _('credentials').style.display = 'block';
258 _('nt1').onclick = () => _('credentials').style.display = 'block';
259 _('nt2').onclick = () => _('credentials').style.display = 'none';
260 _('nt3').onclick = () => _('credentials').style.display = 'none';
261 @@if not isTX:
262 // setup model match checkbox handler
263 _('model-match').onclick = () => {
264 if (_('model-match').checked) {
265 _('modelNum').style.display = 'block';
266 if (storedModelId === 255) {
267 _('modelid').value = '';
268 } else {
269 _('modelid').value = storedModelId;
271 } else {
272 _('modelNum').style.display = 'none';
273 _('modelid').value = '255';
276 // Start on the model tab
277 mui.tabs.activate('pane-justified-3');
278 @@else:
279 // Start on the options tab
280 mui.tabs.activate('pane-justified-1');
281 @@end
282 initFiledrag();
283 initOptions();
286 function updateUIDType(uidtype) {
287 let bg = '';
288 let fg = 'white';
289 let desc = '';
291 if (!uidtype || uidtype.startsWith('Not set')) // TX
293 bg = '#D50000'; // red/white
294 uidtype = 'Not set';
295 desc = 'Using autogenerated binding UID';
297 else if (uidtype === 'Flashed') // TX
299 bg = '#1976D2'; // blue/white
300 desc = 'The binding UID was generated from a binding phrase set at flash time';
302 else if (uidtype === 'Overridden') // TX
304 bg = '#689F38'; // green/black
305 fg = 'black';
306 desc = 'The binding UID has been generated from a binding phrase previously entered into the "binding phrase" field above';
308 else if (uidtype === 'Modified') // edited here
310 bg = '#7c00d5'; // purple
311 desc = 'The binding UID has been modified, but not yet saved';
313 else if (uidtype === 'Volatile') // RX
315 bg = '#FFA000'; // amber
316 desc = 'The binding UID will be cleared on boot';
318 else if (uidtype === 'Loaned') // RX
320 bg = '#FFA000'; // amber
321 desc = 'This receiver is on loan and can be returned using Lua or three-plug';
323 else // RX
325 if (_('uid').value.endsWith('0,0,0,0'))
327 bg = '#FFA000'; // amber
328 uidtype = 'Not bound';
329 desc = 'This receiver is unbound and will boot to binding mode';
331 else
333 bg = '#1976D2'; // blue/white
334 uidtype = 'Bound';
335 desc = 'This receiver is bound and will boot waiting for connection';
339 _('uid-type').style.backgroundColor = bg;
340 _('uid-type').style.color = fg;
341 _('uid-type').textContent = uidtype;
342 _('uid-text').textContent = desc;
345 function updateConfig(data, options) {
346 if (data.product_name) _('product_name').textContent = data.product_name;
347 if (data.reg_domain) _('reg_domain').textContent = data.reg_domain;
348 if (data.uid) {
349 _('uid').value = data.uid.toString();
350 originalUID = data.uid;
352 originalUIDType = data.uidtype;
353 updateUIDType(data.uidtype);
355 if (data.mode==='STA') {
356 _('stamode').style.display = 'block';
357 _('ssid').textContent = data.ssid;
358 } else {
359 _('apmode').style.display = 'block';
361 @@if not isTX:
362 if (data.hasOwnProperty('modelid') && data.modelid !== 255) {
363 _('modelNum').style.display = 'block';
364 _('model-match').checked = true;
365 storedModelId = data.modelid;
366 } else {
367 _('modelNum').style.display = 'none';
368 _('model-match').checked = false;
369 storedModelId = 255;
371 _('modelid').value = storedModelId;
372 _('force-tlm').checked = data.hasOwnProperty('force-tlm') && data['force-tlm'];
373 _('serial-protocol').onchange = () => {
374 const proto = Number(_('serial-protocol').value);
375 if (_('is-airport').checked) {
376 _('rcvr-uart-baud').disabled = false;
377 _('rcvr-uart-baud').value = options['rcvr-uart-baud'];
378 _('serial-config').style.display = 'none';
379 _('sbus-config').style.display = 'none';
380 return;
382 _('serial-config').style.display = 'block';
383 if (proto === 0 || proto === 1) { // Airport or CRSF
384 _('rcvr-uart-baud').disabled = false;
385 _('rcvr-uart-baud').value = options['rcvr-uart-baud'];
386 _('sbus-config').style.display = 'none';
388 else if (proto === 2 || proto === 3 || proto === 5) { // SBUS (and inverted) or DJI-RS Pro
389 _('rcvr-uart-baud').disabled = true;
390 _('rcvr-uart-baud').value = '100000';
391 _('sbus-config').style.display = 'block';
392 _('sbus-failsafe').value = data['sbus-failsafe'];
394 else if (proto === 4) { // SUMD
395 _('rcvr-uart-baud').disabled = true;
396 _('rcvr-uart-baud').value = '115200';
397 _('sbus-config').style.display = 'none';
399 else if (proto === 6) { // HoTT
400 _('rcvr-uart-baud').disabled = true;
401 _('rcvr-uart-baud').value = '19200';
402 _('sbus-config').style.display = 'none';
406 _('serial1-protocol').onchange = () => {
407 if (_('is-airport').checked) {
408 _('rcvr-uart-baud').disabled = false;
409 _('rcvr-uart-baud').value = options['rcvr-uart-baud'];
410 _('serial1-config').style.display = 'none';
411 _('sbus-config').style.display = 'none';
412 return;
416 updatePwmSettings(data.pwm);
417 _('serial-protocol').value = data['serial-protocol'];
418 _('serial-protocol').onchange();
419 _('serial1-protocol').value = data['serial1-protocol'];
420 _('serial1-protocol').onchange();
421 _('is-airport').onchange = () => {
422 _('serial-protocol').onchange();
423 _('serial1-protocol').onchange();
425 _('is-airport').onchange;
426 _('vbind').value = data.vbind;
427 _('vbind').onchange = () => {
428 _('bindphrase').style.display = _('vbind').value === '1' ? 'none' : 'block';
430 _('vbind').onchange();
432 // set initial visibility status of Serial2 protocol selection
433 _('serial1-config').style.display = 'none';
434 data.pwm?.forEach((item,index) => {
435 const _pinMode = _(`pwm_${index}_mode`)
436 if (_pinMode.value == 14) // Serial2 TX
437 _('serial1-config').style.display = 'block';
440 @@end
441 @@if isTX:
442 if (data.hasOwnProperty['button-colors']) {
443 if (_('button1-color')) _('button1-color').oninput = changeCurrentColors;
444 if (data['button-colors'][0] === -1) _('button1-color-div').style.display = 'none';
445 else _('button1-color').value = color(data['button-colors'][0]);
447 if (_('button2-color')) _('button2-color').oninput = changeCurrentColors;
448 if (data['button-colors'][1] === -1) _('button2-color-div').style.display = 'none';
449 else _('button2-color').value = color(data['button-colors'][1]);
451 if (data.hasOwnProperty('button-actions')) {
452 updateButtons(data['button-actions']);
453 } else {
454 _('button-tab').style.display = 'none';
456 @@end
459 function initOptions() {
460 const xmlhttp = new XMLHttpRequest();
461 xmlhttp.onreadystatechange = function() {
462 if (this.readyState === 4 && this.status === 200) {
463 const data = JSON.parse(this.responseText);
464 updateOptions(data['options']);
465 updateConfig(data['config'], data['options']);
466 initBindingPhraseGen();
469 xmlhttp.open('GET', '/config', true);
470 xmlhttp.send();
473 function getNetworks() {
474 const xmlhttp = new XMLHttpRequest();
475 xmlhttp.onload = function() {
476 if (this.status === 204) {
477 setTimeout(getNetworks, 2000);
478 } else {
479 const data = JSON.parse(this.responseText);
480 if (data.length > 0) {
481 _('loader').style.display = 'none';
482 autocomplete(_('network'), data);
486 xmlhttp.onerror = function() {
487 setTimeout(getNetworks, 2000);
489 xmlhttp.open('GET', 'networks.json', true);
490 xmlhttp.send();
493 _('network-tab').addEventListener('mui.tabs.showstart', getNetworks);
495 // =========================================================
497 function initFiledrag() {
498 const fileselect = _('firmware_file');
499 const filedrag = _('filedrag');
501 fileselect.addEventListener('change', fileSelectHandler, false);
503 const xhr = new XMLHttpRequest();
504 if (xhr.upload) {
505 filedrag.addEventListener('dragover', fileDragHover, false);
506 filedrag.addEventListener('dragleave', fileDragHover, false);
507 filedrag.addEventListener('drop', fileSelectHandler, false);
508 filedrag.style.display = 'block';
512 function fileDragHover(e) {
513 e.stopPropagation();
514 e.preventDefault();
515 if (e.target === _('filedrag')) e.target.className = (e.type === 'dragover' ? 'hover' : '');
518 function fileSelectHandler(e) {
519 fileDragHover(e);
520 // ESP32 expects .bin, ESP8285 RX expect .bin.gz
521 const files = e.target.files || e.dataTransfer.files;
522 const fileExt = files[0].name.split('.').pop();
523 @@if (is8285 and not isTX):
524 const expectedFileExt = 'gz';
525 const expectedFileExtDesc = '.bin.gz file. <br />Do NOT decompress/unzip/extract the file!';
526 @@else:
527 const expectedFileExt = 'bin';
528 const expectedFileExtDesc = '.bin file.';
529 @@endif
530 if (fileExt === expectedFileExt) {
531 uploadFile(files[0]);
532 } else {
533 cuteAlert({
534 type: 'error',
535 title: 'Incorrect File Format',
536 message: 'You selected the file &quot;' + files[0].name.toString() + '&quot;.<br />The firmware file must be a ' + expectedFileExtDesc
541 function uploadFile(file) {
542 _('upload_btn').disabled = true
543 try {
544 const formdata = new FormData();
545 formdata.append('upload', file, file.name);
546 const ajax = new XMLHttpRequest();
547 ajax.upload.addEventListener('progress', progressHandler, false);
548 ajax.addEventListener('load', completeHandler, false);
549 ajax.addEventListener('error', errorHandler, false);
550 ajax.addEventListener('abort', abortHandler, false);
551 ajax.open('POST', '/update');
552 ajax.setRequestHeader('X-FileSize', file.size);
553 ajax.send(formdata);
555 catch (e) {
556 _('upload_btn').disabled = false
560 function progressHandler(event) {
561 // _("loaded_n_total").innerHTML = "Uploaded " + event.loaded + " bytes of " + event.total;
562 const percent = Math.round((event.loaded / event.total) * 100);
563 _('progressBar').value = percent;
564 _('status').innerHTML = percent + '% uploaded... please wait';
567 function completeHandler(event) {
568 _('status').innerHTML = '';
569 _('progressBar').value = 0;
570 _('upload_btn').disabled = false
571 const data = JSON.parse(event.target.responseText);
572 if (data.status === 'ok') {
573 function showMessage() {
574 cuteAlert({
575 type: 'success',
576 title: 'Update Succeeded',
577 message: data.msg
580 // This is basically a delayed display of the success dialog with a fake progress
581 let percent = 0;
582 const interval = setInterval(()=>{
583 @@if (is8285):
584 percent = percent + 1;
585 @@else:
586 percent = percent + 2;
587 @@end
588 _('progressBar').value = percent;
589 _('status').innerHTML = percent + '% flashed... please wait';
590 if (percent === 100) {
591 clearInterval(interval);
592 _('status').innerHTML = '';
593 _('progressBar').value = 0;
594 showMessage();
596 }, 100);
597 } else if (data.status === 'mismatch') {
598 cuteAlert({
599 type: 'question',
600 title: 'Targets Mismatch',
601 message: data.msg,
602 confirmText: 'Flash anyway',
603 cancelText: 'Cancel'
604 }).then((e)=>{
605 const xmlhttp = new XMLHttpRequest();
606 xmlhttp.onreadystatechange = function() {
607 if (this.readyState === 4) {
608 _('status').innerHTML = '';
609 _('progressBar').value = 0;
610 if (this.status === 200) {
611 const data = JSON.parse(this.responseText);
612 cuteAlert({
613 type: 'info',
614 title: 'Force Update',
615 message: data.msg
617 } else {
618 cuteAlert({
619 type: 'error',
620 title: 'Force Update',
621 message: 'An error occurred trying to force the update'
626 xmlhttp.open('POST', '/forceupdate', true);
627 const data = new FormData();
628 data.append('action', e);
629 xmlhttp.send(data);
631 } else {
632 cuteAlert({
633 type: 'error',
634 title: 'Update Failed',
635 message: data.msg
640 function errorHandler(event) {
641 _('status').innerHTML = '';
642 _('progressBar').value = 0;
643 _('upload_btn').disabled = false
644 cuteAlert({
645 type: 'error',
646 title: 'Update Failed',
647 message: event.target.responseText
651 function abortHandler(event) {
652 _('status').innerHTML = '';
653 _('progressBar').value = 0;
654 _('upload_btn').disabled = false
655 cuteAlert({
656 type: 'info',
657 title: 'Update Aborted',
658 message: event.target.responseText
662 @@if isTX:
663 _('fileselect').addEventListener('change', (e) => {
664 const files = e.target.files || e.dataTransfer.files;
665 const reader = new FileReader();
666 reader.onload = function(x) {
667 xmlhttp = new XMLHttpRequest();
668 xmlhttp.onreadystatechange = function() {
669 _('fileselect').value = '';
670 if (this.readyState === 4) {
671 if (this.status === 200) {
672 cuteAlert({
673 type: 'info',
674 title: 'Upload Model Configuration',
675 message: this.responseText
677 } else {
678 cuteAlert({
679 type: 'error',
680 title: 'Upload Model Configuration',
681 message: 'An error occurred while uploading model configuration file'
686 xmlhttp.open('POST', '/import', true);
687 xmlhttp.setRequestHeader('Content-Type', 'application/json');
688 xmlhttp.send(x.target.result);
690 reader.readAsText(files[0]);
691 }, false);
692 @@end
694 // =========================================================
696 function callback(title, msg, url, getdata, success) {
697 return function(e) {
698 e.stopPropagation();
699 e.preventDefault();
700 xmlhttp = new XMLHttpRequest();
701 xmlhttp.onreadystatechange = function() {
702 if (this.readyState === 4) {
703 if (this.status === 200) {
704 if (success) success();
705 cuteAlert({
706 type: 'info',
707 title: title,
708 message: this.responseText
710 } else {
711 cuteAlert({
712 type: 'error',
713 title: title,
714 message: msg
719 xmlhttp.open('POST', url, true);
720 if (getdata) data = getdata(xmlhttp);
721 else data = null;
722 xmlhttp.send(data);
726 function setupNetwork(event) {
727 if (_('nt0').checked) {
728 callback('Set Home Network', 'An error occurred setting the home network', '/sethome?save', function() {
729 return new FormData(_('sethome'));
730 }, function() {
731 _('wifi-ssid').value = _('network').value;
732 _('wifi-password').value = _('password').value;
733 })(event);
735 if (_('nt1').checked) {
736 callback('Connect To Network', 'An error occurred connecting to the network', '/sethome', function() {
737 return new FormData(_('sethome'));
738 })(event);
740 if (_('nt2').checked) {
741 callback('Start Access Point', 'An error occurred starting the Access Point', '/access', null)(event);
743 if (_('nt3').checked) {
744 callback('Forget Home Network', 'An error occurred forgetting the home network', '/forget', null)(event);
748 @@if not isTX:
749 _('reset-model').addEventListener('click', callback('Reset Model Settings', 'An error occurred reseting model settings', '/reset?model', null));
750 @@end
751 _('reset-options').addEventListener('click', callback('Reset Runtime Options', 'An error occurred reseting runtime options', '/reset?options', null));
753 _('sethome').addEventListener('submit', setupNetwork);
754 _('connect').addEventListener('click', callback('Connect to Home Network', 'An error occurred connecting to the Home network', '/connect', null));
755 if (_('config')) {
756 _('config').addEventListener('submit', callback('Set Configuration', 'An error occurred updating the configuration', '/config',
757 (xmlhttp) => {
758 xmlhttp.setRequestHeader('Content-Type', 'application/json');
759 return JSON.stringify({
760 "pwm": getPwmFormData(),
761 "serial-protocol": +_('serial-protocol').value,
762 "serial1-protocol": +_('serial1-protocol').value,
763 "sbus-failsafe": +_('sbus-failsafe').value,
764 "modelid": +_('modelid').value,
765 "force-tlm": +_('force-tlm').checked,
766 "vbind": +_('vbind').value,
767 "uid": _('uid').value.split(',').map(Number),
769 }, () => {
770 originalUID = _('uid').value;
771 originalUIDType = 'Bound';
772 _('phrase').value = '';
773 updateUIDType(originalUIDType);
774 }));
777 function submitOptions(e) {
778 e.stopPropagation();
779 e.preventDefault();
780 const xhr = new XMLHttpRequest();
781 xhr.open('POST', '/options.json');
782 xhr.setRequestHeader('Content-Type', 'application/json');
783 // Convert the DOM element into a JSON object containing the form elements
784 const formElem = _('upload_options');
785 const formObject = Object.fromEntries(new FormData(formElem));
786 // Add in all the unchecked checkboxes which will be absent from a FormData object
787 formElem.querySelectorAll('input[type=checkbox]:not(:checked)').forEach((k) => formObject[k.name] = false);
788 // Force customised to true as this is now customising it
789 formObject['customised'] = true;
791 // Serialize and send the formObject
792 xhr.send(JSON.stringify(formObject, function(k, v) {
793 if (v === '') return undefined;
794 if (_(k)) {
795 if (_(k).type === 'color') return undefined;
796 if (_(k).type === 'checkbox') return v === 'on';
797 if (_(k).classList.contains('datatype-boolean')) return v === 'true';
798 if (_(k).classList.contains('array')) {
799 const arr = v.split(',').map((element) => {
800 return Number(element);
802 return arr.length === 0 ? undefined : arr;
805 if (typeof v === 'boolean') return v;
806 if (v === 'true') return true;
807 if (v === 'false') return false;
808 return isNaN(v) ? v : +v;
809 }));
811 xhr.onreadystatechange = function() {
812 if (this.readyState === 4) {
813 if (this.status === 200) {
814 cuteAlert({
815 type: 'question',
816 title: 'Upload Succeeded',
817 message: 'Reboot to take effect',
818 confirmText: 'Reboot',
819 cancelText: 'Close'
820 }).then((e) => {
821 @@if isTX:
822 originalUID = _('uid').value;
823 originalUIDType = 'Overridden';
824 _('phrase').value = '';
825 updateUIDType(originalUIDType);
826 @@end
827 if (e === 'confirm') {
828 const xhr = new XMLHttpRequest();
829 xhr.open('POST', '/reboot');
830 xhr.setRequestHeader('Content-Type', 'application/json');
831 xhr.onreadystatechange = function() {};
832 xhr.send();
835 } else {
836 cuteAlert({
837 type: 'error',
838 title: 'Upload Failed',
839 message: this.responseText
846 _('submit-options').addEventListener('click', submitOptions);
848 @@if isTX:
849 function submitButtonActions(e) {
850 e.stopPropagation();
851 e.preventDefault();
852 const xhr = new XMLHttpRequest();
853 xhr.open('POST', '/config');
854 xhr.setRequestHeader('Content-Type', 'application/json');
855 // put in the colors
856 if (buttonActions[0]) buttonActions[0].color = to8bit(_(`button1-color`).value)
857 if (buttonActions[1]) buttonActions[1].color = to8bit(_(`button2-color`).value)
858 xhr.send(JSON.stringify({'button-actions': buttonActions}));
860 xhr.onreadystatechange = function() {
861 if (this.readyState === 4) {
862 if (this.status === 200) {
863 cuteAlert({
864 type: 'info',
865 title: 'Success',
866 message: 'Button actions have been saved'
868 } else {
869 cuteAlert({
870 type: 'error',
871 title: 'Failed',
872 message: 'An error occurred while saving button configuration'
878 _('submit-actions').addEventListener('click', submitButtonActions);
879 @@end
881 function updateOptions(data) {
882 for (const [key, value] of Object.entries(data)) {
883 if (key ==='wifi-on-interval' && value === -1) continue;
884 if (_(key)) {
885 if (_(key).type === 'checkbox') {
886 _(key).checked = value;
887 } else {
888 if (Array.isArray(value)) _(key).value = value.toString();
889 else _(key).value = value;
891 if (_(key).onchange) _(key).onchange();
894 if (data['wifi-ssid']) _('homenet').textContent = data['wifi-ssid'];
895 else _('connect').style.display = 'none';
896 if (data['customised']) _('reset-options').style.display = 'block';
897 _('submit-options').disabled = false;
900 @@if isTX:
901 function toRGB(c)
903 r = c & 0xE0 ;
904 r = ((r << 16) + (r << 13) + (r << 10)) & 0xFF0000;
905 g = c & 0x1C;
906 g = ((g<< 11) + (g << 8) + (g << 5)) & 0xFF00;
907 b = ((c & 0x3) << 1) + ((c & 0x3) >> 1);
908 b = (b << 5) + (b << 2) + (b >> 1);
909 s = (r+g+b).toString(16);
910 return '#' + "000000".substring(0, 6-s.length) + s;
913 function updateButtons(data) {
914 buttonActions = data;
915 for (const [b, _v] of Object.entries(data)) {
916 for (const [p, v] of Object.entries(_v['action'])) {
917 appendRow(parseInt(b), parseInt(p), v);
919 if (_v['color'] !== undefined) {
920 _(`button${parseInt(b)+1}-color-div`).style.display = 'block';
922 _(`button${parseInt(b)+1}-color`).value = toRGB(_v['color']);
924 _('button1-color').oninput = changeCurrentColors;
925 _('button2-color').oninput = changeCurrentColors;
928 function changeCurrentColors() {
929 if (colorTimer === undefined) {
930 sendCurrentColors();
931 colorTimer = setInterval(timeoutCurrentColors, 50);
932 } else {
933 colorUpdated = true;
937 function to8bit(v)
939 v = parseInt(v.substring(1), 16)
940 return ((v >> 16) & 0xE0) + ((v >> (8+3)) & 0x1C) + ((v >> 6) & 0x3)
943 function sendCurrentColors() {
944 const formData = new FormData(_('button_actions'));
945 const data = Object.fromEntries(formData);
946 colors = [];
947 for (const [k, v] of Object.entries(data)) {
948 if (_(k) && _(k).type === 'color') {
949 const index = parseInt(k.substring('6')) - 1;
950 if (_(k + '-div').style.display === 'none') colors[index] = -1;
951 else colors[index] = to8bit(v);
954 const xmlhttp = new XMLHttpRequest();
955 xmlhttp.open('POST', '/buttons', true);
956 xmlhttp.setRequestHeader('Content-type', 'application/json');
957 xmlhttp.send(JSON.stringify(colors));
958 colorUpdated = false;
961 function timeoutCurrentColors() {
962 if (colorUpdated) {
963 sendCurrentColors();
964 } else {
965 clearInterval(colorTimer);
966 colorTimer = undefined;
970 function checkEnableButtonActionSave() {
971 let disable = false;
972 for (const [b, _v] of Object.entries(buttonActions)) {
973 for (const [p, v] of Object.entries(_v['action'])) {
974 if (v['action'] !== 0 && (_(`select-press-${b}-${p}`).value === '' || _(`select-long-${b}-${p}`).value === '' || _(`select-short-${b}-${p}`).value === '')) {
975 disable = true;
979 _('submit-actions').disabled = disable;
982 function changeAction(b, p, value) {
983 buttonActions[b]['action'][p]['action'] = value;
984 if (value === 0) {
985 _(`select-press-${b}-${p}`).value = '';
986 _(`select-long-${b}-${p}`).value = '';
987 _(`select-short-${b}-${p}`).value = '';
989 checkEnableButtonActionSave();
992 function changePress(b, p, value) {
993 buttonActions[b]['action'][p]['is-long-press'] = (value==='true');
994 _(`mui-long-${b}-${p}`).style.display = value==='true' ? 'block' : 'none';
995 _(`mui-short-${b}-${p}`).style.display = value==='true' ? 'none' : 'block';
996 checkEnableButtonActionSave();
999 function changeCount(b, p, value) {
1000 buttonActions[b]['action'][p]['count'] = parseInt(value);
1001 _(`select-long-${b}-${p}`).value = value;
1002 _(`select-short-${b}-${p}`).value = value;
1003 checkEnableButtonActionSave();
1006 function appendRow(b,p,v) {
1007 const row = _('button-actions').insertRow();
1008 row.innerHTML = `
1009 <td>
1010 Button ${parseInt(b)+1}
1011 </td>
1012 <td>
1013 <div class="mui-select">
1014 <select onchange="changeAction(${b}, ${p}, parseInt(this.value));">
1015 <option value='0' ${v['action']===0 ? 'selected' : ''}>Unused</option>
1016 <option value='1' ${v['action']===1 ? 'selected' : ''}>Increase Power</option>
1017 <option value='2' ${v['action']===2 ? 'selected' : ''}>Go to VTX Band Menu</option>
1018 <option value='3' ${v['action']===3 ? 'selected' : ''}>Go to VTX Channel Menu</option>
1019 <option value='4' ${v['action']===4 ? 'selected' : ''}>Send VTX Settings</option>
1020 <option value='5' ${v['action']===5 ? 'selected' : ''}>Start WiFi</option>
1021 <option value='6' ${v['action']===6 ? 'selected' : ''}>Enter Binding Mode</option>
1022 <option value='7' ${v['action']===7 ? 'selected' : ''}>Start BLE Joystick</option>
1023 </select>
1024 <label>Action</label>
1025 </div>
1026 </td>
1027 <td>
1028 <div class="mui-select">
1029 <select id="select-press-${b}-${p}" onchange="changePress(${b}, ${p}, this.value);">
1030 <option value='' disabled hidden ${v['action']===0 ? 'selected' : ''}></option>
1031 <option value='false' ${v['is-long-press']===false ? 'selected' : ''}>Short press (click)</option>
1032 <option value='true' ${v['is-long-press']===true ? 'selected' : ''}>Long press (hold)</option>
1033 </select>
1034 <label>Press</label>
1035 </div>
1036 </td>
1037 <td>
1038 <div class="mui-select" id="mui-long-${b}-${p}" style="display:${buttonActions[b]['action'][p]['is-long-press'] ? "block": "none"};">
1039 <select id="select-long-${b}-${p}" onchange="changeCount(${b}, ${p}, this.value);">
1040 <option value='' disabled hidden ${v['action']===0 ? 'selected' : ''}></option>
1041 <option value='0' ${v['count']===0 ? 'selected' : ''}>for 0.5 seconds</option>
1042 <option value='1' ${v['count']===1 ? 'selected' : ''}>for 1 second</option>
1043 <option value='2' ${v['count']===2 ? 'selected' : ''}>for 1.5 seconds</option>
1044 <option value='3' ${v['count']===3 ? 'selected' : ''}>for 2 seconds</option>
1045 <option value='4' ${v['count']===4 ? 'selected' : ''}>for 2.5 seconds</option>
1046 <option value='5' ${v['count']===5 ? 'selected' : ''}>for 3 seconds</option>
1047 <option value='6' ${v['count']===6 ? 'selected' : ''}>for 3.5 seconds</option>
1048 <option value='7' ${v['count']===7 ? 'selected' : ''}>for 4 seconds</option>
1049 </select>
1050 <label>Count</label>
1051 </div>
1052 <div class="mui-select" id="mui-short-${b}-${p}" style="display:${buttonActions[b]['action'][p]['is-long-press'] ? "none": "block"};">
1053 <select id="select-short-${b}-${p}" onchange="changeCount(${b}, ${p}, this.value);">
1054 <option value='' disabled hidden ${v['action']===0 ? 'selected' : ''}></option>
1055 <option value='0' ${v['count']===0 ? 'selected' : ''}>1 time</option>
1056 <option value='1' ${v['count']===1 ? 'selected' : ''}>2 times</option>
1057 <option value='2' ${v['count']===2 ? 'selected' : ''}>3 times</option>
1058 <option value='3' ${v['count']===3 ? 'selected' : ''}>4 times</option>
1059 <option value='4' ${v['count']===4 ? 'selected' : ''}>5 times</option>
1060 <option value='5' ${v['count']===5 ? 'selected' : ''}>6 times</option>
1061 <option value='6' ${v['count']===6 ? 'selected' : ''}>7 times</option>
1062 <option value='7' ${v['count']===7 ? 'selected' : ''}>8 times</option>
1063 </select>
1064 <label>Count</label>
1065 </div>
1066 </td>
1069 @@end
1071 md5 = function() {
1072 const k = [];
1073 let i = 0;
1075 for (; i < 64;) {
1076 k[i] = 0 | (Math.abs(Math.sin(++i)) * 4294967296);
1079 function calcMD5(str) {
1080 let b; let c; let d; let j;
1081 const x = [];
1082 const str2 = unescape(encodeURI(str));
1083 let a = str2.length;
1084 const h = [b = 1732584193, c = -271733879, ~b, ~c];
1085 let i = 0;
1087 for (; i <= a;) x[i >> 2] |= (str2.charCodeAt(i) || 128) << 8 * (i++ % 4);
1089 x[str = (a + 8 >> 6) * 16 + 14] = a * 8;
1090 i = 0;
1092 for (; i < str; i += 16) {
1093 a = h; j = 0;
1094 for (; j < 64;) {
1095 a = [
1096 d = a[3],
1097 ((b = a[1] | 0) +
1098 ((d = (
1099 (a[0] +
1101 b & (c = a[2]) | ~b & d,
1102 d & b | ~d & c,
1103 b ^ c ^ d,
1104 c ^ (b | ~d)
1105 ][a = j >> 4]
1107 (k[j] +
1108 (x[[
1110 5 * j + 1,
1111 3 * j + 5,
1112 7 * j
1113 ][a] % 16 + i] | 0)
1115 )) << (a = [
1116 7, 12, 17, 22,
1117 5, 9, 14, 20,
1118 4, 11, 16, 23,
1119 6, 10, 15, 21
1120 ][4 * a + j++ % 4]) | d >>> 32 - a)
1126 for (j = 4; j;) h[--j] = h[j] + a[j];
1129 str = [];
1130 for (; j < 32;) str.push(((h[j >> 3] >> ((1 ^ j++ & 7) * 4)) & 15) * 16 + ((h[j >> 3] >> ((1 ^ j++ & 7) * 4)) & 15));
1132 return new Uint8Array(str);
1134 return calcMD5;
1135 }();
1137 function isValidUidByte(s) {
1138 let f = parseFloat(s);
1139 return !isNaN(f) && isFinite(s) && Number.isInteger(f) && f >= 0 && f < 256;
1142 function uidBytesFromText(text) {
1143 // If text is 4-6 numbers separated with [commas]/[spaces] use as a literal UID
1144 // This is a strict parser to not just extract numbers from text, but only accept if text is only UID bytes
1145 if (/^[0-9, ]+$/.test(text))
1147 let asArray = text.split(',').filter(isValidUidByte).map(Number);
1148 if (asArray.length >= 4 && asArray.length <= 6)
1150 while (asArray.length < 6)
1151 asArray.unshift(0);
1152 return asArray;
1156 const bindingPhraseFull = `-DMY_BINDING_PHRASE="${text}"`;
1157 const bindingPhraseHashed = md5(bindingPhraseFull);
1158 return bindingPhraseHashed.subarray(0, 6);
1161 function initBindingPhraseGen() {
1162 const uid = _('uid');
1164 function setOutput(text) {
1165 if (text.length === 0) {
1166 uid.value = originalUID.toString();
1167 updateUIDType(originalUIDType);
1169 else {
1170 uid.value = uidBytesFromText(text.trim());
1171 updateUIDType('Modified');
1175 function updateValue(e) {
1176 setOutput(e.target.value);
1179 _('phrase').addEventListener('input', updateValue);
1180 setOutput('');
1183 @@include("libs.js")