// ============================================ // 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 = `
Preparing test...
`; // 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 = `
Collecting system information...
`; // 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 += `
${sensor.name}
Waiting...
Pending
`; }); addSelfTestLog(''); addSelfTestLog('────────────────────────────────────────────────────────'); addSelfTestLog('SENSOR TESTS'); addSelfTestLog('────────────────────────────────────────────────────────'); // Run each sensor test for (const sensor of sensorTests) { await delaySelfTest(500); document.getElementById('selftest_status').innerHTML = `
Testing ${sensor.name}...
`; 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 = `
Checking network status...
`; 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 = `
Enabling configuration mode...
`; 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 = `
Testing modem connection...
`; 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 = `
Testing SIM card...
`; 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 = `
Testing signal strength...
`; 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 = `
Testing network connection...
`; 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 = `
Disabling configuration mode...
`; 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 = `
${statusIcon} ${statusText}
`; document.getElementById('selftest_summary').innerHTML = ` ${testsPassed} passed ${testsFailed} failed`; // 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 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);