365 lines
15 KiB
JavaScript
365 lines
15 KiB
JavaScript
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 += "<br>" + 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`;
|
|
}
|
|
}
|