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;
17 return document
.getElementById(el
);
20 function getPwmFormData() {
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}`);
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>`;
51 }).join('') + '</select></div>';
55 function generateFeatureBadges(features
) {
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>`;
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>`;
72 function updatePwmSettings(arPwm
) {
73 if (arPwm
=== undefined) {
74 if (_('model_tab')) _('model_tab').style
.display
= 'none';
77 var pinRxIndex
= undefined;
78 var pinTxIndex
= undefined;
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'];
94 modes
.push(undefined);
97 modes
.push('Serial TX');
98 modes
.push(undefined); // SCL
99 modes
.push(undefined); // SDA
100 modes
.push(undefined); // true PWM
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
109 modes
.push(undefined); // Serial
111 modes
.push('I2C SCL');
113 modes
.push(undefined);
116 modes
.push('I2C SDA');
118 modes
.push(undefined);
120 modes
.push(undefined); // true PWM
124 modes
.push('Serial2 RX');
126 modes
.push(undefined);
129 modes
.push('Serial2 TX');
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
)
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';
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';
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
223 setDisabled(pinRxIndex
, true);
224 setDisabled(pinTxIndex
, true);
225 pinTxMode
.disabled
= true;
226 _('serial-config').style
.display
= 'block';
227 _('baud-config').style
.display
= 'block';
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
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
;
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';
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
= '';
269 _('modelid').value
= storedModelId
;
272 _('modelNum').style
.display
= 'none';
273 _('modelid').value
= '255';
276 // Start on the model tab
277 mui
.tabs
.activate('pane-justified-3');
279 // Start on the options tab
280 mui
.tabs
.activate('pane-justified-1');
286 function updateUIDType(uidtype
) {
291 if (!uidtype
|| uidtype
.startsWith('Not set')) // TX
293 bg
= '#D50000'; // red/white
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
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';
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';
333 bg
= '#1976D2'; // blue/white
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
;
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
;
359 _('apmode').style
.display
= 'block';
362 if (data
.hasOwnProperty('modelid') && data
.modelid
!== 255) {
363 _('modelNum').style
.display
= 'block';
364 _('model-match').checked
= true;
365 storedModelId
= data
.modelid
;
367 _('modelNum').style
.display
= 'none';
368 _('model-match').checked
= false;
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';
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';
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';
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']);
454 _('button-tab').style
.display
= 'none';
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);
473 function getNetworks() {
474 const xmlhttp
= new XMLHttpRequest();
475 xmlhttp
.onload = function() {
476 if (this.status
=== 204) {
477 setTimeout(getNetworks
, 2000);
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);
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();
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
) {
515 if (e
.target
=== _('filedrag')) e
.target
.className
= (e
.type
=== 'dragover' ? 'hover' : '');
518 function fileSelectHandler(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!';
527 const expectedFileExt
= 'bin';
528 const expectedFileExtDesc
= '.bin file.';
530 if (fileExt
=== expectedFileExt
) {
531 uploadFile(files
[0]);
535 title
: 'Incorrect File Format',
536 message
: 'You selected the file "' + files
[0].name
.toString() + '".<br />The firmware file must be a ' + expectedFileExtDesc
541 function uploadFile(file
) {
542 _('upload_btn').disabled
= true
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
);
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() {
576 title
: 'Update Succeeded',
580 // This is basically a delayed display of the success dialog with a fake progress
582 const interval
= setInterval(()=>{
584 percent
= percent
+ 1;
586 percent
= percent
+ 2;
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;
597 } else if (data
.status
=== 'mismatch') {
600 title
: 'Targets Mismatch',
602 confirmText
: 'Flash anyway',
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
);
614 title
: 'Force Update',
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
);
634 title
: 'Update Failed',
640 function errorHandler(event
) {
641 _('status').innerHTML
= '';
642 _('progressBar').value
= 0;
643 _('upload_btn').disabled
= false
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
657 title
: 'Update Aborted',
658 message
: event
.target
.responseText
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) {
674 title
: 'Upload Model Configuration',
675 message
: this.responseText
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]);
694 // =========================================================
696 function callback(title
, msg
, url
, getdata
, success
) {
700 xmlhttp
= new XMLHttpRequest();
701 xmlhttp
.onreadystatechange = function() {
702 if (this.readyState
=== 4) {
703 if (this.status
=== 200) {
704 if (success
) success();
708 message
: this.responseText
719 xmlhttp
.open('POST', url
, true);
720 if (getdata
) data
= getdata(xmlhttp
);
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'));
731 _('wifi-ssid').value
= _('network').value
;
732 _('wifi-password').value
= _('password').value
;
735 if (_('nt1').checked
) {
736 callback('Connect To Network', 'An error occurred connecting to the network', '/sethome', function() {
737 return new FormData(_('sethome'));
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
);
749 _('reset-model').addEventListener('click', callback('Reset Model Settings', 'An error occurred reseting model settings', '/reset?model', null));
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));
756 _('config').addEventListener('submit', callback('Set Configuration', 'An error occurred updating the configuration', '/config',
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
),
770 originalUID
= _('uid').value
;
771 originalUIDType
= 'Bound';
772 _('phrase').value
= '';
773 updateUIDType(originalUIDType
);
777 function submitOptions(e
) {
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;
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
;
811 xhr
.onreadystatechange = function() {
812 if (this.readyState
=== 4) {
813 if (this.status
=== 200) {
816 title
: 'Upload Succeeded',
817 message
: 'Reboot to take effect',
818 confirmText
: 'Reboot',
822 originalUID
= _('uid').value
;
823 originalUIDType
= 'Overridden';
824 _('phrase').value
= '';
825 updateUIDType(originalUIDType
);
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() {};
838 title
: 'Upload Failed',
839 message
: this.responseText
846 _('submit-options').addEventListener('click', submitOptions
);
849 function submitButtonActions(e
) {
852 const xhr
= new XMLHttpRequest();
853 xhr
.open('POST', '/config');
854 xhr
.setRequestHeader('Content-Type', 'application/json');
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) {
866 message
: 'Button actions have been saved'
872 message
: 'An error occurred while saving button configuration'
878 _('submit-actions').addEventListener('click', submitButtonActions
);
881 function updateOptions(data
) {
882 for (const [key
, value
] of Object
.entries(data
)) {
883 if (key
==='wifi-on-interval' && value
=== -1) continue;
885 if (_(key
).type
=== 'checkbox') {
886 _(key
).checked
= value
;
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;
904 r
= ((r
<< 16) + (r
<< 13) + (r
<< 10)) & 0xFF0000;
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) {
931 colorTimer
= setInterval(timeoutCurrentColors
, 50);
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
);
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() {
965 clearInterval(colorTimer
);
966 colorTimer
= undefined;
970 function checkEnableButtonActionSave() {
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
=== '')) {
979 _('submit-actions').disabled
= disable
;
982 function changeAction(b
, p
, value
) {
983 buttonActions
[b
]['action'][p
]['action'] = value
;
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();
1010 Button ${parseInt(b)+1}
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>
1024 <label>Action</label>
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>
1034 <label>Press</label>
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>
1050 <label>Count</label>
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>
1064 <label>Count</label>
1076 k
[i
] = 0 | (Math
.abs(Math
.sin(++i
)) * 4294967296);
1079 function calcMD5(str
) {
1080 let b
; let c
; let d
; let j
;
1082 const str2
= unescape(encodeURI(str
));
1083 let a
= str2
.length
;
1084 const h
= [b
= 1732584193, c
= -271733879, ~b
, ~c
];
1087 for (; i
<= a
;) x
[i
>> 2] |= (str2
.charCodeAt(i
) || 128) << 8 * (i
++ % 4);
1089 x
[str
= (a
+ 8 >> 6) * 16 + 14] = a
* 8;
1092 for (; i
< str
; i
+= 16) {
1101 b
& (c
= a
[2]) | ~b
& d
,
1120 ][4 * a
+ j
++ % 4]) | d
>>> 32 - a
)
1126 for (j
= 4; j
;) h
[--j
] = h
[j
] + a
[j
];
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
);
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)
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
);
1170 uid
.value
= uidBytesFromText(text
.trim());
1171 updateUIDType('Modified');
1175 function updateValue(e
) {
1176 setOutput(e
.target
.value
);
1179 _('phrase').addEventListener('input', updateValue
);
1183 @@include("libs.js")