From 6c0318ba6e980183bbf3a2401d6eae641c6d0b89 Mon Sep 17 00:00:00 2001 From: PaulVua Date: Thu, 28 May 2026 09:25:18 +0200 Subject: [PATCH] v1.9.10: Self Test - check sous-tension (vcgencmd get_throttled) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Ajoute un test 'Power Supply' au Self Test pour détecter une sous-tension du Pi (cause fréquente de capteurs USB instables, corruptions SD, reboots). Endpoint launcher.php?type=throttled + script power/get_throttled.py (lancé via sudo python3, déjà whitelisté — pas de modif sudoers). Affiché en tête des résultats et dans le rapport copiable. Co-Authored-By: Claude Opus 4.7 (1M context) --- VERSION | 2 +- changelog.json | 15 +++++++ html/assets/js/selftest.js | 58 +++++++++++++++++++++++++++ html/launcher.php | 7 ++++ html/selftest-modal.html | 9 +++++ power/get_throttled.py | 82 ++++++++++++++++++++++++++++++++++++++ 6 files changed, 172 insertions(+), 1 deletion(-) create mode 100644 power/get_throttled.py diff --git a/VERSION b/VERSION index 6ae756c..f0a2883 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -1.9.9 +1.9.10 diff --git a/changelog.json b/changelog.json index 3af5b08..3bf24ba 100644 --- a/changelog.json +++ b/changelog.json @@ -1,5 +1,20 @@ { "versions": [ + { + "version": "1.9.10", + "date": "2026-05-28", + "changes": { + "features": [ + "Self Test: nouveau check 'Power Supply' qui lit 'vcgencmd get_throttled' et détecte la sous-tension du Pi. Passed = alim OK, Warning = sous-tension survenue depuis le boot, Failed = sous-tension active. Apparaît en tête des résultats et dans le rapport copiable. La sous-tension est une cause fréquente de capteurs USB instables, corruptions SD et reboots." + ], + "improvements": [], + "fixes": [], + "compatibility": [ + "Backend: nouvel endpoint launcher.php?type=throttled + script power/get_throttled.py (lancé via sudo python3, déjà whitelisté dans sudoers — aucun changement /etc/sudoers requis)." + ] + }, + "notes": "Complément de la v1.9.9 (retry sonde bruit): permet de diagnostiquer à distance la cause racine (alimentation 5V insuffisante / câble) depuis l'interface admin, sur n'importe quel boîtier de la flotte." + }, { "version": "1.9.9", "date": "2026-05-28", diff --git a/html/assets/js/selftest.js b/html/assets/js/selftest.js index e2709c7..f7f5bc2 100644 --- a/html/assets/js/selftest.js +++ b/html/assets/js/selftest.js @@ -74,6 +74,10 @@ function resetSelfTestUI() { `; // Reset test items + document.getElementById('test_power_status').className = 'badge bg-secondary'; + document.getElementById('test_power_status').textContent = 'Pending'; + document.getElementById('test_power_detail').textContent = 'Waiting...'; + document.getElementById('test_wifi_status').className = 'badge bg-secondary'; document.getElementById('test_wifi_status').textContent = 'Pending'; document.getElementById('test_wifi_detail').textContent = 'Waiting...'; @@ -260,6 +264,59 @@ async function selfTestSequence() { await delaySelfTest(300); + // ═══════════════════════════════════════════════════════ + // SYSTEM TEST - Power supply (under-voltage detection) + // ═══════════════════════════════════════════════════════ + addSelfTestLog(''); + addSelfTestLog('────────────────────────────────────────────────────────'); + addSelfTestLog('SYSTEM TEST'); + addSelfTestLog('────────────────────────────────────────────────────────'); + + document.getElementById('selftest_status').innerHTML = ` +
+
+ Checking power supply... +
`; + + updateTestStatus('power', 'Testing...', 'Reading vcgencmd get_throttled...', 'bg-info'); + addSelfTestLog('Checking power supply (under-voltage)...'); + + try { + const powerResult = await new Promise((resolve, reject) => { + $.ajax({ + url: 'launcher.php?type=throttled', + dataType: 'json', + method: 'GET', + cache: false, + timeout: 10000, + success: function(data) { resolve(data); }, + error: function(xhr, status, error) { reject(new Error(error || status)); } + }); + }); + + selfTestReport.rawResponses['Power Supply'] = JSON.stringify(powerResult, null, 2); + addSelfTestLog(`Power response: ${JSON.stringify(powerResult)}`); + + if (!powerResult.available) { + updateTestStatus('power', 'Warning', powerResult.error || 'vcgencmd indisponible', 'bg-warning'); + testsFailed++; + } else if (powerResult.status === 'critical') { + updateTestStatus('power', 'Failed', powerResult.message, 'bg-danger'); + testsFailed++; + } else if (powerResult.status === 'warning') { + updateTestStatus('power', 'Warning', powerResult.message, 'bg-warning'); + testsFailed++; + } else { + updateTestStatus('power', 'Passed', powerResult.message || 'Alimentation OK', 'bg-success'); + testsPassed++; + } + } catch (error) { + addSelfTestLog(`Power supply test error: ${error.message}`); + updateTestStatus('power', 'Failed', error.message, 'bg-danger'); + selfTestReport.rawResponses['Power Supply'] = `ERROR: ${error.message}`; + testsFailed++; + } + // ═══════════════════════════════════════════════════════ // SENSOR TESTS - Test enabled sensors based on config // ═══════════════════════════════════════════════════════ @@ -856,6 +913,7 @@ GPS Location: ${selfTestReport.latitude || 'N/A'}, ${selfTestReport.longitud // Add test results (sensors first, then communication) const testNames = { + power: 'Power Supply', npm: 'NextPM (Particles)', bme280: 'BME280 (Temp/Hum)', noise: 'Noise Sensor', diff --git a/html/launcher.php b/html/launcher.php index 9967d87..7a7e20d 100755 --- a/html/launcher.php +++ b/html/launcher.php @@ -990,6 +990,13 @@ if ($type == "noise") { echo $output; } +if ($type == "throttled") { + header('Content-Type: application/json'); + $command = 'sudo /usr/bin/python3 /var/www/nebuleair_pro_4g/power/get_throttled.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); diff --git a/html/selftest-modal.html b/html/selftest-modal.html index a96ea17..0b4bfd2 100644 --- a/html/selftest-modal.html +++ b/html/selftest-modal.html @@ -15,6 +15,15 @@
+ +
+
+ Power Supply +
Waiting...
+
+ Pending +
+
diff --git a/power/get_throttled.py b/power/get_throttled.py new file mode 100644 index 0000000..416d70d --- /dev/null +++ b/power/get_throttled.py @@ -0,0 +1,82 @@ +#!/usr/bin/env python3 +""" +python3 /var/www/nebuleair_pro_4g/power/get_throttled.py + +Lit l'état d'alimentation du Raspberry Pi via `vcgencmd get_throttled` et +renvoie un JSON décodé. Utilisé par le Self Test (launcher.php?type=throttled) +pour détecter une sous-tension (cause fréquente de capteurs USB instables, +corruptions SD, reboots). + +Doit tourner en root (vcgencmd a besoin de /dev/vcio) : appelé via +`sudo /usr/bin/python3 ...` depuis launcher.php. + +Bits de get_throttled (cf. doc Raspberry Pi) : + 0 : sous-tension active maintenant + 1 : freq ARM bridée maintenant + 2 : throttling actif maintenant + 3 : limite temperature douce active maintenant + 16 : sous-tension survenue depuis le boot + 17 : bridage freq ARM survenu depuis le boot + 18 : throttling survenu depuis le boot + 19 : limite temperature douce survenue depuis le boot +""" + +import json +import subprocess + + +def main(): + try: + raw = subprocess.check_output( + ['/usr/bin/vcgencmd', 'get_throttled'], + stderr=subprocess.STDOUT, + timeout=5, + ).decode('utf-8', errors='ignore').strip() + except FileNotFoundError: + print(json.dumps({"available": False, "error": "vcgencmd introuvable (pas un Raspberry Pi ?)"})) + return + except Exception as e: + print(json.dumps({"available": False, "error": str(e)})) + return + + # Sortie attendue : "throttled=0x50000" + if '=' not in raw: + print(json.dumps({"available": False, "error": f"sortie inattendue: {raw}"})) + return + + hex_str = raw.split('=', 1)[1].strip() + try: + value = int(hex_str, 16) + except ValueError: + print(json.dumps({"available": False, "error": f"valeur illisible: {raw}"})) + return + + flags = { + "under_voltage_now": bool(value & 0x1), + "arm_freq_capped_now": bool(value & 0x2), + "throttled_now": bool(value & 0x4), + "soft_temp_limit_now": bool(value & 0x8), + "under_voltage_occurred": bool(value & 0x10000), + "arm_freq_capped_occurred": bool(value & 0x20000), + "throttling_occurred": bool(value & 0x40000), + "soft_temp_limit_occurred": bool(value & 0x80000), + } + + # Niveau de gravite pour le Self Test + if flags["under_voltage_now"] or flags["throttled_now"]: + status = "critical" + message = "Sous-tension ACTIVE — alimentation 5V insuffisante (alim/cable a remplacer)" + elif flags["under_voltage_occurred"] or flags["throttling_occurred"]: + status = "warning" + message = "Sous-tension survenue depuis le demarrage — verifier alim 5V / cable USB" + else: + status = "ok" + message = "Alimentation OK" + + result = {"available": True, "raw": hex_str, "value": value, "status": status, "message": message} + result.update(flags) + print(json.dumps(result)) + + +if __name__ == "__main__": + main()