added recent changes

This commit is contained in:
Russel Yasol
2024-05-05 11:07:49 +08:00
parent 4680237d49
commit 41ee583c8d
20 changed files with 3918 additions and 0 deletions

View File

@@ -0,0 +1,35 @@
#!/bin/bash
QUERY_STRING=$(echo "${QUERY_STRING}" | sed 's/;//g')
function urldecode() { : "${*//+/ }"; echo -e "${_//%/\\x}"; }
if [ "${QUERY_STRING}" ]; then
export IFS="&"
for cmd in ${QUERY_STRING}; do
if [ "$(echo $cmd | grep '=')" ]; then
key=$(echo $cmd | awk -F '=' '{print $1}')
value=$(echo $cmd | awk -F '=' '{print $2}')
eval $key=$value
fi
done
fi
MYATCMD=$(printf '%b\n' "${atcmd//%/\\x}")
if [ -n "${MYATCMD}" ]; then
x=$(urldecode "$atcmd")
# Initialize wait time to 200 ms
wait_time=200
while true; do
runcmd=$(echo -en "$x\r\n" | microcom -t $wait_time /dev/ttyOUT2)
# Check if "OK" or "ERROR" is present in the response
if [[ $runcmd =~ "OK" ]] || [[ $runcmd =~ "ERROR" ]]; then
break # Exit the loop if "OK" or "ERROR" is found
fi
# If neither "OK" nor "ERROR" is found, increment wait time by 1 second
((wait_time++))
done
fi
echo "Content-type: text/plain"
echo $x
echo ""
echo $runcmd

View File

@@ -0,0 +1,19 @@
#!/bin/bash
# This script will ping 8.8.8.8 and return the result
# If the ping is successful, it will return "OK"
# If the ping fails, it will return "ERROR"
# Send the ping command and store the output
ping_output=$(ping -c 1 8.8.8.8)
# Check if the output contains "0% packet loss"
if echo "$ping_output" | grep -q "0% packet loss"; then
echo "Content-type: text/plain"
echo ""
echo "OK"
else
echo "Content-type: text/plain"
echo ""
echo "ERROR"
fi

View File

@@ -0,0 +1,20 @@
#!/bin/bash
# This is a simple scrip that fetches the SMS messages from the device
SMS_MESSAGE_INDICATION="AT+CNMI=2,1"
SMS_FORMAT="AT+CMGF=1"
SMS_LIST="AT+CMGL=\"ALL\""
while true; do
runcmd=$(echo -en "$SMS_LIST\r\n" | microcom -t 2000 /dev/ttyOUT2)
if [[ $runcmd =~ "OK" ]] || [[ $runcmd =~ "ERROR" ]]; then
break
fi
((wait_time++))
done
# Print the list of SMS messages as JSON plain text
echo "Content-type: text/plain"
echo ""
echo $runcmd

View File

@@ -0,0 +1,20 @@
#!/bin/bash
# Check iptables for ttlvalue
ttlvalue=$(iptables -t mangle -vnL | awk '/TTL/ {print $13; exit}')
ttlenabled=true;
# Set Variables
if [ -z "${ttlvalue}" ]; then
ttlvalue=0
ttlenabled=false
fi
echo "Content-type: text/json"
echo ""
cat <<EOT
{
"isEnabled": $ttlenabled,
"ttl": $ttlvalue
}
EOT

View File

@@ -0,0 +1,44 @@
#!/bin/bash
QUERY_STRING=$(echo "${QUERY_STRING}" | sed 's/;//g')
function urldecode() { : "${*//+/ }"; echo -e "${_//%/\\x}"; }
if [ "${QUERY_STRING}" ]; then
export IFS="&"
for cmd in ${QUERY_STRING}; do
if [ "$(echo $cmd | grep '=')" ]; then
key=$(echo $cmd | awk -F '=' '{print $1}')
value=$(echo $cmd | awk -F '=' '{print $2}')
eval $key=$value
fi
done
fi
# Extract phone number and message from inputs
phone_number="$number"
message="$msg"
# Prepare AT command with phone number and message
ATCMD="AT+CMGS=\"$phone_number\""
MYATCMD=$(printf '%b\n' "${ATCMD//%/\\x}")
if [ -n "${MYATCMD}" ]; then
x=$(urldecode "$ATCMD")
# Send the AT command to initiate message sending
echo -en "$x\r\n" | microcom /dev/ttyOUT2
# Wait for a brief moment (assuming the message sending is instantaneous)
sleep 1
fi
# Send the message
echo -en "$message\c"
# Send Ctrl+Z to terminate the message
echo -en "\032"
# Ensure microcom reads the response (assuming microcom will show response instantly)
sleep 1
# Capture and output the response
runcmd=$(microcom /dev/ttyOUT2)
echo "Content-type: text/plain"
echo "$runcmd"

View File

@@ -0,0 +1,61 @@
#!/bin/bash
# Get query
QUERY_STRING=$(echo "${QUERY_STRING}" | sed 's/;//g')
if [ "${QUERY_STRING}" ]; then
export IFS="&"
for cmd in ${QUERY_STRING}; do
if [ "$(echo $cmd | grep '=')" ]; then
key=$(echo $cmd | awk -F '=' '{print $1}')
value=$(echo $cmd | awk -F '=' '{print $2}')
eval $key=$value
fi
done
fi
setTTL=$(printf '%b\n' "${ttlvalue//%/\\x}")
if [ -n "${setTTL}" ]; then
# Stop Service To Remove Rules
/usrdata/simplefirewall/ttl-override stop
# Check iptables is still set
ttlcheck=$(iptables -t mangle -vnL | grep TTL | awk '{print $13}')
# If TTL is still set manually remove values
if [ !-z "${ttlcheck}" ]; then
iptables -t mangle -D POSTROUTING -o rmnet+ -j TTL --ttl-set ${ttlcheck} &>/dev/null || true
ip6tables -t mangle -D POSTROUTING -o rmnet+ -j HL --hl-set ${ttlcheck} &>/dev/null || true
fi
# Echo TTL to file
echo $setTTL > /usrdata/simplefirewall/ttlvalue
# Set Start Service
/usrdata/simplefirewall/ttl-override start
fi
# Check iptables for ttlvalue
ttlvalue=$(iptables -t mangle -vnL | grep TTL | awk '{print $13}')
ttlenabled=true;
# Set Variables
if [ -z "${ttlvalue}" ]; then
ttlvalue=0
ttlenabled=false
fi
echo "Content-type: text/json"
echo ""
cat <<EOT
{
"isEnabled": $ttlenabled,
"ttl": $ttlvalue
}

6
simpleadmin/css/bootstrap.min.css vendored Normal file

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,95 @@
/* import poppins */
@import url("https://fonts.googleapis.com/css2?family=Poppins:wght@300;400;500;600;700&display=swap");
/* import fontawesome icons */
@import url("https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.15.3/css/all.min.css");
* {
font-family: "Poppins", sans-serif;
}
.custom-checkbox .form-check-input {
margin-right: 1px;
}
.modal-overlay {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.5);
z-index: 1000;
display: flex;
justify-content: center;
align-items: center;
}
.loading-modal {
background-color: #fff;
padding: 3rem;
border-radius: 10px;
text-align: center;
}
.loading-text {
font-size: 18px;
color: #333;
}
.loader {
width: 64px;
height: 64px;
border: 3px dotted #000;
border-style: solid solid dotted dotted;
border-radius: 50%;
display: inline-block;
position: relative;
box-sizing: border-box;
animation: rotation 2s linear infinite;
margin-bottom: 2rem;
}
.loader::after {
content: "";
box-sizing: border-box;
position: absolute;
left: 0;
right: 0;
top: 0;
bottom: 0;
margin: auto;
border: 3px dotted #0b5ed7;
border-style: solid solid dotted;
width: 24px;
height: 24px;
border-radius: 50%;
animation: rotationBack 1s linear infinite;
transform-origin: center center;
}
@keyframes rotation {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}
@keyframes rotationBack {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(-360deg);
}
}
.is-warning {
background-color: #ffb70f !important;
color: #000 !important;
}
.is-medium {
font-weight: 600;
}

358
simpleadmin/deviceinfo.html Normal file
View File

@@ -0,0 +1,358 @@
<!DOCTYPE html>
<html lang="en" data-bs-theme="light">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Simple Admin</title>
<!-- <link
href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css"
rel="stylesheet"
integrity="sha384-QWTKZyjpPEjISv5WaRU9OFeRpok6YctnYmDr5pNlyT2bRjXh0JMhjY6hW+ALEwIH"
crossorigin="anonymous"
/> -->
<!-- Import all the bootstrap css files from css folder -->
<link rel="stylesheet" href="/css/styles.css" />
<link rel="stylesheet" href="/css/bootstrap.min.css" />
<!-- Import BootStrap Javascript -->
<script src="/js/bootstrap.bundle.min.js"></script>
<script src="/js/alpinejs.min.js" defer></script>
</head>
<body>
<main>
<div class="container my-4" x-data="fetchDeviceInfo()">
<nav class="navbar navbar-expand-lg mt-2">
<div class="container-fluid">
<a class="navbar-brand" href="/"
><span class="mb-0 h4">Simple Admin</span></a
>
<button
class="navbar-toggler"
type="button"
data-bs-toggle="collapse"
data-bs-target="#navbarText"
aria-controls="navbarText"
aria-expanded="false"
aria-label="Toggle navigation"
>
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbarText">
<ul class="navbar-nav me-auto mb-2 mb-lg-0">
<li class="nav-item">
<a class="nav-link" href="/">Home</a>
</li>
<li class="nav-item">
<a class="nav-link" href="/network.html">Simple Network</a>
</li>
<li class="nav-item">
<a class="nav-link" href="/settings.html">Simple Settings</a>
</li>
<li class="nav-item">
<a class="nav-link" href="/sms.html">SMS</a>
</li>
<li class="nav-item">
<a
class="nav-link active"
href="/deviceinfo.html"
aria-current="page"
>Device Information</a
>
</li>
</ul>
<span class="navbar-text">
<button class="btn btn-link text-reset" id="darkModeToggle">
Dark Mode
</button>
</span>
</div>
</div>
</nav>
<div class="row mt-5 gap-3">
<div class="col">
<div class="card">
<div class="card-header">Device Information</div>
<div class="card-body">
<div class="card-text">
<div class="table-responsive">
<table class="table">
<tbody>
<tr>
<th scope="row">Manufacturer</th>
<td x-text="manufacturer"></td>
</tr>
<tr>
<th scope="row">Model Name</th>
<td x-text="modelName"></td>
</tr>
<tr>
<th scope="row">Firmware version</th>
<td class="col-md-2" x-text="firmwareVersion"></td>
</tr>
<tr>
<th scope="row">IMSI</th>
<td class="col-md-2" x-text="imsi"></td>
</tr>
<tr>
<th scope="row">ICCID</th>
<td class="col-md-2" x-text="iccid"></td>
</tr>
<tr>
<th scope="row">IMEI</th>
<td class="col-md-2">
<input
class="form-control"
type="text"
x-model="newImei"
x-bind:placeholder="imei === '-' ? 'Fetching IMEI...' : imei"
/>
</td>
<td>
<button
type="button"
class="btn btn-primary"
@click="openModal()"
>
Update
</button>
</td>
</tr>
<tr>
<th scope="row">LAN IP</th>
<td class="col-md-2" x-text="lanIp"></td>
</tr>
<tr>
<th scope="row">WWAN IPv<sup>4</sup></th>
<td class="col-md-2" x-text="wwanIpv4"></td>
</tr>
<tr>
<th scope="row">WWAN IPv<sup>6</sup></th>
<td class="col-md-2" x-text="wwanIpv6"></td>
</tr>
<tr>
<th scope="row">Simple Admin Version</th>
<td class="col-md-2">SimpleAdminRev-Alpha-0.3</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
<div class="card-footer">
Visit our
<a
href="https://github.com/iamromulan/quectel-rgmii-toolkit.gits"
target="_blank"
class="text-reset"
>repository</a
>
or
<a
href="https://github.com/iamromulan/quectel-rgmii-configuration-notes.git"
target="_blank"
class="text-reset"
>documentation</a
>
for more information. All rights reserved. 2024
</div>
</div>
</div>
</div>
<!-- Loading Modal for Reboot -->
<div class="modal-overlay" x-show="showModal">
<div class="loading-modal">
<div
class="loading-text"
style="display: flex; flex-direction: column"
>
<h3>This will reboot the modem.</h3>
<p style="margin-top: 0.5rem">Continue?</p>
</div>
<div class="d-grid gap-2 d-md-block">
<button
class="btn btn-primary"
type="button"
@click="updateIMEI()"
>
Reboot
</button>
<button
class="btn btn-danger"
type="button"
@click="closeModal()"
>
Cancel
</button>
</div>
</div>
</div>
<!-- Reboot Modal Countdown -->
<div class="modal-overlay" x-show="isRebooting">
<div class="loading-modal">
<div class="loader"></div>
<div
class="loading-text"
style="display: flex; flex-direction: column"
>
<h3>Rebooting...</h3>
<p style="margin-top: 0.5rem">
Please wait for
<span x-text="countdown" style="font-weight: 500"></span>
seconds.
</p>
</div>
</div>
</div>
</div>
</main>
<script src="/js/dark-mode.js"></script>
<script>
function fetchDeviceInfo() {
return {
manufacturer: "-",
modelName: "-",
firmwareVersion: "-",
imsi: "-",
iccid: "-",
imei: "-",
newImei: null,
lanIp: "-",
wwanIpv4: "-",
wwanIpv6: "-",
simpleAdminVersion: "-",
atcmd: null,
atCommandResponse: "",
showModal: false,
isLoading: false,
isRebooting: false,
countdown: 3,
sendATCommand() {
if (!this.atcmd) {
// Use ATI as default command
console.log(
"AT Command is empty, using ATI as default command: "
);
}
this.isLoading = true;
fetch(
"/cgi-bin/get_atcommand?" +
new URLSearchParams({
atcmd: this.atcmd,
})
)
.then((res) => {
return res.text();
})
.then((data) => {
this.atCommandResponse = data;
})
.catch((error) => {
console.error("Error: ", error);
this.showError = true;
});
},
fetchATCommand() {
this.atcmd =
'AT+CGMI;+CGMM;+QGMR;+CIMI;+ICCID;+CGSN;+QMAP="LANIP";+QMAP="WWAN"';
this.isLoading = true;
fetch(
"/cgi-bin/get_atcommand?" +
new URLSearchParams({
atcmd: this.atcmd,
})
)
.then((res) => {
return res.text();
})
.then((data) => {
this.atCommandResponse = data;
this.parseFetchedData();
})
.catch((error) => {
console.error("Error: ", error);
this.showError = true;
});
},
parseFetchedData() {
const lines = this.atCommandResponse.split("\n");
console.log("AT Command Response: ", lines);
this.manufacturer = lines[1].trim();
this.modelName = lines[3].trim();
this.firmwareVersion = lines[5].trim();
this.imsi = lines[7].trim();
this.iccid = lines[9].trim().replace("+ICCID: ", "");
this.imei = lines[11].trim();
this.lanIp = lines[13].trim().split(",")[3];
this.wwanIpv4 = lines[15].trim().split(",")[4].replace(/"/g, "");
this.wwanIpv6 = lines[16].trim().split(",")[4].replace(/"/g, "");
this.simpleAdminVersion = "SimpleAdminRev-Alpha-0.5";
this.isLoading = false;
},
updateIMEI() {
this.atcmd = `AT+EGMR=1,7,"${this.newImei}"`;
this.sendATCommand();
this.rebootDevice();
},
rebootDevice() {
this.atcmd = "AT+CFUN=1,1";
this.sendATCommand();
this.isLoading = true;
this.showModal = false;
this.isRebooting = true;
this.countdown = 40;
const interval = setInterval(() => {
this.countdown--;
if (this.countdown === 0) {
clearInterval(interval);
this.isLoading = false;
this.showModal = false;
this.isRebooting = false;
this.init();
}
}, 1000);
},
openModal() {
if (!this.newImei) {
alert("No new IMEI provided.");
return;
}
if (this.newImei.length !== 15) {
alert("IMEI is invalid");
return;
}
if (this.newImei === this.imei) {
alert("IMEI is the same as the current IMEI");
return;
}
this.showModal = true;
},
closeModal() {
this.showModal = false;
},
init() {
this.fetchATCommand();
},
};
}
</script>
</body>
</html>

1140
simpleadmin/index.html Normal file

File diff suppressed because it is too large Load Diff

5
simpleadmin/js/alpinejs.min.js vendored Normal file

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,29 @@
// Function to toggle dark mode
const toggleDarkMode = () => {
const html = document.querySelector('html');
const currentTheme = html.getAttribute('data-bs-theme');
if (currentTheme === 'dark') {
html.removeAttribute('data-bs-theme');
darkModeToggle.textContent = 'Dark Mode';
localStorage.setItem('theme', 'light'); // Store the theme in localStorage
} else {
html.setAttribute('data-bs-theme', 'dark');
darkModeToggle.textContent = 'Light Mode';
localStorage.setItem('theme', 'dark'); // Store the theme in localStorage
}
};
const darkModeToggle = document.getElementById('darkModeToggle');
// Check if theme preference is stored in localStorage
const storedTheme = localStorage.getItem('theme');
if (storedTheme) {
const html = document.querySelector('html');
html.setAttribute('data-bs-theme', storedTheme);
if (storedTheme === 'dark') {
darkModeToggle.textContent = 'Light Mode';
}
}
darkModeToggle.addEventListener('click', toggleDarkMode);

View File

@@ -0,0 +1,37 @@
const freqNumbersContainer = document.getElementById(
"freqNumbersContainer"
);
function generateFreqNumberInputs(num) {
let html = "";
const maxFields = Math.min(num, 10); // Limit to a maximum of 10 fields
for (let i = 1; i <= maxFields; i++) {
html += `
<div class="input-group mb-3" x-show="cellNum >= ${i} && networkModeCell == 'LTE'">
<input
type="text"
aria-label="EARFCN"
placeholder="EARFCN"
class="form-control"
x-model="earfcn${i}"
/>
<input
type="text"
aria-label="PCI"
placeholder="PCI"
class="form-control"
x-model="pci${i}"
/>
</div>
`;
}
return html;
}
document.addEventListener("DOMContentLoaded", function () {
const cellNumInput = document.querySelector("[aria-label='NumCells']");
cellNumInput.addEventListener("input", function () {
const cellNum = parseInt(this.value);
freqNumbersContainer.innerHTML = generateFreqNumberInputs(cellNum);
});
});

View File

@@ -0,0 +1,60 @@
function parseCurrentSettings(rawdata) {
const data = rawdata;
const lines = data.split("\n");
console.log(lines);
// Remove QUIMSLOT and only take 1 or 2
this.sim = lines[1].split(":")[1].trim();
this.apn = lines[3].split(",")[2].replace(/\"/g, "");
this.cellLock4GStatus = lines[5].split(",")[1].replace(/\"/g, "");
this.cellLock5GStatus = lines[7].split(",")[1].replace(/\"/g, "");
this.prefNetwork = lines[9].split(",")[1].replace(/\"/g, "");
this.nrModeControlStatus = lines[11].split(",")[1].replace(/\"/g, "");
let bands = [];
// Append the values if there is separated by comma with a space.
// i.e. LTE BAND 3, LTE BAND 1
for (let i = 13; i < 17; i++) {
if (lines[i].split(",").length > 1) {
bands.push(lines[i].split(",")[3].replace(/\"/g, " "));
}
}
this.bands = bands;
if (this.cellLock4GStatus == 1 && this.cellLock5GStatus == 1) {
this.cellLockStatus = "Locked to 4G and 5G";
} else if (this.cellLock4GStatus == 1) {
this.cellLockStatus = "Locked to 4G";
}
else if (this.cellLock5GStatus == 1) {
this.cellLockStatus = "Locked to 5G";
}
else {
this.cellLockStatus = "Not Locked";
}
if (this.nrModeControlStatus == 0) {
this.nrModeControlStatus = "Not Disabled";
}
else if (this.nrModeControlStatus == 1) {
this.nrModeControlStatus = "SA Disabled";
}
else {
this.nrModeControlStatus = "NSA Disabled";
}
return {
sim: sim,
apn: apn,
cellLockStatus: cellLockStatus,
prefNetwork: prefNetwork,
nrModeControl: nrModeControlStatus,
bands: bands
};
}

View File

@@ -0,0 +1,76 @@
function populateCheckboxes(lte_band, nsa_nr5g_band, nr5g_band, locked_lte_bands, locked_nsa_bands, locked_sa_bands, cellLock) {
var checkboxesForm = document.getElementById("checkboxForm");
var selectedMode = document.getElementById("networkModeBand").value;
var bands;
// Determine bands based on selected network mode
if (selectedMode === "LTE") {
bands = lte_band;
} else if (selectedMode === "NSA") {
bands = nsa_nr5g_band;
} else if (selectedMode === "SA") {
bands = nr5g_band;
}
checkboxesForm.innerHTML = ""; // Clear existing checkboxes
var bandsArray;
if (bands !== null) {
bandsArray = bands.split(":");
bandsArray.forEach(function(band, index) {
if (index % 5 === 0) {
currentRow = document.createElement("div");
currentRow.className = "row mb-2 mx-auto"; // Add margin bottom for spacing
checkboxesForm.appendChild(currentRow);
}
var checkboxDiv = document.createElement("div");
checkboxDiv.className = "form-check form-check-reverse col-2"; // Each checkbox takes a column
var checkboxInput = document.createElement("input");
checkboxInput.className = "form-check-input";
checkboxInput.type = "checkbox";
checkboxInput.id = "inlineCheckbox" + band;
checkboxInput.value = band;
checkboxInput.autocomplete = "off";
// Store the locked bands in an array
var locked_lte_bands_array = locked_lte_bands.split(":");
var locked_nsa_bands_array = locked_nsa_bands.split(":");
var locked_sa_bands_array = locked_sa_bands.split(":");
// Check if the current band is locked
var isLocked = false;
if (selectedMode === "LTE") {
if (locked_lte_bands_array.includes(band)) {
isLocked = true;
}
} else if (selectedMode === "NSA") {
if (locked_nsa_bands_array.includes(band)) {
isLocked = true;
}
} else if (selectedMode === "SA") {
if (locked_sa_bands_array.includes(band)) {
isLocked = true;
}
}
if (isLocked) {
checkboxInput.checked = true;
}
var checkboxLabel = document.createElement("label");
checkboxLabel.className = "form-check-label";
checkboxLabel.htmlFor = "inlineCheckbox" + band;
checkboxLabel.innerText = "B" + band;
checkboxDiv.appendChild(checkboxInput);
checkboxDiv.appendChild(checkboxLabel);
currentRow.appendChild(checkboxDiv);
});
} else {
// Do nothing
}
var currentRow;
addCheckboxListeners(cellLock);
}

933
simpleadmin/network.html Normal file
View File

@@ -0,0 +1,933 @@
<!DOCTYPE html>
<html lang="en" data-bs-theme="light">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Simple Admin</title>
<!-- <link
href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css"
rel="stylesheet"
integrity="sha384-QWTKZyjpPEjISv5WaRU9OFeRpok6YctnYmDr5pNlyT2bRjXh0JMhjY6hW+ALEwIH"
crossorigin="anonymous"
/> -->
<!-- Import all the bootstrap css files from css folder -->
<link rel="stylesheet" href="css/styles.css" />
<link rel="stylesheet" href="css/bootstrap.min.css" />
<!-- Import BootStrap Javascript -->
<script src="js/bootstrap.bundle.min.js"></script>
<script src="js/alpinejs.min.js" defer></script>
</head>
<body>
<main>
<div class="container my-4" x-data="cellLocking()">
<nav class="navbar navbar-expand-lg mt-2">
<div class="container-fluid">
<a class="navbar-brand" href="/"
><span class="mb-0 h4">Simple Admin</span></a
>
<button
class="navbar-toggler"
type="button"
data-bs-toggle="collapse"
data-bs-target="#navbarText"
aria-controls="navbarText"
aria-expanded="false"
aria-label="Toggle navigation"
>
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbarText">
<ul class="navbar-nav me-auto mb-2 mb-lg-0">
<li class="nav-item">
<a class="nav-link" href="/">Home</a>
</li>
<li class="nav-item">
<a
class="nav-link active"
href="network.html"
aria-current="page"
>Simple Network</a
>
</li>
<li class="nav-item">
<a class="nav-link" href="/settings.html">Simple Settings</a>
</li>
<li class="nav-item">
<a class="nav-link" href="/sms.html">SMS</a>
</li>
<li class="nav-item">
<a class="nav-link" href="deviceinfo.html"
>Device Information</a
>
</li>
</ul>
<span class="navbar-text">
<button class="btn btn-link text-reset" id="darkModeToggle">
Dark Mode
</button>
</span>
</div>
</div>
</nav>
<div class="row mt-5 gap-3">
<div class="col">
<div class="card">
<div class="card-header">
<div class="row align-items-center">
<div class="col">Band Locking</div>
<div class="col-md-3">
<select
class="form-select"
id="networkModeBand"
aria-label="LTE"
>
<option selected value="LTE">LTE</option>
<option value="NSA">NR5G-NSA</option>
<option value="SA">NR5G-SA</option>
</select>
</div>
</div>
</div>
<div class="card-body">
<h5 class="card-subtitle" x-show="isLoading">
Fetching supported bands...
</h5>
<form id="checkboxForm" x-show="isLoading === false">
<!-- Checkboxes will be populated here -->
</form>
</div>
<div class="card-footer">
<div class="d-flex align-items-center">
<div class="col">
<button
type="button"
class="btn btn-primary m-2"
@click="lockSelectedBands()"
>
Lock Bands
</button>
<button
type="button"
class="btn btn-info m-2"
id="uncheckAll"
>
Uncheck All
</button>
<button
type="button"
class="btn btn-danger m-2"
@click="resetBandLocking()"
>
Reset
</button>
</div>
</div>
</div>
<div class="card-footer">
<p x-text="'Active bands: ' + bands"></p>
</div>
</div>
</div>
</div>
<div class="row gap-3 mt-4">
<div class="col">
<form>
<div class="card">
<div class="card-header">Network Utilities</div>
<div class="card-body row">
<div class="col">
<div class="mb-4">
<label for="APN" class="form-label">APN</label>
<input
type="text"
class="form-control"
id="APN"
x-model="newApn"
aria-describedby="APN"
x-bind:placeholder="apn === '-' ? 'Fetching...' : apn"
/>
</div>
<div class="mb-4 input-group grid gap-3">
<label for="SIM1" class="form-label"> Change SIM</label>
<div class="form-check form-check-inline">
<input
class="form-check-input"
type="radio"
name="inlineRadioOptions"
aria-describedby="SIM1"
id="SIM1"
value="option1"
x-bind:checked="sim === '1'"
x-on:click="newSim = '1'"
/>
<label class="form-check-label" for="inlineRadio1"
>1</label
>
</div>
<div class="form-check form-check-inline">
<input
class="form-check-input"
type="radio"
name="inlineRadioOptions"
aria-describedby="SIM2"
id="SIM2"
value="option2"
x-bind:checked="sim === '2'"
x-on:click="newSim = '2'"
/>
<label class="form-check-label" for="inlineRadio2"
>2</label
>
</div>
</div>
</div>
<div class="col">
<div class="mb-4">
<label for="nrModeControl" class="form-label"
>Select Preferred Network</label
>
<select
class="form-select"
id="prefNetworkMode"
x-model="prefNetworkMode"
aria-label="prefNetworkMode"
>
<option
selected
x-text="prefNetwork === '-' ? 'Fetching...' : 'Current: ' + prefNetwork"
></option>
<option value="AUTO">AUTO</option>
<option value="LTE">LTE Only</option>
<option value="LTE:NR5G">NR5G-NSA</option>
<option value="NR5G">NR5G-SA</option>
</select>
</div>
<div class="mb-4">
<label for="prefNetwork" class="form-label"
>NR5G Mode Control</label
>
<select
class="form-select"
id="nrModeControl"
x-model="nrModeControl"
aria-label="nrModeControl"
>
<option
selected
x-text="nrModeControl === '-' ? 'Fetching...' : 'Current: ' + nrModeControl"
></option>
<option value="0">Enable All</option>
<option value="2">Disable NR5G-NSA</option>
<option value="1">Disable NR5G-SA</option>
</select>
</div>
</div>
</div>
<div class="card-footer">
<button
type="button"
class="btn btn-primary"
@click="saveChanges()"
>
Save Changes
</button>
</div>
</div>
</form>
</div>
<div class="col-md-4">
<div class="card">
<div class="card-header">Cell Locking</div>
<div class="card-body">
<select
class="form-select"
id="networkModeCell"
x-model="networkModeCell"
aria-label="LTE"
>
<option
selected
x-text="'Cell Lock: ' + cellLockStatus"
></option>
<option>LTE</option>
<option>NR5G-SA</option>
<option>Unlock LTE</option>
<option>Unlock NR5G-SA</option>
</select>
<div class="my-4">
<!-- For LTE -->
<div id="lteElementsCell" x-show="networkModeCell == 'LTE'">
<div class="input-group mb-3">
<span class="input-group-text" id="basic-addon1"
>Num Cells</span
>
<input
type="number"
class="form-control"
placeholder="1-10"
min="1"
max="10"
aria-label="NumCells"
aria-describedby="basic-addon1"
x-model="cellNum"
/>
</div>
</div>
<div id="freqNumbersContainer">
<!-- Generate EARFCN and PCI here -->
</div>
<!-- For SA -->
<div
id="saElementsCell"
x-show="networkModeCell == 'NR5G-SA'"
>
<div
class="input-group mb-3"
x-show="networkModeCell == 'NR5G-SA'"
>
<input
type="text"
aria-label="EARFCN"
placeholder="EARFCN"
class="form-control"
x-model="earfcn1"
/>
<input
type="text"
aria-label="PCI"
placeholder="PCI"
class="form-control"
x-model="pci1"
/>
</div>
<div
class="input-group mb-3"
x-show="networkModeCell == 'NR5G-SA'"
>
<select
class="form-select"
x-model="scs"
aria-label="SCS"
>
<option selected>SCS</option>
<option>15</option>
<option>30</option>
<option>60</option>
<option>120</option>
<option>240</option>
</select>
<input
type="text"
aria-label="band"
placeholder="Band"
class="form-control"
x-model="band"
/>
</div>
</div>
<button
type="button"
class="btn btn-primary"
x-show="networkModeCell == 'LTE'"
@click="cellLockEnableLTE()"
>
Lock LTE Cells
</button>
<button
type="button"
class="btn btn-primary"
x-show="networkModeCell == 'NR5G-SA'"
@click="cellLockEnableNR()"
>
Lock NR5G-SA Cells
</button>
<button
type="button"
class="btn btn-danger"
x-show="networkModeCell == 'Unlock LTE'"
@click="cellLockDisableLTE()"
>
Unlock LTE Cells
</button>
<button
type="button"
class="btn btn-danger"
x-show="networkModeCell == 'Unlock NR5G-SA'"
@click="cellLockDisableNR()"
>
Unlock NR5G-SA Cells
</button>
</div>
</div>
</div>
</div>
</div>
<!-- Loading Modal for Locking Band -->
<div class="modal-overlay" x-show="showModal">
<div class="loading-modal">
<div class="loader"></div>
<div
class="loading-text"
style="display: flex; flex-direction: column"
>
<h3>Initializing Network...</h3>
<p style="margin-top: 0.5rem">
Please wait for
<span x-text="countdown" style="font-weight: 500"></span>
seconds.
</p>
</div>
</div>
</div>
</div>
</main>
<!-- Import Band Locking GUI JS -->
<!-- <script src="js/band-locking.js"></script> -->
<script src="js/generate-freq-box.js"></script>
<script src="js/populate-checkbox.js"></script>
<script src="js/parse-settings.js"></script>
<script>
function requestATInfo(atcmd) {
return fetch(
"/cgi-bin/get_atcommand?" +
new URLSearchParams({
atcmd: atcmd,
})
)
.then((response) => response.text())
.then((data) => {
return data;
})
.catch((error) => {
console.error("Error:", error);
// Throw the error again to ensure it's propagated
throw error;
});
}
function getSupportedBands() {
const atcmd = 'AT+QNWPREFCFG="policy_band"';
return requestATInfo(atcmd).then((rawdata) => {
let { lte_bands, nsa_bands, sa_bands } = parseSupportedBands(rawdata);
return {
lte_bands: lte_bands,
nsa_bands: nsa_bands,
sa_bands: sa_bands,
};
});
}
function parseSupportedBands(rawdata) {
const data = rawdata;
const regex = /"([^"]+)",([0-9:]+)/g;
// Object to store the results
const bands = {};
let match;
while ((match = regex.exec(data)) !== null) {
const bandType = match[1];
const numbers = match[2].split(":").map(Number);
bands[bandType] = numbers;
}
// Seperate the bands for each network mode
const lte_bands = bands.lte_band.join(":");
const nsa_bands = bands.nsa_nr5g_band.join(":");
const sa_bands = bands.nr5g_band.join(":");
return {
lte_bands,
nsa_bands,
sa_bands,
};
}
function parseLockedBands(rawdata) {
const data = rawdata;
const regex = /"([^"]+)",([0-9:]+)/g;
// Object to store the results
const bands = {};
let match;
while ((match = regex.exec(data)) !== null) {
const bandType = match[1];
const numbers = match[2].split(":").map(Number);
bands[bandType] = numbers;
}
// Seperate the bands for each network mode
const locked_lte_bands = bands.lte_band.join(":");
const locked_nsa_bands = bands.nsa_nr5g_band.join(":");
const locked_sa_bands = bands.nr5g_band.join(":");
return {
locked_lte_bands,
locked_nsa_bands,
locked_sa_bands,
};
}
function getLockedBands() {
const atcmd =
'AT+QNWPREFCFG="lte_band";+QNWPREFCFG= "nsa_nr5g_band";+QNWPREFCFG= "nr5g_band"';
return requestATInfo(atcmd).then((rawdata) => {
const lockedBandsData = parseLockedBands(rawdata);
return lockedBandsData;
});
}
function getCurrentSettings() {
const atcmd =
'AT+QUIMSLOT?;+CGCONTRDP=1;+QNWLOCK="common/4g";+QNWLOCK="common/5g";+QNWPREFCFG="mode_pref";+QNWPREFCFG="nr5g_disable_mode";+QCAINFO';
return requestATInfo(atcmd).then((rawdata) => {
const settings = parseCurrentSettings(rawdata);
return {
sim: settings.sim,
apn: settings.apn,
cellLockStatus: settings.cellLockStatus,
prefNetwork: settings.prefNetwork,
nrModeControl: settings.nrModeControl,
bands: settings.bands,
};
});
}
function cellLocking() {
return {
isLoading: false,
showModal: false,
countdown: 0,
networkModeCell: "-",
earfcn1: null,
pci1: null,
earfcn2: null,
pci2: null,
earfcn3: null,
pci3: null,
earfcn4: null,
pci4: null,
earfcn5: null,
pci5: null,
earfcn6: null,
pci6: null,
earfcn7: null,
pci7: null,
earfcn8: null,
pci8: null,
earfcn9: null,
pci9: null,
earfcn10: null,
pci10: null,
scs: null,
band: null,
apn: "-",
newApn: null,
prefNetwork: "-",
prefNetworkMode: null,
nrModeControl: "-",
cellNum: null,
lte_bands: null,
nsa_bands: null,
sa_bands: null,
locked_lte_bands: null,
locked_nsa_bands: null,
locked_sa_bands: null,
currentNetworkMode: "-",
updatedLockedBands: null,
sim: "-",
newSim: null,
cellLockStatus: "Unknown",
bands: "Fetching Bands...",
init() {
// Function to populate checkboxes
const showPopulateCheckboxes = () => {
this.isLoading = true;
Promise.all([getSupportedBands(), getLockedBands()])
.then(([supportedBandsData, lockedBandsData]) => {
this.lte_bands = supportedBandsData.lte_bands;
this.nsa_bands = supportedBandsData.nsa_bands;
this.sa_bands = supportedBandsData.sa_bands;
this.locked_lte_bands = lockedBandsData.locked_lte_bands;
this.locked_nsa_bands = lockedBandsData.locked_nsa_bands;
this.locked_sa_bands = lockedBandsData.locked_sa_bands;
// Once both promises are resolved, call populateCheckboxes
populateCheckboxes(
this.lte_bands,
this.nsa_bands,
this.sa_bands,
this.locked_lte_bands,
this.locked_nsa_bands,
this.locked_sa_bands,
this
);
// Set isLoading to false after populating checkboxes
this.isLoading = false;
// Add event listeners to checkboxes after populating them
addCheckboxListeners(this);
})
.catch((error) => {
console.error("Error:", error);
// Handle errors if any
});
};
// Function to track checkbox changes
this.trackCheckboxChanges = () => {
var selectedMode =
document.getElementById("networkModeBand").value;
var checkboxes = document.querySelectorAll(
'input[type="checkbox"]'
);
var newCheckedValues = [];
checkboxes.forEach(function (checkbox) {
if (checkbox.checked) {
newCheckedValues.push(checkbox.value);
}
});
// Update currentNetworkMode and updatedLockedBands
this.currentNetworkMode = selectedMode;
this.updatedLockedBands = newCheckedValues;
};
// Function to get the current settings
const getCurrentSettingsData = () => {
getCurrentSettings().then((settings) => {
this.sim = settings.sim;
this.apn = settings.apn;
this.cellLockStatus = settings.cellLockStatus;
this.prefNetwork = settings.prefNetwork;
this.nrModeControl = settings.nrModeControl;
this.bands = settings.bands;
});
};
// Function to add event listener to network mode dropdown
const addNetworkModeListener = () => {
document
.getElementById("networkModeBand")
.addEventListener("change", function () {
showPopulateCheckboxes(); // Update checkboxes when network mode changes
});
};
// Execute necessary functions on initialization
showPopulateCheckboxes();
addNetworkModeListener();
getCurrentSettingsData();
},
lockSelectedBands() {
// Get the updated this.currentNetworkMode = selectedMode; and this.updatedLockedBands = newCheckedValues;
const selectedMode = this.currentNetworkMode;
const newCheckedValues = this.updatedLockedBands;
let atcmd;
// Check if both values are null then show the error message
if (selectedMode === null || newCheckedValues === null) {
} else {
if (selectedMode === "LTE") {
atcmd = `AT+QNWPREFCFG="lte_band",${newCheckedValues.join(
":"
)}`;
this.sendATcommand(atcmd);
} else if (selectedMode === "NSA") {
atcmd = `AT+QNWPREFCFG="nsa_nr5g_band",${newCheckedValues.join(
":"
)}`;
this.sendATcommand(atcmd);
} else if (selectedMode === "SA") {
atcmd = `AT+QNWPREFCFG="nr5g_band",${newCheckedValues.join(
":"
)}`;
this.sendATcommand(atcmd);
} else {
alert("Invalid network mode selected");
}
// Do a 5 second countdown
this.showModal = true;
this.countdown = 5;
const interval = setInterval(() => {
this.countdown--;
if (this.countdown === 0) {
clearInterval(interval);
this.showModal = false;
// Refresh the page to show the updated bands
this.init();
}
}, 1000);
}
},
resetBandLocking() {
// Send the atcmd command to reset the locked bands
const atcmd = 'AT+QNWPREFCFG="restore_band"';
this.showModal = true;
this.sendATcommand(atcmd);
this.countdown = 5;
const interval = setInterval(() => {
this.countdown--;
if (this.countdown === 0) {
clearInterval(interval);
this.showModal = false;
// Refresh the page to show the updated bands
this.init();
}
}, 1000);
// Run init function to repopulate the checkboxes
this.init();
},
saveChanges() {
const newApn = this.newApn;
const newSim = this.newSim;
const prefNetworkMode = this.prefNetworkMode;
const nrModeControl = this.nrModeControl;
let atAPN, atSIM, ATNetwork, ATNRMode, atcmd;
atAPN = "";
atSIM = "";
ATNetwork = "";
ATNRMode = "";
if (newApn !== null) {
atAPN = `+CGDCONT=1,"IP","${newApn}";`;
}
if (newSim !== null) {
atSIM = `+QUIMSLOT=${newSim};`;
}
if (prefNetworkMode !== null) {
ATNetwork = `+QNWPREFCFG="mode_pref",${prefNetworkMode};`;
}
if (nrModeControl !== this.nrModeControl) {
ATNRMode = `+QNWPREFCFG="nr5g_disable_mode",${nrModeControl}`;
}
atcmd = `AT+${atAPN}${atSIM}${ATNetwork}${ATNRMode}`;
// If there is double + (++), remove 1 + from the command
atcmd = atcmd.replace("++", "+");
if (atcmd !== "AT+") {
this.showModal = true;
console.log(atcmd);
this.sendATcommand(atcmd);
// Do a 15 second countdown
this.countdown = 15;
const interval = setInterval(() => {
this.countdown--;
if (this.countdown === 0) {
clearInterval(interval);
this.showModal = false;
this.init();
}
}, 1000);
} else {
alert("No changes made");
}
},
cellLockEnableLTE() {
const cellNum = this.cellNum;
if (cellNum === null) {
alert("Please enter the number of cells to lock");
return; // Exit the function early if cellNum is null
}
// Create an array to hold earfcn and pci pairs
const earfcnPciPairs = [
{ earfcn: this.earfcn1, pci: this.pci1 },
{ earfcn: this.earfcn2, pci: this.pci2 },
{ earfcn: this.earfcn3, pci: this.pci3 },
{ earfcn: this.earfcn4, pci: this.pci4 },
{ earfcn: this.earfcn5, pci: this.pci5 },
{ earfcn: this.earfcn6, pci: this.pci6 },
{ earfcn: this.earfcn7, pci: this.pci7 },
{ earfcn: this.earfcn8, pci: this.pci8 },
{ earfcn: this.earfcn9, pci: this.pci9 },
{ earfcn: this.earfcn10, pci: this.pci10 },
];
// Filter out pairs where either earfcn or pci is null
const validPairs = earfcnPciPairs.filter(
(pair) => pair.earfcn !== null && pair.pci !== null
);
if (validPairs.length === 0) {
alert("Please enter at least one valid earfcn and pci pair");
return; // Exit the function early if no valid pairs are found
}
// Construct the AT command using the valid pairs
let atcmd = `+QNWLOCK="common/4g",${cellNum},${validPairs
.map((pair) => `${pair.earfcn},${pair.pci}`)
.join(",")}`;
atcmd = "AT+CFUN=0;" + atcmd + ";+CFUN=1";
// Mock data
this.showModal = true;
this.sendATcommand(atcmd);
// Do a 15 second countdown
this.countdown = 15;
const interval = setInterval(() => {
this.countdown--;
if (this.countdown === 0) {
clearInterval(interval);
this.showModal = false;
}
}, 1000);
},
cellLockEnableNR() {
const earfcn = this.earfcn1;
const pci = this.pci1;
const scs = this.scs;
const band = this.band;
if (
earfcn === null ||
pci === null ||
scs === null ||
band === null
) {
alert("Please enter all the required fields");
return; // Exit the function early if any of the fields are null
}
// Construct the AT command using the valid pairs
let atcmd = `+QNWLOCK="common/5g",${earfcn},${pci},${scs},${band}`;
atcmd = "AT+CFUN=0;" + atcmd + ";+CFUN=1";
// Mock data
this.showModal = true;
this.sendATcommand(atcmd);
// Do a 15 second countdown
this.countdown = 15;
const interval = setInterval(() => {
this.countdown--;
if (this.countdown === 0) {
clearInterval(interval);
this.showModal = false;
}
}, 1000);
},
cellLockDisableLTE() {
// Send the atcmd command to reset the locked bands
const atcmd = 'AT+CFUN=0;+QNWLOCK="common/4g,0;+CFUN=1"';
this.showModal = true;
this.sendATcommand(atcmd);
this.countdown = 15;
const interval = setInterval(() => {
this.countdown--;
if (this.countdown === 0) {
clearInterval(interval);
this.showModal = false;
}
}, 1000);
},
cellLockDisableNR() {
// Send the atcmd command to reset the locked bands
const atcmd = 'AT+CFUN=0;+QNWLOCK="common/5g,0;+CFUN=1"';
this.showModal = true;
this.sendATcommand(atcmd);
this.countdown = 15;
const interval = setInterval(() => {
this.countdown--;
if (this.countdown === 0) {
clearInterval(interval);
this.showModal = false;
}
}, 1000);
},
sendATcommand(atcmd) {
return fetch(
"/cgi-bin/get_atcommand?" +
new URLSearchParams({
atcmd: atcmd,
})
)
.then((response) => response.text())
.then((data) => {
return data;
})
.catch((error) => {
console.error("Error:", error);
// Throw the error again to ensure it's propagated
throw error;
});
},
};
}
function addCheckboxListeners(cellLock) {
// Remove existing event listeners
document
.querySelectorAll('input[type="checkbox"]')
.forEach(function (checkbox) {
checkbox.removeEventListener(
"change",
cellLock.trackCheckboxChanges
);
});
// Add new event listeners
document
.querySelectorAll('input[type="checkbox"]')
.forEach(function (checkbox) {
checkbox.addEventListener("change", cellLock.trackCheckboxChanges);
});
}
// Function for unchecking all checkboxes
document
.getElementById("uncheckAll")
.addEventListener("click", function () {
document
.querySelectorAll('input[type="checkbox"]')
.forEach(function (checkbox) {
checkbox.checked = false;
});
});
</script>
<script src="/js/dark-mode.js"></script>
</body>
</html>

667
simpleadmin/settings.html Normal file
View File

@@ -0,0 +1,667 @@
<!DOCTYPE html>
<html lang="en" data-bs-theme="light">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Simple Admin</title>
<link rel="stylesheet" href="/css/styles.css" />
<link rel="stylesheet" href="/css/bootstrap.min.css" />
<!-- Import BootStrap Javascript -->
<script src="/js/bootstrap.bundle.min.js"></script>
<script src="/js/alpinejs.min.js" defer></script>
</head>
<body>
<main>
<div class="container my-4" x-data="simpleSettings()">
<nav class="navbar navbar-expand-lg mt-2">
<div class="container-fluid">
<a class="navbar-brand" href="/"
><span class="mb-0 h4">Simple Admin</span></a
>
<button
class="navbar-toggler"
type="button"
data-bs-toggle="collapse"
data-bs-target="#navbarText"
aria-controls="navbarText"
aria-expanded="false"
aria-label="Toggle navigation"
>
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbarText">
<ul class="navbar-nav me-auto mb-2 mb-lg-0">
<li class="nav-item">
<a class="nav-link" href="/">Home</a>
</li>
<li class="nav-item">
<a class="nav-link" href="/network.html">Simple Network</a>
</li>
<li class="nav-item">
<a
class="nav-link active"
href="/settings.html"
aria-current="page"
>Simple Settings</a
>
</li>
<li class="nav-item">
<a class="nav-link" href="/sms.html">SMS</a>
</li>
<li class="nav-item">
<a class="nav-link" href="/deviceinfo.html"
>Device Information</a
>
</li>
</ul>
<span class="navbar-text">
<button class="btn btn-link text-reset" id="darkModeToggle">
Dark Mode
</button>
</span>
</div>
</div>
</nav>
<div class="row mt-5 mb-4">
<div class="col">
<div class="card">
<div class="card-header">AT Terminal</div>
<div class="card-body">
<div class="card-text">
<div class="form-floating mb-4">
<!-- At commands output here -->
<textarea
class="form-control"
placeholder="ATI"
id="atOutputBox"
style="height: 220px"
x-text="atCommandResponse"
readonly
>
<label for="floatingTextarea">ATI</label>
</textarea>
</div>
<div>
<div class="col-md-4 mb-3">
<label for="exampleInputEmail1" class="form-label"
>AT Command</label
>
<input
type="text"
class="form-control"
id="atCommandInput"
placeholder="ATI"
aria-describedby="atCommandInput"
x-model="atcmd"
@keydown.enter = "sendATCommand()"
/>
<div id="atCommandInputHelper" class="form-text">
Seperate multiple commands with comma (,).
</div>
</div>
<div
class="d-grid gap-2 d-md-flex justify-content-md-start"
>
<button
class="btn btn-primary me-md-2"
type="button"
@click="sendATCommand()"
:disabled="isLoading"
>
Submit
</button>
<button
class="btn btn-danger"
type="button"
@click="clearResponses()"
:disabled="isClean"
>
Clear
</button>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="row">
<div class="col">
<div class="card">
<div class="card-header">One Click Utilities</div>
<div class="card-body">
<div class="card-text">
<div class="table-responsive">
<table class="table">
<tbody>
<tr>
<th scope="row">Reboot</th>
<td>
<button
type="submit"
class="btn btn-danger"
@click="showRebootModal()"
:disabled="isLoading"
>
Reboot
</button>
</td>
</tr>
<tr>
<th scope="row">Reset AT Commands Settings</th>
<td>
<button
type="submit"
class="btn btn-danger"
@click="resetATCommands()"
:disabled="isLoading"
>
Reset
</button>
</td>
</tr>
<tr>
<th scope="row">ETH IP Passthrough</th>
<td>
<button
type="submit"
class="btn btn-primary"
@click="ethPassthroughEnable()"
x-show="ethPassStatus === false"
:disabled="isLoading"
>
Enable
</button>
<button
type="submit"
class="btn btn-danger"
@click="ethPassthroughDisable()"
x-show="ethPassStatus === true"
:disabled="isLoading"
>
Disable
</button>
</td>
</tr>
<tr>
<th scope="row">Onboard DNS Proxy</th>
<td>
<button
type="submit"
class="btn btn-primary"
@click="onBoardDNSProxyEnable()"
x-show="DNSProxyStatus === false"
:disabled="isLoading"
>
Enable
</button>
<button
type="submit"
class="btn btn-danger"
@click="onBoardDNSProxyDisable()"
x-show="DNSProxyStatus === true"
:disabled="isLoading"
>
Disable
</button>
</td>
</tr>
<tr>
<th scope="row">USB Mode</th>
<td>
<button
type="submit"
class="btn btn-primary"
@click="usbModeEnable()"
x-show="USBModeStatus === true"
:disabled="isLoading"
>
Enable
</button>
<button
type="submit"
class="btn btn-danger"
@click="usbModeDisable()"
x-show="USBModeStatus === false"
:disabled="isLoading"
>
Disable
</button>
</td>
</tr>
<tr>
<th scope="row">Scan Neighbour LTE</th>
<td>
<button
type="submit"
class="btn btn-primary"
@click="scanLTE()"
:disabled="isLoading"
>
Scan LTE
</button>
</td>
</tr>
<tr>
<th scope="row">Scan Neighbour NSA</th>
<td>
<button
type="submit"
class="btn btn-primary"
@click="scanNSA()"
:disabled="isLoading"
>
Scan NSA
</button>
</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
<div class="col">
<div class="card mb-3">
<div class="card-header">TTL and Network Scan Settings</div>
<div class="card-body">
<label for="TTLState" class="form-label"
>TTL State and Value</label
>
<div class="row">
<div class="col">
<div
class="p-3 text-primary-emphasis bg-primary-subtle border border-primary-subtle rounded-3"
x-show="ttlStatus === true"
>
TTL is Active
</div>
<div
class="p-3 text-danger-emphasis bg-danger-subtle border border-danger-subtle rounded-3"
x-show="ttlStatus === false"
>
TTL is Inactive
</div>
</div>
<div class="col">
<div
class="p-3 text-info-emphasis bg-info-subtle border border-info-subtle rounded-3 mb-4"
x-text="ttlvalue"
></div>
</div>
</div>
<div class="card-text mb-3">
<div class="mb-4">
<input
type="text"
class="form-control"
id="ttlInput"
placeholder="TTL Value"
x-model="newTTL"
/>
<div id="ttlValueHelper" class="form-text">
Set TTL Value to 0 to disable.
</div>
</div>
<div class="d-grid gap-2">
<button
class="btn btn-primary"
type="button"
@click="setTTL()"
>
Update
</button>
</div>
</div>
<div class="card-text">
<!-- Select Input Scan Here -->
<div class="mb-3">
<label for="networkScan" class="form-label"
>Network Scan</label
>
<select
class="form-select"
id="networkScan"
x-model="fullScanModeType"
>
<option selected>Choose Scan Mode</option>
<option value="LTE">LTE</option>
<option value="NR5G">NR5G</option>
<option value="ALL">ALL</option>
</select>
</div>
<div class="d-grid gap-2">
<button
class="btn btn-primary"
type="button"
@click="fullScanMode()"
:disabled="isLoading"
x-text="scanStart ? 'Scanning... Please wait.' : 'Start Scan'"
></button>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Loading Modal for Reboot -->
<div class="modal-overlay" x-show="showModal">
<div class="loading-modal">
<div
class="loading-text"
style="display: flex; flex-direction: column"
>
<h3>This will reboot the modem.</h3>
<p style="margin-top: 0.5rem">Continue?</p>
</div>
<div class="d-grid gap-2 d-md-block">
<button
class="btn btn-primary"
type="button"
@click="rebootDevice()"
>
Reboot
</button>
<button
class="btn btn-danger"
type="button"
@click="closeModal()"
>
Cancel
</button>
</div>
</div>
</div>
<!-- Reboot Modal Countdown -->
<div class="modal-overlay" x-show="isRebooting">
<div class="loading-modal">
<div class="loader"></div>
<div
class="loading-text"
style="display: flex; flex-direction: column"
>
<h3>Rebooting...</h3>
<p style="margin-top: 0.5rem">
Please wait for
<span x-text="countdown" style="font-weight: 500"></span>
seconds.
</p>
</div>
</div>
</div>
</div>
</main>
<script src="/js/dark-mode.js"></script>
<script>
function simpleSettings() {
return {
isLoading: false,
showSuccess: false,
showError: false,
isClean: true,
showModal: false,
isRebooting: false,
atcmd: "",
fetchATCommand: "",
countdown: 0,
atCommandResponse: "",
currentSettingsResponse: "",
ttldata: null,
ttlvalue: 0,
ttlStatus: false,
newTTL: null,
ethPassStatus: false,
DNSProxyStatus: true,
USBModeStatus: true,
fullScanModeType: "",
scanStart: false,
closeModal() {
this.confirmModal = false;
this.showModal = false;
},
showRebootModal() {
this.showModal = true;
},
sendATCommand() {
if (!this.atcmd) {
// Use ATI as default command
this.atcmd = "ATI";
console.log(
"AT Command is empty, using ATI as default command: ",
this.atcmd
);
}
this.isLoading = true;
fetch(
"/cgi-bin/get_atcommand?" +
new URLSearchParams({
atcmd: this.atcmd,
})
)
.then((res) => {
return res.text();
})
.then((data) => {
this.atCommandResponse = data;
this.isLoading = false;
this.isClean = false;
})
.catch((error) => {
console.error("Error: ", error);
this.showError = true;
});
},
clearResponses() {
this.atCommandResponse = "";
this.isClean = true;
},
rebootDevice() {
this.atcmd = "AT+CFUN=1,1";
this.sendATCommand();
this.atCommandResponse = "";
this.showModal = false;
this.isRebooting = true;
this.countdown = 40;
// Do the countdown
const interval = setInterval(() => {
this.countdown--;
if (this.countdown === 0) {
clearInterval(interval);
this.isRebooting = false;
this.init();
}
}, 1000);
},
resetATCommands() {
this.atcmd = "AT&F";
this.sendATCommand();
console.log("Resetting AT Commands");
this.atcmd = "";
this.atCommandResponse = "";
this.showRebootModal();
},
ethPassthroughEnable() {
this.atcmd = 'AT+QMAP="MPDN_RULE",0,1,0,1,1,"FF:FF:FF:FF:FF:FF"';
this.sendATCommand();
this.fetchCurrentSettings();
},
ethPassthroughDisable() {
this.atcmd = 'AT+QMAP="MPDN_RULE",0';
this.sendATCommand();
this.fetchCurrentSettings();
},
onBoardDNSProxyEnable() {
this.atcmd = 'AT+QMAP="DHCPV4DNS","enable"';
this.sendATCommand();
this.fetchCurrentSettings();
},
onBoardDNSProxyDisable() {
this.atcmd = 'AT+QMAP="DHCPV4DNS","disable"';
this.sendATCommand();
this.fetchCurrentSettings();
},
usbModeEnable() {
this.atcmd = "AT+QMAPWAC=1";
this.sendATCommand();
this.fetchCurrentSettings();
},
usbModeDisable() {
this.atcmd = "AT+QMAPWAC=0";
this.sendATCommand();
this.fetchCurrentSettings();
},
scanLTE() {
this.atcmd = 'AT+QENG="neighbourcell"';
this.sendATCommand();
},
scanNSA() {
this.atcmd =
'AT+QNWCFG="nr5g_meas_info",1;+QNWCFG="nr5g_meas_info"';
this.sendATCommand();
},
fullScanMode() {
switch (this.fullScanModeType) {
case "LTE":
this.atcmd = "AT+QSCAN=1,1";
this.scanStart = true;
this.atCommandResponse = "Scanning all available LTE networks... This might take a while."
this.sendATCommand();
this.scanStart = false;
break;
case "NR5G":
this.atcmd = "AT+QSCAN=2,1";
this.scanStart = true;
this.atCommandResponse = "Scanning all available NR5G-SA networks... This might take a while."
this.sendATCommand();
this.scanStart = false;
break;
case "ALL":
this.atcmd = "AT+QSCAN=3,1";
this.scanStart = true;
this.atCommandResponse = "Scanning all available networks... This might take a while."
this.sendATCommand();
this.scanStart = false;
break;
default:
alert("Select a Scan Mode First");
}
},
fetchCurrentSettings() {
this.fetchATCommand =
'AT+QMAP="MPDN_RULE";+QMAP="DHCPV4DNS";+QMAPWAC?';
fetch(
"/cgi-bin/get_atcommand?" +
new URLSearchParams({
atcmd: this.fetchATCommand,
})
)
.then((res) => {
return res.text();
})
.then((data) => {
// Set the value of currentSettingsResponse
this.currentSettingsResponse = data;
const currentData = data.split("\n");
const testEthpass = currentData[1].match(
/\+QMAP: "MPDN_rule",0,0,0,0,0/
);
if (testEthpass) {
this.ethPassStatus = false;
} else {
this.ethPassStatus = true;
}
const testDNSProxy = currentData[6].match(
/\+QMAP: "DHCPV4DNS","enable"/
);
if (testDNSProxy) {
this.DNSProxyStatus = true;
} else {
this.DNSProxyStatus = false;
}
const testUSBMode = currentData[8].match(/\+QMAPWAC: 1/);
if (testUSBMode) {
this.USBModeStatus = true;
} else {
this.USBModeStatus = false;
}
// clear atcmd
this.atcmd = "";
})
.catch((error) => {
console.error("Error: ", error);
this.showError = true;
});
},
fetchTTL() {
fetch("/cgi-bin/get_ttl_status")
.then((res) => res.json())
.then((data) => {
this.ttldata = data;
this.ttlStatus = this.ttldata.isEnabled;
this.ttlvalue = this.ttldata.ttl;
});
},
setTTL() {
this.isLoading = true; // Set loading state while updating TTL
fetch(
"/cgi-bin/set_ttl?" +
new URLSearchParams({
ttlvalue: this.newTTL,
})
)
.then((res) => res.json())
.then((data) => {
// Once TTL is updated, fetch the updated TTL data
this.fetchTTL();
this.isLoading = false; // Set loading state back to false
})
.catch((error) => {
console.error("Error updating TTL: ", error);
this.isLoading = false; // Ensure loading state is properly handled in case of error
});
},
init() {
this.fetchTTL();
this.fetchCurrentSettings();
},
};
}
</script>
</body>
</html>

295
simpleadmin/sms.html Normal file
View File

@@ -0,0 +1,295 @@
<!DOCTYPE html>
<html lang="en" data-bs-theme="light">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Simple Admin</title>
<!-- <link
href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css"
rel="stylesheet"
integrity="sha384-QWTKZyjpPEjISv5WaRU9OFeRpok6YctnYmDr5pNlyT2bRjXh0JMhjY6hW+ALEwIH"
crossorigin="anonymous"
/> -->
<!-- Import all the bootstrap css files from css folder -->
<link rel="stylesheet" href="/css/styles.css" />
<link rel="stylesheet" href="/css/bootstrap.min.css" />
<!-- Import BootStrap Javascript -->
<script src="/js/bootstrap.bundle.min.js"></script>
<script src="/js/alpinejs.min.js" defer></script>
<script src="/js/auth.js"></script>
</head>
<body onload="isAuthenticated()">
<main>
<div class="container my-4" x-data="fetchSMS()">
<nav class="navbar navbar-expand-lg mt-2">
<div class="container-fluid">
<a class="navbar-brand" href="/"
><span class="mb-0 h4">Simple Admin</span></a
>
<button
class="navbar-toggler"
type="button"
data-bs-toggle="collapse"
data-bs-target="#navbarText"
aria-controls="navbarText"
aria-expanded="false"
aria-label="Toggle navigation"
>
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbarText">
<ul class="navbar-nav me-auto mb-2 ml-4 mb-lg-0">
<li class="nav-item">
<a class="nav-link" href="/">Home</a>
</li>
<li class="nav-item">
<a class="nav-link" href="/network.html">Simple Network</a>
</li>
<li class="nav-item">
<a class="nav-link" href="/settings.html">Simple Settings</a>
</li>
<li class="nav-item">
<a
class="nav-link active"
href="/sms.html"
aria-current="page"
>SMS</a
>
</li>
<li class="nav-item">
<a class="nav-link" href="/deviceinfo.html"
>Device Information</a
>
</li>
</ul>
<span class="navbar-text">
<button class="btn btn-link text-reset" id="darkModeToggle">
Dark Mode
</button>
</span>
</div>
</div>
</nav>
<div class="row mt-5 mb-4">
<div class="col">
<div class="card">
<div class="card-header">SMS Manager</div>
<div class="card-body">
<div class="card-text">
<div class="col">
<div
style="
max-height: 400px;
overflow-y: scroll;
overflow-x: hidden;
"
>
<table class="table table-hover border-success">
<tbody>
<div class="card-subtitle" x-show="isLoading">
<h4>Fetching SMS...</h4>
</div>
<!-- Only show template if isLoading is False -->
<template x-if="messages.length === 0 && isLoading === false" >
<div>
<p>Message Empty</p>
</div>
</template>
<template
x-for="(message, index) in messages"
:key="index"
>
<tr class="">
<td>
<div class="row column-gap-1 mb-2">
<div class="col-md-3">
<p x-text="'Sender: ' + senders[index]"></p>
</div>
<div class="col">
<p
x-text="'Date and Time: ' + dates[index]"
></p>
</div>
</div>
<div class="col-md-9">
<p x-text="message"></p>
</div>
</td>
</tr>
</template>
</tbody>
</table>
</div>
</div>
<form>
<div class="col-md-4 my-4">
<label for="PhoneNumber" class="form-label"
>Send SMS</label
>
<input
type="text"
class="form-control"
placeholder="Phone Number"
aria-label="Phone Number"
x-model="phoneNumber"
/>
</div>
<div class="col-md-8 mb-3">
<!-- Text Area Here for SMS Input -->
<textarea
class="form-control"
id="smsInputBox"
rows="5"
placeholder="Enter SMS here (!!!CURRENTLY UNDER DEVELOPMENT!!!)"
x-model="messageToSend"
></textarea>
</div>
<div
class="d-grid gap-2 d-md-flex justify-content-md-start"
>
<button class="btn btn-primary me-md-2" type="button" @click="sendSMS()" :disabled="true" >
Send SMS
</button>
<button
class="btn btn-success"
type="button"
@click="init()"
>
Refresh
</button>
<button
class="btn btn-danger"
type="button"
@click="deleteAllSMS()"
>
Delete All SMS
</button>
</div>
</form>
</div>
</div>
</div>
</div>
</div>
</div>
</main>
<script src="/js/dark-mode.js"></script>
<script>
function fetchSMS() {
return {
isLoading: false,
atCommandResponse: "",
messageToSend: "",
phoneNumber: "",
messages: [],
senders: [],
dates: [],
requestSMS() {
this.isLoading = true;
this.atCommandResponse = "Loading...";
// Expect a text response from the server
fetch("/cgi-bin/get_sms")
.then((response) => response.text())
.then((data) => {
this.isLoading = false;
this.atCommandResponse = data;
})
.finally(() => {
this.parseSMSData(this.atCommandResponse);
});
},
parseSMSData(data) {
const cmglRegex =
/^\s*\+CMGL:\s*(\d+),"[^"]*","([^"]*)"[^"]*,"([^"]*)"/gm;
let match;
while ((match = cmglRegex.exec(data)) !== null) {
const index = parseInt(match[1]);
const sender = match[2];
let date = match[3];
// remove +32 from the date
date = date.replace("+32", "");
// Find the start and end positions of the message
const startIndex = cmglRegex.lastIndex;
let endIndex = data.indexOf("+CMGL:", startIndex + 1);
if (endIndex === -1) {
// If no more +CMGL lines, set end index to end of string
endIndex = data.length;
}
// Extract the message from start to end index
const message = data.substring(startIndex, endIndex).trim();
this.messages.push(message);
this.senders.push(sender);
this.dates.push(date);
}
},
deleteAllSMS() {
const atcmd = "AT+CMGD=,4";
fetch(
"/cgi-bin/get_atcommand?" +
new URLSearchParams({
atcmd: atcmd,
})
)
.then((response) => response.text())
.then((data) => {
console.log(data);
})
.finally(() => {
this.requestSMS();
});
},
sendSMS() {
this.isLoading = true;
fetch(
"/cgi-bin/send_sms?" +
new URLSearchParams({
phone_number: this.phoneNumber,
message: this.messageToSend,
})
)
.then((response) => response.text())
.then((data) => {
console.log(data);
})
.catch((error) => {
console.error("Error:", error);
})
.finally(() => {
this.isLoading = false;
this.requestSMS();
});
},
init() {
// Send AT+CMGF=1 once to set the modem to text mode
const atcmd = "AT+CMGF=1";
fetch(
"/cgi-bin/get_atcommand?" +
new URLSearchParams({
atcmd: atcmd,
})
)
.then((response) => response.text())
.then((data) => {
console.log(data);
})
.finally(() => {
this.requestSMS();
});
},
};
}
</script>
</body>
</html>

View File

@@ -0,0 +1,11 @@
simpleadmin_httpd.service[Unit]
Description=SimpleAdmin httpd service
After=network.target
[Service]
Type=simple
ExecStart=/usr/sbin/httpd -f -h /usrdata/simpleadmin/www -p 8080
ExecStop=/bin/kill -WINCH ${MAINPID}
[Install]
WantedBy=multi-user.target