feat(ui): reorder self-test to run sensor tests before communication

Sensor tests (NPM, BME280, Noise, Envea) now run first, followed by
communication tests (WiFi, Modem, SIM, Signal, Network) with a
visual separator between the two sections.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
PaulVua
2026-02-10 17:05:35 +01:00
parent 544eebd715
commit dc1739e033

View File

@@ -1774,256 +1774,6 @@ async function selfTestSequence() {
await delay(300); await delay(300);
// Step 0: 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 modeIcon = '';
let modeLabel = '';
let badgeClass = 'bg-info';
if (wifiResponse.mode === 'hotspot') {
modeIcon = '📡';
modeLabel = 'Hotspot';
badgeClass = 'bg-warning text-dark';
} else if (wifiResponse.mode === 'wifi') {
modeIcon = '📶';
modeLabel = 'WiFi';
badgeClass = 'bg-info';
} else if (wifiResponse.mode === 'ethernet') {
modeIcon = '🔌';
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 delay(500);
// Step 1: 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 delay(2000);
// Step 2: 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 delay(1000);
// Step 3: 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 delay(1000);
// Step 4: 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]);
const qual = parseInt(csqMatch[2]);
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 delay(1000);
// Step 5: 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 loadOperatorsData();
} 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++;
}
// ═══════════════════════════════════════════════════════ // ═══════════════════════════════════════════════════════
// SENSOR TESTS - Test enabled sensors based on config // SENSOR TESTS - Test enabled sensors based on config
// ═══════════════════════════════════════════════════════ // ═══════════════════════════════════════════════════════
@@ -2049,8 +1799,6 @@ async function selfTestSequence() {
} }
// Create sensor test UI entries dynamically // Create sensor test UI entries dynamically
if (sensorTests.length > 0) {
document.getElementById('sensor_tests_separator').style.display = '';
const sensorContainer = document.getElementById('sensor_tests_container'); const sensorContainer = document.getElementById('sensor_tests_container');
sensorContainer.innerHTML = ''; sensorContainer.innerHTML = '';
@@ -2064,7 +1812,6 @@ async function selfTestSequence() {
<span id="test_${sensor.id}_status" class="badge bg-secondary">Pending</span> <span id="test_${sensor.id}_status" class="badge bg-secondary">Pending</span>
</div>`; </div>`;
}); });
}
addSelfTestLog(''); addSelfTestLog('');
addSelfTestLog('────────────────────────────────────────────────────────'); addSelfTestLog('────────────────────────────────────────────────────────');
@@ -2204,6 +1951,266 @@ async function selfTestSequence() {
} }
} }
// ═══════════════════════════════════════════════════════
// 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 modeIcon = '';
let modeLabel = '';
let badgeClass = 'bg-info';
if (wifiResponse.mode === 'hotspot') {
modeIcon = '📡';
modeLabel = 'Hotspot';
badgeClass = 'bg-warning text-dark';
} else if (wifiResponse.mode === 'wifi') {
modeIcon = '📶';
modeLabel = 'WiFi';
badgeClass = 'bg-info';
} else if (wifiResponse.mode === 'ethernet') {
modeIcon = '🔌';
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 delay(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 delay(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 delay(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 delay(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]);
const qual = parseInt(csqMatch[2]);
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 delay(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 loadOperatorsData();
} 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) { } catch (error) {
addSelfTestLog(`Test sequence error: ${error.message}`); addSelfTestLog(`Test sequence error: ${error.message}`);
} finally { } finally {
@@ -2288,17 +2295,17 @@ GPS Location: ${selfTestReport.latitude || 'N/A'}, ${selfTestReport.longitude
`; `;
// Add test results // Add test results (sensors first, then communication)
const testNames = { const testNames = {
npm: 'NextPM (Particles)',
bme280: 'BME280 (Temp/Hum)',
noise: 'Noise Sensor',
envea: 'Envea (Gas Sensors)',
wifi: 'WiFi/Network', wifi: 'WiFi/Network',
modem: 'Modem Connection', modem: 'Modem Connection',
sim: 'SIM Card', sim: 'SIM Card',
signal: 'Signal Strength', signal: 'Signal Strength',
network: 'Network Connection', network: 'Network Connection'
npm: 'NextPM (Particles)',
bme280: 'BME280 (Temp/Hum)',
noise: 'Noise Sensor',
envea: 'Envea (Gas Sensors)'
}; };
for (const [testId, name] of Object.entries(testNames)) { for (const [testId, name] of Object.entries(testNames)) {