Files
nebuleair_pro_4g/html/launcher.php
PaulVua c593b152e8 v1.8.3: Affichage version firmware dans la sidebar
Ajoute la version firmware sous le nom du capteur dans la sidebar, visible
sur toutes les pages. Permet d'identifier d'un coup d'oeil le chemin de
mise a jour disponible (online git pull vs offline ZIP upload >= v1.4.0).

- launcher.php: get_config_sqlite injecte firmware_version (lu depuis VERSION)
- sidebar.html: ajout d'un <small> sous sideBar_sensorName (statique)
- topbar-logo.js: peuple .sideBar_firmwareVersion via le fetch existant +
  MutationObserver (aucun nouveau fetch -> respecte la limite 6 connexions)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-13 12:39:40 +02:00

2150 lines
75 KiB
PHP
Executable File

<?php
//Prevents caching → Adds headers to ensure fresh response.
// to test this page http://192.168.1.127/html/launcher.php?type=get_config_scripts_sqlite
header("Content-Type: application/json");
header("Cache-Control: no-store, no-cache, must-revalidate, max-age=0");
header("Pragma: no-cache");
$database_path = "/var/www/nebuleair_pro_4g/sqlite/sensors.db";
$type=$_GET['type'];
if ($type == "get_npm_sqlite_data") {
//echo "Getting data from sqlite database";
try {
$db = new PDO("sqlite:$database_path");
$db->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
// Fetch the last 30 records
$stmt = $db->query("SELECT timestamp, PM1, PM25, PM10 FROM data_NPM ORDER BY timestamp DESC LIMIT 30");
$data = $stmt->fetchAll(PDO::FETCH_ASSOC);
$reversedData = array_reverse($data); // Reverse the order
echo json_encode($reversedData);
} catch (PDOException $e) {
echo json_encode(["error" => $e->getMessage()]);
}
}
/*
*/
//GETING data from config_table (SQLite DB)
if ($type == "get_config_sqlite") {
try {
$db = new PDO("sqlite:$database_path");
$db->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
// Get all main configuration entries
$config_query = $db->query("SELECT key, value, type FROM config_table");
$config_data = $config_query->fetchAll(PDO::FETCH_ASSOC);
// Convert data types according to their 'type' field
$result = [];
foreach ($config_data as $item) {
$key = $item['key'];
$value = $item['value'];
$type = $item['type'];
// Convert value based on its type
switch ($type) {
case 'bool':
$parsed_value = ($value == '1' || $value == 'true') ? true : false;
break;
case 'int':
$parsed_value = intval($value);
break;
case 'float':
$parsed_value = floatval($value);
break;
default:
$parsed_value = $value;
}
$result[$key] = $parsed_value;
}
// Inject firmware version (read from VERSION file, not from DB)
// Piggybacks on this existing fetch so the sidebar can show it without
// a separate AJAX call (avoids hitting the 6-connection browser limit).
$versionFile = '/var/www/nebuleair_pro_4g/VERSION';
$result['firmware_version'] = file_exists($versionFile)
? trim(file_get_contents($versionFile))
: 'unknown';
// Return JSON response
header('Content-Type: application/json');
echo json_encode($result, JSON_PRETTY_PRINT);
} catch (PDOException $e) {
// Return error as JSON
header('Content-Type: application/json');
echo json_encode(['error' => 'Database error: ' . $e->getMessage()]);
}
}
// GET language preference from SQLite
if ($type == "get_language") {
try {
$db = new PDO("sqlite:$database_path");
$db->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
$stmt = $db->prepare("SELECT value FROM config_table WHERE key = 'language'");
$stmt->execute();
$result = $stmt->fetch(PDO::FETCH_ASSOC);
$language = $result ? $result['value'] : 'fr'; // Default to French
echo json_encode(['language' => $language]);
} catch (Exception $e) {
echo json_encode(['language' => 'fr', 'error' => $e->getMessage()]);
}
}
// SET language preference in SQLite
if ($type == "set_language") {
$language = $_GET['language'];
// Validate language (only allow fr or en)
if (!in_array($language, ['fr', 'en'])) {
echo json_encode(['success' => false, 'error' => 'Invalid language']);
exit;
}
try {
$db = new PDO("sqlite:$database_path");
$db->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
$stmt = $db->prepare("UPDATE config_table SET value = ? WHERE key = 'language'");
$stmt->execute([$language]);
echo json_encode(['success' => true, 'language' => $language]);
} catch (Exception $e) {
echo json_encode(['success' => false, 'error' => $e->getMessage()]);
}
}
/*
*/
//GETING data from config_scrips_table (SQLite DB)
if ($type == "get_config_scripts_sqlite") {
try {
$db = new PDO("sqlite:$database_path");
$db->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
// Get all main configuration entries
$config_query = $db->query("SELECT * FROM config_scripts_table");
$config_data = $config_query->fetchAll(PDO::FETCH_ASSOC);
// Convert data types according to their 'type' field
$result = [];
foreach ($config_data as $item) {
$script_path = $item['script_path'];
$enabled = $item['enabled'];
// Convert the enabled field to a proper boolean
$result[$script_path] = ($enabled == 1);
}
// Return JSON response
header('Content-Type: application/json');
echo json_encode($result, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES);
} catch (PDOException $e) {
// Return error as JSON
header('Content-Type: application/json');
echo json_encode(['error' => 'Database error: ' . $e->getMessage()]);
}
}
/*
*/
//GETING data from envea_sondes_table (SQLite DB)
if ($type == "get_envea_sondes_table_sqlite") {
try {
$db = new PDO("sqlite:$database_path");
$db->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
// Get all entries from envea_sondes_table
$query = $db->query("SELECT id, connected, port, name, coefficient FROM envea_sondes_table");
$data = $query->fetchAll(PDO::FETCH_ASSOC);
// Convert data types appropriately
$result = [];
foreach ($data as $item) {
// Create object for each sonde with proper data types
$sonde = [
'id' => (int)$item['id'],
'connected' => $item['connected'] == 1, // Convert to boolean
'port' => $item['port'],
'name' => $item['name'],
'coefficient' => (float)$item['coefficient'] // Convert to float
];
// Add to results array
$result[] = $sonde;
}
// Return JSON response
header('Content-Type: application/json');
echo json_encode($result, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES);
} catch (PDOException $e) {
// Return error as JSON
header('Content-Type: application/json');
echo json_encode(['error' => 'Database error: ' . $e->getMessage()]);
}
}
//UPDATING the config_table from SQLite DB
if ($type == "update_config_sqlite") {
$param = $_GET['param'] ?? null;
$value = $_GET['value'] ?? null;
if ($param === null || $value === null) {
echo json_encode(["error" => "Missing parameter or value"]);
exit;
}
try {
$db = new PDO("sqlite:$database_path");
$db->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
// First, check if parameter exists and get its type
$checkStmt = $db->prepare("SELECT type FROM config_table WHERE key = :param");
$checkStmt->bindParam(':param', $param);
$checkStmt->execute();
$result = $checkStmt->fetch(PDO::FETCH_ASSOC);
if ($result) {
// Parameter exists, determine type and update
$type = $result['type'];
// Convert value according to type if needed
$convertedValue = $value;
if ($type == "bool") {
// Convert various boolean representations to 0/1
$convertedValue = (filter_var($value, FILTER_VALIDATE_BOOLEAN)) ? "1" : "0";
} elseif ($type == "int") {
$convertedValue = (string)intval($value);
} elseif ($type == "float") {
$convertedValue = (string)floatval($value);
}
// Update the value
$updateStmt = $db->prepare("UPDATE config_table SET value = :value WHERE key = :param");
$updateStmt->bindParam(':value', $convertedValue);
$updateStmt->bindParam(':param', $param);
$updateStmt->execute();
echo json_encode([
"success" => true,
"message" => "Configuration updated successfully",
"param" => $param,
"value" => $convertedValue,
"type" => $type
]);
} else {
echo json_encode([
"error" => "Parameter not found in configuration",
"param" => $param
]);
}
} catch (PDOException $e) {
echo json_encode(["error" => $e->getMessage()]);
}
}
//UPDATING the envea_sondes_table table from SQLite DB
if ($type == "update_sonde") {
$id = $_GET['id'] ?? null;
$field = $_GET['field'] ?? null;
$value = $_GET['value'] ?? null;
// Validate parameters
if ($id === null || $field === null || $value === null) {
echo json_encode([
"success" => false,
"error" => "Missing required parameters (id, field, or value)"
]);
exit;
}
// Validate field name (whitelist approach for security)
$allowed_fields = ['connected', 'port', 'name', 'coefficient'];
if (!in_array($field, $allowed_fields)) {
echo json_encode([
"success" => false,
"error" => "Invalid field name: " . $field
]);
exit;
}
try {
// Connect to the database
$db = new PDO("sqlite:$database_path");
$db->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
// Check if the sonde exists
$checkStmt = $db->prepare("SELECT id FROM envea_sondes_table WHERE id = :id");
$checkStmt->bindParam(':id', $id, PDO::PARAM_INT);
$checkStmt->execute();
if (!$checkStmt->fetch()) {
echo json_encode([
"success" => false,
"error" => "Sonde with ID $id not found"
]);
exit;
}
// Process value based on field type
if ($field == 'connected') {
// Convert to integer (0 or 1)
$processedValue = filter_var($value, FILTER_VALIDATE_BOOLEAN) ? 1 : 0;
$paramType = PDO::PARAM_INT;
} else if ($field == 'coefficient') {
// Convert to float
$processedValue = floatval($value);
$paramType = PDO::PARAM_STR; // SQLite doesn't have PARAM_FLOAT
} else {
// For text fields (port, name)
$processedValue = $value;
$paramType = PDO::PARAM_STR;
}
// Update the sonde record
$updateStmt = $db->prepare("UPDATE envea_sondes_table SET $field = :value WHERE id = :id");
$updateStmt->bindParam(':value', $processedValue, $paramType);
$updateStmt->bindParam(':id', $id, PDO::PARAM_INT);
$updateStmt->execute();
// Return success response
echo json_encode([
"success" => true,
"message" => "Sonde $id updated successfully",
"field" => $field,
"value" => $processedValue
]);
} catch (PDOException $e) {
// Return error as JSON
echo json_encode([
"success" => false,
"error" => "Database error: " . $e->getMessage()
]);
}
}
//update the config (old JSON updating)
if ($type == "update_config") {
echo "updating.... ";
$param=$_GET['param'];
$value=$_GET['value'];
$configFile = '../config.json';
// Convert value to boolean, integer, or string
if ($value === "true") {
$value = true; // Convert "true" string to boolean true
} elseif ($value === "false") {
$value = false; // Convert "false" string to boolean false
} elseif (is_numeric($value)) {
$value = $value + 0; // Convert numeric strings to int or float
}
$configData = json_decode(file_get_contents($configFile), true);
$configData[$param] = $value;
file_put_contents($configFile, json_encode($configData, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES));
echo "Config updated!";
}
if ($type == "getModem_busy") {
$command = 'sudo /usr/bin/python3 /var/www/nebuleair_pro_4g/SARA/check_running.py';
$output = shell_exec($command);
echo $output;
}
if ($type == "RTC_time") {
$time = shell_exec("date '+%d/%m/%Y %H:%M:%S'");
echo $time;
}
if ($type == "sys_RTC_module_time") {
$command = 'sudo /usr/bin/python3 /var/www/nebuleair_pro_4g/RTC/read.py';
$output = shell_exec($command);
echo $output;
}
if ($type == "sara_ping") {
$command = 'sudo /usr/bin/python3 /var/www/nebuleair_pro_4g/SARA/sara_ping.py';
$output = shell_exec($command);
echo $output;
}
if ($type == "sara_psd_setup") {
$command = 'sudo /usr/bin/python3 /var/www/nebuleair_pro_4g/SARA/R5/setPDP.py';
$output = shell_exec($command);
echo $output;
}
if ($type == "sara_test_udp") {
$command = 'sudo /usr/bin/python3 -u /var/www/nebuleair_pro_4g/SARA/sara_test_udp.py';
$output = shell_exec($command);
echo $output;
}
if ($type == "sara_ping_miotiq") {
$command = 'sudo /usr/bin/python3 -u /var/www/nebuleair_pro_4g/SARA/sara_ping_miotiq.py';
$output = shell_exec($command);
echo $output;
}
if ($type == "sara_check_pdp") {
$command = 'sudo /usr/bin/python3 -u /var/www/nebuleair_pro_4g/SARA/sara_check_pdp.py';
$output = shell_exec($command);
echo $output;
}
if ($type == "git_pull") {
$command = 'sudo git pull';
$output = shell_exec($command);
echo $output;
}
if ($type == "update_firmware") {
// Execute the comprehensive update script
$command = 'sudo /var/www/nebuleair_pro_4g/update_firmware.sh 2>&1';
$output = shell_exec($command);
// Return the output as JSON for better web display
header('Content-Type: application/json');
echo json_encode([
'success' => true,
'output' => $output,
'timestamp' => date('Y-m-d H:i:s')
]);
}
// Pre-flight: check that www-data can sudo the firmware update script without a password.
// On older sensors the sudoers rule for /var/www/nebuleair_pro_4g/* may be missing,
// in which case sudo would block on a password prompt and the update silently fails.
// Returns ['ok' => true] on success, or ['ok' => false, 'message' => ...] with a fix hint.
function preflight_sudo_check($scriptPath) {
// `sudo -n -l <cmd>` is a non-interactive check: succeeds only if the user is
// allowed to run <cmd> via sudo WITHOUT a password. Doesn't actually run anything.
exec('sudo -n -l ' . escapeshellarg($scriptPath) . ' 2>&1', $out, $rc);
if ($rc === 0) return ['ok' => true];
$output = implode("\n", $out);
$msg = "Configuration sudoers manquante sur ce capteur (www-data n'a pas le droit d'exécuter le script de mise à jour sans mot de passe). "
. "Voir l'erreur ci-dessous pour appliquer le fix.";
return [
'ok' => false,
'error_type' => 'sudoers_missing',
'message' => $msg,
'raw' => $output
];
}
// Start firmware update in background, returns immediately so the UI can poll progress.
// Output is written to a temp log file. A 'done' marker file is created when finished.
if ($type == "update_firmware_start") {
// Pre-flight: fail fast with a clear message if sudoers is misconfigured
$preflight = preflight_sudo_check('/var/www/nebuleair_pro_4g/update_firmware.sh');
if (!$preflight['ok']) {
header('Content-Type: application/json');
echo json_encode([
'success' => false,
'error_type' => $preflight['error_type'],
'message' => $preflight['message'],
'raw' => $preflight['raw']
]);
exit;
}
$logFile = '/tmp/nebuleair_firmware_update.log';
$doneFile = '/tmp/nebuleair_firmware_update.done';
// Reset previous run markers
@file_put_contents($logFile, '');
@unlink($doneFile);
// Launch in background:
// - run the update script, capture stdout/stderr into log
// - append "EXIT_CODE=N" so the UI can detect success/failure
// - touch the done file so the UI knows the run finished
// - detach all stdio from PHP so this call returns immediately
$cmd = '(sudo /var/www/nebuleair_pro_4g/update_firmware.sh > '
. escapeshellarg($logFile) . ' 2>&1; '
. 'echo "EXIT_CODE=$?" >> ' . escapeshellarg($logFile) . '; '
. 'touch ' . escapeshellarg($doneFile) . ') > /dev/null 2>&1 &';
shell_exec($cmd);
header('Content-Type: application/json');
echo json_encode([
'success' => true,
'started_at' => time(),
'log_file' => $logFile
]);
exit;
}
// Poll firmware update progress. The UI sends the byte offset it has already read,
// we return any new content since that offset and whether the run has finished.
if ($type == "update_firmware_progress") {
$logFile = '/tmp/nebuleair_firmware_update.log';
$doneFile = '/tmp/nebuleair_firmware_update.done';
$offset = isset($_GET['offset']) ? (int)$_GET['offset'] : 0;
$content = '';
$newOffset = $offset;
if (file_exists($logFile)) {
$size = filesize($logFile);
if ($size > $offset) {
$fp = fopen($logFile, 'r');
if ($fp) {
fseek($fp, $offset);
$content = fread($fp, $size - $offset);
fclose($fp);
}
$newOffset = $size;
}
}
$done = file_exists($doneFile);
header('Content-Type: application/json');
echo json_encode([
'success' => true,
'content' => $content,
'offset' => $newOffset,
'done' => $done
]);
exit;
}
if ($type == "upload_firmware") {
// Firmware update via ZIP file upload (offline mode)
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
echo json_encode(['success' => false, 'message' => 'POST method required']);
exit;
}
// Pre-flight sudoers check: fail fast before the user uploads the ZIP only
// to discover their sensor has no sudo NOPASSWD rule.
$preflight = preflight_sudo_check('/var/www/nebuleair_pro_4g/update_firmware_from_file.sh');
if (!$preflight['ok']) {
echo json_encode([
'success' => false,
'error_type' => $preflight['error_type'],
'message' => $preflight['message'],
'raw' => $preflight['raw']
]);
exit;
}
// Check file upload
if (!isset($_FILES['firmware_file']) || $_FILES['firmware_file']['error'] !== UPLOAD_ERR_OK) {
$max_upload = ini_get('upload_max_filesize');
$upload_errors = [
UPLOAD_ERR_INI_SIZE => "Le fichier depasse la limite serveur (actuellement $max_upload). Effectuez d'abord une mise a jour via WiFi (bouton Update firmware) pour debloquer l'upload hors-ligne.",
UPLOAD_ERR_FORM_SIZE => 'File exceeds form upload limit',
UPLOAD_ERR_PARTIAL => 'File was only partially uploaded',
UPLOAD_ERR_NO_FILE => 'No file was uploaded',
UPLOAD_ERR_NO_TMP_DIR => 'Missing temporary folder',
UPLOAD_ERR_CANT_WRITE => 'Failed to write file to disk',
];
$error_code = $_FILES['firmware_file']['error'] ?? UPLOAD_ERR_NO_FILE;
$error_msg = $upload_errors[$error_code] ?? 'Unknown upload error';
echo json_encode(['success' => false, 'message' => $error_msg]);
exit;
}
$file = $_FILES['firmware_file'];
// Validate extension
$ext = strtolower(pathinfo($file['name'], PATHINFO_EXTENSION));
if ($ext !== 'zip') {
echo json_encode(['success' => false, 'message' => 'Only .zip files are allowed']);
exit;
}
// Validate size (50MB max)
if ($file['size'] > 50 * 1024 * 1024) {
echo json_encode(['success' => false, 'message' => 'File too large (max 50MB)']);
exit;
}
// Get current version before update
$old_version = 'unknown';
if (file_exists('/var/www/nebuleair_pro_4g/VERSION')) {
$old_version = trim(file_get_contents('/var/www/nebuleair_pro_4g/VERSION'));
}
// Prepare extraction directory
$tmp_dir = '/tmp/nebuleair_update';
$extract_dir = "$tmp_dir/extracted";
shell_exec("rm -rf $tmp_dir");
mkdir($extract_dir, 0755, true);
// Move uploaded file
$zip_path = "$tmp_dir/firmware.zip";
if (!move_uploaded_file($file['tmp_name'], $zip_path)) {
echo json_encode(['success' => false, 'message' => 'Failed to move uploaded file']);
exit;
}
// Extract ZIP
$unzip_output = shell_exec("unzip -o '$zip_path' -d '$extract_dir' 2>&1");
// Detect project root folder (Gitea creates nebuleair_pro_4g-main/ inside the zip)
$source_dir = $extract_dir;
$entries = scandir($extract_dir);
$subdirs = array_filter($entries, function($e) use ($extract_dir) {
return $e !== '.' && $e !== '..' && is_dir("$extract_dir/$e");
});
if (count($subdirs) === 1) {
$subdir = reset($subdirs);
$candidate = "$extract_dir/$subdir";
if (file_exists("$candidate/VERSION")) {
$source_dir = $candidate;
}
}
// Validate VERSION exists in the archive
if (!file_exists("$source_dir/VERSION")) {
shell_exec("rm -rf $tmp_dir");
echo json_encode(['success' => false, 'message' => 'Invalid archive: VERSION file not found']);
exit;
}
$new_version = trim(file_get_contents("$source_dir/VERSION"));
// Launch update script in background, reusing the same log/done file mechanism
// as the online update. The frontend can then poll update_firmware_progress
// to get a live view and final status.
$logFile = '/tmp/nebuleair_firmware_update.log';
$doneFile = '/tmp/nebuleair_firmware_update.done';
@file_put_contents($logFile, '');
@unlink($doneFile);
// Note: $source_dir is escaped because it can contain a version-derived folder name.
// $tmp_dir is removed by the script itself (Step 7) but we also clean here as a safety net
// — done AFTER the script finishes, so we chain it inside the background block.
$cmd = '(sudo /var/www/nebuleair_pro_4g/update_firmware_from_file.sh '
. escapeshellarg($source_dir) . ' > '
. escapeshellarg($logFile) . ' 2>&1; '
. 'echo "EXIT_CODE=$?" >> ' . escapeshellarg($logFile) . '; '
. 'rm -rf ' . escapeshellarg($tmp_dir) . '; '
. 'touch ' . escapeshellarg($doneFile) . ') > /dev/null 2>&1 &';
shell_exec($cmd);
echo json_encode([
'success' => true,
'started_at' => time(),
'old_version' => $old_version,
'new_version' => $new_version
]);
exit;
}
if ($type == "set_RTC_withNTP") {
$command = 'sudo /usr/bin/python3 /var/www/nebuleair_pro_4g/RTC/set_with_NTP.py';
$output = shell_exec($command);
echo $output;
}
if ($type == "set_RTC_withBrowser") {
$time = $_GET['time'];
// Validate time format
if (!strtotime($time)) {
echo json_encode(['success' => false, 'message' => 'Invalid time format']);
exit;
}
// Convert to RTC-compatible format (e.g., 'YYYY-MM-DD HH:MM:SS')
$rtc_time = date('Y-m-d H:i:s', strtotime($time));
// Execute Python script to update the RTC
$command = escapeshellcmd("sudo /usr/bin/python3 /var/www/nebuleair_pro_4g/RTC/set_with_browserTime.py '$rtc_time'");
$output = shell_exec($command);
if ($output === null) {
echo json_encode(['success' => false, 'message' => 'Failed to update RTC']);
} else {
echo json_encode(['success' => true, 'message' => 'RTC updated successfully']);
}
}
if ($type == "clear_loopLogs") {
$response = array();
try {
$logPath = '/var/www/nebuleair_pro_4g/logs/master.log';
// Check if file exists and is writable
if (!file_exists($logPath)) {
throw new Exception("Log file does not exist");
}
if (!is_writable($logPath)) {
throw new Exception("Log file is not writable");
}
// Execute the command
$command = 'truncate -s 0 ' . escapeshellarg($logPath);
$output = shell_exec($command . ' 2>&1');
// Check if there was any error output
if (!empty($output)) {
throw new Exception("Command error: " . $output);
}
// Success response
$response = array(
'status' => 'success',
'message' => 'Logs cleared successfully',
'timestamp' => date('Y-m-d H:i:s')
);
} catch (Exception $e) {
// Error response
$response = array(
'status' => 'error',
'message' => $e->getMessage(),
'timestamp' => date('Y-m-d H:i:s')
);
}
// Set content type to JSON
header('Content-Type: application/json');
// Return the JSON response
echo json_encode($response);
exit;
}
if ($type == "database_size") {
// Path to the SQLite database file
$databasePath = '/var/www/nebuleair_pro_4g/sqlite/sensors.db';
// Check if the file exists
if (file_exists($databasePath)) {
try {
// Connect to the SQLite database
$db = new PDO("sqlite:$databasePath");
// Get the file size in bytes
$fileSizeBytes = filesize($databasePath);
// Convert the file size to human-readable formats
$fileSizeKilobytes = $fileSizeBytes / 1024; // KB
$fileSizeMegabytes = $fileSizeKilobytes / 1024; // MB
// Prepare the JSON response
$data = [
'path' => $databasePath,
'size_bytes' => $fileSizeBytes,
'size_kilobytes' => round($fileSizeKilobytes, 2),
'size_megabytes' => round($fileSizeMegabytes, 2),
];
// Output the JSON response
echo json_encode($data, JSON_PRETTY_PRINT);
} catch (PDOException $e) {
// Handle database connection errors
echo json_encode([
'error' => 'Database query failed: ' . $e->getMessage()
]);
}
} else {
// Handle error if the file doesn't exist
echo json_encode([
'error' => 'Database file not found',
'path' => $databasePath
]);
}
}
if ($type == "db_table_stats") {
$databasePath = '/var/www/nebuleair_pro_4g/sqlite/sensors.db';
if (file_exists($databasePath)) {
try {
$db = new PDO("sqlite:$databasePath");
// Database file size
$fileSizeBytes = filesize($databasePath);
$fileSizeMB = round($fileSizeBytes / (1024 * 1024), 2);
// Sensor data tables to inspect
$tables = ['data_NPM', 'data_NPM_5channels', 'data_BME280', 'data_envea', 'data_WIND', 'data_MPPT', 'data_NOISE', 'data_MHZ19'];
$tableStats = [];
foreach ($tables as $tableName) {
// Check if table exists
$check = $db->query("SELECT name FROM sqlite_master WHERE type='table' AND name='$tableName'");
if ($check->fetch()) {
$countResult = $db->query("SELECT COUNT(*) as cnt FROM $tableName")->fetch();
$count = (int)$countResult['cnt'];
$oldest = null;
$newest = null;
if ($count > 0) {
// Exclude 'not connected' rows: these are sensor readings collected
// while the RTC service was down. They are lexicographically > real
// timestamps so they corrupt MAX() (and MIN() too, less obviously).
$oldestResult = $db->query("SELECT MIN(timestamp) as ts FROM $tableName WHERE timestamp != 'not connected'")->fetch();
$newestResult = $db->query("SELECT MAX(timestamp) as ts FROM $tableName WHERE timestamp != 'not connected'")->fetch();
$oldest = $oldestResult['ts'];
$newest = $newestResult['ts'];
}
$tableStats[] = [
'name' => $tableName,
'count' => $count,
'oldest' => $oldest,
'newest' => $newest
];
}
}
echo json_encode([
'success' => true,
'size_mb' => $fileSizeMB,
'size_bytes' => $fileSizeBytes,
'tables' => $tableStats
]);
} catch (PDOException $e) {
echo json_encode(['success' => false, 'error' => 'Database query failed: ' . $e->getMessage()]);
}
} else {
echo json_encode(['success' => false, 'error' => 'Database file not found']);
}
}
if ($type == "download_full_table") {
$databasePath = '/var/www/nebuleair_pro_4g/sqlite/sensors.db';
$table = $_GET['table'] ?? '';
// Whitelist of allowed tables
$allowedTables = ['data_NPM', 'data_NPM_5channels', 'data_BME280', 'data_envea', 'data_WIND', 'data_MPPT', 'data_NOISE', 'data_MHZ19'];
if (!in_array($table, $allowedTables)) {
header('Content-Type: application/json');
echo json_encode(['error' => 'Invalid table name']);
exit;
}
// CSV headers per table
$csvHeaders = [
'data_NPM' => 'TimestampUTC,PM1,PM2.5,PM10,Temperature_sensor,Humidity_sensor',
'data_NPM_5channels' => 'TimestampUTC,PM_ch1,PM_ch2,PM_ch3,PM_ch4,PM_ch5',
'data_BME280' => 'TimestampUTC,Temperature,Humidity,Pressure',
'data_envea' => 'TimestampUTC,NO2,H2S,NH3,CO,O3,SO2',
'data_WIND' => 'TimestampUTC,Wind_speed_kmh,Wind_direction_V',
'data_MPPT' => 'TimestampUTC,Battery_voltage,Battery_current,Solar_voltage,Solar_power,Charger_status',
'data_NOISE' => 'TimestampUTC,Current_LEQ,DB_A_value',
'data_MHZ19' => 'TimestampUTC,CO2_ppm'
];
try {
$db = new PDO("sqlite:$databasePath");
$rows = $db->query("SELECT * FROM $table ORDER BY timestamp ASC")->fetchAll(PDO::FETCH_NUM);
header('Content-Type: text/csv; charset=utf-8');
header('Content-Disposition: attachment; filename="' . $table . '_full.csv"');
$output = fopen('php://output', 'w');
// Write header
fputcsv($output, explode(',', $csvHeaders[$table]));
// Write data rows
foreach ($rows as $row) {
fputcsv($output, $row);
}
fclose($output);
} catch (PDOException $e) {
header('Content-Type: application/json');
echo json_encode(['error' => 'Database query failed: ' . $e->getMessage()]);
}
exit;
}
if ($type == "linux_disk") {
$command = 'df -h /';
$output = shell_exec($command);
echo $output;
}
if ($type == "linux_memory") {
$command = 'free -h';
$output = shell_exec($command);
echo $output;
}
if ($type == "wifi_status") {
header('Content-Type: application/json');
$result = array(
'connected' => false,
'mode' => 'unknown',
'ssid' => '',
'ip' => '',
'hostname' => ''
);
// Get hostname
$result['hostname'] = trim(shell_exec('hostname'));
// Get wlan0 connection info
$connection = trim(shell_exec("nmcli -t -f GENERAL.CONNECTION device show wlan0 2>/dev/null | cut -d: -f2"));
if (!empty($connection) && $connection != '--') {
$result['connected'] = true;
$result['ssid'] = $connection;
// Check if it's a hotspot
if (strpos(strtolower($connection), 'hotspot') !== false || strpos($connection, 'nebuleair') !== false) {
$result['mode'] = 'hotspot';
} else {
$result['mode'] = 'wifi';
}
// Get IP address
$ip = trim(shell_exec("nmcli -t -f IP4.ADDRESS device show wlan0 2>/dev/null | head -1 | cut -d: -f2 | cut -d/ -f1"));
if (!empty($ip)) {
$result['ip'] = $ip;
}
} else {
// Check if eth0 is connected
$eth_ip = trim(shell_exec("nmcli -t -f IP4.ADDRESS device show eth0 2>/dev/null | head -1 | cut -d: -f2 | cut -d/ -f1"));
if (!empty($eth_ip)) {
$result['connected'] = true;
$result['mode'] = 'ethernet';
$result['ssid'] = 'Ethernet';
$result['ip'] = $eth_ip;
}
}
echo json_encode($result);
}
if ($type == "sshTunnel") {
$ssh_port=$_GET['ssh_port'];
$command = 'sudo ssh -i /var/www/.ssh/id_rsa -f -N -R "'.$ssh_port.':localhost:22" -p 50221 -o StrictHostKeyChecking=no "airlab_server1@aircarto.fr"';
$output = shell_exec($command);
echo $output;
}
if ($type == "reboot") {
$command = 'sudo reboot';
$output = shell_exec($command);
}
if ($type == "npm") {
$command = 'sudo /usr/bin/python3 /var/www/nebuleair_pro_4g/NPM/get_data_modbus_v3.py --dry-run';
$output = shell_exec($command);
echo $output;
}
if ($type == "npm_firmware") {
$port=$_GET['port'];
$command = 'sudo /usr/bin/python3 /var/www/nebuleair_pro_4g/NPM/firmware_version.py ' . $port;
$output = shell_exec($command);
echo $output;
}
if ($type == "envea") {
$port=$_GET['port'];
$name=$_GET['name'];
$command = 'sudo /usr/bin/python3 /var/www/nebuleair_pro_4g/envea/read_value.py ' . $port;
$output = shell_exec($command);
echo $output;
}
if ($type == "envea_debug") {
$command = 'sudo /usr/bin/python3 /var/www/nebuleair_pro_4g/envea/read_value_v2.py -d 2>&1';
$output = shell_exec($command);
echo $output;
}
if ($type == "noise") {
$command = 'sudo /usr/bin/python3 /var/www/nebuleair_pro_4g/sound_meter/read.py';
$output = shell_exec($command);
echo $output;
}
if ($type == "BME280") {
$command = 'sudo /usr/bin/python3 /var/www/nebuleair_pro_4g/BME280/read.py';
$output = shell_exec($command);
echo $output;
}
if ($type == "mhz19") {
$command = 'sudo /usr/bin/python3 /var/www/nebuleair_pro_4g/MH-Z19/get_data.py ttyAMA4';
$output = shell_exec($command);
echo $output;
}
if ($type == "table_mesure") {
$table=$_GET['table'];
$limit=$_GET['limit'];
$download=$_GET['download'];
if ($download==="false") {
$command = 'sudo /usr/bin/python3 /var/www/nebuleair_pro_4g/sqlite/read.py '.$table.' '.$limit;
$output = shell_exec($command);
echo $output;
} else{
$start_date=$_GET['start_date'];
$end_date=$_GET['end_date'];
$command = 'sudo /usr/bin/python3 /var/www/nebuleair_pro_4g/sqlite/read_select_date.py '.$table.' '.$start_date.' '.$end_date;
$output = shell_exec($command);
echo $output;
}
}
# SARA R4 COMMANDS
if ($type == "sara") {
$port=$_GET['port'];
$sara_command=$_GET['command'];
$sara_command = escapeshellcmd($sara_command);
$timeout=$_GET['timeout'];
$command = 'sudo /usr/bin/python3 /var/www/nebuleair_pro_4g/SARA/sara.py ' . $port . ' ' . $sara_command . ' ' . $timeout;
$output = shell_exec($command);
echo $output;
}
# SARA HARDWARE REBOOT (GPIO 16)
if ($type == "sara_hardware_reboot") {
$command = 'sudo /usr/bin/python3 /var/www/nebuleair_pro_4g/SARA/reboot/hardware_reboot.py';
$output = shell_exec($command);
echo $output;
}
# SARA R4 COMMANDS (MQTT)
if ($type == "sara_getMQTT_config") {
$port=$_GET['port'];
$timeout=$_GET['timeout'];
$command = '/usr/bin/python3 /var/www/nebuleair_pro_4g/SARA/MQTT/get_config.py ' . $port . ' ' . $timeout;
$output = shell_exec($command);
echo $output;
}
# SARA R4 COMMANDS (MQTT)
if ($type == "sara_getMQTT_login_logout") {
$port=$_GET['port'];
$timeout=$_GET['timeout'];
$login_logout=$_GET['login_logout'];
$command = 'sudo /usr/bin/python3 /var/www/nebuleair_pro_4g/SARA/MQTT/login_logout.py ' . $port . ' ' . $login_logout . ' ' . $timeout;
$output = shell_exec($command);
echo $output;
}
# SARA R4 COMMANDS (MQTT -> publish)
if ($type == "sara_MQTT_publish") {
$port=$_GET['port'];
$timeout=$_GET['timeout'];
$message=$_GET['message'];
$command = 'sudo /usr/bin/python3 /var/www/nebuleair_pro_4g/SARA/MQTT/publish.py ' . $port . ' ' . $message . ' ' . $timeout;
$output = shell_exec($command);
echo $output;
}
#Connect to network
if ($type == "sara_connectNetwork") {
$port=$_GET['port'];
$timeout=$_GET['timeout'];
$networkID=$_GET['networkID'];
$param="SARA_R4_neworkID";
//echo "updating SARA_R4_networkID in config file";
//OLD way to store data (JSON file)
// Convert `networkID` to an integer (or float if needed)
//$networkID = is_numeric($networkID) ? (strpos($networkID, '.') !== false ? (float)$networkID : (int)$networkID) : 0;
#save to config.json
//$configFile = '/var/www/nebuleair_pro_4g/config.json';
// Read the JSON file
//$jsonData = file_get_contents($configFile);
// Decode JSON data into an associative array
//$config = json_decode($jsonData, true);
// Check if decoding was successful
//if ($config === null) {
// die("Error: Could not decode JSON file.");
//}
// Update the value of SARA_R4_networkID
//$config['SARA_R4_neworkID'] = $networkID; // Replace 42 with the desired value
// Encode the array back to JSON with pretty printing
//$newJsonData = json_encode($config, JSON_PRETTY_PRINT);
// Check if encoding was successful
//if ($newJsonData === false) {
// die("Error: Could not encode JSON data.");
//}
// Write the updated JSON back to the file
//if (file_put_contents($configFile, $newJsonData) === false) {
// die("Error: Could not write to JSON file.");
//}
//echo "SARA_R4_networkID updated successfully.";
//NEW way to store data -> use SQLITE
try {
$db = new PDO("sqlite:$database_path");
$db->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
$updateStmt = $db->prepare("UPDATE config_table SET value = :value WHERE key = :param");
$updateStmt->bindParam(':value', $networkID);
$updateStmt->bindParam(':param', $param);
$updateStmt->execute();
echo "SARA_R4_networkID updated successfully.";
} catch (PDOException $e) {
// Return error as JSON
header('Content-Type: application/json');
echo json_encode(['error' => 'Database error: ' . $e->getMessage()]);
}
//echo "connecting to network... please wait...";
$command = 'sudo /usr/bin/python3 /var/www/nebuleair_pro_4g/SARA/sara_connectNetwork.py ' . $port . ' ' . $networkID . ' ' . $timeout;
$output = shell_exec($command);
echo $output;
}
#Setup Hostnmae
if ($type == "sara_setupHostname") {
$port=$_GET['port'];
$server_hostname=$_GET['networkID'];
$profileID=$_GET['profileID'];
//echo "connecting to network... please wait...";
$command = 'sudo /usr/bin/python3 /var/www/nebuleair_pro_4g/SARA/sara_setURL.py ' . $port . ' ' . $server_hostname . ' ' . $profileID;
$output = shell_exec($command);
echo $output;
}
#SET THE URL for messaging (profile id 2)
if ($type == "sara_setURL") {
$port=$_GET['port'];
$url=$_GET['url'];
$profile_id = 2;
$command = 'sudo /usr/bin/python3 /var/www/nebuleair_pro_4g/SARA/sara_setURL.py ' . $port . ' ' . $url . ' ' . $profile_id;
$output = shell_exec($command);
echo $output;
}
#SET APN
if ($type == "sara_APN") {
$port=$_GET['port'];
$timeout=$_GET['timeout'];
$APN_address=$_GET['APN_address'];
$command = 'sudo /usr/bin/python3 /var/www/nebuleair_pro_4g/SARA/sara_setAPN.py ' . $port . ' ' . $APN_address . ' ' . $timeout;
$output = shell_exec($command);
echo $output;
}
#TO WRITE MESSAGE TO MEMORY
if ($type == "sara_writeMessage") {
$port=$_GET['port'];
$message=$_GET['message'];
$message = escapeshellcmd($message);
$type2=$_GET['type2'];
if ($type2 === "write") {
$command = 'sudo /usr/bin/python3 /var/www/nebuleair_pro_4g/SARA/sara_writeMessage.py ' . $port . ' ' . $message;
$output = shell_exec($command);
}
if ($type2 === "read") {
$command = 'sudo /usr/bin/python3 /var/www/nebuleair_pro_4g/SARA/sara_readMessage.py ' . $port . ' ' . $message;
$output = shell_exec($command);
}
if ($type2 === "erase") {
$command = 'sudo /usr/bin/python3 /var/www/nebuleair_pro_4g/SARA/sara_eraseMessage.py ' . $port . ' ' . $message;
$output = shell_exec($command);
}
echo $output;
}
#Send the typed message to server (for ntfy notification)
if ($type == "sara_sendMessage") {
$port=$_GET['port'];
$endpoint=$_GET['endpoint'];
$endpoint = escapeshellcmd($endpoint);
$profile_id = 2;
$command = 'sudo /usr/bin/python3 /var/www/nebuleair_pro_4g/SARA/sara_sendMessage.py ' . $port . ' ' . $endpoint. ' ' . $profile_id;
$output = shell_exec($command);
echo $output;
}
if ($type == "internet") {
// eth0
$eth0_connStatus = str_replace("\n", "", shell_exec('nmcli -g GENERAL.STATE device show eth0 2>/dev/null'));
$eth0_IPAddr = str_replace("\n", "", shell_exec('nmcli -g IP4.ADDRESS device show eth0 2>/dev/null'));
// wlan0 basic
$wlan0_connStatus = str_replace("\n", "", shell_exec('nmcli -g GENERAL.STATE device show wlan0 2>/dev/null'));
$wlan0_IPAddr = str_replace("\n", "", shell_exec('nmcli -g IP4.ADDRESS device show wlan0 2>/dev/null'));
// wlan0 detailed info (connection name, signal, frequency, security, gateway, etc.)
$wlan0_connection = str_replace("\n", "", shell_exec("nmcli -t -f GENERAL.CONNECTION device show wlan0 2>/dev/null | cut -d: -f2"));
$wlan0_gateway = str_replace("\n", "", shell_exec('nmcli -g IP4.GATEWAY device show wlan0 2>/dev/null'));
// Get active WiFi details (signal, frequency, security) from nmcli
$wifi_signal = '';
$wifi_freq = '';
$wifi_security = '';
$wifi_ssid = '';
$wifi_output = shell_exec('nmcli -t -f ACTIVE,SSID,SIGNAL,FREQ,SECURITY device wifi list ifname wlan0 2>/dev/null');
if ($wifi_output) {
$lines = explode("\n", trim($wifi_output));
foreach ($lines as $line) {
// Active connection line starts with "yes:" (nmcli -t uses : separator)
if (strpos($line, 'yes:') === 0) {
// Format: yes:SSID:SIGNAL:FREQ:SECURITY
// Use explode with limit to handle SSIDs containing ':'
$parts = explode(':', $line);
if (count($parts) >= 5) {
$wifi_ssid = $parts[1];
$wifi_signal = $parts[2];
$wifi_freq = $parts[3];
$wifi_security = $parts[4];
}
break;
}
}
}
// Hostname
$hostname = trim(shell_exec('hostname 2>/dev/null'));
$data = array(
"ethernet" => array(
"connection" => $eth0_connStatus,
"IP" => $eth0_IPAddr
),
"wifi" => array(
"connection" => $wlan0_connStatus,
"IP" => $wlan0_IPAddr,
"ssid" => $wifi_ssid ?: $wlan0_connection,
"signal" => $wifi_signal,
"frequency" => $wifi_freq,
"security" => $wifi_security,
"gateway" => $wlan0_gateway,
"hostname" => $hostname
)
);
echo json_encode($data);
}
# IMPORTANT
# c'est ici que la connexion vers le WIFI du client s'effectue.
if ($type == "wifi_connect") {
$SSID=$_GET['SSID'];
$PASS=$_GET['pass'];
// Get device name and hostname for instructions
try {
$db = new PDO("sqlite:$database_path");
$db->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
$stmt = $db->prepare("SELECT value FROM config_table WHERE key = 'deviceName'");
$stmt->execute();
$result = $stmt->fetch(PDO::FETCH_ASSOC);
$deviceName = $result ? $result['value'] : 'NebuleAir';
$db = null;
} catch (PDOException $e) {
$deviceName = 'NebuleAir';
}
$hostname = trim(shell_exec('hostname 2>/dev/null')) ?: 'aircarto';
// Launch connection script in background
$script_path = '/var/www/nebuleair_pro_4g/connexion.sh';
$log_file = '/var/www/nebuleair_pro_4g/logs/app.log';
shell_exec("$script_path $SSID $PASS >> $log_file 2>&1 &");
// Return JSON response with instructions
header('Content-Type: application/json');
echo json_encode([
'success' => true,
'ssid' => $SSID,
'deviceName' => $deviceName,
'hostname' => $hostname,
'message' => 'Connection attempt started',
'instructions' => [
'fr' => [
'title' => 'Connexion en cours...',
'step1' => "Le capteur tente de se connecter au réseau « $SSID »",
'step2' => "Vous allez être déconnecté du hotspot dans quelques secondes",
'step3' => "Reconnectez-vous au WiFi « $SSID » sur votre appareil",
'step4' => "Accédez au capteur via http://$hostname.local/html/ ou cherchez son IP dans votre routeur",
'warning' => "Si la connexion échoue, le capteur recréera automatiquement le hotspot"
],
'en' => [
'title' => 'Connection in progress...',
'step1' => "The sensor is attempting to connect to network « $SSID »",
'step2' => "You will be disconnected from the hotspot in a few seconds",
'step3' => "Reconnect your device to WiFi « $SSID »",
'step4' => "Access the sensor via http://$hostname.local/html/ or find its IP in your router",
'warning' => "If connection fails, the sensor will automatically recreate the hotspot"
]
]
]);
}
if ($type == "wifi_forget") {
// Get device name from database
try {
$db = new PDO("sqlite:$database_path");
$db->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
$stmt = $db->prepare("SELECT value FROM config_table WHERE key = 'deviceName'");
$stmt->execute();
$result = $stmt->fetch(PDO::FETCH_ASSOC);
$deviceName = $result ? $result['value'] : 'NebuleAir';
$db = null;
} catch (PDOException $e) {
$deviceName = 'NebuleAir';
}
// Launch forget script in background
$script_path = '/var/www/nebuleair_pro_4g/forget_wifi.sh';
$log_file = '/var/www/nebuleair_pro_4g/logs/app.log';
shell_exec("bash $script_path >> $log_file 2>&1 &");
header('Content-Type: application/json');
echo json_encode([
'success' => true,
'deviceName' => $deviceName,
'instructions' => [
'fr' => [
'title' => 'Réseau WiFi oublié',
'step1' => "Le capteur oublie le réseau WiFi actuel",
'step2' => "Le hotspot va démarrer automatiquement",
'step3' => "Connectez-vous au WiFi « $deviceName » (mot de passe : nebuleaircfg)",
'step4' => "Accédez au capteur via http://10.42.0.1/html/",
'warning' => "Le capteur ne se reconnectera plus automatiquement à ce réseau"
],
'en' => [
'title' => 'WiFi network forgotten',
'step1' => "The sensor is forgetting the current WiFi network",
'step2' => "The hotspot will start automatically",
'step3' => "Connect to WiFi « $deviceName » (password: nebuleaircfg)",
'step4' => "Access the sensor via http://10.42.0.1/html/",
'warning' => "The sensor will no longer auto-connect to this network"
]
]
]);
}
if ($type == "wifi_scan") {
$jsonData = [];
// Check if wlan0 is in hotspot mode — if so, use cached CSV from boot scan
// (live scan is impossible while wlan0 is serving the hotspot)
$wlan0_connection = trim(shell_exec("nmcli -t -f GENERAL.CONNECTION device show wlan0 2>/dev/null | cut -d: -f2"));
$is_hotspot = (strpos(strtolower($wlan0_connection), 'hotspot') !== false);
if ($is_hotspot) {
// Read cached scan from boot (wifi_list.csv)
$csv_path = '/var/www/nebuleair_pro_4g/wifi_list.csv';
if (file_exists($csv_path)) {
$lines = file($csv_path, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);
// Skip header line (SSID,SIGNAL,SECURITY)
for ($i = 1; $i < count($lines); $i++) {
$parts = str_getcsv($lines[$i]);
if (count($parts) >= 2 && !empty(trim($parts[0]))) {
$jsonData[] = [
'SSID' => trim($parts[0]),
'SIGNAL' => trim($parts[1]),
'SECURITY' => isset($parts[2]) ? trim($parts[2]) : '--',
'cached' => true
];
}
}
}
} else {
// Live scan (wlan0 is free)
$output = shell_exec('timeout 10 nmcli -f SSID,SIGNAL,SECURITY device wifi list ifname wlan0 2>/dev/null');
if ($output) {
$lines = explode("\n", trim($output));
for ($i = 1; $i < count($lines); $i++) {
$line = trim($lines[$i]);
if (empty($line)) continue;
$parts = preg_split('/\s{2,}/', $line, 3);
if (count($parts) >= 2) {
$jsonData[] = [
'SSID' => trim($parts[0]),
'SIGNAL' => trim($parts[1]),
'SECURITY' => isset($parts[2]) ? trim($parts[2]) : '--'
];
}
}
}
}
header('Content-Type: application/json');
echo json_encode($jsonData, JSON_PRETTY_PRINT);
}
if ($type == "wifi_scan_old") {
$output = shell_exec('nmcli device wifi list ifname wlan0');
// Split the output into lines
$lines = explode("\n", trim($output));
// Initialize an array to hold the results
$wifiNetworks = [];
// Loop through each line and extract the relevant information
foreach ($lines as $index => $line) {
// Skip the header line
if ($index === 0) {
continue;
}
// Split the line by whitespace and filter empty values
$columns = preg_split('/\s+/', $line);
// If the line has enough columns, store the relevant data
if (count($columns) >= 5) {
$wifiNetworks[] = [
'SSID' => $columns[2], // Network name
'BARS' => $columns[8],
'SIGNAL' => $columns[7], // Signal strength
];
}
}
$json_data = json_encode($wifiNetworks);
echo $json_data;
}
/*
_____ _ _
|_ _|__ _ __ _ __ ___ (_)_ __ __ _| |
| |/ _ \ '__| '_ ` _ \| | '_ \ / _` | |
| | __/ | | | | | | | | | | | (_| | |
|_|\___|_| |_| |_| |_|_|_| |_|\__,_|_|
*/
// Execute shell command with security restrictions
if ($type == "execute_command") {
// Verify that the request is using POST method
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
echo json_encode(['success' => false, 'message' => 'Invalid request method']);
exit;
}
// Get the command from POST data
$command = isset($_POST['command']) ? $_POST['command'] : '';
if (empty($command)) {
echo json_encode(['success' => false, 'message' => 'No command provided']);
exit;
}
// List of allowed commands (prefixes)
$allowedCommands = [
'ls', 'cat', 'cd', 'pwd', 'df', 'free', 'ifconfig', 'ip', 'ps', 'date', 'uptime',
'systemctl status', 'whoami', 'hostname', 'uname', 'grep', 'tail', 'head', 'find',
'less', 'more', 'du', 'echo', 'git'
];
// Check if command is allowed
$allowed = false;
foreach ($allowedCommands as $allowedCmd) {
if (strpos($command, $allowedCmd) === 0) {
$allowed = true;
break;
}
}
// Special case for systemctl restart and reboot
if (strpos($command, 'systemctl restart') === 0 || $command === 'reboot') {
// These commands don't return output through shell_exec since they change process state
// We'll just acknowledge them
if ($command === 'reboot') {
// Execute the command with exec to avoid waiting for output
exec('sudo reboot > /dev/null 2>&1 &');
echo json_encode([
'success' => true,
'output' => 'System is rebooting...'
]);
} else {
// For systemctl restart, execute it and acknowledge
$serviceName = str_replace('systemctl restart ', '', $command);
exec('sudo systemctl restart ' . escapeshellarg($serviceName) . ' > /dev/null 2>&1 &');
echo json_encode([
'success' => true,
'output' => 'Service ' . $serviceName . ' is restarting...'
]);
}
exit;
}
// Check for prohibited patterns
$prohibitedPatterns = [
'sudo rm', ';', '&&', '||', '|', '>', '>>', '&',
'wget', 'curl', 'nc', 'ssh', 'scp', 'ftp', 'telnet',
'iptables', 'passwd', 'chown', 'chmod', 'mkfs', ' dd ',
'mount', 'umount', 'kill', 'killall'
];
foreach ($prohibitedPatterns as $pattern) {
if (strpos($command, $pattern) !== false) {
echo json_encode([
'success' => false,
'message' => 'Command contains prohibited operation: ' . $pattern
]);
exit;
}
}
if (!$allowed) {
echo json_encode([
'success' => false,
'message' => 'Command not allowed for security reasons'
]);
exit;
}
// Execute the command with timeout protection
$descriptorspec = [
0 => ["pipe", "r"], // stdin
1 => ["pipe", "w"], // stdout
2 => ["pipe", "w"] // stderr
];
// Escape the command to prevent shell injection
$escapedCommand = escapeshellcmd($command);
// Add timeout of 5 seconds to prevent long-running commands
$process = proc_open("timeout 5 $escapedCommand", $descriptorspec, $pipes);
if (is_resource($process)) {
// Close stdin pipe
fclose($pipes[0]);
// Get output from stdout
$output = stream_get_contents($pipes[1]);
fclose($pipes[1]);
// Get any errors
$errors = stream_get_contents($pipes[2]);
fclose($pipes[2]);
// Close the process
$returnValue = proc_close($process);
// Check for errors
if ($returnValue !== 0) {
// If there was an error, but we have output, consider it a partial success
if (!empty($output)) {
echo json_encode([
'success' => true,
'output' => $output . "\n" . $errors . "\nCommand exited with code $returnValue"
]);
} else {
echo json_encode([
'success' => false,
'message' => empty($errors) ? "Command failed with exit code $returnValue" : $errors
]);
}
} else {
// Success
echo json_encode([
'success' => true,
'output' => $output
]);
}
} else {
echo json_encode([
'success' => false,
'message' => 'Failed to execute command'
]);
}
}
/*
____ _ ____ _ __ __ _
/ ___| _ _ ___| |_ ___ _ __ ___ | _ \ / ___| ___ _ ____ _(_) ___ ___| \/ | __ _ _ __ __ _ __ _ ___ _ __ ___ ___ _ __ | |_
\___ \| | | / __| __/ _ \ '_ ` _ \| | | | \___ \ / _ \ '__\ \ / / |/ __/ _ \ |\/| |/ _` | '_ \ / _` |/ _` |/ _ \ '_ ` _ \ / _ \ '_ \| __|
___) | |_| \__ \ || __/ | | | | | |_| | ___) | __/ | \ V /| | (_| __/ | | | (_| | | | | (_| | (_| | __/ | | | | | __/ | | | |_
|____/ \__, |___/\__\___|_| |_| |_|____/ |____/ \___|_| \_/ |_|\___\___|_| |_|\__,_|_| |_|\__,_|\__, |\___|_| |_| |_|\___|_| |_|\__|
|___/ |___/
*/
// Get systemd services status
if ($type == "get_systemd_services") {
try {
// List of NebuleAir services to monitor with descriptions and frequencies
// Optional 'display_name' overrides the auto-generated cleaned-up name.
$services = [
'nebuleair-npm-data.timer' => [
'description' => 'Collects particulate matter data from NextPM sensor',
'frequency' => 'Every 10 seconds'
],
'nebuleair-envea-data.timer' => [
'description' => 'Reads environmental data from Envea sensors',
'frequency' => 'Every 10 seconds'
],
'nebuleair-sara-data.timer' => [
'description' => 'Transmits collected data via 4G cellular modem',
'frequency' => 'Every 60 seconds'
],
'nebuleair-bme280-data.timer' => [
'description' => 'Monitors temperature and humidity (BME280)',
'frequency' => 'Every 2 minutes'
],
'nebuleair-mppt-data.timer' => [
'description' => 'Tracks solar panel and battery status',
'frequency' => 'Every 2 minutes'
],
'nebuleair-noise-data.timer' => [
'description' => 'Get Data from noise sensor',
'frequency' => 'Every minute'
],
'nebuleair-mhz19-data.timer' => [
'description' => 'Reads CO2 concentration from MH-Z19 sensor',
'frequency' => 'Every 2 minutes'
],
'nebuleair-db-cleanup-data.timer' => [
'description' => 'Cleans up old data from database',
'frequency' => 'Daily'
],
'nebuleair-wifi-powersave.timer' => [
'display_name' => 'Wifi powersave',
'description' => 'Enables WiFi power-saving mode to reduce battery consumption',
'frequency' => '10 min after boot'
],
'nebuleair-cpu-power.service' => [
'display_name' => 'Cpu power',
'description' => 'Applies CPU power mode from DB config at boot',
'frequency' => 'At boot'
],
'rtc_save_to_db.service' => [
'display_name' => 'Rtc save to db',
'description' => 'Reads DS3231 RTC over I2C and saves timestamp to DB',
'frequency' => 'Continuous (1Hz)'
]
];
$serviceStatus = [];
foreach ($services as $service => $serviceInfo) {
// Get service active status
$activeCmd = "systemctl is-active " . escapeshellarg($service) . " 2>/dev/null";
$activeStatus = trim(shell_exec($activeCmd));
$isActive = ($activeStatus === 'active');
// Get service enabled status
$enabledCmd = "systemctl is-enabled " . escapeshellarg($service) . " 2>/dev/null";
$enabledStatus = trim(shell_exec($enabledCmd));
$isEnabled = ($enabledStatus === 'enabled');
// Display name: explicit override if provided, otherwise auto-cleanup
if (isset($serviceInfo['display_name'])) {
$displayName = $serviceInfo['display_name'];
} else {
$displayName = str_replace(['.timer', '.service', 'nebuleair-', '-data'], '', $service);
$displayName = ucfirst(str_replace('-', ' ', $displayName));
}
$serviceStatus[] = [
'name' => $service,
'display_name' => $displayName,
'description' => $serviceInfo['description'],
'frequency' => $serviceInfo['frequency'],
'active' => $isActive,
'enabled' => $isEnabled
];
}
echo json_encode([
'success' => true,
'services' => $serviceStatus
], JSON_PRETTY_PRINT);
} catch (Exception $e) {
echo json_encode([
'success' => false,
'error' => $e->getMessage()
]);
}
}
// Restart a systemd service
if ($type == "restart_systemd_service") {
$service = $_GET['service'] ?? null;
if (empty($service)) {
echo json_encode([
'success' => false,
'error' => 'No service specified'
]);
exit;
}
// Validate service name (security check)
$allowedServices = [
'nebuleair-npm-data.timer',
'nebuleair-envea-data.timer',
'nebuleair-sara-data.timer',
'nebuleair-bme280-data.timer',
'nebuleair-mppt-data.timer',
'nebuleair-noise-data.timer',
'nebuleair-mhz19-data.timer',
'nebuleair-db-cleanup-data.timer',
'nebuleair-wifi-powersave.timer',
'nebuleair-cpu-power.service',
'rtc_save_to_db.service'
];
if (!in_array($service, $allowedServices)) {
echo json_encode([
'success' => false,
'error' => 'Invalid service name'
]);
exit;
}
try {
// Restart the service
$command = "sudo systemctl restart " . escapeshellarg($service) . " 2>&1";
$output = shell_exec($command);
// Check if restart was successful
$statusCmd = "systemctl is-active " . escapeshellarg($service) . " 2>/dev/null";
$status = trim(shell_exec($statusCmd));
if ($status === 'active' || empty($output)) {
echo json_encode([
'success' => true,
'message' => "Service $service restarted successfully"
]);
} else {
echo json_encode([
'success' => false,
'error' => "Failed to restart service: $output"
]);
}
} catch (Exception $e) {
echo json_encode([
'success' => false,
'error' => $e->getMessage()
]);
}
}
// Enable/disable a systemd service
if ($type == "toggle_systemd_service") {
$service = $_GET['service'] ?? null;
$enable = $_GET['enable'] ?? null;
if (empty($service) || $enable === null) {
echo json_encode([
'success' => false,
'error' => 'Missing service name or enable parameter'
]);
exit;
}
// Validate service name (security check)
$allowedServices = [
'nebuleair-npm-data.timer',
'nebuleair-envea-data.timer',
'nebuleair-sara-data.timer',
'nebuleair-bme280-data.timer',
'nebuleair-mppt-data.timer',
'nebuleair-noise-data.timer',
'nebuleair-mhz19-data.timer',
'nebuleair-db-cleanup-data.timer',
'nebuleair-wifi-powersave.timer',
'nebuleair-cpu-power.service',
'rtc_save_to_db.service'
];
if (!in_array($service, $allowedServices)) {
echo json_encode([
'success' => false,
'error' => 'Invalid service name'
]);
exit;
}
try {
$enable = filter_var($enable, FILTER_VALIDATE_BOOLEAN);
$action = $enable ? 'enable' : 'disable';
// Enable/disable the service
$command = "sudo systemctl $action " . escapeshellarg($service) . " 2>&1";
$output = shell_exec($command);
// If disabling, also stop the service
if (!$enable) {
$stopCommand = "sudo systemctl stop " . escapeshellarg($service) . " 2>&1";
$stopOutput = shell_exec($stopCommand);
}
// If enabling, also start the service
if ($enable) {
$startCommand = "sudo systemctl start " . escapeshellarg($service) . " 2>&1";
$startOutput = shell_exec($startCommand);
}
// Check if the operation was successful
$statusCmd = "systemctl is-enabled " . escapeshellarg($service) . " 2>/dev/null";
$status = trim(shell_exec($statusCmd));
$expectedStatus = $enable ? 'enabled' : 'disabled';
if ($status === $expectedStatus) {
echo json_encode([
'success' => true,
'message' => "Service $service " . ($enable ? 'enabled and started' : 'disabled and stopped') . " successfully"
]);
} else {
echo json_encode([
'success' => false,
'error' => "Failed to $action service: $output"
]);
}
} catch (Exception $e) {
echo json_encode([
'success' => false,
'error' => $e->getMessage()
]);
}
}
// Empty all sensor tables (preserve config and timestamp tables)
if ($type == "empty_sensor_tables") {
try {
// Execute the empty sensor tables script
$command = 'sudo /usr/bin/python3 /var/www/nebuleair_pro_4g/sqlite/empty_sensor_tables.py 2>&1';
$output = shell_exec($command);
// Try to extract JSON result from output
$json_start = strpos($output, '[JSON_RESULT]');
if ($json_start !== false) {
$json_data = substr($output, $json_start + strlen('[JSON_RESULT]'));
$json_data = trim($json_data);
// Find the first { and last }
$first_brace = strpos($json_data, '{');
$last_brace = strrpos($json_data, '}');
if ($first_brace !== false && $last_brace !== false) {
$json_data = substr($json_data, $first_brace, $last_brace - $first_brace + 1);
$result = json_decode($json_data, true);
if ($result !== null) {
echo json_encode($result);
} else {
// JSON decode failed, return raw output
echo json_encode([
'success' => true,
'message' => 'Tables emptied',
'output' => $output
]);
}
} else {
echo json_encode([
'success' => true,
'message' => 'Tables emptied',
'output' => $output
]);
}
} else {
// No JSON marker found, return raw output
echo json_encode([
'success' => true,
'message' => 'Tables emptied',
'output' => $output
]);
}
} catch (Exception $e) {
echo json_encode([
'success' => false,
'error' => 'Script execution failed: ' . $e->getMessage()
]);
}
}
/*
_____ ____ _ _ _
| ____|_ ____ _____ __ _ | _ \ ___| |_ ___ ___| |_(_) ___ _ __
| _| | '_ \ \ / / _ \/ _` | | | | |/ _ \ __/ _ \/ __| __| |/ _ \| '_ \
| |___| | | \ V / __/ (_| | | |_| | __/ || __/ (__| |_| | (_) | | | |
|_____|_| |_|\_/ \___|\__,_| |____/ \___|\__\___|\___|\__|_|\___/|_| |_|
*/
// Detect Envea devices on specified port
if ($type == "detect_envea_device") {
$port = $_GET['port'] ?? null;
if (empty($port)) {
echo json_encode([
'success' => false,
'error' => 'No port specified'
]);
exit;
}
// Validate port name (security check)
$allowedPorts = ['ttyAMA2', 'ttyAMA3', 'ttyAMA4', 'ttyAMA5'];
if (!in_array($port, $allowedPorts)) {
echo json_encode([
'success' => false,
'error' => 'Invalid port name'
]);
exit;
}
try {
// Execute the envea detection script
$command = "sudo /usr/bin/python3 /var/www/nebuleair_pro_4g/envea/read_ref.py " . escapeshellarg($port) . " 2>&1";
$output = shell_exec($command);
// Check if we got any meaningful output
$detected = false;
$device_info = '';
$raw_data = $output;
if (!empty($output)) {
// Look for indicators that a device is connected
if (strpos($output, 'Connexion ouverte') !== false) {
// Connection was successful
if (strpos($output, 'Données reçues brutes') !== false &&
strpos($output, 'b\'\'') === false) {
// We received actual data (not empty)
$detected = true;
$device_info = 'Envea CAIRSENS Device';
// Try to extract device type from ASCII data if available
if (preg_match('/Valeurs converties en ASCII : (.+)/', $output, $matches)) {
$ascii_data = trim($matches[1]);
if (!empty($ascii_data) && $ascii_data !== '........') {
$device_info = "Envea Device: " . $ascii_data;
}
}
} else {
// Connection successful but no data
$device_info = 'Port accessible but no Envea device detected';
}
} else if (strpos($output, 'Erreur de connexion série') !== false) {
// Serial connection error
$device_info = 'Serial connection error - port may be busy or not available';
} else {
// Other output
$device_info = 'Unexpected response from port';
}
} else {
// No output at all
$device_info = 'No response from port';
}
echo json_encode([
'success' => true,
'port' => $port,
'detected' => $detected,
'device_info' => $device_info,
'data' => $raw_data,
'timestamp' => date('Y-m-d H:i:s')
], JSON_PRETTY_PRINT);
} catch (Exception $e) {
echo json_encode([
'success' => false,
'error' => 'Script execution failed: ' . $e->getMessage(),
'port' => $port
]);
}
}
/*
____ ____ _ _ ____ __ __ _
/ ___| _ \| | | | | _ \ _____ _____ _ _| \/ | __ _ _ __ __ _ __ _ ___ _ __ ___ ___ _ __ | |_
| | | |_) | | | | | |_) / _ \ \ /\ / / _ \ '__| |\/| |/ _` | '_ \ / _` |/ _` |/ _ \ '_ ` _ \ / _ \ '_ \| __|
| |___| __/| |_| | | __/ (_) \ V V / __/ | | | | | (_| | | | | (_| | (_| | __/ | | | | | __/ | | | |_
\____|_| \___/ |_| \___/ \_/\_/ \___|_| |_| |_|\__,_|_| |_|\__,_|\__, |\___|_| |_| |_|\___|_| |_|\__|
|___/
*/
// Get firmware version from VERSION file
if ($type == "get_firmware_version") {
$versionFile = '/var/www/nebuleair_pro_4g/VERSION';
if (file_exists($versionFile)) {
$version = trim(file_get_contents($versionFile));
echo json_encode([
'success' => true,
'version' => $version
]);
} else {
echo json_encode([
'success' => false,
'version' => 'unknown'
]);
}
}
// Get changelog from changelog.json
if ($type == "get_changelog") {
$changelogFile = '/var/www/nebuleair_pro_4g/changelog.json';
if (file_exists($changelogFile)) {
$changelog = json_decode(file_get_contents($changelogFile), true);
if ($changelog !== null) {
echo json_encode([
'success' => true,
'changelog' => $changelog
]);
} else {
echo json_encode([
'success' => false,
'error' => 'Invalid changelog format'
]);
}
} else {
echo json_encode([
'success' => false,
'error' => 'Changelog file not found'
]);
}
}
// 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()
]);
}
}
if ($type == "screen_control") {
$action = $_GET['action'];
if ($action == "start") {
// Run as background process with sudo (requires nopasswd in sudoers)
// Redirecting to a temp log file to debug startup issues
$command = 'export DISPLAY=:0 && nohup sudo /usr/bin/python3 /var/www/nebuleair_pro_4g/screen_control/screen.py > /tmp/screen_control.log 2>&1 &';
shell_exec($command);
echo "Started. Check /tmp/screen_control.log for details.";
} elseif ($action == "stop") {
$command = 'sudo pkill -f "screen.py" 2>&1';
$output = shell_exec($command);
echo "Stopped. Output: " . $output;
}
}