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>
</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">
<span class="fw-bold">Protected Settings</span>
<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_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 (response["envea"]) {
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){
console.log("Updating ",param," : ", value);
$.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'"
fi
# Open all UART serial ports and disable HDMI (avoid duplication)
info "Configuring UART serial ports and disabling HDMI to save power..."
# Open all UART serial ports and disable HDMI + Bluetooth (avoid duplication)
info "Configuring UART serial ports and disabling HDMI/Bluetooth to save power..."
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
success "UART configuration and HDMI disable added."
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, HDMI and Bluetooth disable added."
else
warning "UART configuration already set. Skipping."
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
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
systemctl daemon-reload
@@ -286,6 +305,11 @@ systemctl enable nebuleair-wifi-powersave.timer
systemctl start 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..."
systemctl list-timers | grep nebuleair

View File

@@ -52,7 +52,8 @@ config_entries = [
("NOISE", "0", "bool"),
("modem_version", "XXX", "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: