Mu2_Deploy/ui/general/spirilink.js

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`;
}
}