add cpu power mode

This commit is contained in:
PaulVua
2026-01-15 14:13:41 +01:00
parent 994bbf7a8d
commit 8291475e36
8 changed files with 565 additions and 5 deletions

View File

@@ -128,6 +128,20 @@
</small> </small>
</div> </div>
<div class="mb-3">
<label for="cpu_power_mode" class="form-label">CPU Power Mode</label>
<select class="form-select" id="cpu_power_mode" onchange="set_cpu_power_mode(this.value)">
<option value="normal">Normal (600-1500MHz dynamic)</option>
<option value="powersave">Power Saving (600MHz fixed)</option>
</select>
<small class="form-text text-muted d-block">
<span id="cpu_mode_status" class="text-success"></span>
</small>
<small class="form-text text-muted d-block">
Power saving mode reduces CPU performance by ~30-40% but saves power.
</small>
</div>
<div class="d-flex justify-content-between align-items-center mb-2"> <div class="d-flex justify-content-between align-items-center mb-2">
<span class="fw-bold">Protected Settings</span> <span class="fw-bold">Protected Settings</span>
<button type="button" class="btn btn-sm btn-outline-primary" onclick="toggleProtectedSettings()" id="unlockBtn"> <button type="button" class="btn btn-sm btn-outline-primary" onclick="toggleProtectedSettings()" id="unlockBtn">
@@ -423,6 +437,16 @@ window.onload = function() {
checkbox_aircarto.checked = response["send_aircarto"]; checkbox_aircarto.checked = response["send_aircarto"];
checkbox_miotiq.checked = response["send_miotiq"]; checkbox_miotiq.checked = response["send_miotiq"];
// Set CPU power mode
const cpu_power_mode_select = document.getElementById("cpu_power_mode");
if (response["cpu_power_mode"]) {
cpu_power_mode_select.value = response["cpu_power_mode"];
// Update status display
const statusElement = document.getElementById('cpu_mode_status');
statusElement.textContent = `Current: ${response["cpu_power_mode"]}`;
statusElement.className = 'text-success';
}
// If envea is enabled, show the envea sondes container // If envea is enabled, show the envea sondes container
if (response["envea"]) { if (response["envea"]) {
add_sondeEnveaContainer(); add_sondeEnveaContainer();
@@ -589,6 +613,83 @@ function update_config_sqlite(param, value){
} }
function set_cpu_power_mode(mode) {
console.log("Setting CPU power mode to:", mode);
const toastLiveExample = document.getElementById('liveToast');
const toastBody = toastLiveExample.querySelector('.toast-body');
const statusElement = document.getElementById('cpu_mode_status');
// Show loading status
statusElement.textContent = 'Applying mode...';
statusElement.className = 'text-warning';
$.ajax({
url: 'launcher.php?type=set_cpu_power_mode&mode=' + mode,
dataType: 'json',
method: 'GET',
cache: false,
success: function(response) {
console.log(response);
let formattedMessage;
if (response.success) {
// Success message
toastLiveExample.classList.remove('text-bg-danger');
toastLiveExample.classList.add('text-bg-success');
formattedMessage = `
<strong>Success!</strong><br>
CPU mode set to: <strong>${mode}</strong><br>
${response.description || ''}
`;
// Update status
statusElement.textContent = `Current: ${mode}`;
statusElement.className = 'text-success';
} else {
// Error message
toastLiveExample.classList.remove('text-bg-success');
toastLiveExample.classList.add('text-bg-danger');
formattedMessage = `
<strong>Error!</strong><br>
${response.error || 'Failed to set CPU power mode'}
`;
// Reset status
statusElement.textContent = 'Error setting mode';
statusElement.className = 'text-danger';
}
// Update the toast body with formatted content
toastBody.innerHTML = formattedMessage;
// Show the toast
const toastBootstrap = bootstrap.Toast.getOrCreateInstance(toastLiveExample);
toastBootstrap.show();
},
error: function(xhr, status, error) {
console.error('AJAX request failed:', status, error);
// Show error in toast
toastLiveExample.classList.remove('text-bg-success');
toastLiveExample.classList.add('text-bg-danger');
toastBody.innerHTML = `<strong>Error!</strong><br>Network error: ${error}`;
const toastBootstrap = bootstrap.Toast.getOrCreateInstance(toastLiveExample);
toastBootstrap.show();
// Update status
statusElement.textContent = 'Network error';
statusElement.className = 'text-danger';
}
});
}
function update_config(param, value){ function update_config(param, value){
console.log("Updating ",param," : ", value); console.log("Updating ",param," : ", value);
$.ajax({ $.ajax({

View File

@@ -1423,3 +1423,95 @@ if ($type == "detect_envea_device") {
]); ]);
} }
} }
/*
____ ____ _ _ ____ __ __ _
/ ___| _ \| | | | | _ \ _____ _____ _ _| \/ | __ _ _ __ __ _ __ _ ___ _ __ ___ ___ _ __ | |_
| | | |_) | | | | | |_) / _ \ \ /\ / / _ \ '__| |\/| |/ _` | '_ \ / _` |/ _` |/ _ \ '_ ` _ \ / _ \ '_ \| __|
| |___| __/| |_| | | __/ (_) \ V V / __/ | | | | | (_| | | | | (_| | (_| | __/ | | | | | __/ | | | |_
\____|_| \___/ |_| \___/ \_/\_/ \___|_| |_| |_|\__,_|_| |_|\__,_|\__, |\___|_| |_| |_|\___|_| |_|\__|
|___/
*/
// Get current CPU power mode
if ($type == "get_cpu_power_mode") {
try {
$command = 'sudo /usr/bin/python3 /var/www/nebuleair_pro_4g/power/set_cpu_mode.py get 2>&1';
$output = shell_exec($command);
// Try to parse JSON output
$result = json_decode($output, true);
if ($result && isset($result['success']) && $result['success']) {
echo json_encode([
'success' => true,
'mode' => $result['config_mode'] ?? 'unknown',
'cpu_state' => $result['cpu_state'] ?? null
], JSON_PRETTY_PRINT);
} else {
echo json_encode([
'success' => false,
'error' => 'Failed to get CPU power mode',
'output' => $output
]);
}
} catch (Exception $e) {
echo json_encode([
'success' => false,
'error' => 'Script execution failed: ' . $e->getMessage()
]);
}
}
// Set CPU power mode
if ($type == "set_cpu_power_mode") {
$mode = $_GET['mode'] ?? null;
if (empty($mode)) {
echo json_encode([
'success' => false,
'error' => 'No mode specified'
]);
exit;
}
// Validate mode (whitelist)
$allowedModes = ['normal', 'powersave'];
if (!in_array($mode, $allowedModes)) {
echo json_encode([
'success' => false,
'error' => 'Invalid mode. Allowed: normal, powersave'
]);
exit;
}
try {
// Execute the CPU power mode script
$command = 'sudo /usr/bin/python3 /var/www/nebuleair_pro_4g/power/set_cpu_mode.py ' . escapeshellarg($mode) . ' 2>&1';
$output = shell_exec($command);
// Try to parse JSON output
$result = json_decode($output, true);
if ($result && isset($result['success']) && $result['success']) {
echo json_encode([
'success' => true,
'mode' => $mode,
'message' => "CPU power mode set to: $mode",
'description' => $result['description'] ?? ''
], JSON_PRETTY_PRINT);
} else {
echo json_encode([
'success' => false,
'error' => $result['error'] ?? 'Failed to set CPU power mode',
'output' => $output
]);
}
} catch (Exception $e) {
echo json_encode([
'success' => false,
'error' => 'Script execution failed: ' . $e->getMessage()
]);
}
}

View File

@@ -141,11 +141,11 @@ if ! sudo visudo -c; then
error "Sudoers file has syntax errors! Please fix manually with 'sudo visudo'" error "Sudoers file has syntax errors! Please fix manually with 'sudo visudo'"
fi fi
# Open all UART serial ports and disable HDMI (avoid duplication) # Open all UART serial ports and disable HDMI + Bluetooth (avoid duplication)
info "Configuring UART serial ports and disabling HDMI to save power..." info "Configuring UART serial ports and disabling HDMI/Bluetooth to save power..."
if ! grep -q "enable_uart=1" /boot/firmware/config.txt; then if ! grep -q "enable_uart=1" /boot/firmware/config.txt; then
echo -e "\nenable_uart=1\ndtoverlay=uart0\ndtoverlay=uart1\ndtoverlay=uart2\ndtoverlay=uart3\ndtoverlay=uart4\ndtoverlay=uart5\n\n# Disable HDMI to save power (~20-30mA)\nhdmi_blanking=2" | sudo tee -a /boot/firmware/config.txt > /dev/null echo -e "\nenable_uart=1\ndtoverlay=uart0\ndtoverlay=uart1\ndtoverlay=uart2\ndtoverlay=uart3\ndtoverlay=uart4\ndtoverlay=uart5\n\n# Disable HDMI to save power (~20-30mA)\nhdmi_blanking=2\n\n# Disable Bluetooth to save power (~20-30mA)\ndtoverlay=disable-bt" | sudo tee -a /boot/firmware/config.txt > /dev/null
success "UART configuration and HDMI disable added." success "UART configuration, HDMI and Bluetooth disable added."
else else
warning "UART configuration already set. Skipping." warning "UART configuration already set. Skipping."
fi fi

View File

@@ -0,0 +1,71 @@
#!/usr/bin/env python3
"""
Apply CPU Power Mode from Database at Boot
This script is called by systemd at boot to apply the CPU power mode
stored in the database (cpu_power_mode config parameter).
Usage:
/usr/bin/python3 /var/www/nebuleair_pro_4g/power/apply_cpu_mode_from_db.py
"""
import sqlite3
import sys
import subprocess
DB_PATH = "/var/www/nebuleair_pro_4g/sqlite/sensors.db"
SET_MODE_SCRIPT = "/var/www/nebuleair_pro_4g/power/set_cpu_mode.py"
def get_cpu_mode_from_db():
"""Read cpu_power_mode from database"""
try:
conn = sqlite3.connect(DB_PATH)
cursor = conn.cursor()
cursor.execute("SELECT value FROM config_table WHERE key = 'cpu_power_mode'")
result = cursor.fetchone()
conn.close()
return result[0] if result else None
except sqlite3.Error as e:
print(f"Database error: {e}", file=sys.stderr)
return None
def main():
"""Main function"""
print("=== Applying CPU Power Mode from Database ===")
# Get mode from database
mode = get_cpu_mode_from_db()
if mode is None:
print("No cpu_power_mode found in database, using default: normal")
mode = "normal"
print(f"CPU power mode from database: {mode}")
# Call set_cpu_mode.py to apply the mode
try:
result = subprocess.run(
["/usr/bin/python3", SET_MODE_SCRIPT, mode],
capture_output=True,
text=True,
timeout=30
)
if result.returncode == 0:
print(f"Successfully applied CPU power mode: {mode}")
print(result.stdout)
return 0
else:
print(f"Failed to apply CPU power mode: {mode}", file=sys.stderr)
print(result.stderr, file=sys.stderr)
return 1
except subprocess.TimeoutExpired:
print("Timeout while applying CPU power mode", file=sys.stderr)
return 1
except Exception as e:
print(f"Error: {e}", file=sys.stderr)
return 1
if __name__ == "__main__":
sys.exit(main())

256
power/set_cpu_mode.py Normal file
View File

@@ -0,0 +1,256 @@
#!/usr/bin/env python3
"""
____ ____ _ _ ____ __ __ _
/ ___| _ \| | | | | _ \ _____ _____ _ _| \/ | ___ __| | ___
| | | |_) | | | | | |_) / _ \ \ /\ / / _ \ '__| |\/| |/ _ \ / _` |/ _ \
| |___| __/| |_| | | __/ (_) \ V V / __/ | | | | | (_) | (_| | __/
\____|_| \___/ |_| \___/ \_/\_/ \___|_| |_| |_|\___/ \__,_|\___|
CPU Power Mode Management Script
Switches between Normal and Power Saving CPU modes.
Modes:
- normal: CPU governor ondemand (600MHz-1500MHz dynamic)
- powersave: CPU governor powersave (600MHz fixed)
Usage:
/usr/bin/python3 /var/www/nebuleair_pro_4g/power/set_cpu_mode.py <mode>
/usr/bin/python3 /var/www/nebuleair_pro_4g/power/set_cpu_mode.py normal
/usr/bin/python3 /var/www/nebuleair_pro_4g/power/set_cpu_mode.py powersave
Or get current mode:
/usr/bin/python3 /var/www/nebuleair_pro_4g/power/set_cpu_mode.py get
"""
import sqlite3
import subprocess
import sys
import datetime
import json
# Paths
DB_PATH = "/var/www/nebuleair_pro_4g/sqlite/sensors.db"
LOG_PATH = "/var/www/nebuleair_pro_4g/logs/app.log"
# Available modes
MODES = {
"normal": {
"governor": "ondemand",
"description": "Normal mode - CPU 600MHz-1500MHz dynamic",
"min_freq": 600000, # 600 MHz in kHz
"max_freq": 1500000 # 1500 MHz in kHz
},
"powersave": {
"governor": "powersave",
"description": "Power saving mode - CPU 600MHz fixed",
"min_freq": 600000,
"max_freq": 600000
}
}
def log_message(message):
"""Write message to log file with timestamp"""
timestamp = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")
log_entry = f"[{timestamp}] [CPU Power Mode] {message}\n"
try:
with open(LOG_PATH, "a") as log_file:
log_file.write(log_entry)
except Exception as e:
print(f"Failed to write to log: {e}", file=sys.stderr)
print(log_entry.strip())
def get_cpu_count():
"""Get number of CPU cores"""
try:
result = subprocess.run(
["nproc"],
capture_output=True,
text=True,
timeout=5
)
return int(result.stdout.strip())
except Exception as e:
log_message(f"Failed to get CPU count: {e}")
return 4 # Default to 4 cores for CM4
def set_cpu_governor(governor, cpu_id):
"""Set CPU governor for specific CPU core"""
try:
with open(f"/sys/devices/system/cpu/cpu{cpu_id}/cpufreq/scaling_governor", "w") as f:
f.write(governor)
return True
except Exception as e:
log_message(f"Failed to set governor for CPU{cpu_id}: {e}")
return False
def set_cpu_freq(min_freq, max_freq, cpu_id):
"""Set CPU frequency limits for specific CPU core"""
try:
# Set min frequency
with open(f"/sys/devices/system/cpu/cpu{cpu_id}/cpufreq/scaling_min_freq", "w") as f:
f.write(str(min_freq))
# Set max frequency
with open(f"/sys/devices/system/cpu/cpu{cpu_id}/cpufreq/scaling_max_freq", "w") as f:
f.write(str(max_freq))
return True
except Exception as e:
log_message(f"Failed to set frequency for CPU{cpu_id}: {e}")
return False
def get_current_cpu_state():
"""Get current CPU governor and frequencies"""
try:
with open("/sys/devices/system/cpu/cpu0/cpufreq/scaling_governor", "r") as f:
governor = f.read().strip()
with open("/sys/devices/system/cpu/cpu0/cpufreq/scaling_min_freq", "r") as f:
min_freq = int(f.read().strip())
with open("/sys/devices/system/cpu/cpu0/cpufreq/scaling_max_freq", "r") as f:
max_freq = int(f.read().strip())
with open("/sys/devices/system/cpu/cpu0/cpufreq/scaling_cur_freq", "r") as f:
cur_freq = int(f.read().strip())
return {
"governor": governor,
"min_freq_khz": min_freq,
"max_freq_khz": max_freq,
"current_freq_khz": cur_freq,
"min_freq_mhz": min_freq / 1000,
"max_freq_mhz": max_freq / 1000,
"current_freq_mhz": cur_freq / 1000
}
except Exception as e:
log_message(f"Failed to get current CPU state: {e}")
return None
def update_config_db(mode):
"""Update cpu_power_mode in database"""
try:
conn = sqlite3.connect(DB_PATH)
cursor = conn.cursor()
cursor.execute(
"UPDATE config_table SET value = ? WHERE key = 'cpu_power_mode'",
(mode,)
)
conn.commit()
conn.close()
return True
except sqlite3.Error as e:
log_message(f"Database error: {e}")
return False
def get_config_from_db():
"""Read cpu_power_mode from database"""
try:
conn = sqlite3.connect(DB_PATH)
cursor = conn.cursor()
cursor.execute("SELECT value FROM config_table WHERE key = 'cpu_power_mode'")
result = cursor.fetchone()
conn.close()
return result[0] if result else None
except sqlite3.Error as e:
log_message(f"Database error: {e}")
return None
def apply_mode(mode):
"""Apply CPU power mode"""
if mode not in MODES:
log_message(f"Invalid mode: {mode}. Valid modes: {', '.join(MODES.keys())}")
return False
mode_config = MODES[mode]
log_message(f"Applying mode: {mode} - {mode_config['description']}")
cpu_count = get_cpu_count()
log_message(f"Detected {cpu_count} CPU cores")
success = True
# Apply settings to all CPU cores
for cpu_id in range(cpu_count):
# Set frequency limits first
if not set_cpu_freq(mode_config['min_freq'], mode_config['max_freq'], cpu_id):
success = False
# Then set governor
if not set_cpu_governor(mode_config['governor'], cpu_id):
success = False
if success:
log_message(f"Successfully applied {mode} mode to all {cpu_count} cores")
# Update database
if update_config_db(mode):
log_message(f"Updated database: cpu_power_mode = {mode}")
else:
log_message("Warning: Failed to update database")
# Log current state
state = get_current_cpu_state()
if state:
log_message(f"Current CPU state: {state['governor']} governor, "
f"{state['min_freq_mhz']:.0f}-{state['max_freq_mhz']:.0f} MHz "
f"(current: {state['current_freq_mhz']:.0f} MHz)")
else:
log_message(f"Failed to apply {mode} mode completely")
return success
def main():
"""Main function"""
if len(sys.argv) < 2:
print("Usage: set_cpu_mode.py <mode>")
print("Modes: normal, powersave")
print("Or: set_cpu_mode.py get (to get current state)")
return 1
command = sys.argv[1].lower()
# Handle "get" command to return current state
if command == "get":
state = get_current_cpu_state()
db_mode = get_config_from_db()
if state:
output = {
"success": True,
"cpu_state": state,
"config_mode": db_mode
}
print(json.dumps(output))
return 0
else:
output = {
"success": False,
"error": "Failed to read CPU state"
}
print(json.dumps(output))
return 1
# Handle mode setting
log_message("=== CPU Power Mode Script Started ===")
if apply_mode(command):
log_message("=== CPU Power Mode Script Completed Successfully ===")
output = {
"success": True,
"mode": command,
"description": MODES[command]['description']
}
print(json.dumps(output))
return 0
else:
log_message("=== CPU Power Mode Script Failed ===")
output = {
"success": False,
"error": f"Failed to apply mode: {command}"
}
print(json.dumps(output))
return 1
if __name__ == "__main__":
sys.exit(main())

View File

@@ -0,0 +1,15 @@
[Unit]
Description=NebuleAir CPU Power Mode Service
After=multi-user.target
Wants=multi-user.target
[Service]
Type=oneshot
ExecStart=/usr/bin/python3 /var/www/nebuleair_pro_4g/power/apply_cpu_mode_from_db.py
User=root
StandardOutput=journal
StandardError=journal
RemainAfterExit=yes
[Install]
WantedBy=multi-user.target

View File

@@ -270,6 +270,25 @@ Persistent=false
WantedBy=timers.target WantedBy=timers.target
EOL EOL
# Create service file for CPU Power Mode (runs once at boot)
cat > /etc/systemd/system/nebuleair-cpu-power.service << 'EOL'
[Unit]
Description=NebuleAir CPU Power Mode Service
After=multi-user.target
Wants=multi-user.target
[Service]
Type=oneshot
ExecStart=/usr/bin/python3 /var/www/nebuleair_pro_4g/power/apply_cpu_mode_from_db.py
User=root
StandardOutput=journal
StandardError=journal
RemainAfterExit=yes
[Install]
WantedBy=multi-user.target
EOL
# Reload systemd to recognize new services # Reload systemd to recognize new services
systemctl daemon-reload systemctl daemon-reload
@@ -286,6 +305,11 @@ systemctl enable nebuleair-wifi-powersave.timer
systemctl start nebuleair-wifi-powersave.timer systemctl start nebuleair-wifi-powersave.timer
echo "Started nebuleair-wifi-powersave timer" echo "Started nebuleair-wifi-powersave timer"
# Enable and start CPU power mode service (runs once at boot)
systemctl enable nebuleair-cpu-power.service
systemctl start nebuleair-cpu-power.service
echo "Started nebuleair-cpu-power service"
echo "Checking status of all timers..." echo "Checking status of all timers..."
systemctl list-timers | grep nebuleair systemctl list-timers | grep nebuleair

View File

@@ -52,7 +52,8 @@ config_entries = [
("NOISE", "0", "bool"), ("NOISE", "0", "bool"),
("modem_version", "XXX", "str"), ("modem_version", "XXX", "str"),
("language", "fr", "str"), ("language", "fr", "str"),
("wifi_power_saving", "0", "bool") ("wifi_power_saving", "0", "bool"),
("cpu_power_mode", "normal", "str")
] ]
for key, value, value_type in config_entries: for key, value, value_type in config_entries: