const scriptLocation = "/usr/local/echopilot/"; const confLocation = "/etc/wifibroadcast.cfg"; const encryptionFileLocation = "/etc/drone.key"; // Destination for encryption key // Elements const version = document.getElementById("version"); const file_location = document.getElementById("file_location"); const wifiChannel = document.getElementById("wifiChannel"); const wifiRegion = document.getElementById("wifiRegion"); const wifiTxPower = document.getElementById("wifiTxPower"); const tempInterval = document.getElementById("tempInterval"); const tempWarning = document.getElementById("tempWarning"); const droneMavlinkFwmark = document.getElementById("droneMavlinkFwmark"); const droneMavlinkType = document.getElementById("droneMavlinkType"); const droneMavlinkIP = document.getElementById("droneMavlinkIP"); const droneMavlinkPort = document.getElementById("droneMavlinkPort"); const droneVideoFwmark = document.getElementById("droneVideoFwmark"); const droneVideoType = document.getElementById("droneVideoType"); const droneVideoIP = document.getElementById("droneVideoIP"); const droneVideoPort = document.getElementById("droneVideoPort"); const injectRssi = document.getElementById("injectRssi"); const mavlinkSysId = document.getElementById("mavlinkSysId"); const mavlinkCompId = document.getElementById("mavlinkCompId"); const mavlinkTcpPort = document.getElementById("mavlinkTcpPort"); const spiriLinkForm = document.getElementById("spiriLinkForm"); const output = document.getElementById("output"); const wifiChannels = [ // 2.4 GHz Channels ["1", "1 (2.412 GHz)"], ["2", "2 (2.417 GHz)"], ["3", "3 (2.422 GHz)"], ["4", "4 (2.427 GHz)"], ["5", "5 (2.432 GHz)"], ["6", "6 (2.437 GHz)"], ["7", "7 (2.442 GHz)"], ["8", "8 (2.447 GHz)"], ["9", "9 (2.452 GHz)"], ["10", "10 (2.457 GHz)"], ["11", "11 (2.462 GHz)"], ["12", "12 (2.467 GHz)"], ["13", "13 (2.472 GHz)"], ["14", "14 (2.484 GHz, Japan only)"], // 5 GHz Channels ["36", "36 (5.180 GHz)"], ["40", "40 (5.200 GHz)"], ["44", "44 (5.220 GHz)"], ["48", "48 (5.240 GHz)"], ["52", "52 (5.260 GHz)"], ["56", "56 (5.280 GHz)"], ["60", "60 (5.300 GHz)"], ["64", "64 (5.320 GHz)"], ["100", "100 (5.500 GHz)"], ["104", "104 (5.520 GHz)"], ["108", "108 (5.540 GHz)"], ["112", "112 (5.560 GHz)"], ["116", "116 (5.580 GHz)"], ["120", "120 (5.600 GHz)"], ["124", "124 (5.620 GHz)"], ["128", "128 (5.640 GHz)"], ["132", "132 (5.660 GHz)"], ["136", "136 (5.680 GHz)"], ["140", "140 (5.700 GHz)"], ["149", "149 (5.745 GHz)"], ["153", "153 (5.765 GHz)"], ["157", "157 (5.785 GHz)"], ["161", "161 (5.805 GHz)"], ["165", "165 (5.825 GHz)"] ] const wifiRegions = [ ["BO", "BO (Max TX)"], ["US", "US (United States)"], ["EU", "EU (Europe)"], ["JP", "JP (Japan)"], ["AU", "AU (Australia, Max TX)"], ["CA", "CA (Canada)"], ["CN", "CN (China)"], ["IN", "IN (India)"], ["ZA", "ZA (South Africa)"], ["KR", "KR (South Korea)"] ] const connectionTypes = [ ["listen", "Listen"], ["connect", "Connect"] ] // Load initial settings document.onload = initPage(); // Save file button document.getElementById("save").addEventListener("click", saveSettings); // Apply encryption file button document.getElementById("applyEncryptionFile").addEventListener("click", applyEncryptionFile); // Function to initialize the page function initPage() { file_location.innerHTML = confLocation; const inputs = document.querySelectorAll("input, select, textarea"); inputs.forEach(input => { input.addEventListener("focus", resetForm); }); cockpit.file(confLocation) .read().then((content) => successReadFile(content)) .catch(error => failureReadFile(error)); } // Load configuration values from the configuration file function successReadFile(content) { try { // WiFi Configuration // Remove surrounding quotes from wifi_region value if present const currentWifiChannel = getValueByKey(content, "common", "wifi_channel"); const currentWifiRegion = getValueByKey(content, "common", "wifi_region").replace(/^['"]|['"]$/g, ""); const txPowerValue = getValueByKey(content, "common", "wifi_txpower"); addDropDown(wifiChannel, wifiChannels, currentWifiChannel); addDropDown(wifiRegion, wifiRegions, currentWifiRegion); wifiTxPower.value = txPowerValue === "None" ? "" : txPowerValue; // None should result in an empty string // Temperature Configuration tempInterval.value = getValueByKey(content, "common", "temp_measurement_interval"); tempWarning.value = getValueByKey(content, "common", "temp_overheat_warning"); // Drone Video Configuration droneVideoFwmark.value = getValueByKey(content, "drone_video", "fwmark"); const droneVideoPeer = getValueByKey(content, "drone_video", "peer"); if (droneVideoPeer) { const peer = parsePeer(droneVideoPeer); addDropDown(droneVideoType, connectionTypes, droneVideoType.value); droneVideoIP.value = peer.ip; droneVideoPort.value = peer.port; } // Drone MAVLink Configuration droneMavlinkFwmark.value = getValueByKey(content, "drone_mavlink", "fwmark"); const droneMavlinkPeer = getValueByKey(content, "drone_mavlink", "peer"); if (droneMavlinkPeer) { const peer = parsePeer(droneMavlinkPeer); addDropDown(droneMavlinkType, connectionTypes, peer.type); droneMavlinkIP.value = peer.ip; droneMavlinkPort.value = peer.port; } // MAVLink Settings injectRssi.checked = getValueByKey(content, "mavlink", "inject_rssi") === "True"; mavlinkSysId.value = getValueByKey(content, "mavlink", "mavlink_sys_id"); mavlinkCompId.value = getValueByKey(content, "mavlink", "mavlink_comp_id"); const tcpPortValue = getValueByKey(content, "mavlink", "mavlink_tcp_port"); mavlinkTcpPort.value = tcpPortValue === "None" ? "" : tcpPortValue; } catch (e) { failureReadFile(e); } } // Log error message and display it // TODO: Add more error handling for failed file reads function failureReadFile(error) { console.error("Error : " + error.message); displayFail(error.message) } // Validate the form using non-standard input validation conditions. // Additional checks for IP, port, and system/component ID fields // Returns true if all fields are valid, false otherwise function validateSpiriLinkForm(form) { const ipformat = /^(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$/; const portformat = /^([1-9][0-9]{0,3}|[1-5][0-9]{4}|6[0-4][0-9]{3}|65[0-4][0-9]{2}|655[0-2][0-9]|6553[0-5])$/; let isValid = true; defaultValidation = form.checkValidity(); const inputs = form.querySelectorAll("input, select, textarea"); inputs.forEach(input => { if (input.value === "") { isValid = !input.hasAttribute("required"); setValidationVisuals(input, isValid); } else { if (input.id === "droneMavlinkIP" || input.id === "droneVideoIP") { isValid = input.value.match(ipformat); setValidationVisuals(input, isValid); } else if (input.id === "droneMavlinkPort" || input.id === "droneVideoPort" || input.id === "mavlinkTcpPort") { isValid = input.value.match(portformat); setValidationVisuals(input, isValid); } else if (input.id === "mavlinkSysId" || input.id === "mavlinkCompId") { isValid = input.value > 1 || input.value < 255; setValidationVisuals(input, isValid); } } }); form.classList.add("was-validated"); if (!isValid || !defaultValidation) return false; return true; } // Set validation visuals for input fields // Adds or removes is-invalid class based on the validity of the input // The setCustomValidity function triggers the actual field visuals. // Giving it an empty string assumes the input is valid. function setValidationVisuals(input, isValid) { if (isValid) { input.classList.remove("is-invalid"); input.setCustomValidity(""); } else { input.classList.add("is-invalid"); input.setCustomValidity("Invalid input"); } } // Save configuration values to the configuration file function saveSettings() { const dataIsValid = validateSpiriLinkForm(spiriLinkForm); if (!dataIsValid) return; try { cockpit.file(confLocation) .read() .then((content) => { // WiFi & Temperature Configuration content = setValueByKey(content, "[common]", "wifi_channel", wifiChannel.value); content = setValueByKey(content, "[common]", "wifi_region", wifiRegion.value); content = setValueByKey(content, "[common]", "wifi_txpower", wifiTxPower.value === "" ? "None" : wifiTxPower.value); content = setValueByKey(content, "[common]", "temp_measurement_interval", tempInterval.value); content = setValueByKey(content, "[common]", "temp_overheat_warning", tempWarning.value); // Drone Video Configuration const droneVideoPeer = `${droneVideoType.value}://${droneVideoIP.value}:${droneVideoPort.value}`; content = setValueByKey(content, "[drone_video]", "fwmark", droneVideoFwmark.value); content = setValueByKey(content, "[drone_video]", "peer", droneVideoPeer); // Drone MAVLink Configuration const droneMavlinkPeer = `${droneMavlinkType.value}://${droneMavlinkIP.value}:${droneMavlinkPort.value}`; content = setValueByKey(content, "[drone_mavlink]", "fwmark", droneMavlinkFwmark.value); content = setValueByKey(content, "[drone_mavlink]", "peer", droneMavlinkPeer); // MAVLink Configuration content = setValueByKey(content, "[mavlink]", "inject_rssi", injectRssi.checked ? "True" : "False"); content = setValueByKey(content, "[mavlink]", "mavlink_sys_id", mavlinkSysId.value); content = setValueByKey(content, "[mavlink]", "mavlink_comp_id", mavlinkCompId.value); content = setValueByKey(content, "[mavlink]", "mavlink_tcp_port", mavlinkTcpPort.value === "" ? "None" : mavlinkTcpPort.value); cockpit.file(confLocation, { superuser: "try" }).replace(content) .then(() => { restartWifibroadcastService(); displaySuccess("Configuration saved successfully."); }) .catch((error) => { displayFail("Failed to save configuration: " + error.message); }); }) .catch(error => { displayFail("Failed to save configuration: " + error.message); }); } catch (e) { console.error("Error during save operation:", e); failureReadFile(e); } } // Apply encryption file from base station function applyEncryptionFile() { const command = ` /usr/bin/sshpass -p 'spirifriend' scp -o StrictHostKeyChecking=no spiri@spiri-base.local:/home/spiri/drone.key /tmp/drone.key && /usr/bin/sudo /bin/mv /tmp/drone.key ${encryptionFileLocation} && /usr/bin/sudo /bin/systemctl restart wifibroadcast@drone `; cockpit.spawn(["bash", "-c", command], { superuser: "require" }) .then(() => { displaySuccess("Encryption key applied and wifibroadcast service restarted."); }) .catch((error) => { console.error("Failed to apply encryption key or restart service:", error); displayFail("Failed to apply encryption key or restart service: " + error); }); } // Restart wifibroadcast service function restartWifibroadcastService() { cockpit.spawn(["systemctl", "restart", "wifibroadcast@drone"], { superuser: "require" }) .then(() => { displaySuccess("wifibroadcast@drone service restarted."); }) .catch((error) => { console.error("Failed to restart wifibroadcast@drone service:", error); displayFail("Failed to restart wifibroadcast@drone service: " + error); }); } // Display Success result function displaySuccess(msg) { result.style.color = "green"; if (result.innerHTML === "") result.innerHTML = msg; else result.innerHTML += "
" + msg; // setTimeout(() => result.innerHTML = "", 5000); } // Display failure message function displayFail(error) { result.style.color = "red"; result.innerHTML = "Error : " + error; } // Reset & clear form data function resetForm() { result.innerHTML = ""; const inputs = spiriLinkForm.querySelectorAll("input, select, textarea"); inputs.forEach(input => { input.setCustomValidity(""); input.classList.remove("is-invalid"); input.classList.remove("is-valid"); }); spiriLinkForm.classList.remove("was-validated"); spiriLinkForm.classList.add("needs-validation"); } function addDropDown(box, pairs, defaultValue) { try { for(let i = 0; i < pairs.length; i++){ if (pairs[i].length == 0 || pairs[i][0] === "" || pairs[i][1] === "") continue; const option = document.createElement("option"); option.value = pairs[i][0]; option.text = pairs[i][1]; box.add(option); if (defaultValue === option.value) { box.value = option.value; } } } catch(e) { displayFail(e) } } // Parse Peer configuration (type, IP, port) function parsePeer(peerString) { const peer = { type: "", ip: "", port: "" }; if (!peerString) return peer; const regex = /(listen|connect):\/\/([\d.]+):(\d+)/; const match = peerString.match(regex); if (match) { peer.type = match[1]; peer.ip = match[2]; peer.port = match[3]; } return peer; } // Get value by key from configuration content function getValueByKey(content, section, key) { const sectionRegex = new RegExp(`\\[${section}\\][\\s\\S]*?^${key}\\s*=\\s*(.*)`, "m"); const match = content.match(sectionRegex); if (match) { const value = match[1].trim(); return value; } else { return ""; } } // Set value by key in configuration content function setValueByKey(content, section, key, value) { const regex = new RegExp(`(${section}[\\s\\S]*?)(${key}\\s*=\\s*)(.*)`, "m"); if (content.match(regex)) { return content.replace(regex, `$1$2${value}`); } else { // Add key if not found const sectionRegex = new RegExp(`(${section}[\\s\\S]*?)\\n`, "m"); return content.match(sectionRegex) ? content.replace(sectionRegex, `$1\n${key} = ${value}\n`) : `${content}\n${section}\n${key} = ${value}\n`; } }