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:
311
html/saraR4.html
311
html/saraR4.html
@@ -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¶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
|
||||||
|
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>
|
||||||
|
|||||||
Reference in New Issue
Block a user