504 lines
18 KiB
HTML
504 lines
18 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>ATA Firmware Update</title>
|
|
<style>
|
|
body {
|
|
font-family: Arial, sans-serif;
|
|
margin: 0;
|
|
padding: 20px;
|
|
background-color: #f4f4f4;
|
|
text-align: center;
|
|
}
|
|
|
|
h1 {
|
|
font-size: 22px;
|
|
margin-bottom: 5px;
|
|
}
|
|
|
|
.status-container {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: left;
|
|
margin-bottom: 4px;
|
|
}
|
|
|
|
.status-indicator-ble {
|
|
width: 20px;
|
|
height: 20px;
|
|
border-radius: 50%;
|
|
background-color: gray;
|
|
margin-right: 10px;
|
|
}
|
|
|
|
.status-indicator-wifi {
|
|
width: 20px;
|
|
height: 20px;
|
|
border-radius: 50%;
|
|
background-color: gray;
|
|
margin-right: 10px;
|
|
}
|
|
|
|
.status-indicator-internet {
|
|
width: 20px;
|
|
height: 20px;
|
|
border-radius: 50%;
|
|
background-color: gray;
|
|
margin-right: 10px;
|
|
}
|
|
|
|
.btn-container {
|
|
display: flex;
|
|
flex-wrap: wrap;
|
|
justify-content: center;
|
|
gap: 10px;
|
|
margin-bottom: 10px;
|
|
}
|
|
|
|
/* Adds space above the WiFi Connect button */
|
|
.btn-container.wifi {
|
|
margin-top: 20px;
|
|
}
|
|
|
|
button {
|
|
flex: 1;
|
|
max-width: 130px;
|
|
padding: 10px;
|
|
font-size: 16px;
|
|
border: none;
|
|
border-radius: 5px;
|
|
cursor: pointer;
|
|
background-color: #007bff;
|
|
color: white;
|
|
transition: background 0.3s ease;
|
|
}
|
|
|
|
button:disabled {
|
|
background-color: #ccc;
|
|
}
|
|
|
|
button:hover:not(:disabled) {
|
|
background-color: #0056b3;
|
|
}
|
|
|
|
textarea {
|
|
width: 100%;
|
|
height: 300px;
|
|
font-size: 14px;
|
|
padding: 10px;
|
|
border-radius: 5px;
|
|
border: 1px solid #ccc;
|
|
resize: none;
|
|
}
|
|
|
|
.input-container {
|
|
display: flex;
|
|
flex-direction: column;
|
|
align-items: center;
|
|
gap: 10px;
|
|
margin-top: 15px;
|
|
}
|
|
|
|
input {
|
|
width: 90%;
|
|
max-width: 300px;
|
|
padding: 10px;
|
|
font-size: 16px;
|
|
border: 1px solid #ccc;
|
|
border-radius: 5px;
|
|
text-align: center;
|
|
}
|
|
|
|
input::placeholder {
|
|
text-align: center;
|
|
}
|
|
|
|
@media (max-width: 480px) {
|
|
body {
|
|
padding: 15px;
|
|
}
|
|
|
|
h1 {
|
|
font-size: 20px;
|
|
}
|
|
|
|
button {
|
|
font-size: 14px;
|
|
padding: 8px;
|
|
}
|
|
|
|
input, textarea {
|
|
font-size: 14px;
|
|
}
|
|
}
|
|
|
|
/* Tabs */
|
|
.tab-bar { display:flex; gap:8px; justify-content:center; margin:12px 0 16px; flex-wrap:wrap; }
|
|
.tab-bar button { max-width:none; flex:0 0 auto; background:#6c757d; }
|
|
.tab-bar button.active { background:#007bff; }
|
|
.tab-panel { display:none; }
|
|
.tab-panel.active { display:block; }
|
|
</style>
|
|
</head>
|
|
<body>
|
|
|
|
<h1>ATA Firmware Update</h1>
|
|
|
|
<!-- Tab Buttons -->
|
|
<div class="tab-bar">
|
|
<button class="tab-btn active" data-tab="tab-upgrade">Upgrade</button>
|
|
<button class="tab-btn" data-tab="tab-wifi">WiFi Comm</button>
|
|
</div>
|
|
|
|
<div id="tab-upgrade" class="tab-panel active">
|
|
<!-- Status Indicators -->
|
|
<div class="status-container">
|
|
<span class="status-indicator-ble"></span>
|
|
<label id="status-ble-connection">BLE Status: ...</label>
|
|
</div>
|
|
|
|
<div class="status-container">
|
|
<span class="status-indicator-wifi"></span>
|
|
<label id="status-wifi-client">Wifi Client: ...</label>
|
|
</div>
|
|
|
|
<div class="status-container">
|
|
<span class="status-indicator-internet"></span>
|
|
<label id="status-internet">Internet: ...</label>
|
|
</div>
|
|
|
|
<div class="status-container">
|
|
<label id="status-current-version">Curr Version: ...</label>
|
|
</div>
|
|
<div class="status-container">
|
|
<label id="status-new-version">New Version: ...</label>
|
|
</div>
|
|
|
|
<div class="btn-container">
|
|
<label id="ble-device-name">Device Name:</label>
|
|
</div>
|
|
|
|
<div class="btn-container">
|
|
<input type="text" id="input-DeviceName" placeholder="..." style="width: 100%; max-width: 220px;" required>
|
|
</div>
|
|
|
|
<!-- Buttons -->
|
|
<div class="btn-container">
|
|
<button id="bleConnectBtn" onclick="connectToBle()">Connect</button>
|
|
<button id="checkStatusBtn" onclick="checkStatus()" disabled>Check Status</button>
|
|
</div>
|
|
|
|
<!-- Log Area -->
|
|
<textarea id="logArea" readonly></textarea>
|
|
|
|
<div class="btn-container">
|
|
<button id="checkVersionBtn" onclick="checkVersion()" disabled>Check Version</button>
|
|
<button id="startUpgradeBtn" onclick="startUpgrade()" disabled>Start Update</button>
|
|
</div>
|
|
</div> <!-- /tab-upgrade -->
|
|
|
|
<div id="tab-wifi" class="tab-panel">
|
|
<h2 style="margin-top:0; font-size:18px;">WiFi Connection</h2>
|
|
<div class="input-container">
|
|
<input type="text" id="wifissid" name="wifissid" placeholder="Enter WiFi SSID" required>
|
|
<input type="password" id="wifipassword" name="wifipassword" placeholder="Enter WiFi Password" required>
|
|
<div style="display: flex; align-items: center; gap: 5px;">
|
|
<input type="checkbox" id="showPassword" onclick="togglePasswordVisibility()" style="width: auto;">
|
|
<label for="showPassword">Show Password</label>
|
|
</div>
|
|
</div>
|
|
<div class="btn-container wifi">
|
|
<button id="wifiConnectBtn" onclick="wifiConnect()" disabled>Connect Wifi</button>
|
|
</div>
|
|
</div><!-- /tab-wifi -->
|
|
|
|
<script>
|
|
(function(){
|
|
'use strict';
|
|
|
|
/* ================= Constants & Packet Layout ================= */
|
|
const BLE_SERVER_NAME = "ATALIGHTS"; // Keep hardcoded per instruction (ignore external JSON)
|
|
const BLE_SERVICE_UUID = "abcdef01-2345-6789-1234-56789abcdef0";
|
|
const BLE_CHARACTERISTIC1_UUID = "abcdef01-2345-6789-1234-56789abcdef1"; // Control / status
|
|
const BLE_CHARACTERISTIC2_UUID = "abcdef02-2345-6789-1234-56789abcdef1"; // Logs / events
|
|
|
|
// Packet layout (mirrors firmware struct updateStatus)
|
|
// byte 0 : wifiStatus (enum)
|
|
// byte 1 : wifiOnline (bool)
|
|
// bytes2-5: wifiIP
|
|
// bytes6-8: currVersion (major,minor,patch)
|
|
// bytes9-11: newVersion (major,minor,patch)
|
|
// bytes12-31: wifiSSID (20 bytes, null padded)
|
|
const PACKET_LEN = 32;
|
|
const OFF_WIFI_STATUS = 0;
|
|
const OFF_WIFI_ONLINE = 1;
|
|
const OFF_WIFI_IP = 2;
|
|
const OFF_CURR_VER = 6;
|
|
const OFF_NEW_VER = 9;
|
|
const OFF_WIFI_SSID = 12;
|
|
|
|
const WIFI_STAT = { DISCONNECTED:0, BAD_CREDS:1, NO_AP:2, CONNECTED:3 };
|
|
const WIFI_STAT_TEXT = ["Disconnected","Bad Creds","No AP","Connected"];
|
|
const MAX_LOG_LINES = 400;
|
|
|
|
/* ================= State ================= */
|
|
let bleDevice=null, bleCharacteristic1=null, bleCharacteristic2=null;
|
|
let bleConnected=false;
|
|
const state = {
|
|
wifiStatus: WIFI_STAT.DISCONNECTED,
|
|
wifiOnline:false,
|
|
wifiIP:[0,0,0,0],
|
|
currVersion:[0,0,0],
|
|
newVersion:[0,0,0],
|
|
wifiSSID:""
|
|
};
|
|
|
|
/* ================= Cached DOM ================= */
|
|
const el = {};
|
|
function cacheDom(){
|
|
el.bleIndicator = document.querySelector('.status-indicator-ble');
|
|
el.wifiIndicator = document.querySelector('.status-indicator-wifi');
|
|
el.internetIndicator = document.querySelector('.status-indicator-internet');
|
|
el.lblBle = document.getElementById('status-ble-connection');
|
|
el.lblWifi = document.getElementById('status-wifi-client');
|
|
el.lblInternet = document.getElementById('status-internet');
|
|
el.lblCurrVer = document.getElementById('status-current-version');
|
|
el.lblNewVer = document.getElementById('status-new-version');
|
|
el.inDeviceName = document.getElementById('input-DeviceName');
|
|
el.inSsid = document.getElementById('wifissid');
|
|
el.inPass = document.getElementById('wifipassword');
|
|
el.chkShowPass = document.getElementById('showPassword');
|
|
el.btnBleConnect = document.getElementById('bleConnectBtn');
|
|
el.btnCheckStatus = document.getElementById('checkStatusBtn');
|
|
el.btnCheckVersion = document.getElementById('checkVersionBtn');
|
|
el.btnStartUpgrade = document.getElementById('startUpgradeBtn');
|
|
el.btnWifiConnect = document.getElementById('wifiConnectBtn');
|
|
el.logArea = document.getElementById('logArea');
|
|
}
|
|
|
|
/* ================= Utilities ================= */
|
|
function logMessage(msg){
|
|
const lines = el.logArea.value.trim().length ? el.logArea.value.split(/\n/) : [];
|
|
lines.push(msg);
|
|
if(lines.length > MAX_LOG_LINES){
|
|
lines.splice(0, lines.length - MAX_LOG_LINES);
|
|
}
|
|
el.logArea.value = lines.join('\n') + '\n';
|
|
el.logArea.scrollTop = el.logArea.scrollHeight;
|
|
}
|
|
|
|
function compareVersions(a,b){
|
|
for(let i=0;i<3;i++){ if(a[i]>b[i]) return 1; if(a[i]<b[i]) return -1; }
|
|
return 0;
|
|
}
|
|
|
|
function colorIndicator(elm, color){ if(elm) elm.style.backgroundColor = color; }
|
|
|
|
function ipToString(ip){ return ip.join('.'); }
|
|
|
|
/* ================= Packet Handling ================= */
|
|
function parsePacket(data){
|
|
if(data.length !== PACKET_LEN) return false;
|
|
state.wifiStatus = data[OFF_WIFI_STATUS];
|
|
if(state.wifiStatus > WIFI_STAT.CONNECTED) state.wifiStatus = WIFI_STAT.DISCONNECTED; // clamp
|
|
state.wifiOnline = !!data[OFF_WIFI_ONLINE];
|
|
state.wifiIP = Array.from(data.slice(OFF_WIFI_IP, OFF_WIFI_IP+4));
|
|
state.currVersion = Array.from(data.slice(OFF_CURR_VER, OFF_CURR_VER+3));
|
|
state.newVersion = Array.from(data.slice(OFF_NEW_VER, OFF_NEW_VER+3));
|
|
// Extract SSID (stop at first 0)
|
|
let rawSsidBytes = data.slice(OFF_WIFI_SSID, OFF_WIFI_SSID+20);
|
|
let zeroIndex = rawSsidBytes.indexOf(0);
|
|
if(zeroIndex >= 0) rawSsidBytes = rawSsidBytes.slice(0, zeroIndex);
|
|
state.wifiSSID = rawSsidBytes.length ? String.fromCharCode(...rawSsidBytes) : "";
|
|
return true;
|
|
}
|
|
|
|
function updateUI(){
|
|
// BLE
|
|
el.lblBle.textContent = 'BLE Status: ' + (bleConnected ? 'Connected' : 'Disconnected');
|
|
colorIndicator(el.bleIndicator, bleConnected ? 'green' : 'gray');
|
|
|
|
// WiFi client
|
|
const statText = WIFI_STAT_TEXT[state.wifiStatus] || 'Unknown';
|
|
if(state.wifiStatus === WIFI_STAT.CONNECTED){
|
|
const ssidPart = state.wifiSSID ? ' SSID: '+state.wifiSSID : '';
|
|
el.lblWifi.textContent = 'Wifi Client: ' + statText + (state.wifiIP[0] ? ' ('+ipToString(state.wifiIP)+')':'' ) + ssidPart;
|
|
colorIndicator(el.wifiIndicator, 'green');
|
|
} else if(state.wifiStatus === WIFI_STAT.BAD_CREDS){
|
|
el.lblWifi.textContent = 'Wifi Client: Bad Credentials';
|
|
colorIndicator(el.wifiIndicator, 'orange');
|
|
} else if(state.wifiStatus === WIFI_STAT.NO_AP){
|
|
el.lblWifi.textContent = 'Wifi Client: AP Not Found';
|
|
colorIndicator(el.wifiIndicator, 'orange');
|
|
} else {
|
|
el.lblWifi.textContent = 'Wifi Client: ' + statText;
|
|
colorIndicator(el.wifiIndicator, 'gray');
|
|
}
|
|
|
|
// Internet
|
|
el.lblInternet.textContent = state.wifiOnline ? 'Online' : 'Offline';
|
|
colorIndicator(el.internetIndicator, state.wifiOnline ? 'green' : 'gray');
|
|
|
|
// Versions
|
|
el.lblCurrVer.textContent = state.currVersion[0] ? 'Curr Version: ' + state.currVersion.join('.') : 'Curr Version: ...';
|
|
el.lblNewVer.textContent = state.newVersion[0] ? 'New Version: ' + state.newVersion.join('.') : 'New Version: ...';
|
|
|
|
// Buttons
|
|
el.btnCheckStatus.disabled = !bleConnected;
|
|
el.btnWifiConnect.disabled = !bleConnected;
|
|
el.btnCheckVersion.disabled = !(bleConnected && state.wifiOnline);
|
|
const newerAvail = state.newVersion[0] && state.wifiOnline && compareVersions(state.newVersion, state.currVersion) > 0;
|
|
el.btnStartUpgrade.disabled = !newerAvail;
|
|
}
|
|
|
|
/* ================= BLE Operations ================= */
|
|
async function connectToBle(){
|
|
if(!navigator.bluetooth){ logMessage('Web Bluetooth not supported.'); return; }
|
|
try{
|
|
bleDevice = await navigator.bluetooth.requestDevice({
|
|
filters:[{ name: el.inDeviceName.value || BLE_SERVER_NAME }],
|
|
optionalServices:[BLE_SERVICE_UUID]
|
|
});
|
|
const server = await bleDevice.gatt.connect();
|
|
const service = await server.getPrimaryService(BLE_SERVICE_UUID);
|
|
bleCharacteristic1 = await service.getCharacteristic(BLE_CHARACTERISTIC1_UUID);
|
|
bleCharacteristic2 = await service.getCharacteristic(BLE_CHARACTERISTIC2_UUID);
|
|
await bleCharacteristic2.startNotifications();
|
|
bleCharacteristic2.addEventListener('characteristicvaluechanged', e => {
|
|
try{
|
|
const txt = new TextDecoder().decode(e.target.value);
|
|
console.log('--> ' + txt);
|
|
logMessage('--> ' + txt.trim());
|
|
}catch(_){ /* ignore */ }
|
|
});
|
|
bleConnected=true;
|
|
bleDevice.addEventListener('gattserverdisconnected', onDisconnect);
|
|
const connectedName = bleDevice.name || el.inDeviceName.value || 'Device';
|
|
logMessage('Connected to ' + connectedName);
|
|
const nameLabel = document.getElementById('ble-device-name');
|
|
if(nameLabel){ nameLabel.textContent = 'Device Name: ' + connectedName; }
|
|
await readPacket();
|
|
updateUI();
|
|
}catch(err){
|
|
logMessage( err.message.includes('cancel') ? 'Connection cancelled.' : ('Connection failed: '+err.message) );
|
|
}
|
|
}
|
|
|
|
function onDisconnect(){
|
|
bleConnected=false; updateUI(); logMessage('BLE disconnected');
|
|
}
|
|
|
|
async function sendPacket(msg){
|
|
if(!bleCharacteristic1) return;
|
|
const enc = new TextEncoder();
|
|
for(let attempt=0; attempt<3; attempt++){
|
|
try{ await bleCharacteristic1.writeValueWithResponse(enc.encode(msg)); return true; }
|
|
catch(e){ if(attempt===2) logMessage('Send failed: '+e.message); else await delay(1000); }
|
|
}
|
|
return false;
|
|
}
|
|
|
|
async function readPacket(){
|
|
if(!bleCharacteristic1) return false;
|
|
for(let attempt=0; attempt<3; attempt++){
|
|
try{
|
|
const val = await bleCharacteristic1.readValue();
|
|
const data = new Uint8Array(val.buffer);
|
|
if(parsePacket(data)) return true; else { logMessage('Packet parse failed (len='+data.length+')'); return false; }
|
|
}catch(e){ if(attempt===2) logMessage('Read failed: '+e.message); else await delay(1000); }
|
|
}
|
|
return false;
|
|
}
|
|
|
|
/* ================= Actions ================= */
|
|
async function wifiConnect(){
|
|
const ssid = el.inSsid.value.trim();
|
|
const pw = el.inPass.value;
|
|
if(!ssid || !pw){ alert('Enter SSID & password'); return; }
|
|
logMessage('Sending WiFi credentials...');
|
|
el.btnWifiConnect.disabled = true; // prevent multiple submissions while polling
|
|
await sendPacket('wifi-connect {"ssid":"'+ssid+'","pass":"'+pw+'"} ');
|
|
// Poll for status for up to 15s
|
|
const start = Date.now();
|
|
while(Date.now()-start < 15000){
|
|
await delay(1000);
|
|
await readPacket();
|
|
updateUI();
|
|
if(state.wifiStatus === WIFI_STAT.CONNECTED){
|
|
logMessage('WiFi Connected: '+ipToString(state.wifiIP));
|
|
break;
|
|
}
|
|
if(state.wifiStatus === WIFI_STAT.BAD_CREDS){ logMessage('WiFi Error: Bad Credentials'); break; }
|
|
if(state.wifiStatus === WIFI_STAT.NO_AP){ logMessage('WiFi Error: AP Not Found'); break; }
|
|
}
|
|
if(state.wifiStatus !== WIFI_STAT.CONNECTED){ logMessage('WiFi connect attempt finished with status: '+WIFI_STAT_TEXT[state.wifiStatus]); }
|
|
if(!bleConnected) return; // keep disabled if BLE disconnected during process
|
|
// Re-enable for retry unless connected
|
|
if(state.wifiStatus !== WIFI_STAT.CONNECTED){ el.btnWifiConnect.disabled = false; }
|
|
}
|
|
|
|
async function checkStatus(){ if(await readPacket()) updateUI(); }
|
|
|
|
async function checkVersion(){
|
|
el.btnCheckVersion.disabled = true;
|
|
logMessage('Checking for new version...');
|
|
await sendPacket('version-check');
|
|
const start = Date.now();
|
|
while(Date.now()-start < 15000){
|
|
await delay(750);
|
|
await readPacket();
|
|
if(state.newVersion[0]){ logMessage('Latest version: '+state.newVersion.join('.')); break; }
|
|
}
|
|
if(!state.newVersion[0]) logMessage('No new version info received');
|
|
updateUI();
|
|
}
|
|
|
|
async function startUpgrade(){
|
|
if(el.btnStartUpgrade.disabled) return;
|
|
logMessage('Starting upgrade...');
|
|
el.btnStartUpgrade.disabled = true;
|
|
await sendPacket('upgrade-start');
|
|
// Progress will arrive via characteristic2 logs
|
|
}
|
|
|
|
/* ================= Helpers ================= */
|
|
const delay = ms => new Promise(r=>setTimeout(r,ms));
|
|
|
|
function togglePasswordVisibility(){ el.inPass.type = el.chkShowPass.checked ? 'text' : 'password'; }
|
|
|
|
function init(){
|
|
cacheDom();
|
|
el.inDeviceName.value = BLE_SERVER_NAME;
|
|
el.chkShowPass.addEventListener('change', togglePasswordVisibility);
|
|
// Inline onclicks already wired; ensure functions are in scope
|
|
// Tab switching
|
|
document.querySelectorAll('.tab-btn').forEach(btn=>{
|
|
btn.addEventListener('click', ()=>{
|
|
document.querySelectorAll('.tab-btn').forEach(b=>b.classList.remove('active'));
|
|
document.querySelectorAll('.tab-panel').forEach(p=>p.classList.remove('active'));
|
|
btn.classList.add('active');
|
|
const id = btn.getAttribute('data-tab');
|
|
const panel = document.getElementById(id);
|
|
if(panel) panel.classList.add('active');
|
|
});
|
|
});
|
|
updateUI();
|
|
logMessage('Ready. Enter device name or use default and press Connect.');
|
|
}
|
|
|
|
// Expose functions for inline handlers
|
|
window.connectToBle = connectToBle;
|
|
window.checkStatus = checkStatus;
|
|
window.checkVersion = checkVersion;
|
|
window.startUpgrade = startUpgrade;
|
|
window.wifiConnect = wifiConnect;
|
|
window.togglePasswordVisibility = togglePasswordVisibility;
|
|
|
|
window.addEventListener('DOMContentLoaded', init);
|
|
})();
|
|
</script>
|
|
</body>
|
|
</html>
|