1255 lines
37 KiB
JavaScript
1255 lines
37 KiB
JavaScript
// Constants
|
|
const ACCESS_TECH_MAP = {
|
|
2: "UTRAN",
|
|
4: "UTRAN / HSDPA",
|
|
5: "UTRAN / HSUPA",
|
|
6: "UTRAN / HSDPA & HSUPA",
|
|
7: "E-UTRAN",
|
|
10: "E-UTRAN - 5GCN",
|
|
11: "NR - 5GCN",
|
|
12: "NG-RAN",
|
|
13: "E-UTRAN-NR dual",
|
|
};
|
|
|
|
const OPERATOR_STATE_MAP = {
|
|
0: { label: "Not Registered", class: "is-danger" },
|
|
1: { label: "Registered", class: "is-success" },
|
|
2: { label: "Searching", class: "is-warning" },
|
|
3: { label: "Denied", class: "is-danger" },
|
|
4: { label: "Unknown", class: "is-warning" },
|
|
5: { label: "Roaming", class: "is-success" },
|
|
};
|
|
|
|
const BANDWIDTH_MAP = {
|
|
6: "1.4 MHz",
|
|
15: "3 MHz",
|
|
25: "5 MHz",
|
|
50: "10 MHz",
|
|
75: "15 MHz",
|
|
100: "20 MHz",
|
|
};
|
|
|
|
const NR_BANDWIDTH_MAP = {
|
|
0: "5 MHz",
|
|
1: "10 MHz",
|
|
2: "15 MHz",
|
|
3: "20 MHz",
|
|
4: "25 MHz",
|
|
5: "30 MHz",
|
|
6: "40 MHz",
|
|
7: "50 MHz",
|
|
8: "60 MHz",
|
|
9: "70 MHz",
|
|
10: "80 MHz",
|
|
11: "90 MHz",
|
|
12: "100 MHz",
|
|
13: "200 MHz",
|
|
14: "400 MHz",
|
|
15: "35 MHz",
|
|
16: "45 MHz",
|
|
};
|
|
|
|
// Global variables for intervals
|
|
let atCommandInterval;
|
|
let connectionStatusInterval;
|
|
let trafficStatsInterval;
|
|
const DEFAULT_REFRESH_RATE = 5000; // 5 seconds
|
|
const TRAFFIC_STATS_REFRESH_RATE = 1000; // 1 second
|
|
const CONNECTION_CHECK_MULTIPLIER = 6; // Will make connection check 6 times slower
|
|
const STORAGE_KEY = "modemRefreshRate";
|
|
|
|
// Utility functions
|
|
function setText(id, text) {
|
|
const element = document.getElementById(id);
|
|
if (element) {
|
|
element.textContent = text;
|
|
}
|
|
}
|
|
|
|
// Helper function to format bytes to human-readable format
|
|
function formatBytes(bytes, decimals = 2) {
|
|
if (bytes === 0) return "0 Bytes";
|
|
|
|
const k = 1024;
|
|
const dm = decimals < 0 ? 0 : decimals;
|
|
const sizes = ["Bytes", "KB", "MB", "GB", "TB"];
|
|
|
|
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
|
|
|
return parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + " " + sizes[i];
|
|
}
|
|
|
|
function createTag(classes, text) {
|
|
const tag = document.createElement("span");
|
|
tag.classList.add(...classes);
|
|
tag.textContent = text;
|
|
return tag;
|
|
}
|
|
|
|
// Refresh control functions
|
|
function handleRefreshClick() {
|
|
const refreshButton = document.getElementById("handleRefreshClickButton");
|
|
if (refreshButton) {
|
|
refreshButton.disabled = true;
|
|
const icon = refreshButton.querySelector("i");
|
|
if (icon) {
|
|
icon.classList.add("fa-spin");
|
|
}
|
|
}
|
|
|
|
Promise.all([
|
|
fetchATCommandData(),
|
|
fetchConnectionStatus(),
|
|
fetchTrafficStats,
|
|
]).finally(() => {
|
|
if (refreshButton) {
|
|
refreshButton.disabled = false;
|
|
const icon = refreshButton.querySelector("i");
|
|
if (icon) {
|
|
icon.classList.remove("fa-spin");
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
// Modified and new functions for refresh rate control with persistence
|
|
function setupRefreshControls() {
|
|
const dropdownItems = document.querySelectorAll(
|
|
".dropdown-content .dropdown-item"
|
|
);
|
|
const dropdownButton = document.querySelector(
|
|
".dropdown-trigger button span"
|
|
);
|
|
|
|
// Get stored refresh rate or use default
|
|
const storedRate = localStorage.getItem(STORAGE_KEY);
|
|
const initialRate = storedRate ? parseInt(storedRate) : DEFAULT_REFRESH_RATE;
|
|
|
|
dropdownItems.forEach((item) => {
|
|
item.addEventListener("click", (e) => {
|
|
e.preventDefault();
|
|
const seconds = parseInt(e.target.textContent.trim().replace("s", ""));
|
|
updateRefreshRate(seconds);
|
|
|
|
// Update active state in dropdown
|
|
dropdownItems.forEach((di) => di.classList.remove("is-active"));
|
|
e.target.classList.add("is-active");
|
|
|
|
// Update dropdown button text
|
|
if (dropdownButton) {
|
|
dropdownButton.textContent = `${seconds}s`;
|
|
}
|
|
|
|
// Store the selected rate
|
|
localStorage.setItem(STORAGE_KEY, seconds * 1000);
|
|
});
|
|
});
|
|
|
|
// Set initial active state and start refresh
|
|
setInitialState(dropdownItems, dropdownButton, initialRate);
|
|
startPeriodicRefresh(initialRate);
|
|
}
|
|
|
|
function setInitialState(dropdownItems, dropdownButton, initialRate) {
|
|
const seconds = initialRate / 1000;
|
|
|
|
// Update dropdown button text
|
|
if (dropdownButton) {
|
|
dropdownButton.textContent = `${seconds}s`;
|
|
}
|
|
|
|
// Set active state in dropdown
|
|
const activeItem = Array.from(dropdownItems).find(
|
|
(item) => parseInt(item.textContent.trim().replace("s", "")) === seconds
|
|
);
|
|
if (activeItem) {
|
|
dropdownItems.forEach((di) => di.classList.remove("is-active"));
|
|
activeItem.classList.add("is-active");
|
|
}
|
|
}
|
|
|
|
function updateRefreshRate(seconds) {
|
|
const newRate = seconds * 1000; // Convert to milliseconds
|
|
|
|
// Clear existing intervals
|
|
clearInterval(atCommandInterval);
|
|
clearInterval(connectionStatusInterval);
|
|
clearInterval(trafficStatsInterval);
|
|
|
|
// Start new intervals
|
|
startPeriodicRefresh(newRate);
|
|
}
|
|
|
|
function startPeriodicRefresh(refreshRate = DEFAULT_REFRESH_RATE) {
|
|
// Clear any existing intervals
|
|
clearInterval(atCommandInterval);
|
|
clearInterval(connectionStatusInterval);
|
|
clearInterval(trafficStatsInterval);
|
|
|
|
// Start new intervals
|
|
atCommandInterval = setInterval(fetchATCommandData, refreshRate);
|
|
trafficStatsInterval = setInterval(
|
|
fetchTrafficStats,
|
|
TRAFFIC_STATS_REFRESH_RATE
|
|
);
|
|
connectionStatusInterval = setInterval(
|
|
fetchConnectionStatus,
|
|
refreshRate * CONNECTION_CHECK_MULTIPLIER
|
|
);
|
|
}
|
|
|
|
// AT Commands functions
|
|
async function fetchATCommandData() {
|
|
try {
|
|
const jsonData = await fetchAndParseData();
|
|
processATCommandData(jsonData);
|
|
console.log("Data fetched and processed successfully");
|
|
console.log(jsonData);
|
|
} catch (error) {
|
|
console.error("There was a problem with the fetch operation:", error);
|
|
}
|
|
}
|
|
|
|
async function fetchAndParseData() {
|
|
const response = await fetch("/cgi-bin/home_data.sh", {
|
|
method: "GET",
|
|
headers: {
|
|
"Content-Type": "application/json",
|
|
},
|
|
});
|
|
|
|
const rawData = await response.text();
|
|
|
|
if (!rawData || rawData.trim() === "") {
|
|
throw new Error("Empty or malformed response");
|
|
}
|
|
|
|
return JSON.parse(rawData);
|
|
}
|
|
|
|
function processATCommandData(jsonData) {
|
|
processSimData(jsonData);
|
|
processNetworkData(jsonData);
|
|
processModemData(jsonData);
|
|
processBandwidthData(jsonData);
|
|
processConnectedBands(jsonData);
|
|
processSignalStrength(jsonData);
|
|
processBandsTable(jsonData);
|
|
processCellInfo(jsonData);
|
|
processWANIPData(jsonData);
|
|
}
|
|
|
|
function processSimData(jsonData) {
|
|
const [simSlotResponse, phoneResponse, providerResponse] = jsonData;
|
|
|
|
// SIM Slot
|
|
const simSlot = extractValue(simSlotResponse.response);
|
|
setText("simSlot", simSlot);
|
|
|
|
// Phone Number
|
|
const phoneNumber = extractValue(phoneResponse.response).replace(
|
|
/["\,]/g,
|
|
""
|
|
);
|
|
setText("phoneNumber", phoneNumber);
|
|
|
|
// SIM Provider and Access Technology
|
|
const providerData = extractValue(providerResponse.response).split(",");
|
|
const simProvider = providerData[2].replace(/"/g, "").trim();
|
|
const accessTech = providerData[3].replace(/"/g, "").trim();
|
|
|
|
setText("simProvider", simProvider);
|
|
setText("accessTech", ACCESS_TECH_MAP[accessTech] || "Unknown");
|
|
|
|
// Additional SIM data
|
|
setText("imsi", jsonData[3].response.split("\n")[1].trim());
|
|
setText("iccid", extractValue(jsonData[4].response));
|
|
setText("imei", jsonData[5].response.split("\n")[1].trim());
|
|
}
|
|
|
|
function processNetworkData(jsonData) {
|
|
// SIM State
|
|
const simState = extractValue(jsonData[6].response);
|
|
const simStateElement = createTag(
|
|
simState === "READY"
|
|
? ["tag", "is-success", "has-text-white"]
|
|
: ["tag", "is-danger", "has-text-white"],
|
|
simState === "READY" ? "Inserted" : "Missing!"
|
|
);
|
|
const simStateContainer = document.getElementById("simState");
|
|
if (simStateContainer) {
|
|
simStateContainer.innerHTML = "";
|
|
simStateContainer.appendChild(simStateElement);
|
|
}
|
|
|
|
// APN
|
|
const apnData = extractValue(jsonData[7].response).split(",");
|
|
setText("apn", apnData[2].replace(/"/g, "").trim());
|
|
|
|
// Operator State
|
|
const operatorState = extractValue(jsonData[8].response).split(",")[1].trim();
|
|
const { label, class: className } = OPERATOR_STATE_MAP[operatorState] || {
|
|
label: "Unknown",
|
|
class: "is-warning",
|
|
};
|
|
const operatorStateElement = createTag(
|
|
["tag", className, "has-text-white"],
|
|
label
|
|
);
|
|
const operatorStateContainer = document.getElementById("operatorState");
|
|
if (operatorStateContainer) {
|
|
operatorStateContainer.innerHTML = "";
|
|
operatorStateContainer.appendChild(operatorStateElement);
|
|
}
|
|
}
|
|
|
|
function processModemData(jsonData) {
|
|
// Functionality State
|
|
const functionalityState = extractValue(jsonData[9].response);
|
|
const functionalityStateElement = createTag(
|
|
functionalityState === "1"
|
|
? ["tag", "is-success", "has-text-white"]
|
|
: ["tag", "is-danger", "has-text-white"],
|
|
functionalityState === "1" ? "Enabled" : "Disabled"
|
|
);
|
|
const functionalityStateContainer =
|
|
document.getElementById("functionalityState");
|
|
if (functionalityStateContainer) {
|
|
functionalityStateContainer.innerHTML = "";
|
|
functionalityStateContainer.appendChild(functionalityStateElement);
|
|
}
|
|
|
|
// Network Type
|
|
const servingCell = jsonData[10].response;
|
|
let networkType = determineNetworkType(servingCell);
|
|
setText("networkType", networkType);
|
|
|
|
// Modem Temperature
|
|
processTemperature(jsonData[11].response);
|
|
}
|
|
|
|
function processBandwidthData(jsonData) {
|
|
// Carrier Aggregation
|
|
const caState = jsonData[13].response.includes("SCC") ? "Multi" : "Inactive";
|
|
const caStateElement = createTag(
|
|
caState === "Multi"
|
|
? ["tag", "is-success", "has-text-white"]
|
|
: ["tag", "is-danger", "has-text-white"],
|
|
caState
|
|
);
|
|
const caStateContainer = document.getElementById("caState");
|
|
if (caStateContainer) {
|
|
caStateContainer.innerHTML = "";
|
|
caStateContainer.appendChild(caStateElement);
|
|
}
|
|
|
|
// Process bandwidth information
|
|
const networkTypeElement = document.getElementById("networkType");
|
|
if (networkTypeElement) {
|
|
const networkType = networkTypeElement.textContent;
|
|
if (networkType === "LTE" || networkType === "NR5G-NSA") {
|
|
processBandwidth(jsonData[13].response, networkType);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Helper functions
|
|
function extractValue(response) {
|
|
return response.split("\n")[1].split(":")[1].trim();
|
|
}
|
|
|
|
function determineNetworkType(servingCell) {
|
|
if (servingCell.includes("LTE")) {
|
|
return servingCell.includes("NR5G-NSA") ? "NR5G-NSA" : "LTE";
|
|
} else if (servingCell.includes("NR5G-SA")) {
|
|
return "NR5G-SA";
|
|
}
|
|
return "Unknown / No Signal";
|
|
}
|
|
|
|
function processTemperature(tempResponse) {
|
|
const temps = ["cpuss-0", "cpuss-1", "cpuss-2", "cpuss-3"].map((cpu) => {
|
|
const line = tempResponse.split("\n").find((l) => l.includes(cpu));
|
|
return parseInt(line.split(":")[1].split(",")[1].replace(/"/g, "").trim());
|
|
});
|
|
const avgTemp = temps.reduce((acc, t) => acc + t, 0) / temps.length;
|
|
setText("temp", `${avgTemp} °C`);
|
|
}
|
|
|
|
function processBandwidth(response, networkType) {
|
|
const sccLines = extractSCCData(response);
|
|
const pccLine = response.split("\n").find((line) => line.includes("PCC"));
|
|
const pccBW = pccLine.split(":")[1].split(",")[2].trim();
|
|
const pccBWParsed = BANDWIDTH_MAP[pccBW] || "Unknown";
|
|
|
|
if (networkType === "NR5G-NSA") {
|
|
processNR5GBandwidth(sccLines, pccBWParsed);
|
|
} else if (networkType === "NR5G-SA") {
|
|
processNR5GBandwidth(sccLines, pccBWParsed);
|
|
} else if (networkType === "LTE") {
|
|
processLTEBandwidth(sccLines, pccBWParsed);
|
|
} else {
|
|
setText("allBW", "Unknown");
|
|
}
|
|
}
|
|
|
|
function extractSCCData(response) {
|
|
return response
|
|
.split("\n")
|
|
.filter((line) => line.includes("SCC"))
|
|
.map((line) => line.trim());
|
|
}
|
|
|
|
function processNR5GBandwidth(sccLines, pccBWParsed) {
|
|
const networkType = determineNetworkType(jsonData[10].response);
|
|
|
|
if (networkType === "NR5G-SA") {
|
|
const nrBW = sccLines.slice(0, sccLines.length - 1).map((line) => {
|
|
const bw = line.split(":")[1].split(",")[2].trim();
|
|
return NR_BANDWIDTH_MAP[bw] || "Unknown";
|
|
});
|
|
|
|
if (nrBW.length === 0) {
|
|
setText("allBW", `${pccBWParsed}`);
|
|
return;
|
|
}
|
|
|
|
setText("allBW", `${pccBWParsed} + ${nrBW.join(" / ")}`);
|
|
} else {
|
|
const nrBW = sccLines[sccLines.length - 1]
|
|
.split(":")[1]
|
|
.split(",")[2]
|
|
.trim();
|
|
const nrBWParsed = NR_BANDWIDTH_MAP[nrBW] || "Unknown";
|
|
|
|
const lteBW = sccLines.slice(0, sccLines.length - 1).map((line) => {
|
|
const bw = line.split(":")[1].split(",")[2].trim();
|
|
return BANDWIDTH_MAP[bw] || "Unknown";
|
|
});
|
|
|
|
if (lteBW.length === 0) {
|
|
setText("allBW", `${pccBWParsed} + NR${nrBWParsed}`);
|
|
return;
|
|
}
|
|
setText("allBW", `${pccBWParsed} + ${lteBW.join(" / ")} + NR${nrBWParsed}`);
|
|
}
|
|
}
|
|
|
|
function processLTEBandwidth(sccLines, pccBWParsed) {
|
|
const allBW = sccLines.map((line) => {
|
|
const bw = line.split(":")[1].split(",")[2].trim();
|
|
return BANDWIDTH_MAP[bw] || "Unknown";
|
|
});
|
|
|
|
setText(
|
|
"allBW",
|
|
allBW.length === 0 ? pccBWParsed : `${pccBWParsed} + ${allBW.join(" + ")}`
|
|
);
|
|
}
|
|
|
|
function processConnectedBands(jsonData) {
|
|
let bandLines = [];
|
|
// Get lines that contains either PCC or SCC and append to bandLines
|
|
let pccBand = jsonData[13].response
|
|
.split("\n")
|
|
.find((line) => line.includes("PCC"))
|
|
.split(":")[1]
|
|
.split(",")[3]
|
|
.replace(/"/g, "")
|
|
.trim();
|
|
|
|
// Loop through each line in the jsonData[13] response and get the lines with SCC and append it to bandLines
|
|
jsonData[13].response.split("\n").forEach((line) => {
|
|
if (line.includes("SCC")) {
|
|
line = line.split(":")[1].split(",")[3].replace(/"/g, "");
|
|
bandLines.push(line);
|
|
}
|
|
});
|
|
|
|
// Parse the LTE band numbers from this: LTE BAND 1 to this B1
|
|
bandLines = bandLines.map((band) => {
|
|
return band.replace("LTE BAND ", "B").trim();
|
|
});
|
|
|
|
bandLines = bandLines.map((band) => {
|
|
return band.replace("NR5G BAND ", "N").trim();
|
|
});
|
|
|
|
pccBand = pccBand.replace("LTE BAND ", "B").trim();
|
|
pccBand = pccBand.replace("NR5G BAND ", "N").trim();
|
|
|
|
// allBands
|
|
if (bandLines.length === 0) {
|
|
setText("allBands", pccBand);
|
|
return;
|
|
} else {
|
|
setText("allBands", `${pccBand} + ${bandLines.join(" / ")}`);
|
|
}
|
|
}
|
|
|
|
function processSignalStrength(jsonData) {
|
|
const signalStrength = jsonData[14].response
|
|
.split("\n")[1]
|
|
.split(":")[1]
|
|
.trim();
|
|
// Signal Strength value
|
|
let signalStrengthData = [];
|
|
// Get the values separated by commas
|
|
signalStrengthData = signalStrength.split(",");
|
|
|
|
// Remove indexes that contains "LTE", "NR5G", "-140", or "-32768"
|
|
signalStrengthData = signalStrengthData.filter((value) => {
|
|
return (
|
|
!value.includes("LTE") &&
|
|
!value.includes("NR5G") &&
|
|
!value.includes("-140") &&
|
|
!value.includes("-32768")
|
|
);
|
|
});
|
|
|
|
// Get the average of the signal strength values where -65 is 100% and -140 is 0%
|
|
let signalStrengthAverage = 0;
|
|
signalStrengthData.forEach((value) => {
|
|
signalStrengthAverage += parseInt(value);
|
|
});
|
|
|
|
signalStrengthAverage = signalStrengthAverage / signalStrengthData.length;
|
|
|
|
// Calculate the percentage
|
|
let percentage = 0;
|
|
if (signalStrengthAverage >= -65) {
|
|
percentage = 100;
|
|
} else if (signalStrengthAverage <= -140) {
|
|
percentage = 0;
|
|
assessment = "No Signal";
|
|
} else {
|
|
percentage = 100 - (signalStrengthAverage - -65) * (100 / (-140 - -65));
|
|
}
|
|
|
|
// Set the signal strength value in a span element where 0-40% is danger, 40-70% is warning, and 70-100% is success
|
|
const signalStrengthElement = document.getElementById("signalStrength");
|
|
const signalAssessmentElement = document.getElementById("signalAssessment");
|
|
if (signalStrengthElement && signalAssessmentElement) {
|
|
signalStrengthElement.innerHTML = "";
|
|
signalAssessmentElement.innerHTML = "";
|
|
const signalStrengthTag = document.createElement("span");
|
|
signalStrengthTag.classList.add("tag", "has-text-white");
|
|
|
|
const signalAssessmentTag = document.createElement("span");
|
|
signalAssessmentTag.classList.add("tag", "has-text-white");
|
|
|
|
if (percentage >= 80) {
|
|
signalStrengthTag.classList.add("is-success");
|
|
signalAssessmentTag.classList.add("is-success");
|
|
assessment = "Excellent";
|
|
} else if (percentage >= 50) {
|
|
signalStrengthTag.classList.add("is-warning");
|
|
signalAssessmentTag.classList.add("is-warning");
|
|
assessment = "Fair";
|
|
} else {
|
|
signalStrengthTag.classList.add("is-danger");
|
|
signalAssessmentTag.classList.add("is-danger");
|
|
assessment = "Cell Edge";
|
|
}
|
|
signalStrengthTag.textContent = `${percentage.toFixed(2)}%`;
|
|
signalAssessmentTag.textContent = assessment;
|
|
|
|
signalStrengthElement.appendChild(signalStrengthTag);
|
|
signalAssessmentElement.appendChild(signalAssessmentTag);
|
|
}
|
|
|
|
// Count the number of of remaining indexes and set as the number of MIMO layers
|
|
setText("mimoLayers", signalStrengthData.length);
|
|
}
|
|
|
|
function createBandTableRow(bandData, networkType, servingCellJSON) {
|
|
const row = document.createElement("tr");
|
|
|
|
try {
|
|
// Parse band data
|
|
const [type, ...values] = bandData.split(",");
|
|
const bandType = type.includes("PCC") ? "PCC" : "SCC";
|
|
|
|
let earfcn, bandwidth, bandNumber, pci, rsrp, rsrq, sinr;
|
|
|
|
// Different parsing logic based on network type and band type
|
|
if (networkType === "NR5G-SA") {
|
|
if (bandType === "PCC") {
|
|
[earfcn, bandwidth, bandNumber, pci] = values;
|
|
// Parse the bandwidth using NR_BANDWIDTH_MAP
|
|
bandwidth = bandwidth?.trim();
|
|
bandwidth = NR_BANDWIDTH_MAP[bandwidth] || "Unknown";
|
|
// Get the rsrp, rsrq, and sinr values using the serving cell values
|
|
// servingcell",<state>,"NR5G-SA",<duplex_mode>,<MCC>,<MNC>,<cellID>,<PCID>,<TAC>,<ARFCN>,<band>,<NR_DL_bandwidth>,<RSRP>,<RSRQ>,<SINR>,<scs>,<srxlev>
|
|
const getNR5GSALine = servingCellJSON.find((line) =>
|
|
line.includes("NR5G-SA")
|
|
);
|
|
if (getNR5GSALine) {
|
|
const servingCellValues = getNR5GSALine.split(":")[1].split(",");
|
|
rsrp = servingCellValues[12].trim();
|
|
rsrq = servingCellValues[13].trim();
|
|
sinr = servingCellValues[14].trim();
|
|
}
|
|
} else {
|
|
// SCC
|
|
[earfcn, bandwidth, bandNumber, scell, pci] = values;
|
|
// Parse the bandwidth using NR_BANDWIDTH_MAP
|
|
bandwidth = bandwidth?.trim();
|
|
bandwidth = NR_BANDWIDTH_MAP[bandwidth] || "Unknown";
|
|
rsrp = "N/A";
|
|
rsrq = "N/A";
|
|
sinr = "N/A";
|
|
}
|
|
} else {
|
|
// NSA
|
|
if (bandType === "PCC") {
|
|
[earfcn, bandwidth, bandNumber, scell, pci, rsrp, rssi, rsrq, sinr] =
|
|
values;
|
|
bandwidth = bandwidth?.trim();
|
|
// Convert bandwidth to MHz
|
|
bandwidth = BANDWIDTH_MAP[bandwidth] || "Unknown";
|
|
} else {
|
|
// SCC
|
|
// If band type is SCC with LTE BAND, use this parsing logic
|
|
if (bandData.includes("LTE BAND")) {
|
|
[earfcn, bandwidth, bandNumber, scell, pci, rsrp, rssi, rsrq, sinr] =
|
|
values;
|
|
bandwidth = bandwidth?.trim();
|
|
// Convert bandwidth to MHz
|
|
bandwidth = BANDWIDTH_MAP[bandwidth] || "Unknown";
|
|
} else {
|
|
// If band type is SCC with NR5G BAND, use this parsing logic
|
|
[earfcn, bandwidth, bandNumber, pci] = values;
|
|
// Parse the bandwidth using NR_BANDWIDTH_MAP
|
|
bandwidth = bandwidth?.trim();
|
|
bandwidth = NR_BANDWIDTH_MAP[bandwidth] || "Unknown";
|
|
// Get the rsrp, rsrq, and sinr values using the serving cell values
|
|
// "NR5G-NSA",<MCC>,<MNC>,<PCID>,<RSRP>,<SINR>,<RSRQ>,<ARFCN>,<band>,<NR_DL_bandwidth>,<scs></scs>
|
|
const getNR5GNSALine = servingCellJSON.find((line) =>
|
|
line.includes("NR5G-NSA")
|
|
);
|
|
if (getNR5GNSALine) {
|
|
const servingCellValues = getNR5GNSALine.split(":")[1].split(",");
|
|
rsrp = servingCellValues[4].trim();
|
|
sinr = servingCellValues[5].trim();
|
|
rsrq = servingCellValues[6].trim();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Clean up values
|
|
bandNumber = bandNumber?.replace(/"/g, "").trim();
|
|
pci = pci?.trim();
|
|
rsrp = rsrp?.trim();
|
|
rsrq = rsrq?.trim();
|
|
sinr = sinr?.trim();
|
|
|
|
// Format band number
|
|
const formattedBandNumber = bandNumber
|
|
?.replace("LTE BAND ", "B")
|
|
.replace("NR5G BAND ", "N");
|
|
|
|
// Create both desktop and mobile versions of the content
|
|
const desktopContent = `
|
|
<td>${formattedBandNumber || "N/A"}</td>
|
|
<td>${earfcn || "N/A"}</td>
|
|
<td>${bandwidth || "N/A"}</td>
|
|
<td>${pci || "N/A"}</td>
|
|
<td>${rsrp ? createSignalTag(rsrp, "RSRP") : "N/A"}</td>
|
|
<td>${rsrq ? createSignalTag(rsrq, "RSRQ") : "N/A"}</td>
|
|
<td>${sinr ? createSignalTag(sinr, "SINR") : "N/A"}</td>
|
|
`;
|
|
|
|
const mobileContent = `
|
|
<div class="cell-carousel__slide">
|
|
<div class="cell-card">
|
|
<div class="cell-card__item">
|
|
<span class="cell-card__label">Name</span>
|
|
<span>${formattedBandNumber || "N/A"}</span>
|
|
</div>
|
|
<div class="cell-card__item">
|
|
<span class="cell-card__label">EARFCN</span>
|
|
<span>${earfcn || "N/A"}</span>
|
|
</div>
|
|
<div class="cell-card__item">
|
|
<span class="cell-card__label">Bandwidth</span>
|
|
<span>${bandwidth || "N/A"}</span>
|
|
</div>
|
|
<div class="cell-card__item">
|
|
<span class="cell-card__label">Physical ID</span>
|
|
<span>${pci || "N/A"}</span>
|
|
</div>
|
|
<div class="cell-card__item">
|
|
<span class="cell-card__label">RSRP</span>
|
|
<span>${rsrp ? createSignalTag(rsrp, "RSRP") : "N/A"}</span>
|
|
</div>
|
|
<div class="cell-card__item">
|
|
<span class="cell-card__label">RSRQ</span>
|
|
<span>${rsrq ? createSignalTag(rsrq, "RSRQ") : "N/A"}</span>
|
|
</div>
|
|
<div class="cell-card__item">
|
|
<span class="cell-card__label">SINR</span>
|
|
<span>${sinr ? createSignalTag(sinr, "SINR") : "N/A"}</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
`;
|
|
|
|
// Store both versions in data attributes
|
|
row.setAttribute("data-desktop", desktopContent);
|
|
row.setAttribute("data-mobile", mobileContent);
|
|
row.innerHTML = desktopContent;
|
|
} catch (error) {
|
|
console.error("Error parsing band data:", error);
|
|
row.innerHTML = '<td colspan="7">Error parsing band data</td>';
|
|
}
|
|
|
|
return row;
|
|
}
|
|
|
|
function createSignalTag(value, type) {
|
|
const numValue = parseInt(value);
|
|
let quality, colorClass;
|
|
|
|
switch (type) {
|
|
case "RSRP":
|
|
if (numValue >= -60) {
|
|
quality = "Excellent";
|
|
colorClass = "is-success";
|
|
} else if (numValue >= -80) {
|
|
quality = "Good";
|
|
colorClass = "is-info";
|
|
} else if (numValue >= -100) {
|
|
quality = "Fair";
|
|
colorClass = "is-warning";
|
|
} else {
|
|
quality = "Poor";
|
|
colorClass = "is-danger";
|
|
}
|
|
break;
|
|
case "RSRQ":
|
|
if (numValue >= -10) {
|
|
quality = "Excellent";
|
|
colorClass = "is-success";
|
|
} else if (numValue >= -15) {
|
|
quality = "Good";
|
|
colorClass = "is-info";
|
|
} else if (numValue >= -20) {
|
|
quality = "Fair";
|
|
colorClass = "is-warning";
|
|
} else {
|
|
quality = "Poor";
|
|
colorClass = "is-danger";
|
|
}
|
|
break;
|
|
case "SINR":
|
|
if (numValue >= 25) {
|
|
quality = "Excellent";
|
|
colorClass = "is-success";
|
|
} else if (numValue >= 13) {
|
|
quality = "Good";
|
|
colorClass = "is-info";
|
|
} else if (numValue >= 6) {
|
|
quality = "Fair";
|
|
colorClass = "is-warning";
|
|
} else {
|
|
quality = "Poor";
|
|
colorClass = "is-danger";
|
|
}
|
|
break;
|
|
}
|
|
|
|
return `
|
|
<div class="tags has-addons">
|
|
<span class="tag is-size-7">${value}</span>
|
|
<span class="tag ${colorClass} is-size-7 has-text-white">${quality}</span>
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
function processBandsTable(jsonData) {
|
|
const servingCellJSON = jsonData[10].response.split("\n");
|
|
const bands = jsonData[13].response.split("\n");
|
|
const networkType = determineNetworkType(jsonData[10].response);
|
|
const pccBand = bands.find((band) => band.includes("PCC"));
|
|
const sccBands = bands.filter((band) => band.includes("SCC"));
|
|
|
|
const tableBody = document.querySelector("#bandTable tbody");
|
|
|
|
if (!tableBody) {
|
|
console.error("Table body not found");
|
|
return;
|
|
}
|
|
|
|
// Clear existing rows
|
|
tableBody.innerHTML = "";
|
|
|
|
// Process PCC band
|
|
if (pccBand) {
|
|
const pccRow = createBandTableRow(pccBand, networkType, servingCellJSON);
|
|
tableBody.appendChild(pccRow);
|
|
}
|
|
|
|
// Process SCC bands
|
|
sccBands.forEach((sccBand) => {
|
|
const sccRow = createBandTableRow(sccBand, networkType, servingCellJSON);
|
|
tableBody.appendChild(sccRow);
|
|
});
|
|
|
|
initMobileCarousel();
|
|
}
|
|
|
|
function processCellInfo(jsonData) {
|
|
const servingCell = jsonData[10].response.split("\n");
|
|
const networkType = determineNetworkType(jsonData[10].response);
|
|
if (networkType === "NR5G-SA") {
|
|
const cellID = servingCell.find((line) => line.includes("NR5G-SA"));
|
|
const cellIDValues = cellID.split(":")[1].split(",");
|
|
const pcid = cellIDValues[6].trim();
|
|
setText("cellID", pcid);
|
|
|
|
const lac = cellIDValues[8].trim();
|
|
setText("lac", lac);
|
|
|
|
const mcc = cellIDValues[4].trim();
|
|
setText("mcc", mcc);
|
|
|
|
const mnc = cellIDValues[5].trim();
|
|
setText("mnc", mnc);
|
|
|
|
// Get all EARFCNs and PCIDs
|
|
const caInfoLines = jsonData[13].response.split("\n");
|
|
// Get the PCC line
|
|
const pccLine = caInfoLines.find((line) => line.includes("PCC"));
|
|
const pccEARFCN = pccLine.split(":")[1].split(",")[1].trim();
|
|
const pccPCID = pccLine.split(":")[1].split(",")[4].trim();
|
|
|
|
const sccLines = caInfoLines.filter((line) => line.includes("SCC"));
|
|
|
|
const sccEARFCNs = sccLines.map((line) => {
|
|
return line.split(":")[1].split(",")[1].trim();
|
|
});
|
|
|
|
const sccPCIDs = sccLines.map((line) => {
|
|
return line.split(":")[1].split(",")[5].trim();
|
|
});
|
|
|
|
// Append all the EARFCN seperated by a comma
|
|
if (sccEARFCNs.length === 0) {
|
|
setText("allEARFCN", `${pccEARFCN}`);
|
|
} else {
|
|
setText("allEARFCN", `${pccEARFCN}, ${sccEARFCNs.join(", ")}`);
|
|
}
|
|
|
|
// Append all the PCID seperated by a comma
|
|
if (sccPCIDs.length === 0) {
|
|
setText("allPCID", `${pccPCID}`);
|
|
} else {
|
|
setText("allPCID", `${pccPCID}, ${sccPCIDs.join(", ")}`);
|
|
}
|
|
} else if (networkType === "NR5G-NSA") {
|
|
const cellID = servingCell.find((line) => line.includes("LTE"));
|
|
const cellIDValues = cellID.split(":")[1].split(",");
|
|
const pcid = cellIDValues[4].trim();
|
|
setText("cellID", pcid);
|
|
|
|
const lac = cellIDValues[10].trim();
|
|
setText("lac", lac);
|
|
|
|
const mcc = cellIDValues[2].trim();
|
|
setText("mcc", mcc);
|
|
|
|
const mnc = cellIDValues[3].trim();
|
|
setText("mnc", mnc);
|
|
|
|
// Get all EARFCNs and PCIDs
|
|
const caInfoLines = jsonData[13].response.split("\n");
|
|
// Get the PCC line
|
|
const pccLine = caInfoLines.find((line) => line.includes("PCC"));
|
|
const pccEARFCN = pccLine.split(":")[1].split(",")[1].trim();
|
|
const pccPCID = pccLine.split(":")[1].split(",")[5].trim();
|
|
const sccLines = caInfoLines.filter((line) => line.includes("SCC"));
|
|
|
|
const sccEARFCNs = sccLines.map((line) => {
|
|
return line.split(":")[1].split(",")[1].trim();
|
|
});
|
|
|
|
const sccPCIDs = sccLines.map((line) => {
|
|
if (line.includes("LTE")) {
|
|
return line.split(":")[1].split(",")[5].trim();
|
|
} else {
|
|
return line.split(":")[1].split(",")[4].trim();
|
|
}
|
|
});
|
|
|
|
// Append all the EARFCN seperated by a comma
|
|
if (sccEARFCNs.length === 0) {
|
|
setText("allEARFCN", `${pccEARFCN}`);
|
|
} else {
|
|
setText("allEARFCN", `${pccEARFCN}, ${sccEARFCNs.join(", ")}`);
|
|
}
|
|
|
|
// Append all the PCID seperated by a comma
|
|
if (sccPCIDs.length === 0) {
|
|
setText("allPCID", `${pccPCID}`);
|
|
} else {
|
|
setText("allPCID", `${pccPCID}, ${sccPCIDs.join(", ")}`);
|
|
}
|
|
} else {
|
|
const cellID = servingCell.find((line) => line.includes("LTE"));
|
|
const cellIDValues = cellID.split(":")[1].split(",");
|
|
const pcid = cellIDValues[6].trim();
|
|
setText("cellID", pcid);
|
|
|
|
const lac = cellIDValues[12].trim();
|
|
setText("lac", lac);
|
|
|
|
const mcc = cellIDValues[4].trim();
|
|
setText("mcc", mcc);
|
|
|
|
const mnc = cellIDValues[5].trim();
|
|
setText("mnc", mnc);
|
|
|
|
// Get all EARFCNs and PCIDs
|
|
const caInfoLines = jsonData[13].response.split("\n");
|
|
// Get the PCC line
|
|
const pccLine = caInfoLines.find((line) => line.includes("PCC"));
|
|
const pccEARFCN = pccLine.split(":")[1].split(",")[1].trim();
|
|
const pccPCID = pccLine.split(":")[1].split(",")[5].trim();
|
|
|
|
const sccLines = caInfoLines.filter((line) => line.includes("SCC"));
|
|
|
|
const sccEARFCNs = sccLines.map((line) => {
|
|
return line.split(":")[1].split(",")[1].trim();
|
|
});
|
|
|
|
const sccPCIDs = sccLines.map((line) => {
|
|
return line.split(":")[1].split(",")[5].trim();
|
|
});
|
|
|
|
// Append all the EARFCN seperated by a comma
|
|
if (sccEARFCNs.length === 0) {
|
|
setText("allEARFCN", `${pccEARFCN}`);
|
|
} else {
|
|
setText("allEARFCN", `${pccEARFCN}, ${sccEARFCNs.join(", ")}`);
|
|
}
|
|
|
|
// Append all the PCID seperated by a comma
|
|
if (sccPCIDs.length === 0) {
|
|
setText("allPCID", `${pccPCID}`);
|
|
} else {
|
|
setText("allPCID", `${pccPCID}, ${sccPCIDs.join(", ")}`);
|
|
}
|
|
}
|
|
}
|
|
|
|
function processWANIPData(jsonData) {
|
|
const wanIP = jsonData[15].response.split("\n");
|
|
const wanIPv4Line = wanIP
|
|
.find((line) => line.includes("IPV4"))
|
|
.split(":")[1]
|
|
.split(",")[4]
|
|
.replace(/"/g, "")
|
|
.trim();
|
|
setText("wanIPv4", wanIPv4Line);
|
|
|
|
const wanIPv6Line = wanIP
|
|
.find((line) => line.includes("IPV6"))
|
|
.split(",")[4]
|
|
.replace(/"/g, "")
|
|
.trim();
|
|
if (wanIPv6Line === "0:0:0:0:0:0:0:0") {
|
|
setText("wanIPv6", "Not Available");
|
|
} else {
|
|
setText("wanIPv6", wanIPv6Line);
|
|
}
|
|
}
|
|
|
|
async function fetchTrafficStats() {
|
|
try {
|
|
const response = await fetch("/cgi-bin/traffic_stats.sh", {
|
|
method: "GET",
|
|
headers: {
|
|
"Content-Type": "application/json",
|
|
},
|
|
});
|
|
|
|
const rawData = await response.text();
|
|
|
|
if (!rawData || rawData.trim() === "") {
|
|
throw new Error("Empty or malformed response");
|
|
}
|
|
|
|
const jsonData = JSON.parse(rawData);
|
|
|
|
console.log("Traffic stats fetched successfully");
|
|
|
|
// Parse rx (download) and tx (upload) values
|
|
const download = jsonData.download;
|
|
const upload = jsonData.upload;
|
|
|
|
// Convert to human-readable format
|
|
const downloadFormatted = formatBytes(download);
|
|
const uploadFormatted = formatBytes(upload);
|
|
|
|
// Update the DOM
|
|
setText("download", downloadFormatted);
|
|
setText("upload", uploadFormatted);
|
|
} catch (error) {
|
|
console.error("There was a problem with the fetch operation:", error);
|
|
}
|
|
}
|
|
|
|
async function fetchConnectionStatus() {
|
|
// Get the container element
|
|
const container = document.getElementById("dataConnState");
|
|
if (!container) {
|
|
console.error("Connection status container not found");
|
|
return;
|
|
}
|
|
|
|
try {
|
|
// Clear any existing status elements
|
|
container.innerHTML = "";
|
|
|
|
// Create and append the "Checking..." element
|
|
const checkingElement = document.createElement("span");
|
|
checkingElement.classList.add("tag", "is-warning", "has-text-white");
|
|
checkingElement.textContent = "Checking...";
|
|
container.appendChild(checkingElement);
|
|
|
|
// Fetch the data
|
|
const response = await fetch("/cgi-bin/check_net.sh", {
|
|
method: "GET",
|
|
headers: {
|
|
"Content-Type": "application/json",
|
|
},
|
|
});
|
|
|
|
// Get the raw response
|
|
const rawData = await response.text();
|
|
|
|
// Check for empty or malformed response
|
|
if (!rawData || rawData.trim() === "") {
|
|
throw new Error("Empty or malformed response");
|
|
}
|
|
|
|
// Parse the JSON
|
|
const jsonData = JSON.parse(rawData);
|
|
|
|
// Clear the container again (removes "Checking..." element)
|
|
container.innerHTML = "";
|
|
|
|
// Create the status element
|
|
const statusElement = document.createElement("span");
|
|
statusElement.classList.add("tag", "has-text-white");
|
|
|
|
if (jsonData.connection === "ACTIVE") {
|
|
statusElement.classList.add("is-success");
|
|
statusElement.textContent = "Connected";
|
|
} else {
|
|
statusElement.classList.add("is-danger");
|
|
statusElement.textContent = "Disconnected";
|
|
}
|
|
|
|
// Append the status element
|
|
container.appendChild(statusElement);
|
|
} catch (error) {
|
|
console.error("There was a problem with the fetch operation:", error);
|
|
|
|
// Clear the container in case of error
|
|
container.innerHTML = "";
|
|
|
|
// Create and append an error element
|
|
const errorElement = document.createElement("span");
|
|
errorElement.classList.add("tag", "is-danger", "has-text-white");
|
|
errorElement.textContent = "Error";
|
|
container.appendChild(errorElement);
|
|
}
|
|
}
|
|
|
|
// Event listener setup
|
|
function setupEventListeners() {
|
|
// Bind refresh button
|
|
const refreshButton = document.getElementById("handleRefreshClickButton");
|
|
if (refreshButton) {
|
|
refreshButton.addEventListener("click", handleRefreshClick);
|
|
} else {
|
|
console.warn("Refresh button not found in the DOM");
|
|
}
|
|
|
|
// Setup dropdown functionality
|
|
const dropdownTrigger = document.querySelector(".dropdown-trigger");
|
|
if (dropdownTrigger) {
|
|
dropdownTrigger.addEventListener("click", (e) => {
|
|
e.preventDefault();
|
|
dropdownTrigger.parentElement.classList.toggle("is-active");
|
|
});
|
|
}
|
|
|
|
// Close dropdown when clicking outside
|
|
document.addEventListener("click", (e) => {
|
|
const dropdowns = document.querySelectorAll(".dropdown");
|
|
dropdowns.forEach((dropdown) => {
|
|
if (!dropdown.contains(e.target)) {
|
|
dropdown.classList.remove("is-active");
|
|
}
|
|
});
|
|
});
|
|
}
|
|
|
|
// Main initialization
|
|
document.addEventListener("DOMContentLoaded", () => {
|
|
// Initial data fetch
|
|
fetchATCommandData();
|
|
fetchConnectionStatus();
|
|
fetchTrafficStats();
|
|
|
|
// Setup controls and event listeners
|
|
setupRefreshControls();
|
|
setupEventListeners();
|
|
});
|
|
|
|
let carouselInitialized = false;
|
|
let currentSlide = 0;
|
|
|
|
function initMobileCarousel() {
|
|
const table = document.getElementById("bandTable");
|
|
const tbody = table.querySelector("tbody");
|
|
const rows = tbody.querySelectorAll("tr");
|
|
|
|
if (window.innerWidth <= 768) {
|
|
let carouselWrapper = document.querySelector(".cell-carousel");
|
|
|
|
if (!carouselInitialized) {
|
|
// Create carousel structure only if it doesn't exist
|
|
carouselWrapper = document.createElement("div");
|
|
carouselWrapper.className = "cell-carousel";
|
|
|
|
const carouselContainer = document.createElement("div");
|
|
carouselContainer.className = "cell-carousel__container";
|
|
|
|
carouselWrapper.appendChild(carouselContainer);
|
|
|
|
// Add touch event listeners only once
|
|
carouselContainer.addEventListener("touchstart", handleTouchStart, false);
|
|
carouselContainer.addEventListener("touchmove", handleTouchMove, false);
|
|
carouselContainer.addEventListener("touchend", handleTouchEnd, false);
|
|
|
|
// Insert carousel into DOM
|
|
table.style.display = "none";
|
|
table.parentNode.insertBefore(carouselWrapper, table);
|
|
|
|
carouselInitialized = true;
|
|
}
|
|
|
|
// Update carousel content
|
|
updateCarouselContent(rows);
|
|
} else {
|
|
// Restore desktop view
|
|
const carousel = document.querySelector(".cell-carousel");
|
|
if (carousel) {
|
|
carousel.style.display = "none";
|
|
table.style.display = "";
|
|
}
|
|
|
|
rows.forEach((row) => {
|
|
row.innerHTML = row.getAttribute("data-desktop");
|
|
});
|
|
}
|
|
}
|
|
|
|
function updateCarouselContent(rows) {
|
|
const carouselContainer = document.querySelector(".cell-carousel__container");
|
|
const indicators = document.querySelector(".cell-carousel__indicators");
|
|
|
|
if (!carouselContainer) return;
|
|
|
|
// Clear existing content
|
|
carouselContainer.innerHTML = "";
|
|
if (indicators) indicators.remove();
|
|
|
|
// Add new slides
|
|
rows.forEach((row) => {
|
|
carouselContainer.insertAdjacentHTML(
|
|
"beforeend",
|
|
row.getAttribute("data-mobile")
|
|
);
|
|
});
|
|
|
|
// Update indicators
|
|
const indicatorsHTML = `
|
|
<div class="cell-carousel__indicators">
|
|
${Array.from(
|
|
{ length: rows.length },
|
|
(_, i) =>
|
|
`<span class="cell-carousel__dot ${
|
|
i === currentSlide ? "cell-carousel__dot--active" : ""
|
|
}"
|
|
onclick="goToSlide(${i})"></span>`
|
|
).join("")}
|
|
</div>
|
|
`;
|
|
|
|
carouselContainer.parentNode.insertAdjacentHTML("beforeend", indicatorsHTML);
|
|
|
|
// Reset to first slide and update display
|
|
currentSlide = 0;
|
|
updateCarousel();
|
|
}
|
|
|
|
// Your existing touch handling functions remain the same
|
|
let touchStartX = 0;
|
|
let touchEndX = 0;
|
|
|
|
function handleTouchStart(event) {
|
|
touchStartX = event.touches[0].clientX;
|
|
}
|
|
|
|
function handleTouchMove(event) {
|
|
event.preventDefault();
|
|
}
|
|
|
|
function handleTouchEnd(event) {
|
|
touchEndX = event.changedTouches[0].clientX;
|
|
handleSwipe();
|
|
}
|
|
|
|
function handleSwipe() {
|
|
const swipeThreshold = 50;
|
|
const container = document.querySelector(".cell-carousel__container");
|
|
const slides = container.querySelectorAll(".cell-carousel__slide");
|
|
const diffX = touchStartX - touchEndX;
|
|
|
|
if (Math.abs(diffX) > swipeThreshold) {
|
|
if (diffX > 0 && currentSlide < slides.length - 1) {
|
|
currentSlide++;
|
|
} else if (diffX < 0 && currentSlide > 0) {
|
|
currentSlide--;
|
|
}
|
|
updateCarousel();
|
|
}
|
|
}
|
|
|
|
function goToSlide(index) {
|
|
currentSlide = index;
|
|
updateCarousel();
|
|
}
|
|
|
|
function updateCarousel() {
|
|
const container = document.querySelector(".cell-carousel__container");
|
|
if (!container) return;
|
|
|
|
container.style.transform = `translateX(-${currentSlide * 100}%)`;
|
|
|
|
const dots = document.querySelectorAll(".cell-carousel__dot");
|
|
dots.forEach((dot, index) => {
|
|
dot.classList.toggle("cell-carousel__dot--active", index === currentSlide);
|
|
});
|
|
}
|
|
|
|
// Update your event listeners
|
|
window.addEventListener("load", initMobileCarousel);
|
|
window.addEventListener("resize", initMobileCarousel);
|