Adapte le self-test au nouveau format retourne par get_data_modbus_v3.py (npm_status numerique decode bit par bit au lieu de notReady/fanError/etc.) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
960 lines
36 KiB
JavaScript
960 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(`RTC Time: ${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 (uses get_data_modbus_v3.py --dry-run)
|
|
const npmResult = await new Promise((resolve, reject) => {
|
|
$.ajax({
|
|
url: 'launcher.php?type=npm',
|
|
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}, status=${npmResult.npm_status_hex}`);
|
|
|
|
// Decode npm_status flags
|
|
const status = npmResult.npm_status || 0;
|
|
const statusFlags = {
|
|
0x01: "Sleep mode",
|
|
0x02: "Degraded mode",
|
|
0x04: "Not ready",
|
|
0x08: "Heater error",
|
|
0x10: "THP sensor error",
|
|
0x20: "Fan error",
|
|
0x40: "Memory error",
|
|
0x80: "Laser error"
|
|
};
|
|
const activeErrors = [];
|
|
Object.entries(statusFlags).forEach(([mask, label]) => {
|
|
if (status & mask) activeErrors.push(label);
|
|
});
|
|
|
|
if (activeErrors.length > 0) {
|
|
updateTestStatus(sensor.id, 'Warning', `Status ${npmResult.npm_status_hex}: ${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} µg/m³`, '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) {
|
|
// Compare RTC with browser time (more reliable than system time)
|
|
const rtcDate = new Date(rtcResult.rtc_module_time + ' UTC');
|
|
const browserDate = new Date();
|
|
const timeDiff = Math.abs(Math.round((browserDate - rtcDate) / 1000));
|
|
|
|
if (timeDiff <= 60) {
|
|
updateTestStatus(sensor.id, 'Passed', `${rtcResult.rtc_module_time} (sync OK vs navigateur, ecart: ${timeDiff}s)`, 'bg-success');
|
|
testsPassed++;
|
|
} else {
|
|
const minutes = Math.floor(timeDiff / 60);
|
|
const label = minutes > 0 ? `${minutes}min ${timeDiff % 60}s` : `${timeDiff}s`;
|
|
updateTestStatus(sensor.id, 'Warning', `${rtcResult.rtc_module_time} (desync vs navigateur: ${label})`, 'bg-warning');
|
|
testsFailed++;
|
|
}
|
|
} 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);
|