Le script dans topbar.html ne s'exécutait pas car innerHTML ignore les balises <script>. Déplacé la logique dans un fichier JS séparé (topbar-logo.js) avec MutationObserver pour détecter l'insertion du topbar. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2416 lines
101 KiB
HTML
Executable File
2416 lines
101 KiB
HTML
Executable File
<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>NebuleAir</title>
|
|
<link rel="stylesheet" href="assets/css/bootstrap.min.css">
|
|
<style>
|
|
body {
|
|
overflow-x: hidden;
|
|
}
|
|
#sidebar a.nav-link {
|
|
position: relative;
|
|
display: flex;
|
|
align-items: center;
|
|
}
|
|
#sidebar a.nav-link:hover {
|
|
background-color: rgba(0, 0, 0, 0.5);
|
|
}
|
|
#sidebar a.nav-link svg {
|
|
margin-right: 8px; /* Add spacing between icons and text */
|
|
}
|
|
#sidebar {
|
|
transition: transform 0.3s ease-in-out;
|
|
}
|
|
.offcanvas-backdrop {
|
|
z-index: 1040;
|
|
}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<!-- Topbar -->
|
|
<span id="topbar"></span>
|
|
|
|
<!-- Sidebar Offcanvas for Mobile -->
|
|
<div class="offcanvas offcanvas-start text-white bg-dark" tabindex="-1" id="sidebarOffcanvas" aria-labelledby="sidebarOffcanvasLabel">
|
|
<div class="offcanvas-header">
|
|
<h5 class="offcanvas-title" id="sidebarOffcanvasLabel">NebuleAir</h5>
|
|
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="offcanvas" aria-label="Close"></button>
|
|
</div>
|
|
<div class="offcanvas-body" id="sidebar_mobile">
|
|
|
|
</div>
|
|
</div>
|
|
|
|
<div class="container-fluid mt-5">
|
|
<div class="row">
|
|
<aside class="col-md-2 col-lg-1 d-none d-md-block vh-100 position-fixed bg-dark text-white" id="sidebar">
|
|
</aside>
|
|
<!-- Main content -->
|
|
<main class="col-md-10 ms-sm-auto col-lg-11 offset-md-2 offset-lg-1 px-md-4">
|
|
<h1 class="mt-4">Modem 4G</h1>
|
|
<h4 id="modem_version"></h4>
|
|
<p>Votre capteur est équipé d'un modem 4G et d'une carte SIM afin d'envoyer les mesures sur internet.</p>
|
|
|
|
<div class="form-check form-switch mb-2">
|
|
<input class="form-check-input" type="checkbox" role="switch" id="check_modem_configMode" onchange="update_modem_configMode('modem_config_mode',this.checked)">
|
|
<label class="form-check-label" for="check_modem_configMode">Mode configuration</label>
|
|
</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>
|
|
<!--
|
|
<h3>
|
|
Status
|
|
<span id="modem-status" class="badge">Loading...</span>
|
|
</h3>
|
|
-->
|
|
<div class="row mb-3">
|
|
|
|
<div class="col-sm-3">
|
|
<div class="card">
|
|
|
|
<div class="card-body">
|
|
<p class="card-text">General information. </p>
|
|
<button class="btn btn-primary" onclick="getModemInfo('ttyAMA2', 1)">Get Data</button>
|
|
<div id="loading_ttyAMA2_ATI" class="spinner-border spinner-border-sm" style="display: none;" role="status"></div>
|
|
<div id="modem_info_alert"></div>
|
|
<div class="collapse mt-2" id="modem_info_logs">
|
|
<div class="card card-body bg-light">
|
|
<small><code id="response_ttyAMA2_ATI"></code></small>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="col-sm-3">
|
|
<div class="card">
|
|
|
|
<div class="card-body">
|
|
<p class="card-text">SIM card information.</p>
|
|
<button class="btn btn-primary" onclick="getSimInfo('ttyAMA2', 1)">Get Data</button>
|
|
<div id="loading_ttyAMA2_AT_CCID_" class="spinner-border spinner-border-sm" style="display: none;" role="status"></div>
|
|
<div id="sim_info_alert"></div>
|
|
<div class="collapse mt-2" id="sim_info_logs">
|
|
<div class="card card-body bg-light">
|
|
<small><code id="response_ttyAMA2_AT_CCID_"></code></small>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="col-sm-3">
|
|
<div class="card">
|
|
|
|
<div class="card-body">
|
|
<p class="card-text">Actual Network connection</p>
|
|
<button class="btn btn-primary" onclick="getNetworkInfo('ttyAMA2', 2)">Get Data</button>
|
|
<div id="loading_ttyAMA2_AT_COPS_" class="spinner-border spinner-border-sm" style="display: none;" role="status"></div>
|
|
<div id="network_info_alert"></div>
|
|
<div class="collapse mt-2" id="network_info_logs">
|
|
<div class="card card-body bg-light">
|
|
<small><code id="response_ttyAMA2_AT_COPS_"></code></small>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="col-sm-3">
|
|
<div class="card">
|
|
<div class="card-body">
|
|
<p class="card-text">Signal strength </p>
|
|
<button class="btn btn-primary" onclick="getSignalInfo('ttyAMA2', 1)">Get Data</button>
|
|
<div id="loading_ttyAMA2_AT_CSQ" class="spinner-border spinner-border-sm" style="display: none;" role="status"></div>
|
|
<div id="signal_info_alert"></div>
|
|
<div class="collapse mt-2" id="signal_info_logs">
|
|
<div class="card card-body bg-light">
|
|
<small><code id="response_ttyAMA2_AT_CSQ"></code></small>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="col-sm-2">
|
|
<div class="card">
|
|
<div class="card-body">
|
|
<p class="card-text">Modem Reset </p>
|
|
<button class="btn btn-danger" onclick="getData_saraR4('ttyAMA2', 'AT+CFUN=15', 1)">Reset</button>
|
|
<div id="loading_ttyAMA2_AT_CFUN_15" class="spinner-border spinner-border-sm" style="display: none;" role="status"></div>
|
|
<div id="response_ttyAMA2_AT_CFUN_15"></div>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
</div>
|
|
|
|
<h3>Connexion 4G Network</h3>
|
|
|
|
<div class="row mb-3">
|
|
|
|
<div class="col-sm-6">
|
|
<div class="card text-dark bg-light">
|
|
|
|
<div class="card-body">
|
|
<p class="card-text">Network scan. Attention: 2 min scan.</p>
|
|
<p class="card-text">Orange FR (20801), SFR (20810), Bouygues (20820)</p>
|
|
|
|
<button class="btn btn-primary" onclick="getData_saraR4('ttyAMA2', 'AT+COPS=?', 120)">Scan</button>
|
|
<div id="loading_ttyAMA2_AT_COPS__" class="spinner-border spinner-border-sm" style="display: none;" role="status"></div>
|
|
<div id="response_ttyAMA2_AT_COPS__"></div>
|
|
<div id="table-network"></div>
|
|
|
|
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="col-sm-3">
|
|
<div class="card text-dark bg-light">
|
|
|
|
<div class="card-body">
|
|
<p class="card-text">Network connexion.</p>
|
|
<div class="input-group input-group-sm mb-3">
|
|
<span class="input-group-text" id="inputGroup-sizing-sm">Numeric Operator</span>
|
|
<input type="text" id="messageInput_network" class="form-control" aria-label="Sizing example input" aria-describedby="inputGroup-sizing-sm">
|
|
</div>
|
|
<button class="btn btn-primary" onclick="connectNetwork_saraR4('ttyAMA2', document.getElementById('messageInput_network').value, 60)">Connect</button>
|
|
<div id="loading_ttyAMA2_AT_COPS_Connect" class="spinner-border spinner-border-sm" style="display: none;" role="status"></div>
|
|
<div id="response_ttyAMA2_AT_COPS_Connect"></div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="col-sm-3">
|
|
<div class="card text-dark bg-light">
|
|
<div class="card-body">
|
|
<p class="card-text">APN</p>
|
|
<div class="input-group input-group-sm mb-3">
|
|
<span class="input-group-text" id="inputGroup-sizing-sm">Address</span>
|
|
<input type="text" id="messageInput_APN" class="form-control" aria-label="Sizing example input" aria-describedby="inputGroup-sizing-sm">
|
|
</div>
|
|
<button class="btn btn-primary" onclick="connectAPN_saraR4('ttyAMA2', document.getElementById('messageInput_APN').value, 5)">Set APN</button>
|
|
|
|
<button class="btn btn-secondary" onclick="getData_saraR4('ttyAMA2','AT+CGDCONT?', 5)">Get APN</button>
|
|
|
|
<div id="loading_ttyAMA2_APN" class="spinner-border spinner-border-sm" style="display: none;" role="status"></div>
|
|
<div id="response_ttyAMA2_APN"></div>
|
|
|
|
<div id="loading_ttyAMA2_AT_CGDCONT_" class="spinner-border spinner-border-sm" style="display: none;" role="status"></div>
|
|
<div id="response_ttyAMA2_AT_CGDCONT_"></div>
|
|
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
|
|
</div>
|
|
<!--
|
|
<h3>MQTT</h3>
|
|
<div class="row mb-3">
|
|
<div class="col-sm-4">
|
|
<div class="card">
|
|
<div class="card-body">
|
|
<p class="card-text">Get config.</p>
|
|
<button class="btn btn-primary" onclick="mqtt_getConfig_saraR4('ttyAMA2', 2)">Get Data</button>
|
|
<div id="loading_mqtt_getConfig" class="spinner-border spinner-border-sm" style="display: none;" role="status"></div>
|
|
<div id="response_mqtt_getConfig"></div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
|
|
<div class="col-sm-4">
|
|
<div class="card">
|
|
<div class="card-body">
|
|
<p class="card-text">Login / logout</p>
|
|
|
|
<button class="btn btn-success" onclick="mqtt_login_logout('ttyAMA2', 1, 6)">Login </button>
|
|
<button class="btn btn-danger" onclick="mqtt_login_logout('ttyAMA2', 0, 6)">Logout </button>
|
|
<div id="loading_mqtt_login_logout" class="spinner-border spinner-border-sm" style="display: none;" role="status"></div>
|
|
<div id="response_mqtt_login_logout"></div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
|
|
<div class="col-sm-4">
|
|
<div class="card">
|
|
<div class="card-body">
|
|
<p class="card-text">Send message (MQTT publish) .</p>
|
|
<div class="input-group input-group-sm mb-3">
|
|
<span class="input-group-text" id="inputGroup-sizing-sm">Text</span>
|
|
<input type="text" id="MQTTmessageInput" class="form-control" aria-label="Sizing example input" aria-describedby="inputGroup-sizing-sm">
|
|
</div>
|
|
<button class="btn btn-primary" onclick="mqtt_publish('ttyAMA2', document.getElementById('MQTTmessageInput').value, 2)">Send Message</button>
|
|
<div id="loading_mqtt_publish" class="spinner-border spinner-border-sm" style="display: none;" role="status"></div>
|
|
<div id="response_mqtt_publish"></div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
-->
|
|
<h3>Test HTTP server comm.</h3>
|
|
<div class="row mb-3">
|
|
<!-- SET URL -->
|
|
|
|
<div class="col-sm-4">
|
|
<div class="card">
|
|
<div class="card-body">
|
|
<p class="card-text">Test communication with the server.</p>
|
|
<button class="btn btn-primary" onclick="ping_test()">Test</button>
|
|
<div id="loading_ping" class="spinner-border spinner-border-sm" style="display: none;" role="status"></div>
|
|
<div id="response_ping"></div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="col-sm-4">
|
|
<div class="card">
|
|
<div class="card-body">
|
|
<p class="card-text">Setup PSD connection.</p>
|
|
<button class="btn btn-primary" onclick="PSD_setup()">Start</button>
|
|
<div id="loading_PSD" class="spinner-border spinner-border-sm" style="display: none;" role="status"></div>
|
|
<div id="response_psd_setup"></div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="col-sm-3">
|
|
<div class="card">
|
|
|
|
<div class="card-body">
|
|
<p class="card-text">Setup Server Hostname.</p>
|
|
<div class="input-group input-group-sm mb-3">
|
|
<span class="input-group-text" id="inputGroup-sizing-sm">Server name</span>
|
|
<input type="text" id="messageInput_server" class="form-control" aria-label="Sizing example input" aria-describedby="inputGroup-sizing-sm">
|
|
</div>
|
|
<button class="btn btn-primary" onclick="setupServerHostname('ttyAMA2', document.getElementById('messageInput_server').value, 0)">Set</button>
|
|
<div id="loading_serverHostname" class="spinner-border spinner-border-sm" style="display: none;" role="status"></div>
|
|
<div id="response_serverHostname"></div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
|
|
|
|
</div>
|
|
|
|
<h3>Send message (test)</h3>
|
|
<div class="row mb-3">
|
|
<!-- SET URL -->
|
|
|
|
<div class="col-sm-4">
|
|
<div class="card">
|
|
<div class="card-body">
|
|
<p class="card-text">Set url (HTTP).</p>
|
|
<button class="btn btn-primary" onclick="setURL_saraR4('ttyAMA2', 'data.nebuleair.fr')">Set URL</button>
|
|
<div id="loading_ttyAMA2_setURL" class="spinner-border spinner-border-sm" style="display: none;" role="status"></div>
|
|
<div id="response_ttyAMA2_setURL"></div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- WRITE MESSAGE to memory -->
|
|
|
|
<div class="col-sm-4">
|
|
<div class="card">
|
|
<div class="card-body">
|
|
<p class="card-text">Write message (local storage).</p>
|
|
<div class="input-group input-group-sm mb-3">
|
|
<span class="input-group-text" id="inputGroup-sizing-sm">Text</span>
|
|
<input type="text" id="messageInput" class="form-control" aria-label="Sizing example input" aria-describedby="inputGroup-sizing-sm">
|
|
</div>
|
|
<button class="btn btn-primary" onclick="writeMessage_saraR4('ttyAMA2', document.getElementById('messageInput').value , 'write')">Write </button>
|
|
<button class="btn btn-warning" onclick="writeMessage_saraR4('ttyAMA2', 'Hello', 'read')">Read </button>
|
|
<button class="btn btn-danger" onclick="writeMessage_saraR4('ttyAMA2', 'Hello', 'erase')">Empty </button>
|
|
<div id="loading_ttyAMA2_message_write" class="spinner-border spinner-border-sm" style="display: none;" role="status"></div>
|
|
<div id="response_ttyAMA2_message_write"></div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Send MESSAGE -->
|
|
|
|
<div class="col-sm-4">
|
|
<div class="card">
|
|
<div class="card-body">
|
|
<p class="card-text">Send message .</p>
|
|
<button class="btn btn-primary" onclick="sendMessage_saraR4('ttyAMA2', '/pro_4G/notif_message.php')">Send Message</button>
|
|
<div id="loading_ttyAMA2_message_send" class="spinner-border spinner-border-sm" style="display: none;" role="status"></div>
|
|
<div id="response_ttyAMA2_message_send"></div>
|
|
</div>
|
|
</div>
|
|
</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">
|
|
<!-- Dynamic sensor test entries will be added here -->
|
|
<div id="sensor_tests_container"></div>
|
|
|
|
<!-- Separator for communication tests -->
|
|
<div id="comm_tests_separator" class="list-group-item bg-light text-center py-1" style="display:none;">
|
|
<small class="text-muted fw-bold">COMMUNICATION</small>
|
|
</div>
|
|
|
|
<!-- Info: WiFi Status -->
|
|
<div class="list-group-item d-flex justify-content-between align-items-center" id="test_wifi">
|
|
<div>
|
|
<strong>WiFi / Network</strong>
|
|
<div class="small text-muted" id="test_wifi_detail">Waiting...</div>
|
|
</div>
|
|
<span id="test_wifi_status" class="badge bg-secondary">Pending</span>
|
|
</div>
|
|
|
|
<!-- Test: 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: 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>
|
|
|
|
<!-- Test: Signal Strength -->
|
|
<div class="list-group-item d-flex justify-content-between align-items-center" id="test_signal">
|
|
<div>
|
|
<strong>Signal Strength</strong>
|
|
<div class="small text-muted" id="test_signal_detail">Waiting...</div>
|
|
</div>
|
|
<span id="test_signal_status" class="badge bg-secondary">Pending</span>
|
|
</div>
|
|
|
|
<!-- Test: Network Connection -->
|
|
<div class="list-group-item d-flex justify-content-between align-items-center" id="test_network">
|
|
<div>
|
|
<strong>Network Connection</strong>
|
|
<div class="small text-muted" id="test_network_detail">Waiting...</div>
|
|
</div>
|
|
<span id="test_network_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 detailed logs
|
|
</button>
|
|
<div class="collapse mt-2" id="selftest_logs_collapse">
|
|
<div class="card card-body bg-dark text-light" style="max-height: 250px; overflow-y: auto; font-family: monospace; font-size: 0.75rem;">
|
|
<pre id="selftest_logs" style="margin: 0; white-space: pre-wrap; word-wrap: break-word;"></pre>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="modal-footer">
|
|
<div id="selftest_summary" class="me-auto"></div>
|
|
<button type="button" class="btn btn-primary" id="selfTestCopyBtn" onclick="openShareReportModal()" disabled>
|
|
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-share me-1" viewBox="0 0 16 16">
|
|
<path d="M13.5 1a1.5 1.5 0 1 0 0 3 1.5 1.5 0 0 0 0-3zM11 2.5a2.5 2.5 0 1 1 .603 1.628l-6.718 3.12a2.499 2.499 0 0 1 0 1.504l6.718 3.12a2.5 2.5 0 1 1-.488.876l-6.718-3.12a2.5 2.5 0 1 1 0-3.256l6.718-3.12A2.5 2.5 0 0 1 11 2.5zm-8.5 4a1.5 1.5 0 1 0 0 3 1.5 1.5 0 0 0 0-3zm11 5.5a1.5 1.5 0 1 0 0 3 1.5 1.5 0 0 0 0-3z"/>
|
|
</svg>
|
|
Share Report
|
|
</button>
|
|
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal" id="selfTestDoneBtn" disabled>Close</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Share Report Modal -->
|
|
<div class="modal fade" id="shareReportModal" tabindex="-1" aria-labelledby="shareReportModalLabel" aria-hidden="true">
|
|
<div class="modal-dialog modal-lg modal-dialog-centered">
|
|
<div class="modal-content">
|
|
<div class="modal-header bg-primary text-white">
|
|
<h5 class="modal-title" id="shareReportModalLabel">Share Diagnostic Report</h5>
|
|
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal" aria-label="Close"></button>
|
|
</div>
|
|
<div class="modal-body">
|
|
<div class="alert alert-info">
|
|
<strong>Need help?</strong> You can send this diagnostic report to our support team at
|
|
<a href="mailto:contact@aircarto.fr?subject=NebuleAir%20Diagnostic%20Report" class="alert-link">contact@aircarto.fr</a>
|
|
<br><small>Select all the text below (Ctrl+A) and copy it (Ctrl+C), or use the Download button.</small>
|
|
</div>
|
|
<div class="mb-3">
|
|
<textarea id="shareReportText" class="form-control font-monospace" rows="15" readonly style="font-size: 0.75rem; background-color: #1e1e1e; color: #d4d4d4;"></textarea>
|
|
</div>
|
|
</div>
|
|
<div class="modal-footer">
|
|
<button type="button" class="btn btn-success" onclick="downloadReport()">
|
|
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-download me-1" viewBox="0 0 16 16">
|
|
<path d="M.5 9.9a.5.5 0 0 1 .5.5v2.5a1 1 0 0 0 1 1h12a1 1 0 0 0 1-1v-2.5a.5.5 0 0 1 1 0v2.5a2 2 0 0 1-2 2H2a2 2 0 0 1-2-2v-2.5a.5.5 0 0 1 .5-.5z"/>
|
|
<path d="M7.646 11.854a.5.5 0 0 0 .708 0l3-3a.5.5 0 0 0-.708-.708L8.5 10.293V1.5a.5.5 0 0 0-1 0v8.793L5.354 8.146a.5.5 0 1 0-.708.708l3 3z"/>
|
|
</svg>
|
|
Download (.txt)
|
|
</button>
|
|
<button type="button" class="btn btn-outline-primary" onclick="selectAllReportText()">
|
|
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-cursor-text me-1" viewBox="0 0 16 16">
|
|
<path d="M5 2a.5.5 0 0 1 .5-.5c.862 0 1.573.287 2.06.566.174.099.321.198.44.286.119-.088.266-.187.44-.286A4.165 4.165 0 0 1 10.5 1.5a.5.5 0 0 1 0 1c-.638 0-1.177.213-1.564.434a3.49 3.49 0 0 0-.436.294V7.5H9a.5.5 0 0 1 0 1h-.5v4.272c.1.08.248.187.436.294.387.221.926.434 1.564.434a.5.5 0 0 1 0 1 4.165 4.165 0 0 1-2.06-.566A4.561 4.561 0 0 1 8 13.65a4.561 4.561 0 0 1-.44.285 4.165 4.165 0 0 1-2.06.566.5.5 0 0 1 0-1c.638 0 1.177-.213 1.564-.434.188-.107.335-.214.436-.294V8.5H7a.5.5 0 0 1 0-1h.5V3.228a3.49 3.49 0 0 0-.436-.294A3.166 3.166 0 0 0 5.5 2.5.5.5 0 0 1 5 2z"/>
|
|
</svg>
|
|
Select All
|
|
</button>
|
|
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- toast -->
|
|
|
|
<div class="toast-container position-fixed bottom-0 end-0 p-3">
|
|
<div id="liveToast" class="toast align-items-center text-bg-primary border-1" role="alert" aria-live="assertive" aria-atomic="true">
|
|
<div class="d-flex">
|
|
<div class="toast-body">
|
|
Hello, world! This is a toast message.
|
|
</div>
|
|
<button type="button" class="btn-close btn-close-white me-2 m-auto" data-bs-dismiss="toast" aria-label="Close"></button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
</main>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- JAVASCRIPT -->
|
|
|
|
<!-- Link Ajax locally -->
|
|
<script src="assets/jquery/jquery-3.7.1.min.js"></script>
|
|
<!-- Link Bootstrap JS and Popper.js locally -->
|
|
<script src="assets/js/bootstrap.bundle.js"></script>
|
|
<!-- i18n translation system -->
|
|
<script src="assets/js/i18n.js"></script>
|
|
<script src="assets/js/topbar-logo.js"></script>
|
|
|
|
<script>
|
|
document.addEventListener('DOMContentLoaded', function () {
|
|
const elementsToLoad = [
|
|
{ id: 'topbar', file: 'topbar.html' },
|
|
{ id: 'sidebar', file: 'sidebar.html' },
|
|
{ id: 'sidebar_mobile', file: 'sidebar.html' }
|
|
];
|
|
|
|
elementsToLoad.forEach(({ id, file }) => {
|
|
fetch(file)
|
|
.then(response => response.text())
|
|
.then(data => {
|
|
const element = document.getElementById(id);
|
|
if (element) {
|
|
element.innerHTML = data;
|
|
}
|
|
})
|
|
.catch(error => console.error(`Error loading ${file}:`, error));
|
|
});
|
|
|
|
//OLD way to retreive data from JSON
|
|
/*
|
|
fetch('../config.json') // Replace 'deviceID.txt' with 'config.json'
|
|
.then(response => response.json()) // Parse response as JSON
|
|
.then(data => {
|
|
console.log("Getting config file (onload)");
|
|
//modem config mode
|
|
const check_modem_configMode = document.getElementById("check_modem_configMode");
|
|
check_modem_configMode.checked = data.modem_config_mode;
|
|
console.log("Modem configuration: " + data.modem_config_mode);
|
|
})
|
|
*/
|
|
|
|
//NEW way to get data from SQLITE
|
|
$.ajax({
|
|
url: 'launcher.php?type=get_config_sqlite',
|
|
dataType:'json',
|
|
//dataType: 'json', // Specify that you expect a JSON response
|
|
method: 'GET', // Use GET or POST depending on your needs
|
|
success: function(response) {
|
|
console.log("Getting SQLite config table:");
|
|
console.log(response);
|
|
//modem_version
|
|
const modem_version_html = document.getElementById("modem_version");
|
|
modem_version_html.innerText = response.modem_version;
|
|
|
|
// Set checkbox state based on the response data
|
|
const check_modem_configMode = document.getElementById("check_modem_configMode");
|
|
if (check_modem_configMode) {
|
|
check_modem_configMode.checked = response.modem_config_mode;
|
|
console.log("Modem configuration: " + response.modem_config_mode);
|
|
} else {
|
|
console.error("Checkbox element not found");
|
|
}
|
|
|
|
},
|
|
error: function(xhr, status, error) {
|
|
console.error('AJAX request failed:', status, error);
|
|
}
|
|
});
|
|
|
|
});
|
|
|
|
window.onload = function() {
|
|
getModem_busy_status();
|
|
setInterval(getModem_busy_status, 1000);
|
|
|
|
//NEW way to get config (SQLite)
|
|
$.ajax({
|
|
url: 'launcher.php?type=get_config_sqlite',
|
|
dataType:'json',
|
|
//dataType: 'json', // Specify that you expect a JSON response
|
|
method: 'GET', // Use GET or POST depending on your needs
|
|
success: function(response) {
|
|
console.log("Getting SQLite config table:");
|
|
console.log(response);
|
|
//device name_side bar
|
|
const elements = document.querySelectorAll('.sideBar_sensorName');
|
|
elements.forEach((element) => {
|
|
element.innerText = response.deviceName;
|
|
});
|
|
//device name html page title
|
|
if (response.deviceName) {
|
|
document.title = response.deviceName;
|
|
}
|
|
},
|
|
error: function(xhr, status, error) {
|
|
console.error('AJAX request failed:', status, error);
|
|
}
|
|
});//end AJAX
|
|
|
|
|
|
//get local RTC
|
|
$.ajax({
|
|
url: 'launcher.php?type=RTC_time',
|
|
dataType: 'text', // Specify that you expect a JSON response
|
|
method: 'GET', // Use GET or POST depending on your needs
|
|
success: function(response) {
|
|
console.log("Local RTC: " + response);
|
|
const RTC_Element = document.getElementById("RTC_time");
|
|
RTC_Element.textContent = response;
|
|
|
|
},
|
|
error: function(xhr, status, error) {
|
|
console.error('AJAX request failed:', status, error);
|
|
}
|
|
});
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
function getModemInfo(port, timeout) {
|
|
console.log("Getting modem info from port " + port);
|
|
|
|
$("#loading_ttyAMA2_ATI").show();
|
|
$("#modem_info_alert").empty();
|
|
$("#response_ttyAMA2_ATI").empty();
|
|
|
|
$.ajax({
|
|
url: 'launcher.php?type=sara&port=' + port + '&command=' + encodeURIComponent('ATI') + '&timeout=' + timeout,
|
|
dataType: 'text',
|
|
method: 'GET',
|
|
success: function(response) {
|
|
console.log("ATI response:", response);
|
|
$("#loading_ttyAMA2_ATI").hide();
|
|
|
|
// Store raw logs
|
|
const formattedLogs = response.replace(/\n/g, "<br>");
|
|
$("#response_ttyAMA2_ATI").html(formattedLogs);
|
|
|
|
// Parse response to detect modem model
|
|
let alertHtml = '';
|
|
const responseUpper = response.toUpperCase();
|
|
|
|
if (response.includes('OK') && (responseUpper.includes('SARA-R5') || responseUpper.includes('SARA-R4'))) {
|
|
// Extract model name
|
|
let modelName = 'SARA';
|
|
const modelMatch = response.match(/SARA-R[45]\d*[A-Z]*-\d+[A-Z]*-\d+/i);
|
|
if (modelMatch) {
|
|
modelName = modelMatch[0];
|
|
} else if (responseUpper.includes('SARA-R5')) {
|
|
modelName = 'SARA-R5';
|
|
} else if (responseUpper.includes('SARA-R4')) {
|
|
modelName = 'SARA-R4';
|
|
}
|
|
|
|
alertHtml = `
|
|
<div class="alert alert-success py-2 mb-0 mt-2 d-flex justify-content-between align-items-center" role="alert">
|
|
<div>
|
|
<strong>Modem connected</strong><br>
|
|
<small>Model: ${modelName}</small>
|
|
</div>
|
|
<button class="btn btn-sm btn-outline-secondary" type="button" data-bs-toggle="collapse" data-bs-target="#modem_info_logs" aria-expanded="false" aria-controls="modem_info_logs">
|
|
<small>+</small>
|
|
</button>
|
|
</div>`;
|
|
} else if (response.includes('ERROR') || response.trim() === '' || !response.includes('OK')) {
|
|
alertHtml = `
|
|
<div class="alert alert-danger py-2 mb-0 mt-2 d-flex justify-content-between align-items-center" role="alert">
|
|
<div>
|
|
<strong>Modem not connected</strong><br>
|
|
<small>No response from modem</small>
|
|
</div>
|
|
<button class="btn btn-sm btn-outline-secondary" type="button" data-bs-toggle="collapse" data-bs-target="#modem_info_logs" aria-expanded="false" aria-controls="modem_info_logs">
|
|
<small>+</small>
|
|
</button>
|
|
</div>`;
|
|
} else {
|
|
// Unknown response but got something
|
|
alertHtml = `
|
|
<div class="alert alert-warning py-2 mb-0 mt-2 d-flex justify-content-between align-items-center" role="alert">
|
|
<div>
|
|
<strong>Modem detected</strong><br>
|
|
<small>Unexpected response</small>
|
|
</div>
|
|
<button class="btn btn-sm btn-outline-secondary" type="button" data-bs-toggle="collapse" data-bs-target="#modem_info_logs" aria-expanded="false" aria-controls="modem_info_logs">
|
|
<small>+</small>
|
|
</button>
|
|
</div>`;
|
|
}
|
|
|
|
$("#modem_info_alert").html(alertHtml);
|
|
},
|
|
error: function(xhr, status, error) {
|
|
console.error('AJAX request failed:', status, error);
|
|
$("#loading_ttyAMA2_ATI").hide();
|
|
$("#modem_info_alert").html(`
|
|
<div class="alert alert-danger py-2 mb-0 mt-2" role="alert">
|
|
<strong>Communication error</strong><br>
|
|
<small>${error}</small>
|
|
</div>`);
|
|
}
|
|
});
|
|
}
|
|
|
|
function getSimInfo(port, timeout) {
|
|
console.log("Getting SIM info from port " + port);
|
|
|
|
$("#loading_ttyAMA2_AT_CCID_").show();
|
|
$("#sim_info_alert").empty();
|
|
$("#response_ttyAMA2_AT_CCID_").empty();
|
|
|
|
$.ajax({
|
|
url: 'launcher.php?type=sara&port=' + port + '&command=' + encodeURIComponent('AT+CCID?') + '&timeout=' + timeout,
|
|
dataType: 'text',
|
|
method: 'GET',
|
|
success: function(response) {
|
|
console.log("CCID response:", response);
|
|
$("#loading_ttyAMA2_AT_CCID_").hide();
|
|
|
|
// Store raw logs
|
|
const formattedLogs = response.replace(/\n/g, "<br>");
|
|
$("#response_ttyAMA2_AT_CCID_").html(formattedLogs);
|
|
|
|
// Parse response to extract SIM card number
|
|
let alertHtml = '';
|
|
|
|
// Match CCID number (typically 19-20 digits)
|
|
const ccidMatch = response.match(/\+CCID:\s*(\d{18,22})/);
|
|
|
|
if (response.includes('OK') && ccidMatch) {
|
|
const simNumber = ccidMatch[1];
|
|
alertHtml = `
|
|
<div class="alert alert-success py-2 mb-0 mt-2 d-flex justify-content-between align-items-center" role="alert">
|
|
<div>
|
|
<strong>SIM card connected</strong><br>
|
|
<small>ICCID: ${simNumber}</small>
|
|
</div>
|
|
<button class="btn btn-sm btn-outline-secondary" type="button" data-bs-toggle="collapse" data-bs-target="#sim_info_logs" aria-expanded="false" aria-controls="sim_info_logs">
|
|
<small>+</small>
|
|
</button>
|
|
</div>`;
|
|
} else if (response.includes('ERROR') || response.trim() === '' || !response.includes('OK')) {
|
|
alertHtml = `
|
|
<div class="alert alert-danger py-2 mb-0 mt-2 d-flex justify-content-between align-items-center" role="alert">
|
|
<div>
|
|
<strong>SIM card not detected</strong><br>
|
|
<small>No SIM card or read error</small>
|
|
</div>
|
|
<button class="btn btn-sm btn-outline-secondary" type="button" data-bs-toggle="collapse" data-bs-target="#sim_info_logs" aria-expanded="false" aria-controls="sim_info_logs">
|
|
<small>+</small>
|
|
</button>
|
|
</div>`;
|
|
} else {
|
|
alertHtml = `
|
|
<div class="alert alert-warning py-2 mb-0 mt-2 d-flex justify-content-between align-items-center" role="alert">
|
|
<div>
|
|
<strong>SIM card detected</strong><br>
|
|
<small>Unable to read ICCID</small>
|
|
</div>
|
|
<button class="btn btn-sm btn-outline-secondary" type="button" data-bs-toggle="collapse" data-bs-target="#sim_info_logs" aria-expanded="false" aria-controls="sim_info_logs">
|
|
<small>+</small>
|
|
</button>
|
|
</div>`;
|
|
}
|
|
|
|
$("#sim_info_alert").html(alertHtml);
|
|
},
|
|
error: function(xhr, status, error) {
|
|
console.error('AJAX request failed:', status, error);
|
|
$("#loading_ttyAMA2_AT_CCID_").hide();
|
|
$("#sim_info_alert").html(`
|
|
<div class="alert alert-danger py-2 mb-0 mt-2" role="alert">
|
|
<strong>Communication error</strong><br>
|
|
<small>${error}</small>
|
|
</div>`);
|
|
}
|
|
});
|
|
}
|
|
|
|
// Cache for operators data
|
|
let operatorsData = null;
|
|
|
|
function loadOperatorsData() {
|
|
return new Promise((resolve, reject) => {
|
|
if (operatorsData) {
|
|
resolve(operatorsData);
|
|
return;
|
|
}
|
|
$.ajax({
|
|
url: 'assets/data/operators.json',
|
|
dataType: 'json',
|
|
method: 'GET',
|
|
success: function(data) {
|
|
operatorsData = data;
|
|
resolve(data);
|
|
},
|
|
error: function(xhr, status, error) {
|
|
console.error('Failed to load operators data:', error);
|
|
reject(error);
|
|
}
|
|
});
|
|
});
|
|
}
|
|
|
|
function getNetworkInfo(port, timeout) {
|
|
console.log("Getting network info from port " + port);
|
|
|
|
$("#loading_ttyAMA2_AT_COPS_").show();
|
|
$("#network_info_alert").empty();
|
|
$("#response_ttyAMA2_AT_COPS_").empty();
|
|
|
|
// Load operators data first, then query modem
|
|
loadOperatorsData().then(function(opData) {
|
|
$.ajax({
|
|
url: 'launcher.php?type=sara&port=' + port + '&command=' + encodeURIComponent('AT+COPS?') + '&timeout=' + timeout,
|
|
dataType: 'text',
|
|
method: 'GET',
|
|
success: function(response) {
|
|
console.log("COPS response:", response);
|
|
$("#loading_ttyAMA2_AT_COPS_").hide();
|
|
|
|
// Store raw logs
|
|
const formattedLogs = response.replace(/\n/g, "<br>");
|
|
$("#response_ttyAMA2_AT_COPS_").html(formattedLogs);
|
|
|
|
// Parse response: +COPS: <mode>[,<format>,<oper>[,<AcT>]]
|
|
let alertHtml = '';
|
|
const copsMatch = response.match(/\+COPS:\s*(\d+)(?:,(\d+),"?([^",]+)"?,(\d+))?/);
|
|
|
|
if (response.includes('OK') && copsMatch) {
|
|
const mode = copsMatch[1];
|
|
const format = copsMatch[2];
|
|
const oper = copsMatch[3];
|
|
const act = copsMatch[4];
|
|
|
|
// Get mode description
|
|
const modeDesc = opData.modes[mode] || 'Unknown';
|
|
|
|
// Get operator name
|
|
let operatorName = oper || 'Not registered';
|
|
let operatorCountry = '';
|
|
if (oper && opData.operators[oper]) {
|
|
operatorName = opData.operators[oper].name;
|
|
operatorCountry = opData.operators[oper].country;
|
|
}
|
|
|
|
// Get access technology
|
|
const actDesc = act ? (opData.accessTechnology[act] || 'Unknown') : 'N/A';
|
|
|
|
if (oper) {
|
|
alertHtml = `
|
|
<div class="alert alert-success py-2 mb-0 mt-2 d-flex justify-content-between align-items-center" role="alert">
|
|
<div>
|
|
<strong>Connected to network</strong><br>
|
|
<small>
|
|
Operator: ${operatorName}${operatorCountry ? ' (' + operatorCountry + ')' : ''}<br>
|
|
Technology: ${actDesc}<br>
|
|
Mode: ${modeDesc}
|
|
</small>
|
|
</div>
|
|
<button class="btn btn-sm btn-outline-secondary" type="button" data-bs-toggle="collapse" data-bs-target="#network_info_logs" aria-expanded="false" aria-controls="network_info_logs">
|
|
<small>+</small>
|
|
</button>
|
|
</div>`;
|
|
} else {
|
|
alertHtml = `
|
|
<div class="alert alert-warning py-2 mb-0 mt-2 d-flex justify-content-between align-items-center" role="alert">
|
|
<div>
|
|
<strong>Not registered</strong><br>
|
|
<small>Mode: ${modeDesc}</small>
|
|
</div>
|
|
<button class="btn btn-sm btn-outline-secondary" type="button" data-bs-toggle="collapse" data-bs-target="#network_info_logs" aria-expanded="false" aria-controls="network_info_logs">
|
|
<small>+</small>
|
|
</button>
|
|
</div>`;
|
|
}
|
|
} else if (response.includes('ERROR') || response.trim() === '' || !response.includes('OK')) {
|
|
alertHtml = `
|
|
<div class="alert alert-danger py-2 mb-0 mt-2 d-flex justify-content-between align-items-center" role="alert">
|
|
<div>
|
|
<strong>Network error</strong><br>
|
|
<small>Unable to get network info</small>
|
|
</div>
|
|
<button class="btn btn-sm btn-outline-secondary" type="button" data-bs-toggle="collapse" data-bs-target="#network_info_logs" aria-expanded="false" aria-controls="network_info_logs">
|
|
<small>+</small>
|
|
</button>
|
|
</div>`;
|
|
} else {
|
|
alertHtml = `
|
|
<div class="alert alert-warning py-2 mb-0 mt-2 d-flex justify-content-between align-items-center" role="alert">
|
|
<div>
|
|
<strong>Unknown response</strong><br>
|
|
<small>Check logs for details</small>
|
|
</div>
|
|
<button class="btn btn-sm btn-outline-secondary" type="button" data-bs-toggle="collapse" data-bs-target="#network_info_logs" aria-expanded="false" aria-controls="network_info_logs">
|
|
<small>+</small>
|
|
</button>
|
|
</div>`;
|
|
}
|
|
|
|
$("#network_info_alert").html(alertHtml);
|
|
},
|
|
error: function(xhr, status, error) {
|
|
console.error('AJAX request failed:', status, error);
|
|
$("#loading_ttyAMA2_AT_COPS_").hide();
|
|
$("#network_info_alert").html(`
|
|
<div class="alert alert-danger py-2 mb-0 mt-2" role="alert">
|
|
<strong>Communication error</strong><br>
|
|
<small>${error}</small>
|
|
</div>`);
|
|
}
|
|
});
|
|
}).catch(function(error) {
|
|
$("#loading_ttyAMA2_AT_COPS_").hide();
|
|
$("#network_info_alert").html(`
|
|
<div class="alert alert-danger py-2 mb-0 mt-2" role="alert">
|
|
<strong>Configuration error</strong><br>
|
|
<small>Failed to load operators data</small>
|
|
</div>`);
|
|
});
|
|
}
|
|
|
|
function getSignalInfo(port, timeout) {
|
|
console.log("Getting signal info from port " + port);
|
|
|
|
$("#loading_ttyAMA2_AT_CSQ").show();
|
|
$("#signal_info_alert").empty();
|
|
$("#response_ttyAMA2_AT_CSQ").empty();
|
|
|
|
$.ajax({
|
|
url: 'launcher.php?type=sara&port=' + port + '&command=' + encodeURIComponent('AT+CSQ') + '&timeout=' + timeout,
|
|
dataType: 'text',
|
|
method: 'GET',
|
|
success: function(response) {
|
|
console.log("CSQ response:", response);
|
|
$("#loading_ttyAMA2_AT_CSQ").hide();
|
|
|
|
// Store raw logs
|
|
const formattedLogs = response.replace(/\n/g, "<br>");
|
|
$("#response_ttyAMA2_AT_CSQ").html(formattedLogs);
|
|
|
|
// Parse response: +CSQ: <signal_power>,<qual>
|
|
let alertHtml = '';
|
|
const csqMatch = response.match(/\+CSQ:\s*(\d+),(\d+)/);
|
|
|
|
if (response.includes('OK') && csqMatch) {
|
|
const signalPower = parseInt(csqMatch[1]);
|
|
const qual = parseInt(csqMatch[2]);
|
|
|
|
// Determine signal quality and color (matching Python thresholds)
|
|
let signalDesc, signalColor, signalIcon, alertClass;
|
|
|
|
if (signalPower === 99) {
|
|
signalDesc = 'No signal';
|
|
signalColor = '#333333';
|
|
signalIcon = '⚫';
|
|
alertClass = 'alert-dark';
|
|
} else if (signalPower === 0) {
|
|
signalDesc = 'Very poor';
|
|
signalColor = '#dc3545';
|
|
signalIcon = '🔴';
|
|
alertClass = 'alert-danger';
|
|
} else if (signalPower <= 24) {
|
|
signalDesc = 'Poor';
|
|
signalColor = '#fd7e14';
|
|
signalIcon = '🟠';
|
|
alertClass = 'alert-warning';
|
|
} else if (signalPower <= 26) {
|
|
signalDesc = 'Good';
|
|
signalColor = '#ffc107';
|
|
signalIcon = '🟡';
|
|
alertClass = 'alert-warning';
|
|
} else if (signalPower <= 28) {
|
|
signalDesc = 'Very good';
|
|
signalColor = '#198754';
|
|
signalIcon = '🟢';
|
|
alertClass = 'alert-success';
|
|
} else if (signalPower <= 30) {
|
|
signalDesc = 'Excellent';
|
|
signalColor = '#0d6efd';
|
|
signalIcon = '🔵';
|
|
alertClass = 'alert-primary';
|
|
} else {
|
|
signalDesc = 'Maximum';
|
|
signalColor = '#6f42c1';
|
|
signalIcon = '🟣';
|
|
alertClass = 'alert-primary';
|
|
}
|
|
|
|
// Calculate approximate dBm (for RSSI: -113 + 2*signalPower)
|
|
let rssiDbm = signalPower !== 99 ? (-113 + 2 * signalPower) + ' dBm' : 'N/A';
|
|
|
|
// Signal bars visualization (1-5 bars based on signal power, matching thresholds)
|
|
let bars = 0;
|
|
if (signalPower !== 99) {
|
|
if (signalPower >= 29) bars = 5; // Excellent / Very Strong
|
|
else if (signalPower >= 27) bars = 4; // Very good
|
|
else if (signalPower >= 25) bars = 3; // Good
|
|
else if (signalPower >= 10) bars = 2; // Poor (mid)
|
|
else if (signalPower >= 1) bars = 1; // Poor (low)
|
|
}
|
|
|
|
const barsHtml = `
|
|
<span style="font-size: 1.2em; letter-spacing: 2px;">
|
|
${[1,2,3,4,5].map(i =>
|
|
`<span style="color: ${i <= bars ? signalColor : '#dee2e6'};">▮</span>`
|
|
).join('')}
|
|
</span>`;
|
|
|
|
alertHtml = `
|
|
<div class="alert ${alertClass} py-2 mb-0 mt-2 d-flex justify-content-between align-items-center" role="alert">
|
|
<div>
|
|
<div class="d-flex align-items-center mb-1">
|
|
${barsHtml}
|
|
<span class="ms-2"><strong>${signalDesc}</strong></span>
|
|
</div>
|
|
<small>
|
|
Signal: ${signalPower}/31 (${rssiDbm})<br>
|
|
Quality: ${qual}/7
|
|
</small>
|
|
</div>
|
|
<button class="btn btn-sm btn-outline-secondary" type="button" data-bs-toggle="collapse" data-bs-target="#signal_info_logs" aria-expanded="false" aria-controls="signal_info_logs">
|
|
<small>+</small>
|
|
</button>
|
|
</div>`;
|
|
} else if (response.includes('ERROR') || response.trim() === '' || !response.includes('OK')) {
|
|
alertHtml = `
|
|
<div class="alert alert-danger py-2 mb-0 mt-2 d-flex justify-content-between align-items-center" role="alert">
|
|
<div>
|
|
<strong>Signal error</strong><br>
|
|
<small>Unable to get signal info</small>
|
|
</div>
|
|
<button class="btn btn-sm btn-outline-secondary" type="button" data-bs-toggle="collapse" data-bs-target="#signal_info_logs" aria-expanded="false" aria-controls="signal_info_logs">
|
|
<small>+</small>
|
|
</button>
|
|
</div>`;
|
|
} else {
|
|
alertHtml = `
|
|
<div class="alert alert-warning py-2 mb-0 mt-2 d-flex justify-content-between align-items-center" role="alert">
|
|
<div>
|
|
<strong>Unknown response</strong><br>
|
|
<small>Check logs for details</small>
|
|
</div>
|
|
<button class="btn btn-sm btn-outline-secondary" type="button" data-bs-toggle="collapse" data-bs-target="#signal_info_logs" aria-expanded="false" aria-controls="signal_info_logs">
|
|
<small>+</small>
|
|
</button>
|
|
</div>`;
|
|
}
|
|
|
|
$("#signal_info_alert").html(alertHtml);
|
|
},
|
|
error: function(xhr, status, error) {
|
|
console.error('AJAX request failed:', status, error);
|
|
$("#loading_ttyAMA2_AT_CSQ").hide();
|
|
$("#signal_info_alert").html(`
|
|
<div class="alert alert-danger py-2 mb-0 mt-2" role="alert">
|
|
<strong>Communication error</strong><br>
|
|
<small>${error}</small>
|
|
</div>`);
|
|
}
|
|
});
|
|
}
|
|
|
|
function getData_saraR4(port, command, timeout){
|
|
console.log("Data from SaraR4");
|
|
console.log("Port: " + port );
|
|
console.log("Command: " + command );
|
|
console.log("Timeout: " + timeout );
|
|
|
|
const safeCommand = command.replace(/[?+=]/g, "_");
|
|
console.log(safeCommand);
|
|
|
|
$("#loading_"+port+"_"+safeCommand).show();
|
|
$("#response_"+port+"_"+safeCommand).empty();
|
|
|
|
|
|
$.ajax({
|
|
url: 'launcher.php?type=sara&port='+port+'&command='+encodeURIComponent(command)+'&timeout='+timeout,
|
|
dataType:'text',
|
|
//dataType: 'json', // Specify that you expect a JSON response
|
|
method: 'GET', // Use GET or POST depending on your needs
|
|
success: function(response) {
|
|
//log response
|
|
console.log(response);
|
|
//hide spinning wheel
|
|
$("#loading_"+port+"_"+safeCommand).hide();
|
|
|
|
// si on fait le scan de network on veut une liste des réseaux
|
|
if (command == "AT+COPS=?") {
|
|
// Extract data within parentheses
|
|
const matches = response.match(/\(.*?\)/g); // Matches all `(...)` sections
|
|
const container = document.getElementById('table-network'); // Bootstrap container
|
|
// Check if matches exist
|
|
if (matches) {
|
|
const table = document.createElement('table');
|
|
table.className = 'table table-striped'; // Add Bootstrap table styling
|
|
const thead = document.createElement('thead');
|
|
const tbody = document.createElement('tbody');
|
|
|
|
// Table header (you can customize this based on your data)
|
|
const headerRow = document.createElement('tr');
|
|
const header1 = document.createElement('th');
|
|
header1.textContent = 'Status';
|
|
const header2 = document.createElement('th');
|
|
header2.textContent = 'Long oper';
|
|
const header3 = document.createElement('th');
|
|
header3.textContent = 'Short opeer';
|
|
const header4 = document.createElement('th');
|
|
header4.textContent = 'Numeric oper';
|
|
const header5 = document.createElement('th');
|
|
header5.textContent = 'AcT';
|
|
headerRow.appendChild(header1);
|
|
headerRow.appendChild(header2);
|
|
headerRow.appendChild(header3);
|
|
headerRow.appendChild(header4);
|
|
headerRow.appendChild(header5);
|
|
thead.appendChild(headerRow);
|
|
table.appendChild(thead);
|
|
|
|
// Loop through each match and create a row in the table
|
|
matches.forEach((item) => {
|
|
// Skip empty sections
|
|
if (item === "()") return;
|
|
|
|
const row = document.createElement('tr');
|
|
const values = item.slice(1, -1).split(','); // Remove parentheses and split by commas
|
|
|
|
// Add table cells (td) for each value
|
|
values.forEach((value) => {
|
|
const cell = document.createElement('td');
|
|
cell.textContent = value.trim(); // Remove extra spaces
|
|
row.appendChild(cell);
|
|
});
|
|
|
|
tbody.appendChild(row);
|
|
});
|
|
|
|
// Add tbody to table and append the table to the container
|
|
table.appendChild(tbody);
|
|
container.appendChild(table);
|
|
} else {
|
|
console.error('No valid data found in response.');
|
|
}
|
|
} else{
|
|
// si c'est une commande AT normale
|
|
// Replace newline characters with <br> tags
|
|
const formattedResponse = response.replace(/\n/g, "<br>")
|
|
.replace(/\b(OK)\b/g, '<span style="color: green; font-weight: bold;">$1</span>');;
|
|
$("#response_"+port+"_"+safeCommand).html(formattedResponse);
|
|
|
|
}
|
|
},
|
|
error: function(xhr, status, error) {
|
|
console.error('AJAX request failed:', status, error);
|
|
}
|
|
});
|
|
}
|
|
|
|
function connectNetwork_saraR4(port, networkID, timeout){
|
|
console.log(" Connect to network (port "+port+" and network id "+networkID+"):");
|
|
$("#loading_"+port+"_AT_COPS_Connect").show();
|
|
$.ajax({
|
|
url: 'launcher.php?type=sara_connectNetwork&port='+port+'&networkID='+encodeURIComponent(networkID)+'&timeout='+timeout,
|
|
dataType:'text',
|
|
//dataType: 'json', // Specify that you expect a JSON response
|
|
method: 'GET', // Use GET or POST depending on your needs
|
|
success: function(response) {
|
|
console.log(response);
|
|
$("#loading_"+port+"_AT_COPS_Connect").hide();
|
|
// Replace newline characters with <br> tags
|
|
const formattedResponse = response.replace(/\n/g, "<br>");
|
|
$("#response_"+port+"_AT_COPS_Connect").html(formattedResponse);
|
|
|
|
},
|
|
error: function(xhr, status, error) {
|
|
console.error('AJAX request failed:', status, error);
|
|
}
|
|
});
|
|
}
|
|
|
|
function setupServerHostname(port, serverName, timeout){
|
|
console.log(" Setupt server hostname "+serverName+"):");
|
|
$("#loading_serverHostname").show();
|
|
$.ajax({
|
|
url: 'launcher.php?type=sara_setupHostname&port='+port+'&networkID='+encodeURIComponent(serverName)+'&profileID=0',
|
|
dataType:'text',
|
|
//dataType: 'json', // Specify that you expect a JSON response
|
|
method: 'GET', // Use GET or POST depending on your needs
|
|
success: function(response) {
|
|
console.log(response);
|
|
$("#loading_serverHostname").hide();
|
|
// Replace newline characters with <br> tags
|
|
const formattedResponse = response.replace(/\n/g, "<br>");
|
|
$("#response_serverHostname").html(formattedResponse);
|
|
|
|
},
|
|
error: function(xhr, status, error) {
|
|
console.error('AJAX request failed:', status, error);
|
|
}
|
|
});
|
|
}
|
|
|
|
function mqtt_getConfig_saraR4(port, timeout){
|
|
console.log("GET MQTT config (port "+port+"):");
|
|
$("#loading_mqtt_getConfig").show();
|
|
$.ajax({
|
|
url: 'launcher.php?type=sara_getMQTT_config&port='+port+'&timeout='+timeout,
|
|
dataType:'text',
|
|
//dataType: 'json', // Specify that you expect a JSON response
|
|
method: 'GET', // Use GET or POST depending on your needs
|
|
success: function(response) {
|
|
console.log(response);
|
|
$("#loading_mqtt_getConfig").hide();
|
|
// Replace newline characters with <br> tags
|
|
const formattedResponse = response.replace(/\n/g, "<br>");
|
|
$("#response_mqtt_getConfig").html(formattedResponse);
|
|
},
|
|
error: function(xhr, status, error) {
|
|
console.error('AJAX request failed:', status, error);
|
|
}
|
|
});
|
|
}
|
|
|
|
function mqtt_login_logout(port, login_logout, timeout){
|
|
console.log("GET MQTT login / logout (port "+port+"):");
|
|
$("#loading_mqtt_login_logout").show();
|
|
$.ajax({
|
|
url: 'launcher.php?type=sara_getMQTT_login_logout&port='+port+'&login_logout='+login_logout+'&timeout='+timeout,
|
|
dataType:'text',
|
|
//dataType: 'json', // Specify that you expect a JSON response
|
|
method: 'GET', // Use GET or POST depending on your needs
|
|
success: function(response) {
|
|
console.log(response);
|
|
$("#loading_mqtt_login_logout").hide();
|
|
const formattedResponse = response.replace(/\n/g, "<br>");
|
|
$("#response_mqtt_login_logout").html(formattedResponse);
|
|
|
|
const regex = /^\+UMQTTC:\s*(\d+),(\d+)/m; // Match "+UMQTTC:", followed by two numbers separated by a comma
|
|
const match = response.match(regex);
|
|
|
|
if (match) {
|
|
const firstNumber = match[1]; // The first number after ":"
|
|
const secondNumber = match[2]; // The second number after the ","
|
|
if (firstNumber == 0) {
|
|
console.log("MQTT LOGOUT:");
|
|
$("#response_mqtt_login_logout").append("<p>logout</p>");
|
|
}
|
|
if (firstNumber == 1) {
|
|
console.log("MQTT LOGIN:");
|
|
$("#response_mqtt_login_logout").append("<p>login</p>");
|
|
}
|
|
if (secondNumber == 0) {
|
|
console.log("ERROR");
|
|
$("#response_mqtt_login_logout").append("<p>error</p>");
|
|
}
|
|
if (secondNumber == 1) {
|
|
console.log("SUCCESS");
|
|
$("#response_mqtt_login_logout").append("<p>success</p>");
|
|
}
|
|
|
|
} else {
|
|
console.log("No matching line found");
|
|
}
|
|
|
|
},
|
|
error: function(xhr, status, error) {
|
|
console.error('AJAX request failed:', status, error);
|
|
}
|
|
});
|
|
}
|
|
|
|
function mqtt_publish(port, message, timeout){
|
|
console.log(" MQTT publish (port "+port+"):");
|
|
$("#loading_mqtt_publish").show();
|
|
$.ajax({
|
|
url: 'launcher.php?type=sara_MQTT_publish&port='+port+'&timeout='+timeout+'&message='+message,
|
|
dataType: 'text',
|
|
//dataType: 'json', // Specify that you expect a JSON response
|
|
method: 'GET', // Use GET or POST depending on your needs
|
|
success: function(response) {
|
|
console.log(response);
|
|
$("#loading_mqtt_publish").hide();
|
|
// Replace newline characters with <br> tags
|
|
const formattedResponse = response.replace(/\n/g, "<br>");
|
|
$("#response_mqtt_publish").html(formattedResponse);
|
|
},
|
|
error: function(xhr, status, error) {
|
|
console.error('AJAX request failed:', status, error);
|
|
}
|
|
});
|
|
}
|
|
|
|
function setURL_saraR4(port, url){
|
|
console.log("Set URL for HTTP (port "+port+" and URL "+url+"):");
|
|
$("#loading_"+port+"_setURL").show();
|
|
$.ajax({
|
|
url: 'launcher.php?type=sara_setURL&port='+port+'&url='+encodeURIComponent(url),
|
|
dataType: 'text',
|
|
//dataType: 'json', // Specify that you expect a JSON response
|
|
method: 'GET', // Use GET or POST depending on your needs
|
|
success: function(response) {
|
|
console.log(response);
|
|
$("#loading_"+port+"_setURL").hide();
|
|
// Replace newline characters with <br> tags
|
|
const formattedResponse = response.replace(/\n/g, "<br>");
|
|
$("#response_"+port+"_setURL").html(formattedResponse);
|
|
},
|
|
error: function(xhr, status, error) {
|
|
console.error('AJAX request failed:', status, error);
|
|
}
|
|
});
|
|
}
|
|
|
|
function ping_test(port, url){
|
|
console.log("Test ping to data.nebuleair.fr:");
|
|
$("#response_ping").empty();
|
|
$("#loading_ping").show();
|
|
$.ajax({
|
|
url: 'launcher.php?type=sara_ping',
|
|
dataType: 'text',
|
|
//dataType: 'json', // Specify that you expect a JSON response
|
|
method: 'GET', // Use GET or POST depending on your needs
|
|
success: function(response) {
|
|
console.log(response);
|
|
$("#loading_ping").hide();
|
|
// Replace newline characters with <br> tags
|
|
const formattedResponse = response.replace(/\n/g, "<br>");
|
|
$("#response_ping").html(formattedResponse);
|
|
},
|
|
error: function(xhr, status, error) {
|
|
console.error('AJAX request failed:', status, error);
|
|
}
|
|
});
|
|
}
|
|
|
|
function PSD_setup(port, url){
|
|
console.log("Setup PSD connection:");
|
|
$("#loading_PSD").show();
|
|
$.ajax({
|
|
url: 'launcher.php?type=sara_psd_setup',
|
|
dataType: 'text',
|
|
//dataType: 'json', // Specify that you expect a JSON response
|
|
method: 'GET', // Use GET or POST depending on your needs
|
|
success: function(response) {
|
|
console.log(response);
|
|
$("#loading_PSD").hide();
|
|
// Replace newline characters with <br> tags
|
|
const formattedResponse = response.replace(/\n/g, "<br>");
|
|
$("#response_psd_setup").html(formattedResponse);
|
|
},
|
|
error: function(xhr, status, error) {
|
|
console.error('AJAX request failed:', status, error);
|
|
}
|
|
});
|
|
}
|
|
|
|
function writeMessage_saraR4(port, message, type){
|
|
console.log(type +" message to SARA R4 memory (port "+port+" and message "+message+"):");
|
|
$("#loading_"+port+"_message_write").show();
|
|
$.ajax({
|
|
url: 'launcher.php?type=sara_writeMessage&port='+port+'&message='+encodeURIComponent(message)+'&type2='+type,
|
|
dataType: 'text',
|
|
//dataType: 'json', // Specify that you expect a JSON response
|
|
method: 'GET', // Use GET or POST depending on your needs
|
|
success: function(response) {
|
|
console.log(response);
|
|
$("#loading_"+port+"_message_write").hide();
|
|
// Replace newline characters with <br> tags
|
|
const formattedResponse = response.replace(/\n/g, "<br>");
|
|
$("#response_"+port+"_message_write").html(formattedResponse);
|
|
},
|
|
error: function(xhr, status, error) {
|
|
console.error('AJAX request failed:', status, error);
|
|
}
|
|
});
|
|
}
|
|
|
|
function sendMessage_saraR4(port, endpoint){
|
|
|
|
console.log("Send message from SaraR4 (port "+port+" and endpoint "+endpoint+"):");
|
|
|
|
$("#loading_"+port+"_message_send").show();
|
|
|
|
$.ajax({
|
|
url: 'launcher.php?type=sara_sendMessage&port='+port+'&endpoint='+encodeURIComponent(endpoint),
|
|
dataType: 'text',
|
|
//dataType: 'json', // Specify that you expect a JSON response
|
|
method: 'GET', // Use GET or POST depending on your needs
|
|
success: function(response) {
|
|
console.log(response);
|
|
$("#loading_"+port+"_message_send").hide();
|
|
// Replace newline characters with <br> tags
|
|
const formattedResponse = response.replace(/\n/g, "<br>");
|
|
$("#response_"+port+"_message_send").html(formattedResponse);
|
|
},
|
|
error: function(xhr, status, error) {
|
|
console.error('AJAX request failed:', status, error);
|
|
}
|
|
});
|
|
}
|
|
|
|
function connectAPN_saraR4(port, APN_address, timeout){
|
|
|
|
console.log(" Set APN (port "+port+" and adress "+APN_address+"):");
|
|
|
|
$("#loading_"+port+"_APN").show();
|
|
|
|
$.ajax({
|
|
url: 'launcher.php?type=sara_APN&port='+port+'&APN_address='+encodeURIComponent(APN_address)+'&timeout='+timeout,
|
|
//dataType: 'json', // Specify that you expect a JSON response
|
|
dataType: 'text',
|
|
method: 'GET', // Use GET or POST depending on your needs
|
|
success: function(response) {
|
|
console.log(response);
|
|
$("#loading_"+port+"_APN").hide();
|
|
// Replace newline characters with <br> tags
|
|
const formattedResponse = response.replace(/\n/g, "<br>");
|
|
$("#response_"+port+"_APN").html(formattedResponse);
|
|
|
|
},
|
|
error: function(xhr, status, error) {
|
|
console.error('AJAX request failed:', status, error);
|
|
}
|
|
});
|
|
}
|
|
|
|
function getModem_busy_status() {
|
|
//console.log("Getting modem busy status");
|
|
|
|
const SARA_busy_message = document.getElementById("modem_status_message");
|
|
|
|
$.ajax({
|
|
url: 'launcher.php?type=getModem_busy',
|
|
dataType: 'json', // Expecting JSON response
|
|
method: 'GET',
|
|
success: function(response) {
|
|
//console.log(response);
|
|
|
|
if (response.running) {
|
|
// Script is running → Red button, "Modem is busy"
|
|
|
|
SARA_busy_message.innerHTML= ` <div class="alert alert-warning" role="alert">
|
|
Le modem 4G est en cours d'utilisation! L'utilisation des boutons ci-dessous peut entrainer des erreurs. Veuillez mettre le modem en mode configuration.
|
|
</div>`
|
|
} else {
|
|
// Script is NOT running → Green button, "Modem is available"
|
|
|
|
SARA_busy_message.innerHTML= ` <div class="alert alert-primary" role="alert">
|
|
Veuillez vous assurer de mettre le modem en mode configuration avant de cliquer sur les boutons ci-dessous. <br>
|
|
Une fois terminé veillez à bien désactiver le mode configuration.
|
|
</div>`
|
|
}
|
|
},
|
|
error: function(xhr, status, error) {
|
|
console.error('AJAX request failed:', status, error);
|
|
SARA_busy_status.textContent = "Error checking status";
|
|
SARA_busy_status.className = "btn text-bg-warning"; // Yellow button for errors
|
|
}
|
|
});
|
|
}
|
|
|
|
function update_modem_configMode(param, checked){
|
|
//change ('modem_config_mode', '0', 'bool') inside SQLITE db
|
|
// response type: {"success":true,"message":"Configuration updated successfully","param":"modem_config_mode","value":"0","type":"bool"}
|
|
const toastLiveExample = document.getElementById('liveToast')
|
|
const toastBody = toastLiveExample.querySelector('.toast-body');
|
|
|
|
console.log("updating modem config mode to :" + checked);
|
|
$.ajax({
|
|
url: 'launcher.php?type=update_config_sqlite¶m='+param+'&value='+checked,
|
|
dataType: 'json', // Specify that you expect a JSON response
|
|
method: 'GET', // Use GET or POST depending on your needs
|
|
cache: false, // Prevent AJAX from caching
|
|
success: function(response) {
|
|
|
|
console.log("AJAX success:");
|
|
console.log(response);
|
|
|
|
// Format the response nicely
|
|
let formattedMessage = '';
|
|
|
|
if (response.success) {
|
|
// Success message
|
|
toastLiveExample.classList.remove('text-bg-danger');
|
|
toastLiveExample.classList.add('text-bg-success');
|
|
|
|
formattedMessage = `
|
|
<strong>Success!</strong><br>
|
|
Parameter: ${response.param || param}<br>
|
|
Value: ${response.value || checked}<br>
|
|
${response.message || ''}
|
|
`;
|
|
} else {
|
|
// Error message
|
|
toastLiveExample.classList.remove('text-bg-success');
|
|
toastLiveExample.classList.add('text-bg-danger');
|
|
|
|
formattedMessage = `
|
|
<strong>Error!</strong><br>
|
|
${response.error || 'Unknown error'}<br>
|
|
Parameter: ${response.param || param}
|
|
`;
|
|
}
|
|
|
|
// Update the toast body with formatted content
|
|
toastBody.innerHTML = formattedMessage;
|
|
// Show the toast
|
|
const toastBootstrap = bootstrap.Toast.getOrCreateInstance(toastLiveExample)
|
|
toastBootstrap.show()
|
|
|
|
|
|
},
|
|
error: function(xhr, status, error) {
|
|
console.error('AJAX request failed:', status, error);
|
|
// Update toast with error message
|
|
toastBody.textContent = 'Error: ' + error;
|
|
|
|
// Set toast to danger color
|
|
toastLiveExample.classList.remove('text-bg-success');
|
|
toastLiveExample.classList.add('text-bg-danger');
|
|
|
|
// Show the toast for errors too
|
|
const toastBootstrap = bootstrap.Toast.getOrCreateInstance(toastLiveExample);
|
|
toastBootstrap.show();
|
|
}
|
|
});
|
|
}
|
|
|
|
// ============================================
|
|
// SELF TEST FUNCTIONS
|
|
// ============================================
|
|
|
|
// 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.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_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
|
|
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} (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 delay(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(`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 delay(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' });
|
|
}
|
|
|
|
// 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 delay(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
|
|
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') {
|
|
// Noise sensor test
|
|
const noiseResult = await new Promise((resolve, reject) => {
|
|
$.ajax({
|
|
url: 'launcher.php?type=noise',
|
|
dataType: 'text',
|
|
method: 'GET',
|
|
timeout: 15000,
|
|
success: function(data) { resolve(data); },
|
|
error: function(xhr, status, error) { reject(new Error(error || status)); }
|
|
});
|
|
});
|
|
|
|
selfTestReport.rawResponses['Noise Sensor'] = noiseResult;
|
|
addSelfTestLog(`Noise response: ${noiseResult.trim()}`);
|
|
|
|
const noiseValue = parseFloat(noiseResult.trim());
|
|
if (!isNaN(noiseValue) && noiseValue > 0) {
|
|
updateTestStatus(sensor.id, 'Passed', `${noiseValue} dB`, 'bg-success');
|
|
testsPassed++;
|
|
} else if (noiseResult.trim() !== '') {
|
|
updateTestStatus(sensor.id, 'Warning', `Unexpected value: ${noiseResult.trim()}`, 'bg-warning');
|
|
testsFailed++;
|
|
} else {
|
|
updateTestStatus(sensor.id, 'Failed', 'No data received', 'bg-danger');
|
|
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++;
|
|
}
|
|
}
|
|
} 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 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) {
|
|
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>`;
|
|
|
|
// 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.getElementById('btn_selfTest').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)',
|
|
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);
|
|
}
|
|
|
|
|
|
</script>
|
|
|
|
</body>
|
|
</html>
|