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>
944 lines
36 KiB
JavaScript
944 lines
36 KiB
JavaScript
// ============================================
|
|
// 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¶m=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);
|