Merge pull request #83 from dr-dolomite/QuecManager

Added Cellular Settings Functionality
This commit is contained in:
Cameron Thompson
2024-10-05 02:59:50 -04:00
committed by GitHub
17 changed files with 1124 additions and 267 deletions

View File

@@ -63,12 +63,12 @@
<div class="navbar-item has-dropdown is-hoverable">
<a class="navbar-link"> Cellular </a>
<div class="navbar-dropdown is-boxed">
<a class="navbar-item" href="/cell-settings.html"
>Cell Settings</a
>
<a class="navbar-item" href="/bandlock.html"> Band Locking </a>
<a class="navbar-item" href="/cell-locking.html">Cell Locking</a>
<a class="navbar-item" href="/cell-scanner.html">Cell Scanner</a>
<a class="navbar-item" href="/cell-settings.html"
>Cell Settings</a
>
<a class="navbar-item" href="/cell-sms.html"> Cell SMS </a>
</div>
</div>

View File

@@ -66,12 +66,12 @@
<div class="navbar-item has-dropdown is-hoverable">
<a class="navbar-link"> Cellular </a>
<div class="navbar-dropdown is-boxed">
<a class="navbar-item" href="/bandlock.html"> Band Locking </a>
<a class="navbar-item" href="/cell-locking.html">Cell Locking</a>
<a class="navbar-item" href="/cell-scanner.html">Cell Scanner</a>
<a class="navbar-item" href="/cell-settings.html"
>Cell Settings</a
>
<a class="navbar-item" href="/bandlock.html"> Band Locking </a>
<a class="navbar-item" href="/cell-locking.html">Cell Locking</a>
<a class="navbar-item" href="/cell-scanner.html">Cell Scanner</a>
<a class="navbar-item" href="/cell-sms.html"> Cell SMS </a>
</div>
</div>
@@ -255,7 +255,9 @@
<div class="column">
<div class="field">
<label class="label">Connected Devices MAC</label>
<label class="label"
>Connected Devices MAC</label
>
<p class="control has-icons-left">
<span class="select">
<select id="connected-devices">
@@ -287,7 +289,9 @@
Select USB Modem Protocol
</option>
<option value="RMNET">RMNET</option>
<option value="ECM (Recommended)">ECM (Recommended)</option>
<option value="ECM (Recommended)">
ECM (Recommended)
</option>
<option value="MBIM">MBIM</option>
<option value="RNDIS">RNDIS</option>
</select>
@@ -297,7 +301,8 @@
</span>
</p>
<p class="help">
Selecting a mode will apply immediately. Requires reboot.
Selecting a mode will apply immediately. Requires
reboot.
</p>
</div>
</div>

View File

@@ -63,12 +63,12 @@
<div class="navbar-item has-dropdown is-hoverable">
<a class="navbar-link has-text-weight-bold"> Cellular </a>
<div class="navbar-dropdown is-boxed">
<a class="navbar-item" href="/bandlock.html"> Band Locking </a>
<a class="navbar-item" href="/cell-locking.html">Cell Locking</a>
<a class="navbar-item" href="/cell-scanner.html">Cell Scanner</a>
<a class="navbar-item" href="/cell-settings.html"
>Cell Settings</a
>
<a class="navbar-item" href="/bandlock.html"> Band Locking </a>
<a class="navbar-item" href="/cell-locking.html">Cell Locking</a>
<a class="navbar-item" href="/cell-scanner.html">Cell Scanner</a>
<a class="navbar-item" href="/cell-sms.html"> Cell SMS </a>
</div>
</div>

View File

@@ -63,12 +63,12 @@
<div class="navbar-item has-dropdown is-hoverable">
<a class="navbar-link has-text-weight-bold"> Cellular </a>
<div class="navbar-dropdown is-boxed">
<a class="navbar-item" href="/bandlock.html"> Band Locking </a>
<a class="navbar-item" href="/cell-locking.html">Cell Locking</a>
<a class="navbar-item" href="/cell-scanner.html">Cell Scanner</a>
<a class="navbar-item" href="/cell-settings.html"
>Cell Settings</a
>
<a class="navbar-item" href="/bandlock.html"> Band Locking </a>
<a class="navbar-item" href="/cell-locking.html">Cell Locking</a>
<a class="navbar-item" href="/cell-scanner.html">Cell Scanner</a>
<a class="navbar-item" href="/cell-sms.html"> Cell SMS </a>
</div>
</div>

View File

@@ -63,12 +63,12 @@
<div class="navbar-item has-dropdown is-hoverable">
<a class="navbar-link has-text-weight-bold"> Cellular </a>
<div class="navbar-dropdown is-boxed">
<a class="navbar-item" href="/bandlock.html"> Band Locking </a>
<a class="navbar-item" href="/cell-locking.html">Cell Locking</a>
<a class="navbar-item" href="/cell-scanner.html">Cell Scanner</a>
<a class="navbar-item" href="/cell-settings.html"
>Cell Settings</a
>
<a class="navbar-item" href="/bandlock.html"> Band Locking </a>
<a class="navbar-item" href="/cell-locking.html">Cell Locking</a>
<a class="navbar-item" href="/cell-scanner.html">Cell Scanner</a>
<a class="navbar-item" href="/cell-sms.html"> Cell SMS </a>
</div>
</div>
@@ -89,7 +89,10 @@
<div class="navbar-item">
<div class="buttons is-flex-direction-column-mobile">
<div class="control is-expanded-mobile">
<div id="restartConnectionBtn" class="button is-link is-outlined is-fullwidth-mobile">
<div
id="restartConnectionBtn"
class="button is-link is-outlined is-fullwidth-mobile"
>
<span class="icon">
<i class="fas fa-arrows-rotate"></i>
</span>

View File

@@ -24,6 +24,8 @@
<script src="/js/styles/modal-trigger.js"></script>
<script src="/js/utils/reboot.js"></script>
<script src="/js/utils/restart-connection.js"></script>
<script src="/js/cell-settings/fetch-settings.js"></script>
<script src="/js/cell-settings/apn-profile.js"></script>
<script defer src="/js/auth/auth.js"></script>
<script>
@@ -63,12 +65,12 @@
<div class="navbar-item has-dropdown is-hoverable">
<a class="navbar-link has-text-weight-bold"> Cellular </a>
<div class="navbar-dropdown is-boxed">
<a class="navbar-item" href="/bandlock.html"> Band Locking </a>
<a class="navbar-item" href="/cell-locking.html">Cell Locking</a>
<a class="navbar-item" href="/cell-scanner.html">Cell Scanner</a>
<a class="navbar-item" href="/cell-settings.html"
>Cell Settings</a
>
<a class="navbar-item" href="/bandlock.html"> Band Locking </a>
<a class="navbar-item" href="/cell-locking.html">Cell Locking</a>
<a class="navbar-item" href="/cell-scanner.html">Cell Scanner</a>
<a class="navbar-item" href="/cell-sms.html"> Cell SMS </a>
</div>
</div>
@@ -89,7 +91,10 @@
<div class="navbar-item">
<div class="buttons is-flex-direction-column-mobile">
<div class="control is-expanded-mobile">
<div id="restartConnectionBtn" class="button is-link is-outlined is-fullwidth-mobile">
<div
id="restartConnectionBtn"
class="button is-link is-outlined is-fullwidth-mobile"
>
<span class="icon">
<i class="fas fa-arrows-rotate"></i>
</span>
@@ -136,228 +141,233 @@
</nav>
<div class="column-margin">
<div class="fixed-grid has-2-cols has-1-cols-mobile">
<div class="grid">
<div class="cell">
<div class="card">
<div class="card-header">
<div class="card-header-title">Basic Cellular Settings</div>
</div>
<div class="card-content">
<div class="fixed-grid has-2-cols">
<div class="grid is-gap-5">
<div class="cell">
<div class="field">
<label class="label">Current APN</label>
<div class="control">
<input
class="input"
type="text"
placeholder="Current APN Here"
/>
</div>
<p class="help">
Changing this will disable automatic APN.
</p>
<div class="columns">
<div class="column">
<div class="card">
<div class="card-header">
<div class="card-header-title">Basic Cellular Settings</div>
</div>
<div class="card-content">
<div class="fixed-grid has-2-cols has-1-cols-mobile">
<div class="grid is-gap-5">
<div class="cell">
<div class="field">
<label class="label">Current APN</label>
<div class="control">
<input
class="input"
type="text"
placeholder="Current APN Here"
id="currentAPN"
/>
</div>
<p class="help">
Changing this will disable automatic APN.
</p>
</div>
</div>
<div class="cell">
<div class="field">
<label class="label">APN PDP Type</label>
<p class="control has-icons-left">
<span class="select">
<select>
<option selected>Select APN PDP Type</option>
<option>IPv4 Only</option>
<option>IPv6 Only</option>
<option>IPv4 and IPv6</option>
<option>P2P Protocol</option>
</select>
</span>
<span class="icon is-small is-left">
<i class="fas fa-globe"></i>
</span>
</p>
</div>
<div class="cell">
<div class="field">
<label class="label">APN PDP Type</label>
<p class="control has-icons-left">
<span class="select">
<select id="apnPDP">
<option selected>Select APN PDP Type</option>
<option value="IP">IPv4 Only</option>
<option value="IPV6">IPv6 Only</option>
<option value="IPV4V6">IPv4 and IPv6</option>
<option value="PPP">P2P Protocol</option>
</select>
</span>
<span class="icon is-small is-left">
<i class="fas fa-globe"></i>
</span>
</p>
</div>
</div>
<div class="cell">
<div class="field">
<label class="label">Preferred Network Mode</label>
<p class="control has-icons-left">
<span class="select">
<select>
<option selected>
Select Preferred Network Mode
</option>
<option>Automatic</option>
<option>LTE Only</option>
<option>5G SA Only</option>
<option>5G NSA Only</option>
</select>
</span>
<span class="icon is-small is-left">
<i class="fas fa-signal"></i>
</span>
</p>
</div>
<div class="cell">
<div class="field">
<label class="label">Preferred Network Mode</label>
<p class="control has-icons-left">
<span class="select">
<select id="networkPreference">
<option value="placeholder" selected>
Select Preferred Network Mode
</option>
<option value="AUTO">Automatic</option>
<option value="LTE">LTE Only</option>
<option value="NR5G">5G SA Only</option>
<option value="NR5G:LTE">5G NSA Only</option>
</select>
</span>
<span class="icon is-small is-left">
<i class="fas fa-signal"></i>
</span>
</p>
<p class="help">
Selecting a mode will apply immediately.
</p>
</div>
</div>
<div class="cell">
<div class="field">
<label class="label">NR5G Mode Control</label>
<p class="control has-icons-left">
<span class="select">
<select>
<option selected>Select NR5G Mode Control</option>
<option>Enable NR5G SA and NSA</option>
<option>Enable SA Only</option>
<option>Enable NSA Only</option>
</select>
</span>
<span class="icon is-small is-left">
<i class="fas fa-podcast"></i>
</span>
</p>
</div>
<div class="cell">
<div class="field">
<label class="label">NR5G Mode Control</label>
<p class="control has-icons-left">
<span class="select">
<select id="nr5gModeControl">
<option selected>Select NR5G Mode Control</option>
<option value="0">Enable NR5G SA and NSA</option>
<option value="2">Enable SA Only</option>
<option value="1">Enable NSA Only</option>
</select>
</span>
<span class="icon is-small is-left">
<i class="fas fa-podcast"></i>
</span>
</p>
<p class="help">
Selecting a mode will apply immediately.
</p>
</div>
</div>
</div>
</div>
<div class="card-footer">
<a
href="#"
class="card-footer-item has-text-link has-text-weight-semibold has-text-white"
>
Save
</a>
<a
href="#"
class="card-footer-item has-text-link has-text-weight-semibold has-text-white"
>
Reset
</a>
</div>
</div>
<div class="card-footer">
<a
href="#"
class="card-footer-item has-text-link has-text-weight-semibold has-text-white"
>
Save APN
</a>
<a
href="#"
class="card-footer-item has-text-link has-text-weight-semibold has-text-white"
>
Reset APN
</a>
</div>
</div>
</div>
<div class="cell">
<div class="card">
<div class="card-header">
<div class="card-header-title">APN and ICCID Based Locking</div>
</div>
<div class="card-content">
<div class="fixed-grid has-1-cols">
<div class="grid is-gap-5">
<div class="cell">
<div class="fixed-grid has-2-cols">
<div class="grid is-gap-3">
<div class="cell">
<div class="field">
<label class="label">APN Profile 1</label>
<div class="control">
<input
class="input"
type="text"
placeholder="APN Here"
/>
</div>
<p class="help">
This will override the current APN.
</p>
<div class="column">
<div class="card" id="apnProfileForm">
<div class="card-header">
<div class="card-header-title">APN and ICCID Based Locking</div>
</div>
<div class="card-content">
<div class="fixed-grid has-1-cols">
<div class="grid is-gap-5">
<div class="cell">
<div class="fixed-grid has-2-cols has-1-cols-mobile">
<div class="grid is-gap-3">
<div class="cell is-col-span-2-mobile">
<div class="field">
<label class="label">APN Profile 1</label>
<div class="control">
<input
class="input"
type="text"
placeholder="APN Here"
id="apnProfile1"
/>
</div>
<p class="help">
This will override the current APN.
</p>
</div>
</div>
<div class="cell">
<div class="field">
<label class="label">APN PDP Type</label>
<p class="control has-icons-left">
<span class="select">
<select>
<option selected>
Select APN PDP Type
</option>
<option>IPv4 Only</option>
<option>IPv6 Only</option>
<option>IPv4 and IPv6</option>
<option>P2P Protocol</option>
</select>
</span>
<span class="icon is-small is-left">
<i class="fas fa-globe"></i>
</span>
</p>
</div>
<div class="cell is-col-span-2-mobile">
<div class="field">
<label class="label">APN PDP Type</label>
<p class="control has-icons-left">
<span class="select">
<select id="apnPDPType1">
<option selected>Select APN PDP Type</option>
<option value="IP">IPv4 Only</option>
<option value="IPV6">IPv6 Only</option>
<option value="IPV4V6">IPv4 and IPv6</option>
<option value="PPP">P2P Protocol</option>
</select>
</span>
<span class="icon is-small is-left">
<i class="fas fa-globe"></i>
</span>
</p>
</div>
</div>
<div class="cell is-col-span-2">
<div class="field">
<label class="label">ICCID Profile 1</label>
<div class="control">
<input
class="input"
type="text"
placeholder="ICCID Here"
/>
</div>
<div class="cell is-col-span-2">
<div class="field">
<label class="label">ICCID Profile 1</label>
<div class="control">
<input
class="input"
type="text"
placeholder="ICCID Here"
id="iccidProfile1"
/>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="cell">
<div class="fixed-grid has-2-cols">
<div class="grid is-gap-3">
<div class="cell">
<div class="field">
<label class="label">APN Profile 2</label>
<div class="control">
<input
class="input"
type="text"
placeholder="APN Here"
/>
</div>
<p class="help">
This will override the current APN.
</p>
<div class="cell">
<div class="fixed-grid has-2-cols">
<div class="grid is-gap-3">
<div class="cell is-col-span-2-mobile">
<div class="field">
<label class="label">APN Profile 2</label>
<div class="control">
<input
class="input"
type="text"
placeholder="APN Here"
id="apnProfile2"
/>
</div>
<p class="help">
This will override the current APN.
</p>
</div>
</div>
<div class="cell">
<div class="field">
<label class="label">APN PDP Type</label>
<p class="control has-icons-left">
<span class="select">
<select>
<option selected>
Select APN PDP Type
</option>
<option>IPv4 Only</option>
<option>IPv6 Only</option>
<option>IPv4 and IPv6</option>
<option>P2P Protocol</option>
</select>
</span>
<span class="icon is-small is-left">
<i class="fas fa-globe"></i>
</span>
</p>
</div>
<div class="cell is-col-span-2-mobile">
<div class="field">
<label class="label">APN PDP Type</label>
<p class="control has-icons-left">
<span class="select">
<select id="apnPDPType2">
<option selected>Select APN PDP Type</option>
<option value="IP">IPv4 Only</option>
<option value="IPV6">IPv6 Only</option>
<option value="IPV4V6">IPv4 and IPv6</option>
<option value="PPP">P2P Protocol</option>
</select>
</span>
<span class="icon is-small is-left">
<i class="fas fa-globe"></i>
</span>
</p>
</div>
</div>
<div class="cell is-col-span-2">
<div class="field">
<label class="label">ICCID Profile 2</label>
<div class="control">
<input
class="input"
type="text"
placeholder="ICCID Here"
/>
</div>
<div class="cell is-col-span-2">
<div class="field">
<label class="label">ICCID Profile 2</label>
<div class="control">
<input
class="input"
type="text"
placeholder="ICCID Here"
id="iccidProfile2"
/>
</div>
</div>
</div>
@@ -366,20 +376,22 @@
</div>
</div>
</div>
<div class="card-footer">
<a
href="#"
class="card-footer-item has-text-link has-text-weight-semibold has-text-white"
>
Save
</a>
<a
href="#"
class="card-footer-item has-text-link has-text-weight-semibold has-text-white"
>
Reset
</a>
</div>
</div>
<div class="card-footer">
<a
href="#"
class="card-footer-item has-text-link has-text-weight-semibold has-text-white"
id="saveAPNProfile"
>
Save APN Profile
</a>
<a
href="#"
class="card-footer-item has-text-link has-text-weight-semibold has-text-white"
id="resetAPNProfile"
>
Reset APN Profile
</a>
</div>
</div>
</div>

View File

@@ -63,12 +63,12 @@
<div class="navbar-item has-dropdown is-hoverable">
<a class="navbar-link has-text-weight-bold"> Cellular </a>
<div class="navbar-dropdown is-boxed">
<a class="navbar-item" href="/bandlock.html"> Band Locking </a>
<a class="navbar-item" href="/cell-locking.html">Cell Locking</a>
<a class="navbar-item" href="/cell-scanner.html">Cell Scanner</a>
<a class="navbar-item" href="/cell-settings.html"
>Cell Settings</a
>
<a class="navbar-item" href="/bandlock.html"> Band Locking </a>
<a class="navbar-item" href="/cell-locking.html">Cell Locking</a>
<a class="navbar-item" href="/cell-scanner.html">Cell Scanner</a>
<a class="navbar-item" href="/cell-sms.html"> Cell SMS </a>
</div>
</div>
@@ -89,7 +89,10 @@
<div class="navbar-item">
<div class="buttons is-flex-direction-column-mobile">
<div class="control is-expanded-mobile">
<div id="restartConnectionBtn" class="button is-link is-outlined is-fullwidth-mobile">
<div
id="restartConnectionBtn"
class="button is-link is-outlined is-fullwidth-mobile"
>
<span class="icon">
<i class="fas fa-arrows-rotate"></i>
</span>

153
www/cgi-bin/apn-profile.sh Normal file
View File

@@ -0,0 +1,153 @@
#!/bin/sh
# Parse POST data
read -r QUERY_STRING
# Function to urldecode
urldecode() {
echo -e "$(echo "$1" | sed 's/+/ /g;s/%\([0-9A-F][0-9A-F]\)/\\x\1/g')"
}
# Extract values from POST data
iccidProfile1=$(echo "$QUERY_STRING" | grep -o 'iccidProfile1=[^&]*' | cut -d= -f2)
apnProfile1=$(echo "$QUERY_STRING" | grep -o 'apnProfile1=[^&]*' | cut -d= -f2)
pdpType1=$(echo "$QUERY_STRING" | grep -o 'pdpType1=[^&]*' | cut -d= -f2)
iccidProfile2=$(echo "$QUERY_STRING" | grep -o 'iccidProfile2=[^&]*' | cut -d= -f2)
apnProfile2=$(echo "$QUERY_STRING" | grep -o 'apnProfile2=[^&]*' | cut -d= -f2)
pdpType2=$(echo "$QUERY_STRING" | grep -o 'pdpType2=[^&]*' | cut -d= -f2)
# URL decode the values
iccidProfile1=$(urldecode "$iccidProfile1")
apnProfile1=$(urldecode "$apnProfile1")
pdpType1=$(urldecode "$pdpType1")
iccidProfile2=$(urldecode "$iccidProfile2")
apnProfile2=$(urldecode "$apnProfile2")
pdpType2=$(urldecode "$pdpType2")
echo "Content-type: application/json"
echo ""
# Validate required first profile
if [ -z "$iccidProfile1" ] || [ -z "$apnProfile1" ] || [ -z "$pdpType1" ]; then
echo '{"status": "error", "message": "Profile 1 is required"}'
exit 1
fi
# Create the directory structure
mkdir -p /etc/quecmanager
# Create a configuration file to store APN profiles (as plain text)
cat > /etc/quecmanager/apn_config.txt << EOF
iccidProfile1=$iccidProfile1
apnProfile1=$apnProfile1
pdpType1=$pdpType1
EOF
# Add second profile only if ICCID is provided
if [ -n "$iccidProfile2" ]; then
cat >> /etc/quecmanager/apn_config.txt << EOF
iccidProfile2=$iccidProfile2
apnProfile2=$apnProfile2
pdpType2=$pdpType2
EOF
fi
# Create the apnProfiles.sh script
cat > /etc/quecmanager/apnProfiles.sh << 'EOF'
#!/bin/sh
# Function to read config values
get_config_value() {
local key=$1
grep "^${key}=" /etc/quecmanager/apn_config.txt | cut -d'=' -f2
}
# Read configuration
iccidProfile1=$(get_config_value "iccidProfile1")
apnProfile1=$(get_config_value "apnProfile1")
pdpType1=$(get_config_value "pdpType1")
iccidProfile2=$(get_config_value "iccidProfile2")
apnProfile2=$(get_config_value "apnProfile2")
pdpType2=$(get_config_value "pdpType2")
# Function to get current ICCID
get_current_iccid() {
local input_file="/tmp/inputICCID.txt"
local output_file="/tmp/outputICCID.txt"
echo "AT+ICCID" > "$input_file"
atinout "$input_file" /dev/smd11 "$output_file"
iccid=$(cat "$output_file" | grep "+ICCID:" | cut -d' ' -f2)
rm -f "$input_file" "$output_file"
echo "$iccid"
}
# Function to set APN
set_apn() {
local pdp_type="$1"
local apn="$2"
local input_file="/tmp/inputAPN.txt"
local output_file="/tmp/outputAPN.txt"
echo "AT+CGDCONT=1,\"$pdp_type\",\"$apn\";+COPS=2;+COPS=0" > "$input_file"
atinout "$input_file" /dev/smd11 "$output_file"
local result=$(cat "$output_file")
rm -f "$input_file" "$output_file"
if echo "$result" | grep -q "OK"; then
return 0
else
return 1
fi
}
# Get current ICCID
current_iccid=$(get_current_iccid)
success=false
# Check ICCID against profile 1 (required)
if [ "$current_iccid" = "$iccidProfile1" ]; then
if set_apn "$pdpType1" "$apnProfile1"; then
success=true
fi
# Check ICCID against profile 2 (optional)
elif [ -n "$iccidProfile2" ] && [ "$current_iccid" = "$iccidProfile2" ]; then
if set_apn "$pdpType2" "$apnProfile2"; then
success=true
fi
fi
if [ "$success" = "true" ]; then
echo "APN set successfully" > /tmp/apn_result.txt
else
echo "Failed to set APN" > /tmp/apn_result.txt
fi
EOF
# Make the script executable
chmod +x /etc/quecmanager/apnProfiles.sh
# Add to rc.local if not already present
if ! grep -q "/etc/quecmanager/apnProfiles.sh" /etc/rc.local; then
sed -i '/^exit 0/i /etc/quecmanager/apnProfiles.sh' /etc/rc.local
fi
# Run the script immediately
/etc/quecmanager/apnProfiles.sh
# Check the result
if [ -f /tmp/apn_result.txt ]; then
result=$(cat /tmp/apn_result.txt)
rm -f /tmp/apn_result.txt
if [ "$result" = "APN set successfully" ]; then
echo '{"status": "success", "message": "APN profiles saved and applied successfully"}'
else
echo '{"status": "error", "message": "APN profiles saved but failed to apply"}'
fi
else
echo '{"status": "error", "message": "Something went wrong while processing APN profiles"}'
fi

View File

@@ -0,0 +1,66 @@
#!/bin/sh
# Set content-type for JSON response
echo "Content-type: application/json"
echo ""
# Define the lock file
LOCK_FILE="/tmp/home_data.lock"
# Acquire the lock (wait if needed)
exec 200>$LOCK_FILE
flock -x 200
# Temporary files for input/output and AT port
INPUT_FILE="/tmp/input_$$.txt"
OUTPUT_FILE="/tmp/output_$$.txt"
AT_PORT="/dev/smd11"
# Debug file path
DEBUG_FILE="/tmp/debug-json-result.txt"
# Function to escape JSON strings (handling quotes and newlines)
escape_json() {
# Escape newlines and double quotes
echo "$1" | sed ':a;N;$!ba;s/\n/\\n/g; s/"/\\"/g'
}
# Initialize JSON response array
JSON_RESPONSE="["
# List of AT commands to run, one by one
for COMMAND in 'AT+CGDCONT?' 'AT+QNWPREFCFG="mode_pref"' 'AT+QNWPREFCFG="nr5g_disable_mode"'; do
# Write the command to the input file
echo "$COMMAND" > "$INPUT_FILE"
# Run the command using atinout
atinout "$INPUT_FILE" "$AT_PORT" "$OUTPUT_FILE"
# Read the output from the output file
OUTPUT=$(cat "$OUTPUT_FILE")
# Escape special characters for JSON (escape only output)
ESCAPED_OUTPUT=$(escape_json "$OUTPUT")
# Append the response as an object to the JSON response array
JSON_RESPONSE="${JSON_RESPONSE}{\"response\":\"$ESCAPED_OUTPUT\"},"
done
# Remove the trailing comma and close the JSON array
if [ "${JSON_RESPONSE: -1}" = "," ]; then
JSON_RESPONSE="${JSON_RESPONSE%,}]"
else
JSON_RESPONSE="${JSON_RESPONSE}]"
fi
# Write the JSON response to the debug file for troubleshooting
echo "$JSON_RESPONSE" > "$DEBUG_FILE"
# Return the output as a valid JSON response
echo "$JSON_RESPONSE"
# Clean up temporary files
rm "$INPUT_FILE" "$OUTPUT_FILE"
# Release the lock
flock -u 200

View File

@@ -0,0 +1,45 @@
#!/bin/sh
echo "Content-type: application/json"
echo ""
CONFIG_FILE="/etc/quecmanager/apn_config.txt"
if [ ! -f "$CONFIG_FILE" ]; then
echo '{"status": "error", "message": "No APN profiles found", "profiles": {}}'
exit 0
fi
# Function to read config values
get_config_value() {
local key=$1
local value=$(grep "^${key}=" "$CONFIG_FILE" | cut -d'=' -f2)
echo "$value"
}
# Read all profile values
iccidProfile1=$(get_config_value "iccidProfile1")
apnProfile1=$(get_config_value "apnProfile1")
pdpType1=$(get_config_value "pdpType1")
iccidProfile2=$(get_config_value "iccidProfile2")
apnProfile2=$(get_config_value "apnProfile2")
pdpType2=$(get_config_value "pdpType2")
# Construct JSON response
cat << EOF
{
"status": "success",
"profiles": {
"profile1": {
"iccid": "${iccidProfile1:-}",
"apn": "${apnProfile1:-}",
"pdpType": "${pdpType1:-}"
},
"profile2": {
"iccid": "${iccidProfile2:-}",
"apn": "${apnProfile2:-}",
"pdpType": "${pdpType2:-}"
}
}
}
EOF

View File

@@ -29,7 +29,7 @@ escape_json() {
JSON_RESPONSE="["
# List of AT commands to run, one by one
for COMMAND in "AT+QUIMSLOT?" "AT+CNUM" "AT+COPS?" "AT+CIMI" "AT+ICCID" "AT+CGSN" "AT+CPIN?" "AT+CGCONTRDP=1" "AT+CREG?" "AT+CFUN?" "AT+QENG=\"servingcell\"" "AT+QTEMP" "AT+CGCONTRDP" "AT+QCAINFO" "AT+QRSRP" 'AT+QMAP="WWAN"'; do
for COMMAND in "AT+QUIMSLOT?" "AT+CNUM" "AT+COPS?" "AT+CIMI" "AT+ICCID" "AT+CGSN" "AT+CPIN?" "AT+CGDCONT?" "AT+CREG?" "AT+CFUN?" "AT+QENG=\"servingcell\"" "AT+QTEMP" "AT+CGCONTRDP" "AT+QCAINFO" "AT+QRSRP" 'AT+QMAP="WWAN"'; do
# Write the command to the input file
echo "$COMMAND" > "$INPUT_FILE"

View File

@@ -268,4 +268,67 @@ html.theme-light {
.isCursorPointer {
cursor: pointer;
}
/* For APN Profiles */
.notification {
position: relative;
padding: 1.25rem 2.5rem 1.25rem 1.5rem;
margin-bottom: 1rem;
}
.notification.is-success {
background-color: #48c78e;
color: #fff;
}
.notification.is-danger {
background-color: #f14668;
color: #fff;
}
.notification .delete {
position: absolute;
right: 0.5rem;
top: 0.5rem;
background-color: rgba(10, 10, 10, 0.2);
border: none;
border-radius: 290486px;
cursor: pointer;
display: inline-block;
height: 20px;
width: 20px;
padding: 0;
}
.is-loading {
position: relative;
pointer-events: none;
opacity: 0.5;
}
.is-loading:after {
animation: spinAround 500ms infinite linear;
border: 2px solid #dbdbdb;
border-radius: 290486px;
border-right-color: transparent;
border-top-color: transparent;
content: "";
display: block;
height: 1em;
position: absolute;
width: 1em;
position: absolute;
left: calc(50% - (1em / 2));
top: calc(50% - (1em / 2));
}
.input.is-danger,
.select.is-danger select {
border-color: #f14668;
}
@keyframes spinAround {
from { transform: rotate(0deg); }
to { transform: rotate(359deg); }
}

View File

@@ -64,12 +64,12 @@
<div class="navbar-item has-dropdown is-hoverable">
<a class="navbar-link"> Cellular </a>
<div class="navbar-dropdown is-boxed">
<a class="navbar-item" href="/cell-settings.html"
>Cell Settings</a
>
<a class="navbar-item" href="/bandlock.html"> Band Locking </a>
<a class="navbar-item" href="/cell-locking.html">Cell Locking</a>
<a class="navbar-item" href="/cell-scanner.html">Cell Scanner</a>
<a class="navbar-item" href="/cell-settings.html"
>Cell Settings</a
>
<a class="navbar-item" href="/cell-sms.html"> Cell SMS </a>
</div>
</div>

View File

@@ -0,0 +1,179 @@
document.addEventListener('DOMContentLoaded', function() {
const form = document.getElementById('apnProfileForm');
// Helper function to show notifications
function showNotification(message, isError = false) {
// Remove existing notification if any
const existingNotification = form.previousElementSibling;
if (existingNotification && existingNotification.classList.contains('notification')) {
existingNotification.remove();
}
const notification = document.createElement('div');
notification.className = `notification ${isError ? 'is-danger' : 'is-success'} is-light`;
notification.innerHTML = `
<button class="delete"></button>
${message}
`;
form.insertAdjacentElement('beforebegin', notification);
// Remove notification after 5 seconds
setTimeout(() => notification.remove(), 5000);
// Allow manual close
notification.querySelector('.delete').addEventListener('click', () => notification.remove());
}
// Function to validate ICCID format
function validateICCID(iccid) {
return /^\d{19,20}$/.test(iccid);
}
// Function to validate APN format
function validateAPN(apn) {
return /^[a-zA-Z0-9.-]+$/.test(apn);
}
// Function to set select element value
function setSelectValue(selectElement, value) {
const options = selectElement.options;
for (let i = 0; i < options.length; i++) {
if (options[i].value === value) {
selectElement.selectedIndex = i;
break;
}
}
}
// Function to fetch and display existing profiles
function fetchProfiles() {
fetch('/cgi-bin/fetch-apn-profiles.sh')
.then(response => response.json())
.then(data => {
if (data.status === 'success') {
// Fill Profile 1
if (data.profiles.profile1) {
const p1 = data.profiles.profile1;
if (p1.iccid) document.getElementById('iccidProfile1').value = p1.iccid;
if (p1.apn) document.getElementById('apnProfile1').value = p1.apn;
if (p1.pdpType) setSelectValue(document.getElementById('apnPDPType1'), p1.pdpType);
}
// Fill Profile 2
if (data.profiles.profile2) {
const p2 = data.profiles.profile2;
if (p2.iccid) document.getElementById('iccidProfile2').value = p2.iccid;
if (p2.apn) document.getElementById('apnProfile2').value = p2.apn;
if (p2.pdpType) setSelectValue(document.getElementById('apnPDPType2'), p2.pdpType);
}
} else {
showNotification('No existing profiles found', true);
}
})
.catch(error => {
showNotification('Error fetching profiles: ' + error.message, true);
});
}
// Function to validate form
function validateForm() {
const iccid1 = document.getElementById('iccidProfile1').value;
const apn1 = document.getElementById('apnProfile1').value;
const pdp1 = document.getElementById('apnPDPType1').value;
const iccid2 = document.getElementById('iccidProfile2').value;
const apn2 = document.getElementById('apnProfile2').value;
const pdp2 = document.getElementById('apnPDPType2').value;
// Validate first profile (required)
if (!iccid1 || !apn1 || pdp1 === 'Select APN PDP Type') {
showNotification('Please fill in all fields for Profile 1', true);
return false;
}
if (!validateICCID(iccid1)) {
showNotification('Invalid ICCID format in Profile 1 (should be 19-20 digits)', true);
return false;
}
if (!validateAPN(apn1)) {
showNotification('Invalid APN format in Profile 1 (alphanumeric, dots, and hyphens only)', true);
return false;
}
// Validate second profile only if any field is filled
if (iccid2 || apn2 || pdp2 !== 'Select APN PDP Type') {
if (!validateICCID(iccid2)) {
showNotification('Invalid ICCID format in Profile 2 (should be 19-20 digits)', true);
return false;
}
if (!validateAPN(apn2)) {
showNotification('Invalid APN format in Profile 2 (alphanumeric, dots, and hyphens only)', true);
return false;
}
if (pdp2 === 'Select APN PDP Type') {
showNotification('Please select PDP type for Profile 2', true);
return false;
}
}
return true;
}
// Handle form submission
document.getElementById('saveAPNProfile').addEventListener('click', function(e) {
e.preventDefault();
if (!validateForm()) {
return;
}
const formData = {
iccidProfile1: document.getElementById('iccidProfile1').value,
apnProfile1: document.getElementById('apnProfile1').value,
pdpType1: document.getElementById('apnPDPType1').value,
iccidProfile2: document.getElementById('iccidProfile2').value || '',
apnProfile2: document.getElementById('apnProfile2').value || '',
pdpType2: document.getElementById('apnPDPType2').value || 'IP' // Default value if not selected
};
// Send data to the server
fetch('/cgi-bin/apn-profile.sh', {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
},
body: Object.keys(formData).map(key => {
return encodeURIComponent(key) + '=' + encodeURIComponent(formData[key])
}).join('&')
})
.then(response => response.json())
.then(data => {
if (data.status === 'success') {
showNotification('APN profiles saved successfully');
} else {
showNotification(data.message || 'Error saving APN profiles', true);
}
})
.catch(error => {
showNotification('Error saving APN profiles: ' + error.message, true);
});
});
// Handle reset button
document.getElementById('resetAPNProfile').addEventListener('click', function(e) {
e.preventDefault();
document.getElementById('iccidProfile1').value = '';
document.getElementById('apnProfile1').value = '';
document.getElementById('apnPDPType1').selectedIndex = 0;
document.getElementById('iccidProfile2').value = '';
document.getElementById('apnProfile2').value = '';
document.getElementById('apnPDPType2').selectedIndex = 0;
showNotification('Form has been reset');
});
// Fetch existing profiles when the page loads
fetchProfiles();
});

View File

@@ -0,0 +1,309 @@
// State variables to track current values
let currentSettings = {
apn: "",
pdpType: "",
};
let updatedSettings = {
apn: "",
pdpType: "",
};
let updatedNetworkMode = "";
let currentNetworkMode = "";
let currentNr5GModeControl = "";
let updatedNr5GModeControl = "";
// Function to check if settings have changed
function haveSettingsChanged() {
return (
currentSettings.apn !== updatedSettings.apn ||
currentSettings.pdpType !== updatedSettings.pdpType
);
}
// Function to check if network mode has changed
function haveNetworkModeChanged() {
console.log("Current network mode:", currentNetworkMode);
console.log("Updated network mode:", updatedNetworkMode);
return currentNetworkMode !== updatedNetworkMode;
}
// Function to check if NR5G mode control has changed
function haveNr5GModeControlChanged() {
console.log("Current NR5G mode control:", currentNr5GModeControl);
console.log("Updated NR5G mode control:", updatedNr5GModeControl);
return currentNr5GModeControl !== updatedNr5GModeControl;
}
// Function to apply network mode changes immediately
async function applyNetworkModeChange() {
if (!haveNetworkModeChanged()) {
alert("No changes detected in the network mode.");
return;
}
try {
const atCommand = `AT+QNWPREFCFG="mode_pref",${updatedNetworkMode}`;
console.log("Sending AT command for network mode change:", atCommand);
const response = await sendATCommand(atCommand);
console.log("AT command response:", response);
alert("Network mode applied successfully!");
} catch (error) {
console.error("Error applying network mode:", error);
alert("Error applying network mode. Please try again.");
}
}
// Function to apply NR5G mode control changes immediately
async function applyNr5GModeControlChange() {
if (!haveNr5GModeControlChanged()) {
alert("No changes detected in the NR5G mode control.");
return;
}
try {
const atCommand = `AT+QNWPREFCFG="nr5g_disable_mode",${updatedNr5GModeControl}`;
console.log("Sending AT command for NR5G mode control change:", atCommand);
const response = await sendATCommand(atCommand);
console.log("AT command response:", response);
alert("NR5G mode control applied successfully!");
} catch (error) {
console.error("Error applying NR5G mode control:", error);
alert("Error applying NR5G mode control. Please try again.");
}
}
// Function to send settings to the modem
async function saveSettings() {
if (!haveSettingsChanged()) {
alert("No changes detected in the settings.");
return;
}
try {
const atCommand = `AT+QMBNCFG="AutoSel",0;+CGDCONT=1,"${updatedSettings.pdpType}","${updatedSettings.apn}"`;
console.log("Sending AT command:", atCommand);
// Disable the input fields while the settings are being saved
const inputs = document.querySelectorAll("input, select");
inputs.forEach((input) => {
input.disabled = true;
});
const response = await sendATCommand(atCommand);
console.log("AT command response:", response);
await sendATCommand(`AT+COPS=2`);
// Wait for 2 seconds before turning on the modem
await new Promise((resolve) => setTimeout(resolve, 2000));
await sendATCommand(`AT+COPS=0`);
// Re-enable the input fields after the settings are saved
inputs.forEach((input) => {
input.disabled = false;
});
// Update current settings after successful save
currentSettings = { ...updatedSettings };
alert("Settings saved successfully!");
} catch (error) {
console.error("Error saving settings:", error);
alert("Error saving settings. Please try again.");
}
}
async function resetAPN() {
atCommand = `AT+QMBNCFG="AutoSel",1`;
console.log("Sending AT command:", atCommand);
try {
const response = await sendATCommand(atCommand);
console.log("AT command response:", response);
// Restart connection after resetting APN settings
await sendATCommand("AT+COPS=2");
// Wait for 2 seconds before turning on the modem
await new Promise((resolve) => setTimeout(resolve, 2000));
await sendATCommand("AT+COPS=0");
alert("APN settings reset successfully!");
} catch (error) {
console.error("Error resetting APN settings:", error);
alert("Error resetting APN settings. Please try again.");
}
}
async function sendATCommand(command) {
try {
const response = await fetch("/cgi-bin/atinout_handler.sh", {
method: "POST",
headers: {
"Content-Type": "application/x-www-form-urlencoded",
},
body: "command=" + encodeURIComponent(command),
});
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const data = await response.json();
return data;
} catch (error) {
console.error("Error sending AT command:", error);
throw error;
}
}
// Function to fetch cell settings data
async function fetchCellSettings() {
try {
const response = await fetch("/cgi-bin/cell-settings.sh");
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const data = await response.json();
console.log("Full response:", data);
data.forEach((item) => {
if (item.response.includes("CGDCONT?")) {
const apn = item.response
.split("\n")[1]
.split(":")[1]
.split(",")[2]
.replace(/"/g, "")
.trim();
currentSettings.apn = apn;
updatedSettings.apn = apn;
const apnInput = document.getElementById("currentAPN");
if (apnInput) {
apnInput.value = apn;
// Add event listener for APN changes
if (!apnInput.hasListener) {
apnInput.hasListener = true;
apnInput.addEventListener("input", (e) => {
updatedSettings.apn = e.target.value;
});
}
}
const pdpType = item.response
.split("\n")[1]
.split(":")[1]
.split(",")[1]
.replace(/"/g, "")
.trim();
currentSettings.pdpType = pdpType;
updatedSettings.pdpType = pdpType;
const pdpTypeSelect = document.getElementById("apnPDP");
if (pdpTypeSelect) {
// Set initial value
pdpTypeSelect.value =
pdpType === "IPV4V6"
? "IPV4V6"
: pdpType === "IPV6"
? "IPV6"
: pdpType === "PPP"
? "PPP"
: "IP";
// Add event listener for PDP type changes
if (!pdpTypeSelect.hasListener) {
pdpTypeSelect.hasListener = true;
pdpTypeSelect.addEventListener("change", (e) => {
updatedSettings.pdpType = e.target.value;
});
}
}
} else if (item.response.includes("QNWPREFCFG")) {
console.log("QNWPREFCFG:", item.response);
const networkMode = item.response
.split("\n")[1]
.replace("+QNWPREFCFG: ", "")
.split(",")[1]
.trim();
currentNetworkMode = networkMode;
updatedNetworkMode = networkMode;
const networkSelect = document.getElementById("networkPreference");
if (networkSelect) {
// Set initial value based on actual value from modem
networkSelect.value =
networkMode === "LTE:NR5G"
? "NR5G:LTE"
: networkMode === "NR5G"
? "NR5G"
: networkMode === "LTE"
? "LTE"
: "AUTO";
// Add event listener for network mode changes, if there is, run applyNetworkModeChange
if (!networkSelect.hasListener) {
networkSelect.hasListener = true;
networkSelect.addEventListener("change", (e) => {
updatedNetworkMode = e.target.value;
applyNetworkModeChange();
});
}
}
const nr5GModeControl = item.response
.split("\n")[1]
.split(":")[1]
.split(",")[1]
.trim();
console.log("NR5G mode control:", nr5GModeControl);
currentNr5GModeControl = nr5GModeControl;
updatedNr5GModeControl = nr5GModeControl;
const nr5GControlSelect = document.getElementById("nr5gModeControl");
if (nr5GControlSelect) {
// Set initial value based on actual value from modem
nr5GControlSelect.value =
nr5GModeControl === "0" ? "0" : nr5GModeControl === "1" ? "1" : "2";
// Add event listener for NR5G mode control changes, if there is, run applyNr5GModeControlChange
if (!nr5GControlSelect.hasListener) {
nr5GControlSelect.hasListener = true;
nr5GControlSelect.addEventListener("change", (e) => {
updatedNr5GModeControl = e.target.value;
applyNr5GModeControlChange();
});
}
}
}
});
} catch (error) {
console.error("Error fetching cell settings:", error);
}
}
// Initialize when DOM is loaded
document.addEventListener("DOMContentLoaded", () => {
fetchCellSettings();
// Add event listener for both save buttons
const saveButtons = document.querySelectorAll(".card-footer-item");
saveButtons.forEach((button) => {
if (button.textContent.trim() === "Save APN") {
button.addEventListener("click", saveSettings);
} else if (button.textContent.trim() === "Reset APN") {
button.addEventListener("click", resetAPN);
}
});
// For every alert and close button, add event listener to refetch cell settings
const alertButtons = document.querySelectorAll(".delete");
alertButtons.forEach((button) => {
button.addEventListener("click", fetchCellSettings);
});
});

View File

@@ -202,9 +202,9 @@ function startPeriodicRefresh(refreshRate = DEFAULT_REFRESH_RATE) {
async function fetchATCommandData() {
try {
const jsonData = await fetchAndParseData();
processATCommandData(jsonData);
console.log("Data fetched and processed successfully");
console.log(jsonData);
processATCommandData(jsonData);
} catch (error) {
console.error("There was a problem with the fetch operation:", error);
}
@@ -284,7 +284,13 @@ function processNetworkData(jsonData) {
// APN
const apnData = extractValue(jsonData[7].response).split(",");
setText("apn", apnData[2].replace(/"/g, "").trim());
const apn = apnData[2].replace(/"/g, "").trim();
if (apn === "") {
const automaticAPN = jsonData[12].response.split("\n")[1].split(":")[1].split(",")[2].replace(/"/g, "").trim();
setText("apn", automaticAPN);
} else {
setText("apn", apnData[2].replace(/"/g, "").trim());
}
// Operator State
const operatorState = extractValue(jsonData[8].response).split(",")[1].trim();
@@ -645,7 +651,9 @@ function createBandTableRow(bandData, networkType, servingCellJSON) {
<div class="cell-card">
<div class="cell-card__item">
<span class="cell-card__label">Name</span>
<span class="cell-card__value">${formattedBandNumber || "N/A"}</span>
<span class="cell-card__value">${
formattedBandNumber || "N/A"
}</span>
</div>
<div class="cell-card__item">
<span class="cell-card__label">EARFCN</span>
@@ -661,15 +669,21 @@ function createBandTableRow(bandData, networkType, servingCellJSON) {
</div>
<div class="cell-card__item">
<span class="cell-card__label">RSRP</span>
<span class="cell-card__value">${rsrp ? createSignalTag(rsrp, "RSRP") : "N/A"}</span>
<span class="cell-card__value">${
rsrp ? createSignalTag(rsrp, "RSRP") : "N/A"
}</span>
</div>
<div class="cell-card__item">
<span class="cell-card__label">RSRQ</span>
<span class="cell-card__value">${rsrq ? createSignalTag(rsrq, "RSRQ") : "N/A"}</span>
<span class="cell-card__value">${
rsrq ? createSignalTag(rsrq, "RSRQ") : "N/A"
}</span>
</div>
<div class="cell-card__item">
<span class="cell-card__label">SINR</span>
<span class="cell-card__value">${sinr ? createSignalTag(sinr, "SINR") : "N/A"}</span>
<span class="cell-card__value">${
sinr ? createSignalTag(sinr, "SINR") : "N/A"
}</span>
</div>
</div>
</div>
@@ -1147,7 +1161,7 @@ function initMobileCarousel() {
}
// Add CSS class for smooth transitions
const style = document.createElement('style');
const style = document.createElement("style");
style.textContent = `
.cell-carousel__container {
display: flex;
@@ -1171,10 +1185,10 @@ function updateCarouselContent(rows) {
// Preserve existing transform to maintain current slide position
const currentTransform = carouselContainer.style.transform;
// Clear existing content
carouselContainer.innerHTML = '';
carouselContainer.innerHTML = "";
// Create and append all slides at once
rows.forEach((row) => {
const slide = document.createElement("div");
@@ -1182,13 +1196,13 @@ function updateCarouselContent(rows) {
slide.innerHTML = row.getAttribute("data-mobile");
carouselContainer.appendChild(slide);
});
// Restore transform to maintain position
carouselContainer.style.transform = currentTransform;
// Update indicators
updateIndicators(rows.length);
// Ensure current slide is within bounds
if (currentSlide >= rows.length) {
currentSlide = Math.max(0, rows.length - 1);
@@ -1197,25 +1211,31 @@ function updateCarouselContent(rows) {
}
function updateSlideValues(slide, newDataHtml) {
const tempDiv = document.createElement('div');
const tempDiv = document.createElement("div");
tempDiv.innerHTML = newDataHtml;
const newCard = tempDiv.querySelector('.cell-card');
const currentCard = slide.querySelector('.cell-card');
const newCard = tempDiv.querySelector(".cell-card");
const currentCard = slide.querySelector(".cell-card");
if (!currentCard || !newCard) {
slide.innerHTML = newDataHtml;
return;
}
const currentItems = currentCard.querySelectorAll('.cell-card__item');
const newItems = newCard.querySelectorAll('.cell-card__item');
const currentItems = currentCard.querySelectorAll(".cell-card__item");
const newItems = newCard.querySelectorAll(".cell-card__item");
currentItems.forEach((item, index) => {
const currentValueSpan = item.querySelector('span:not(.cell-card__label)');
const currentValueSpan = item.querySelector("span:not(.cell-card__label)");
const newItem = newItems[index];
const newValueSpan = newItem ? newItem.querySelector('span:not(.cell-card__label)') : null;
if (currentValueSpan && newValueSpan && currentValueSpan.innerHTML !== newValueSpan.innerHTML) {
const newValueSpan = newItem
? newItem.querySelector("span:not(.cell-card__label)")
: null;
if (
currentValueSpan &&
newValueSpan &&
currentValueSpan.innerHTML !== newValueSpan.innerHTML
) {
currentValueSpan.innerHTML = newValueSpan.innerHTML;
}
});
@@ -1224,7 +1244,7 @@ function updateSlideValues(slide, newDataHtml) {
function updateIndicators(slideCount) {
const carouselWrapper = document.querySelector(".cell-carousel");
let indicators = carouselWrapper.querySelector(".cell-carousel__indicators");
if (indicators) {
indicators.remove();
}
@@ -1254,14 +1274,13 @@ function updateCarouselPosition() {
requestAnimationFrame(() => {
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);
});
}
// Touch handling functions remain the same
let touchStartX = 0;
let touchEndX = 0;
@@ -1302,4 +1321,4 @@ function goToSlide(index) {
// Event listeners
window.addEventListener("load", initMobileCarousel);
window.addEventListener("resize", initMobileCarousel);
window.addEventListener("resize", initMobileCarousel);

View File

@@ -21,8 +21,8 @@ document.addEventListener('DOMContentLoaded', function() {
throw new Error(`HTTP error! status: ${response1.status}`);
}
// Wait for 2 seconds
await new Promise(resolve => setTimeout(resolve, 2000));
// Wait for 3 seconds
await new Promise(resolve => setTimeout(resolve, 3000));
// Send AT+CFUN=1
const response2 = await fetch('/cgi-bin/atinout_handler.sh', {