feat(ui): add modem self-test functionality

Add "Run Self Test" button that opens a modal and runs diagnostic tests:
- Automatically enables modem_config_mode before tests
- Test 1: Modem connection (ATI command)
- Test 2: SIM card detection (AT+CCID? command)
- Respects delays between AT commands
- Always disables modem_config_mode after tests (even on failure)
- Shows real-time progress and detailed logs

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
PaulVua
2026-02-10 10:28:59 +01:00
parent d3d72410c1
commit 49a4623d85

View File

@@ -58,6 +58,14 @@
<label class="form-check-label" for="check_modem_configMode">Mode configuration</label> <label class="form-check-label" for="check_modem_configMode">Mode configuration</label>
</div> </div>
<button class="btn btn-success mb-3" onclick="runSelfTest()" id="btn_selfTest">
<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>
<span id="modem_status_message"></span> <span id="modem_status_message"></span>
<!-- <!--
<h3> <h3>
@@ -350,6 +358,62 @@
</div> </div>
<!-- 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">
<!-- Test 1: 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 2: 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>
</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 logs
</button>
<div class="collapse mt-2" id="selftest_logs_collapse">
<div class="card card-body bg-light" style="max-height: 200px; overflow-y: auto;">
<small><code id="selftest_logs"></code></small>
</div>
</div>
</div>
</div>
<div class="modal-footer">
<div id="selftest_summary" class="me-auto"></div>
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal" id="selfTestDoneBtn" disabled>Close</button>
</div>
</div>
</div>
</div>
<!-- toast --> <!-- toast -->
<div class="toast-container position-fixed bottom-0 end-0 p-3"> <div class="toast-container position-fixed bottom-0 end-0 p-3">
@@ -1397,7 +1461,254 @@ function update_modem_configMode(param, checked){
}); });
} }
// ============================================
// SELF TEST FUNCTIONS
// ============================================
function runSelfTest() {
console.log("Starting Self Test...");
// Reset UI
resetSelfTestUI();
// Show modal
const modal = new bootstrap.Modal(document.getElementById('selfTestModal'));
modal.show();
// Disable close buttons during test
document.getElementById('selfTestCloseBtn').disabled = true;
document.getElementById('selfTestDoneBtn').disabled = true;
document.getElementById('btn_selfTest').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_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...';
// Reset logs
document.getElementById('selftest_logs').innerHTML = '';
// Reset summary
document.getElementById('selftest_summary').innerHTML = '';
}
function addSelfTestLog(message) {
const logsEl = document.getElementById('selftest_logs');
const timestamp = new Date().toLocaleTimeString();
logsEl.innerHTML += `[${timestamp}] ${message}<br>`;
// 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;
}
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
document.getElementById('check_modem_configMode').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}`);
$.ajax({
url: `launcher.php?type=sara&port=ttyAMA2&command=${encodeURIComponent(command)}&timeout=${timeout}`,
dataType: 'text',
method: 'GET',
success: function(response) {
addSelfTestLog(`Response: ${response.replace(/\n/g, ' | ')}`);
resolve(response);
},
error: function(xhr, status, error) {
addSelfTestLog(`AT command error: ${error}`);
reject(new Error(error));
}
});
});
}
function delay(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
async function selfTestSequence() {
let testsPassed = 0;
let testsFailed = 0;
try {
// 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++;
}
} 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 delay(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>`;
// Enable close buttons
document.getElementById('selfTestCloseBtn').disabled = false;
document.getElementById('selfTestDoneBtn').disabled = false;
document.getElementById('btn_selfTest').disabled = false;
addSelfTestLog('Self test completed.');
}
}
</script> </script>