v1.4.4 — Self-test partagé sur Accueil/Capteurs/Admin + test RTC DS3231

Extraction du code self-test dans des fichiers partagés (selftest.js +
selftest-modal.html) pour éviter la duplication. Ajout du bouton Run
Self Test sur les pages index, sensors et admin. Nouveau test RTC qui
vérifie la connexion du module DS3231 et la synchronisation horloge.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
PaulVua
2026-03-16 17:56:11 +01:00
parent 5bffec10a1
commit 5a2b3bb19d
7 changed files with 1124 additions and 1000 deletions

View File

@@ -1,5 +1,22 @@
{ {
"versions": [ "versions": [
{
"version": "1.4.4",
"date": "2026-03-16",
"changes": {
"features": [
"Bouton Self Test disponible sur les pages Accueil, Capteurs et Admin (en plus de Modem 4G)",
"Test du module RTC DS3231 integre dans le self-test (connexion + synchronisation horloge)"
],
"improvements": [
"Refactoring self-test : code JS et HTML des modals extraits dans des fichiers partages (selftest.js, selftest-modal.html)",
"Le modal self-test est charge dynamiquement via fetch, plus besoin de dupliquer le HTML"
],
"fixes": [],
"compatibility": []
},
"notes": "Le self-test est maintenant accessible depuis toutes les pages principales. Le test RTC verifie la connexion du module et l'ecart avec l'heure systeme."
},
{ {
"version": "1.4.3", "version": "1.4.3",
"date": "2026-03-16", "date": "2026-03-16",

View File

@@ -51,7 +51,14 @@
<main class="col-md-10 ms-sm-auto col-lg-11 offset-md-2 offset-lg-1 px-md-4"> <main class="col-md-10 ms-sm-auto col-lg-11 offset-md-2 offset-lg-1 px-md-4">
<h1 class="mt-4">Admin</h1> <h1 class="mt-4">Admin</h1>
<button class="btn btn-success mb-3 btn_selfTest" onclick="runSelfTest()">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-check2-circle me-1" viewBox="0 0 16 16">
<path d="M2.5 8a5.5 5.5 0 1 1 11 0 5.5 5.5 0 0 1-11 0z"/>
<path d="M15.354 3.354a.5.5 0 0 0-.708-.708L8 9.293 5.354 6.646a.5.5 0 1 0-.708.708l3 3a.5.5 0 0 0 .708 0l7-7z"/>
</svg>
Run Self Test
</button>
<div class="row mb-3"> <div class="row mb-3">
<div class="col-lg-3 col-12"> <div class="col-lg-3 col-12">
@@ -401,6 +408,7 @@
<!-- i18n translation system --> <!-- i18n translation system -->
<script src="assets/js/i18n.js"></script> <script src="assets/js/i18n.js"></script>
<script src="assets/js/topbar-logo.js"></script> <script src="assets/js/topbar-logo.js"></script>
<script src="assets/js/selftest.js"></script>
<script> <script>
document.addEventListener('DOMContentLoaded', function () { document.addEventListener('DOMContentLoaded', function () {

943
html/assets/js/selftest.js Normal file
View File

@@ -0,0 +1,943 @@
// ============================================
// SELF TEST FUNCTIONS (shared across pages)
// ============================================
// Cache for operators data
let operatorsDataSelfTest = null;
function loadOperatorsDataSelfTest() {
return new Promise((resolve, reject) => {
if (operatorsDataSelfTest) {
resolve(operatorsDataSelfTest);
return;
}
$.ajax({
url: 'assets/data/operators.json',
dataType: 'json',
method: 'GET',
success: function(data) {
operatorsDataSelfTest = data;
resolve(data);
},
error: function(xhr, status, error) {
console.error('Failed to load operators data:', error);
reject(error);
}
});
});
}
// Global object to store test results for report
let selfTestReport = {
timestamp: '',
deviceId: '',
modemVersion: '',
results: {},
rawResponses: {}
};
function runSelfTest() {
console.log("Starting Self Test...");
// Reset report
selfTestReport = {
timestamp: new Date().toISOString(),
deviceId: document.querySelector('.sideBar_sensorName')?.textContent || 'Unknown',
modemVersion: document.getElementById('modem_version')?.textContent || 'Unknown',
results: {},
rawResponses: {}
};
// Reset UI
resetSelfTestUI();
// Show modal
const modal = new bootstrap.Modal(document.getElementById('selfTestModal'));
modal.show();
// Disable buttons during test
document.getElementById('selfTestCloseBtn').disabled = true;
document.getElementById('selfTestDoneBtn').disabled = true;
document.getElementById('selfTestCopyBtn').disabled = true;
document.querySelectorAll('.btn_selfTest').forEach(btn => btn.disabled = true);
// Start test sequence
selfTestSequence();
}
function resetSelfTestUI() {
// Reset status
document.getElementById('selftest_status').innerHTML = `
<div class="d-flex align-items-center text-muted">
<div class="spinner-border spinner-border-sm me-2" role="status"></div>
<span>Preparing test...</span>
</div>`;
// Reset test items
document.getElementById('test_wifi_status').className = 'badge bg-secondary';
document.getElementById('test_wifi_status').textContent = 'Pending';
document.getElementById('test_wifi_detail').textContent = 'Waiting...';
document.getElementById('test_modem_status').className = 'badge bg-secondary';
document.getElementById('test_modem_status').textContent = 'Pending';
document.getElementById('test_modem_detail').textContent = 'Waiting...';
document.getElementById('test_sim_status').className = 'badge bg-secondary';
document.getElementById('test_sim_status').textContent = 'Pending';
document.getElementById('test_sim_detail').textContent = 'Waiting...';
document.getElementById('test_signal_status').className = 'badge bg-secondary';
document.getElementById('test_signal_status').textContent = 'Pending';
document.getElementById('test_signal_detail').textContent = 'Waiting...';
document.getElementById('test_network_status').className = 'badge bg-secondary';
document.getElementById('test_network_status').textContent = 'Pending';
document.getElementById('test_network_detail').textContent = 'Waiting...';
// Reset sensor tests
document.getElementById('sensor_tests_container').innerHTML = '';
document.getElementById('comm_tests_separator').style.display = 'none';
// Reset logs
document.getElementById('selftest_logs').innerHTML = '';
// Reset summary
document.getElementById('selftest_summary').innerHTML = '';
}
function addSelfTestLog(message, isRaw = false) {
const logsEl = document.getElementById('selftest_logs');
const timestamp = new Date().toLocaleTimeString();
if (isRaw) {
// Raw AT response - format nicely
logsEl.textContent += `[${timestamp}] >>> RAW RESPONSE:\n${message}\n<<<\n`;
} else {
logsEl.textContent += `[${timestamp}] ${message}\n`;
}
// Auto-scroll to bottom
logsEl.parentElement.scrollTop = logsEl.parentElement.scrollHeight;
}
function updateTestStatus(testId, status, detail, badge) {
document.getElementById(`test_${testId}_status`).className = `badge ${badge}`;
document.getElementById(`test_${testId}_status`).textContent = status;
document.getElementById(`test_${testId}_detail`).textContent = detail;
// Store result in report
selfTestReport.results[testId] = {
status: status,
detail: detail
};
}
function setConfigMode(enabled) {
return new Promise((resolve, reject) => {
addSelfTestLog(`Setting modem_config_mode to ${enabled}...`);
$.ajax({
url: `launcher.php?type=update_config_sqlite&param=modem_config_mode&value=${enabled}`,
dataType: 'json',
method: 'GET',
cache: false,
success: function(response) {
if (response.success) {
addSelfTestLog(`modem_config_mode set to ${enabled}`);
// Update checkbox state if it exists on the page
const checkbox = document.getElementById('check_modem_configMode');
if (checkbox) checkbox.checked = enabled;
resolve(true);
} else {
addSelfTestLog(`Failed to set modem_config_mode: ${response.error || 'Unknown error'}`);
reject(new Error(response.error || 'Failed to set config mode'));
}
},
error: function(xhr, status, error) {
addSelfTestLog(`AJAX error setting config mode: ${error}`);
reject(new Error(error));
}
});
});
}
function sendATCommand(command, timeout) {
return new Promise((resolve, reject) => {
addSelfTestLog(`Sending AT command: ${command} (timeout: ${timeout}s)`);
$.ajax({
url: `launcher.php?type=sara&port=ttyAMA2&command=${encodeURIComponent(command)}&timeout=${timeout}`,
dataType: 'text',
method: 'GET',
success: function(response) {
// Store raw response in report
selfTestReport.rawResponses[command] = response;
// Log raw response
addSelfTestLog(response.trim(), true);
resolve(response);
},
error: function(xhr, status, error) {
addSelfTestLog(`AT command error: ${error}`);
selfTestReport.rawResponses[command] = `ERROR: ${error}`;
reject(new Error(error));
}
});
});
}
function delaySelfTest(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
async function selfTestSequence() {
let testsPassed = 0;
let testsFailed = 0;
try {
// Collect system info at the start
document.getElementById('selftest_status').innerHTML = `
<div class="d-flex align-items-center text-primary">
<div class="spinner-border spinner-border-sm me-2" role="status"></div>
<span>Collecting system information...</span>
</div>`;
// Get system info from config
try {
const configResponse = await new Promise((resolve, reject) => {
$.ajax({
url: 'launcher.php?type=get_config_sqlite',
dataType: 'json',
method: 'GET',
success: function(data) { resolve(data); },
error: function(xhr, status, error) { reject(new Error(error)); }
});
});
// Store in report
selfTestReport.deviceId = configResponse.deviceID || 'Unknown';
selfTestReport.deviceName = configResponse.deviceName || 'Unknown';
selfTestReport.modemVersion = configResponse.modem_version || 'Unknown';
selfTestReport.latitude = configResponse.latitude_raw || 'N/A';
selfTestReport.longitude = configResponse.longitude_raw || 'N/A';
selfTestReport.config = configResponse;
// Get RTC time
try {
const rtcTime = await new Promise((resolve, reject) => {
$.ajax({
url: 'launcher.php?type=RTC_time',
dataType: 'text',
method: 'GET',
success: function(data) { resolve(data.trim()); },
error: function(xhr, status, error) { resolve('N/A'); }
});
});
selfTestReport.systemTime = rtcTime;
} catch (e) {
selfTestReport.systemTime = 'N/A';
}
// Log system info
addSelfTestLog('════════════════════════════════════════════════════════');
addSelfTestLog(' NEBULEAIR PRO 4G - SELF TEST');
addSelfTestLog('════════════════════════════════════════════════════════');
addSelfTestLog(`Device ID: ${selfTestReport.deviceId}`);
addSelfTestLog(`Device Name: ${selfTestReport.deviceName}`);
addSelfTestLog(`Modem Version: ${selfTestReport.modemVersion}`);
addSelfTestLog(`System Time (RTC): ${selfTestReport.systemTime}`);
addSelfTestLog(`Browser Time: ${new Date().toLocaleString()}`);
addSelfTestLog(`GPS: ${selfTestReport.latitude}, ${selfTestReport.longitude}`);
addSelfTestLog('────────────────────────────────────────────────────────');
addSelfTestLog('');
} catch (error) {
addSelfTestLog(`Warning: Could not get system config: ${error.message}`);
}
await delaySelfTest(300);
// ═══════════════════════════════════════════════════════
// SENSOR TESTS - Test enabled sensors based on config
// ═══════════════════════════════════════════════════════
const config = selfTestReport.config || {};
const sensorTests = [];
// NPM is always present
sensorTests.push({ id: 'npm', name: 'NextPM (Particles)', type: 'npm', port: 'ttyAMA5' });
// BME280 if enabled
if (config.BME280) {
sensorTests.push({ id: 'bme280', name: 'BME280 (Temp/Hum)', type: 'BME280' });
}
// Noise if enabled
if (config.NOISE) {
sensorTests.push({ id: 'noise', name: 'Noise Sensor', type: 'noise' });
}
// Envea if enabled
if (config.envea) {
sensorTests.push({ id: 'envea', name: 'Envea (Gas Sensors)', type: 'envea' });
}
// RTC module is always present (DS3231)
sensorTests.push({ id: 'rtc', name: 'RTC Module (DS3231)', type: 'rtc' });
// Create sensor test UI entries dynamically
const sensorContainer = document.getElementById('sensor_tests_container');
sensorContainer.innerHTML = '';
sensorTests.forEach(sensor => {
sensorContainer.innerHTML += `
<div class="list-group-item d-flex justify-content-between align-items-center" id="test_${sensor.id}">
<div>
<strong>${sensor.name}</strong>
<div class="small text-muted" id="test_${sensor.id}_detail">Waiting...</div>
</div>
<span id="test_${sensor.id}_status" class="badge bg-secondary">Pending</span>
</div>`;
});
addSelfTestLog('');
addSelfTestLog('────────────────────────────────────────────────────────');
addSelfTestLog('SENSOR TESTS');
addSelfTestLog('────────────────────────────────────────────────────────');
// Run each sensor test
for (const sensor of sensorTests) {
await delaySelfTest(500);
document.getElementById('selftest_status').innerHTML = `
<div class="d-flex align-items-center text-primary">
<div class="spinner-border spinner-border-sm me-2" role="status"></div>
<span>Testing ${sensor.name}...</span>
</div>`;
updateTestStatus(sensor.id, 'Testing...', 'Reading sensor data...', 'bg-info');
addSelfTestLog(`Testing ${sensor.name}...`);
try {
if (sensor.type === 'npm') {
// NPM sensor test
const npmResult = await new Promise((resolve, reject) => {
$.ajax({
url: 'launcher.php?type=npm&port=' + sensor.port,
dataType: 'json',
method: 'GET',
timeout: 15000,
success: function(data) { resolve(data); },
error: function(xhr, status, error) { reject(new Error(error || status)); }
});
});
selfTestReport.rawResponses['NPM Sensor'] = JSON.stringify(npmResult, null, 2);
addSelfTestLog(`NPM response: PM1=${npmResult.PM1}, PM2.5=${npmResult.PM25}, PM10=${npmResult.PM10}`);
// Check for errors
const npmErrors = ['notReady', 'fanError', 'laserError', 'heatError', 't_rhError', 'memoryError', 'degradedState'];
const activeErrors = npmErrors.filter(e => npmResult[e] === 1);
if (activeErrors.length > 0) {
updateTestStatus(sensor.id, 'Warning', `Errors: ${activeErrors.join(', ')}`, 'bg-warning');
testsFailed++;
} else if (npmResult.PM1 !== undefined && npmResult.PM25 !== undefined && npmResult.PM10 !== undefined) {
updateTestStatus(sensor.id, 'Passed', `PM1: ${npmResult.PM1} | PM2.5: ${npmResult.PM25} | PM10: ${npmResult.PM10} ug/m3`, 'bg-success');
testsPassed++;
} else {
updateTestStatus(sensor.id, 'Warning', 'Incomplete data received', 'bg-warning');
testsFailed++;
}
} else if (sensor.type === 'BME280') {
// BME280 sensor test
const bme280Result = await new Promise((resolve, reject) => {
$.ajax({
url: 'launcher.php?type=BME280',
dataType: 'text',
method: 'GET',
timeout: 15000,
success: function(data) { resolve(data); },
error: function(xhr, status, error) { reject(new Error(error || status)); }
});
});
const bmeData = JSON.parse(bme280Result);
selfTestReport.rawResponses['BME280 Sensor'] = JSON.stringify(bmeData, null, 2);
addSelfTestLog(`BME280 response: temp=${bmeData.temp}, hum=${bmeData.hum}, press=${bmeData.press}`);
if (bmeData.temp !== undefined && bmeData.hum !== undefined && bmeData.press !== undefined) {
updateTestStatus(sensor.id, 'Passed', `${bmeData.temp}°C | ${bmeData.hum}% | ${bmeData.press} hPa`, 'bg-success');
testsPassed++;
} else {
updateTestStatus(sensor.id, 'Warning', 'Incomplete data received', 'bg-warning');
testsFailed++;
}
} else if (sensor.type === 'noise') {
// NSRT MK4 noise sensor test (returns JSON)
const noiseResult = await new Promise((resolve, reject) => {
$.ajax({
url: 'launcher.php?type=noise',
dataType: 'json',
method: 'GET',
timeout: 15000,
success: function(data) { resolve(data); },
error: function(xhr, status, error) { reject(new Error(error || status)); }
});
});
selfTestReport.rawResponses['Noise Sensor'] = JSON.stringify(noiseResult);
addSelfTestLog(`Noise response: ${JSON.stringify(noiseResult)}`);
if (noiseResult.error) {
updateTestStatus(sensor.id, 'Failed', noiseResult.error, 'bg-danger');
testsFailed++;
} else if (noiseResult.LEQ > 0 && noiseResult.dBA > 0) {
updateTestStatus(sensor.id, 'Passed', `LEQ: ${noiseResult.LEQ} dB | dB(A): ${noiseResult.dBA}`, 'bg-success');
testsPassed++;
} else {
updateTestStatus(sensor.id, 'Warning', `Unexpected values: LEQ=${noiseResult.LEQ}, dBA=${noiseResult.dBA}`, 'bg-warning');
testsFailed++;
}
} else if (sensor.type === 'envea') {
// Envea sensor test - use the debug endpoint for all sensors
const enveaResult = await new Promise((resolve, reject) => {
$.ajax({
url: 'launcher.php?type=envea_debug',
dataType: 'text',
method: 'GET',
timeout: 30000,
success: function(data) { resolve(data); },
error: function(xhr, status, error) { reject(new Error(error || status)); }
});
});
selfTestReport.rawResponses['Envea Sensors'] = enveaResult;
addSelfTestLog(`Envea response: ${enveaResult.trim().substring(0, 200)}`);
if (enveaResult.trim() !== '' && !enveaResult.toLowerCase().includes('error')) {
updateTestStatus(sensor.id, 'Passed', 'Sensors responding', 'bg-success');
testsPassed++;
} else if (enveaResult.toLowerCase().includes('error')) {
updateTestStatus(sensor.id, 'Failed', 'Sensor error detected', 'bg-danger');
testsFailed++;
} else {
updateTestStatus(sensor.id, 'Failed', 'No data received', 'bg-danger');
testsFailed++;
}
} else if (sensor.type === 'rtc') {
// RTC DS3231 module test
const rtcResult = await new Promise((resolve, reject) => {
$.ajax({
url: 'launcher.php?type=sys_RTC_module_time',
dataType: 'json',
method: 'GET',
timeout: 10000,
success: function(data) { resolve(data); },
error: function(xhr, status, error) { reject(new Error(error || status)); }
});
});
selfTestReport.rawResponses['RTC Module'] = JSON.stringify(rtcResult, null, 2);
addSelfTestLog(`RTC response: ${JSON.stringify(rtcResult)}`);
if (rtcResult.rtc_module_time === 'not connected') {
updateTestStatus(sensor.id, 'Failed', 'RTC module not connected', 'bg-danger');
testsFailed++;
} else if (rtcResult.rtc_module_time) {
const timeDiff = rtcResult.time_difference_seconds;
if (typeof timeDiff === 'number' && Math.abs(timeDiff) <= 60) {
updateTestStatus(sensor.id, 'Passed', `${rtcResult.rtc_module_time} (sync OK, diff: ${timeDiff}s)`, 'bg-success');
testsPassed++;
} else if (typeof timeDiff === 'number') {
updateTestStatus(sensor.id, 'Warning', `${rtcResult.rtc_module_time} (out of sync: ${timeDiff}s)`, 'bg-warning');
testsFailed++;
} else {
updateTestStatus(sensor.id, 'Passed', `${rtcResult.rtc_module_time}`, 'bg-success');
testsPassed++;
}
} else {
updateTestStatus(sensor.id, 'Warning', 'Unexpected response', 'bg-warning');
testsFailed++;
}
}
} catch (error) {
addSelfTestLog(`${sensor.name} test error: ${error.message}`);
updateTestStatus(sensor.id, 'Failed', error.message, 'bg-danger');
selfTestReport.rawResponses[`${sensor.name}`] = `ERROR: ${error.message}`;
testsFailed++;
}
}
// ═══════════════════════════════════════════════════════
// COMMUNICATION TESTS - WiFi, Modem, SIM, Signal, Network
// ═══════════════════════════════════════════════════════
addSelfTestLog('');
addSelfTestLog('────────────────────────────────────────────────────────');
addSelfTestLog('COMMUNICATION TESTS');
addSelfTestLog('────────────────────────────────────────────────────────');
document.getElementById('comm_tests_separator').style.display = '';
// Check WiFi / Network status (informational, no pass/fail)
document.getElementById('selftest_status').innerHTML = `
<div class="d-flex align-items-center text-primary">
<div class="spinner-border spinner-border-sm me-2" role="status"></div>
<span>Checking network status...</span>
</div>`;
updateTestStatus('wifi', 'Checking...', 'Getting network info...', 'bg-info');
try {
const wifiResponse = await new Promise((resolve, reject) => {
$.ajax({
url: 'launcher.php?type=wifi_status',
dataType: 'json',
method: 'GET',
success: function(data) {
addSelfTestLog(`WiFi status received`);
// Store raw response
selfTestReport.rawResponses['WiFi Status'] = JSON.stringify(data, null, 2);
resolve(data);
},
error: function(xhr, status, error) {
addSelfTestLog(`WiFi status error: ${error}`);
selfTestReport.rawResponses['WiFi Status'] = `ERROR: ${error}`;
reject(new Error(error));
}
});
});
// Log detailed WiFi info
addSelfTestLog(`Mode: ${wifiResponse.mode}, SSID: ${wifiResponse.ssid}, IP: ${wifiResponse.ip}, Hostname: ${wifiResponse.hostname}`);
if (wifiResponse.connected) {
let modeLabel = '';
let badgeClass = 'bg-info';
if (wifiResponse.mode === 'hotspot') {
modeLabel = 'Hotspot';
badgeClass = 'bg-warning text-dark';
} else if (wifiResponse.mode === 'wifi') {
modeLabel = 'WiFi';
badgeClass = 'bg-info';
} else if (wifiResponse.mode === 'ethernet') {
modeLabel = 'Ethernet';
badgeClass = 'bg-info';
}
const detailText = `${wifiResponse.ssid} | ${wifiResponse.ip} | ${wifiResponse.hostname}.local`;
updateTestStatus('wifi', modeLabel, detailText, badgeClass);
} else {
updateTestStatus('wifi', 'Disconnected', 'No network connection', 'bg-secondary');
}
} catch (error) {
updateTestStatus('wifi', 'Error', error.message, 'bg-secondary');
}
await delaySelfTest(500);
// Enable config mode
document.getElementById('selftest_status').innerHTML = `
<div class="d-flex align-items-center text-primary">
<div class="spinner-border spinner-border-sm me-2" role="status"></div>
<span>Enabling configuration mode...</span>
</div>`;
await setConfigMode(true);
// Wait for SARA script to release the port (2 seconds should be enough)
addSelfTestLog('Waiting for modem port to be available...');
await delaySelfTest(2000);
// Test Modem Connection (ATI)
document.getElementById('selftest_status').innerHTML = `
<div class="d-flex align-items-center text-primary">
<div class="spinner-border spinner-border-sm me-2" role="status"></div>
<span>Testing modem connection...</span>
</div>`;
updateTestStatus('modem', 'Testing...', 'Sending ATI command...', 'bg-info');
try {
const modemResponse = await sendATCommand('ATI', 5);
if (modemResponse.includes('OK') && (modemResponse.toUpperCase().includes('SARA-R5') || modemResponse.toUpperCase().includes('SARA-R4'))) {
// Extract model
const modelMatch = modemResponse.match(/SARA-R[45]\d*[A-Z]*-\d+[A-Z]*-\d+/i);
const model = modelMatch ? modelMatch[0] : 'SARA module';
updateTestStatus('modem', 'Passed', `Model: ${model}`, 'bg-success');
testsPassed++;
} else if (modemResponse.includes('OK')) {
updateTestStatus('modem', 'Passed', 'Modem responding', 'bg-success');
testsPassed++;
} else {
updateTestStatus('modem', 'Failed', 'No valid response', 'bg-danger');
testsFailed++;
}
} catch (error) {
updateTestStatus('modem', 'Failed', error.message, 'bg-danger');
testsFailed++;
}
// Delay between AT commands
await delaySelfTest(1000);
// Test SIM Card (AT+CCID?)
document.getElementById('selftest_status').innerHTML = `
<div class="d-flex align-items-center text-primary">
<div class="spinner-border spinner-border-sm me-2" role="status"></div>
<span>Testing SIM card...</span>
</div>`;
updateTestStatus('sim', 'Testing...', 'Sending AT+CCID? command...', 'bg-info');
try {
const simResponse = await sendATCommand('AT+CCID?', 5);
const ccidMatch = simResponse.match(/\+CCID:\s*(\d{18,22})/);
if (simResponse.includes('OK') && ccidMatch) {
const iccid = ccidMatch[1];
// Show last 4 digits only for privacy
const maskedIccid = '****' + iccid.slice(-4);
updateTestStatus('sim', 'Passed', `ICCID: ...${maskedIccid}`, 'bg-success');
testsPassed++;
} else if (simResponse.includes('ERROR')) {
updateTestStatus('sim', 'Failed', 'SIM card not detected', 'bg-danger');
testsFailed++;
} else {
updateTestStatus('sim', 'Warning', 'Unable to read ICCID', 'bg-warning');
testsFailed++;
}
} catch (error) {
updateTestStatus('sim', 'Failed', error.message, 'bg-danger');
testsFailed++;
}
// Delay between AT commands
await delaySelfTest(1000);
// Test Signal Strength (AT+CSQ)
document.getElementById('selftest_status').innerHTML = `
<div class="d-flex align-items-center text-primary">
<div class="spinner-border spinner-border-sm me-2" role="status"></div>
<span>Testing signal strength...</span>
</div>`;
updateTestStatus('signal', 'Testing...', 'Sending AT+CSQ command...', 'bg-info');
try {
const signalResponse = await sendATCommand('AT+CSQ', 5);
const csqMatch = signalResponse.match(/\+CSQ:\s*(\d+),(\d+)/);
if (signalResponse.includes('OK') && csqMatch) {
const signalPower = parseInt(csqMatch[1]);
if (signalPower === 99) {
updateTestStatus('signal', 'Failed', 'No signal detected', 'bg-danger');
testsFailed++;
} else if (signalPower === 0) {
updateTestStatus('signal', 'Warning', 'Very poor signal (0/31)', 'bg-warning');
testsFailed++;
} else if (signalPower <= 24) {
updateTestStatus('signal', 'Passed', `Poor signal (${signalPower}/31)`, 'bg-success');
testsPassed++;
} else if (signalPower <= 26) {
updateTestStatus('signal', 'Passed', `Good signal (${signalPower}/31)`, 'bg-success');
testsPassed++;
} else if (signalPower <= 28) {
updateTestStatus('signal', 'Passed', `Very good signal (${signalPower}/31)`, 'bg-success');
testsPassed++;
} else {
updateTestStatus('signal', 'Passed', `Excellent signal (${signalPower}/31)`, 'bg-success');
testsPassed++;
}
} else if (signalResponse.includes('ERROR')) {
updateTestStatus('signal', 'Failed', 'Unable to read signal', 'bg-danger');
testsFailed++;
} else {
updateTestStatus('signal', 'Warning', 'Unexpected response', 'bg-warning');
testsFailed++;
}
} catch (error) {
updateTestStatus('signal', 'Failed', error.message, 'bg-danger');
testsFailed++;
}
// Delay between AT commands
await delaySelfTest(1000);
// Test Network Connection (AT+COPS?)
document.getElementById('selftest_status').innerHTML = `
<div class="d-flex align-items-center text-primary">
<div class="spinner-border spinner-border-sm me-2" role="status"></div>
<span>Testing network connection...</span>
</div>`;
updateTestStatus('network', 'Testing...', 'Sending AT+COPS? command...', 'bg-info');
try {
// Load operators data for network name lookup
let opData = null;
try {
opData = await loadOperatorsDataSelfTest();
} catch (e) {
addSelfTestLog('Warning: Could not load operators data');
}
const networkResponse = await sendATCommand('AT+COPS?', 5);
const copsMatch = networkResponse.match(/\+COPS:\s*(\d+)(?:,(\d+),"?([^",]+)"?,(\d+))?/);
if (networkResponse.includes('OK') && copsMatch) {
const mode = copsMatch[1];
const oper = copsMatch[3];
const act = copsMatch[4];
if (oper) {
// Get operator name from lookup table
let operatorName = oper;
if (opData && opData.operators && opData.operators[oper]) {
operatorName = opData.operators[oper].name;
}
// Get access technology
let actDesc = 'Unknown';
if (opData && opData.accessTechnology && opData.accessTechnology[act]) {
actDesc = opData.accessTechnology[act];
}
updateTestStatus('network', 'Passed', `${operatorName} (${actDesc})`, 'bg-success');
testsPassed++;
} else {
updateTestStatus('network', 'Warning', 'Not registered to network', 'bg-warning');
testsFailed++;
}
} else if (networkResponse.includes('ERROR')) {
updateTestStatus('network', 'Failed', 'Unable to get network info', 'bg-danger');
testsFailed++;
} else {
updateTestStatus('network', 'Warning', 'Unexpected response', 'bg-warning');
testsFailed++;
}
} catch (error) {
updateTestStatus('network', 'Failed', error.message, 'bg-danger');
testsFailed++;
}
} catch (error) {
addSelfTestLog(`Test sequence error: ${error.message}`);
} finally {
// Always disable config mode at the end
document.getElementById('selftest_status').innerHTML = `
<div class="d-flex align-items-center text-primary">
<div class="spinner-border spinner-border-sm me-2" role="status"></div>
<span>Disabling configuration mode...</span>
</div>`;
try {
await delaySelfTest(500);
await setConfigMode(false);
} catch (error) {
addSelfTestLog(`Warning: Failed to disable config mode: ${error.message}`);
}
// Show final status
const totalTests = testsPassed + testsFailed;
let statusClass, statusIcon, statusText;
if (testsFailed === 0) {
statusClass = 'text-success';
statusIcon = '✓';
statusText = 'All tests passed';
} else if (testsPassed === 0) {
statusClass = 'text-danger';
statusIcon = '✗';
statusText = 'All tests failed';
} else {
statusClass = 'text-warning';
statusIcon = '!';
statusText = 'Some tests failed';
}
document.getElementById('selftest_status').innerHTML = `
<div class="d-flex align-items-center ${statusClass}">
<span class="fs-4 me-2">${statusIcon}</span>
<span><strong>${statusText}</strong></span>
</div>`;
document.getElementById('selftest_summary').innerHTML = `
<span class="badge bg-success me-1">${testsPassed} passed</span>
<span class="badge bg-danger">${testsFailed} failed</span>`;
// Store summary in report
selfTestReport.summary = {
passed: testsPassed,
failed: testsFailed,
status: statusText
};
// Enable buttons
document.getElementById('selfTestCloseBtn').disabled = false;
document.getElementById('selfTestDoneBtn').disabled = false;
document.getElementById('selfTestCopyBtn').disabled = false;
document.querySelectorAll('.btn_selfTest').forEach(btn => btn.disabled = false);
addSelfTestLog('Self test completed.');
addSelfTestLog('Click "Copy Report" to share results with support.');
}
}
function generateReport() {
// Build formatted report
let report = `===============================================================
NEBULEAIR PRO 4G - SELF TEST REPORT
===============================================================
DEVICE INFORMATION
------------------
Device ID: ${selfTestReport.deviceId || 'Unknown'}
Device Name: ${selfTestReport.deviceName || 'Unknown'}
Modem Version: ${selfTestReport.modemVersion || 'Unknown'}
System Time: ${selfTestReport.systemTime || 'Unknown'}
Report Date: ${selfTestReport.timestamp}
GPS Location: ${selfTestReport.latitude || 'N/A'}, ${selfTestReport.longitude || 'N/A'}
===============================================================
TEST RESULTS
===============================================================
`;
// Add test results (sensors first, then communication)
const testNames = {
npm: 'NextPM (Particles)',
bme280: 'BME280 (Temp/Hum)',
noise: 'Noise Sensor',
envea: 'Envea (Gas Sensors)',
rtc: 'RTC Module (DS3231)',
wifi: 'WiFi/Network',
modem: 'Modem Connection',
sim: 'SIM Card',
signal: 'Signal Strength',
network: 'Network Connection'
};
for (const [testId, name] of Object.entries(testNames)) {
if (selfTestReport.results[testId]) {
const result = selfTestReport.results[testId];
const statusIcon = result.status === 'Passed' ? '[OK]' :
result.status === 'Failed' ? '[FAIL]' :
result.status.includes('Hotspot') || result.status.includes('WiFi') || result.status.includes('Ethernet') ? '[INFO]' : '[WARN]';
report += `${statusIcon} ${name}
Status: ${result.status}
Detail: ${result.detail}
`;
}
}
// Add summary
if (selfTestReport.summary) {
report += `===============================================================
SUMMARY
===============================================================
Passed: ${selfTestReport.summary.passed}
Failed: ${selfTestReport.summary.failed}
Status: ${selfTestReport.summary.status}
`;
}
// Add raw AT responses
report += `===============================================================
RAW AT RESPONSES
===============================================================
`;
for (const [command, response] of Object.entries(selfTestReport.rawResponses)) {
report += `--- ${command} ---
${response}
`;
}
// Add full logs
report += `===============================================================
DETAILED LOGS
===============================================================
${document.getElementById('selftest_logs').textContent}
===============================================================
END OF REPORT - Generated by NebuleAir Pro 4G
===============================================================
`;
return report;
}
function openShareReportModal() {
// Generate the report
const report = generateReport();
// Put report in textarea
document.getElementById('shareReportText').value = report;
// Open the share modal
const shareModal = new bootstrap.Modal(document.getElementById('shareReportModal'));
shareModal.show();
}
function selectAllReportText() {
const textarea = document.getElementById('shareReportText');
textarea.select();
textarea.setSelectionRange(0, textarea.value.length); // For mobile devices
}
function downloadReport() {
const report = generateReport();
// Create filename with device ID
const deviceId = selfTestReport.deviceId || 'unknown';
const date = new Date().toISOString().slice(0, 10);
const filename = `logs_nebuleair_${deviceId}_${date}.txt`;
// Create blob and download
const blob = new Blob([report], { type: 'text/plain;charset=utf-8' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = filename;
document.body.appendChild(a);
a.click();
// Cleanup
setTimeout(function() {
document.body.removeChild(a);
URL.revokeObjectURL(url);
}, 100);
}
// Load the self-test modal HTML into the page
function initSelfTestModal() {
fetch('selftest-modal.html')
.then(response => response.text())
.then(html => {
// Insert modal HTML before </body>
const container = document.createElement('div');
container.id = 'selftest-modal-container';
container.innerHTML = html;
document.body.appendChild(container);
})
.catch(error => console.error('Error loading selftest modal:', error));
}
// Auto-init when DOM is ready
document.addEventListener('DOMContentLoaded', initSelfTestModal);

View File

@@ -63,6 +63,14 @@
<h1 class="mt-4" data-i18n="home.title">Votre capteur</h1> <h1 class="mt-4" data-i18n="home.title">Votre capteur</h1>
<p data-i18n="home.welcome">Bienvenue sur votre interface de configuration de votre capteur.</p> <p data-i18n="home.welcome">Bienvenue sur votre interface de configuration de votre capteur.</p>
<button class="btn btn-success mb-3 btn_selfTest" onclick="runSelfTest()">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-check2-circle me-1" viewBox="0 0 16 16">
<path d="M2.5 8a5.5 5.5 0 1 1 11 0 5.5 5.5 0 0 1-11 0z"/>
<path d="M15.354 3.354a.5.5 0 0 0-.708-.708L8 9.293 5.354 6.646a.5.5 0 1 0-.708.708l3 3a.5.5 0 0 0 .708 0l7-7z"/>
</svg>
Run Self Test
</button>
<div class="row mb-3"> <div class="row mb-3">
<!-- Card NPM values --> <!-- Card NPM values -->
@@ -124,6 +132,7 @@
<!-- i18n translation system --> <!-- i18n translation system -->
<script src="assets/js/i18n.js"></script> <script src="assets/js/i18n.js"></script>
<script src="assets/js/topbar-logo.js"></script> <script src="assets/js/topbar-logo.js"></script>
<script src="assets/js/selftest.js"></script>
<script> <script>
document.addEventListener('DOMContentLoaded', function () { document.addEventListener('DOMContentLoaded', function () {

File diff suppressed because it is too large Load Diff

134
html/selftest-modal.html Normal file
View File

@@ -0,0 +1,134 @@
<!-- Self Test Modal -->
<div class="modal fade" id="selfTestModal" tabindex="-1" aria-labelledby="selfTestModalLabel" aria-hidden="true" data-bs-backdrop="static" data-bs-keyboard="false">
<div class="modal-dialog modal-dialog-centered">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="selfTestModalLabel">Modem Self Test</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close" id="selfTestCloseBtn" disabled></button>
</div>
<div class="modal-body">
<div id="selftest_status" class="mb-3">
<div class="d-flex align-items-center text-muted">
<div class="spinner-border spinner-border-sm me-2" role="status"></div>
<span>Preparing test...</span>
</div>
</div>
<div class="list-group" id="selftest_results">
<!-- Dynamic sensor test entries will be added here -->
<div id="sensor_tests_container"></div>
<!-- Separator for communication tests -->
<div id="comm_tests_separator" class="list-group-item bg-light text-center py-1" style="display:none;">
<small class="text-muted fw-bold">COMMUNICATION</small>
</div>
<!-- Info: WiFi Status -->
<div class="list-group-item d-flex justify-content-between align-items-center" id="test_wifi">
<div>
<strong>WiFi / Network</strong>
<div class="small text-muted" id="test_wifi_detail">Waiting...</div>
</div>
<span id="test_wifi_status" class="badge bg-secondary">Pending</span>
</div>
<!-- Test: Modem Connection -->
<div class="list-group-item d-flex justify-content-between align-items-center" id="test_modem">
<div>
<strong>Modem Connection</strong>
<div class="small text-muted" id="test_modem_detail">Waiting...</div>
</div>
<span id="test_modem_status" class="badge bg-secondary">Pending</span>
</div>
<!-- Test: SIM Card -->
<div class="list-group-item d-flex justify-content-between align-items-center" id="test_sim">
<div>
<strong>SIM Card</strong>
<div class="small text-muted" id="test_sim_detail">Waiting...</div>
</div>
<span id="test_sim_status" class="badge bg-secondary">Pending</span>
</div>
<!-- Test: Signal Strength -->
<div class="list-group-item d-flex justify-content-between align-items-center" id="test_signal">
<div>
<strong>Signal Strength</strong>
<div class="small text-muted" id="test_signal_detail">Waiting...</div>
</div>
<span id="test_signal_status" class="badge bg-secondary">Pending</span>
</div>
<!-- Test: Network Connection -->
<div class="list-group-item d-flex justify-content-between align-items-center" id="test_network">
<div>
<strong>Network Connection</strong>
<div class="small text-muted" id="test_network_detail">Waiting...</div>
</div>
<span id="test_network_status" class="badge bg-secondary">Pending</span>
</div>
</div>
<!-- Logs section -->
<div class="mt-3">
<button class="btn btn-sm btn-outline-secondary" type="button" data-bs-toggle="collapse" data-bs-target="#selftest_logs_collapse">
Show detailed logs
</button>
<div class="collapse mt-2" id="selftest_logs_collapse">
<div class="card card-body bg-dark text-light" style="max-height: 250px; overflow-y: auto; font-family: monospace; font-size: 0.75rem;">
<pre id="selftest_logs" style="margin: 0; white-space: pre-wrap; word-wrap: break-word;"></pre>
</div>
</div>
</div>
</div>
<div class="modal-footer">
<div id="selftest_summary" class="me-auto"></div>
<button type="button" class="btn btn-primary" id="selfTestCopyBtn" onclick="openShareReportModal()" disabled>
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-share me-1" viewBox="0 0 16 16">
<path d="M13.5 1a1.5 1.5 0 1 0 0 3 1.5 1.5 0 0 0 0-3zM11 2.5a2.5 2.5 0 1 1 .603 1.628l-6.718 3.12a2.499 2.499 0 0 1 0 1.504l6.718 3.12a2.5 2.5 0 1 1-.488.876l-6.718-3.12a2.5 2.5 0 1 1 0-3.256l6.718-3.12A2.5 2.5 0 0 1 11 2.5zm-8.5 4a1.5 1.5 0 1 0 0 3 1.5 1.5 0 0 0 0-3zm11 5.5a1.5 1.5 0 1 0 0 3 1.5 1.5 0 0 0 0-3z"/>
</svg>
Share Report
</button>
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal" id="selfTestDoneBtn" disabled>Close</button>
</div>
</div>
</div>
</div>
<!-- Share Report Modal -->
<div class="modal fade" id="shareReportModal" tabindex="-1" aria-labelledby="shareReportModalLabel" aria-hidden="true">
<div class="modal-dialog modal-lg modal-dialog-centered">
<div class="modal-content">
<div class="modal-header bg-primary text-white">
<h5 class="modal-title" id="shareReportModalLabel">Share Diagnostic Report</h5>
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<div class="alert alert-info">
<strong>Need help?</strong> You can send this diagnostic report to our support team at
<a href="mailto:contact@aircarto.fr?subject=NebuleAir%20Diagnostic%20Report" class="alert-link">contact@aircarto.fr</a>
<br><small>Select all the text below (Ctrl+A) and copy it (Ctrl+C), or use the Download button.</small>
</div>
<div class="mb-3">
<textarea id="shareReportText" class="form-control font-monospace" rows="15" readonly style="font-size: 0.75rem; background-color: #1e1e1e; color: #d4d4d4;"></textarea>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-success" onclick="downloadReport()">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-download me-1" viewBox="0 0 16 16">
<path d="M.5 9.9a.5.5 0 0 1 .5.5v2.5a1 1 0 0 0 1 1h12a1 1 0 0 0 1-1v-2.5a.5.5 0 0 1 1 0v2.5a2 2 0 0 1-2 2H2a2 2 0 0 1-2-2v-2.5a.5.5 0 0 1 .5-.5z"/>
<path d="M7.646 11.854a.5.5 0 0 0 .708 0l3-3a.5.5 0 0 0-.708-.708L8.5 10.293V1.5a.5.5 0 0 0-1 0v8.793L5.354 8.146a.5.5 0 1 0-.708.708l3 3z"/>
</svg>
Download (.txt)
</button>
<button type="button" class="btn btn-outline-primary" onclick="selectAllReportText()">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-cursor-text me-1" viewBox="0 0 16 16">
<path d="M5 2a.5.5 0 0 1 .5-.5c.862 0 1.573.287 2.06.566.174.099.321.198.44.286.119-.088.266-.187.44-.286A4.165 4.165 0 0 1 10.5 1.5a.5.5 0 0 1 0 1c-.638 0-1.177.213-1.564.434a3.49 3.49 0 0 0-.436.294V7.5H9a.5.5 0 0 1 0 1h-.5v4.272c.1.08.248.187.436.294.387.221.926.434 1.564.434a.5.5 0 0 1 0 1 4.165 4.165 0 0 1-2.06-.566A4.561 4.561 0 0 1 8 13.65a4.561 4.561 0 0 1-.44.285 4.165 4.165 0 0 1-2.06.566.5.5 0 0 1 0-1c.638 0 1.177-.213 1.564-.434.188-.107.335-.214.436-.294V8.5H7a.5.5 0 0 1 0-1h.5V3.228a3.49 3.49 0 0 0-.436-.294A3.166 3.166 0 0 0 5.5 2.5.5.5 0 0 1 5 2z"/>
</svg>
Select All
</button>
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
</div>
</div>
</div>
</div>

View File

@@ -63,6 +63,14 @@
de mesurer certaines variables environnementales. La mesure de mesurer certaines variables environnementales. La mesure
est automatique mais vous pouvez ici vous assurer de leur bon fonctionnement. est automatique mais vous pouvez ici vous assurer de leur bon fonctionnement.
</p> </p>
<button class="btn btn-success mb-3 btn_selfTest" onclick="runSelfTest()">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-check2-circle me-1" viewBox="0 0 16 16">
<path d="M2.5 8a5.5 5.5 0 1 1 11 0 5.5 5.5 0 0 1-11 0z"/>
<path d="M15.354 3.354a.5.5 0 0 0-.708-.708L8 9.293 5.354 6.646a.5.5 0 1 0-.708.708l3 3a.5.5 0 0 0 .708 0l7-7z"/>
</svg>
Run Self Test
</button>
<div class="row mb-3" id="card-container"></div> <div class="row mb-3" id="card-container"></div>
</main> </main>
</div> </div>
@@ -77,6 +85,7 @@
<!-- i18n translation system --> <!-- i18n translation system -->
<script src="assets/js/i18n.js"></script> <script src="assets/js/i18n.js"></script>
<script src="assets/js/topbar-logo.js"></script> <script src="assets/js/topbar-logo.js"></script>
<script src="assets/js/selftest.js"></script>
<script> <script>
document.addEventListener('DOMContentLoaded', function () { document.addEventListener('DOMContentLoaded', function () {