Files
ota_ws_update/source/ota_ws_update_http_new.html
2025-09-12 12:19:46 +07:00

553 lines
20 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!DOCTYPE html>
<html lang="ru">
<head>
<title>OTA UPDATE - Обновление прошивки</title>
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta http-equiv="content-type" content="text/html; charset=utf-8" />
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
<style>
:root {
--primary: #4361ee;
--primary-dark: #3a56d4;
--secondary: #6c757d;
--success: #28a745;
--warning: #ffc107;
--danger: #dc3545;
--info: #17a2b8;
--light: #f8f9fa;
--dark: #343a40;
--background: #f5f7fa;
--card-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
--transition: all 0.3s ease;
}
* {
box-sizing: border-box;
margin: 0;
padding: 0;
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
}
body {
background-color: var(--background);
color: var(--dark);
line-height: 1.6;
padding: 0;
margin: 0;
min-height: 100vh;
display: flex;
flex-direction: column;
}
.container {
width: 100%;
max-width: 800px;
margin: 0 auto;
padding: 20px;
}
.header {
background: linear-gradient(135deg, var(--primary), var(--primary-dark));
color: white;
padding: 20px;
text-align: center;
border-radius: 10px;
margin-bottom: 25px;
box-shadow: var(--card-shadow);
}
.header h1 {
font-size: 1.8rem;
margin-bottom: 5px;
display: flex;
align-items: center;
justify-content: center;
gap: 10px;
}
.header p {
font-size: 1rem;
opacity: 0.9;
}
.card {
background-color: white;
border-radius: 10px;
padding: 25px;
margin-bottom: 20px;
box-shadow: var(--card-shadow);
transition: var(--transition);
}
.card:hover {
box-shadow: 0 6px 12px rgba(0, 0, 0, 0.15);
}
.btn {
display: inline-flex;
align-items: center;
justify-content: center;
gap: 8px;
padding: 14px 20px;
border: none;
border-radius: 8px;
font-size: 16px;
font-weight: 600;
cursor: pointer;
transition: var(--transition);
width: 100%;
margin-bottom: 12px;
}
.btn-primary {
background-color: var(--primary);
color: white;
}
.btn-primary:hover {
background-color: var(--primary-dark);
transform: translateY(-2px);
}
.btn-success {
background-color: var(--success);
color: white;
}
.btn-success:hover {
background-color: #218838;
transform: translateY(-2px);
}
.btn-warning {
background-color: var(--warning);
color: var(--dark);
}
.btn-warning:hover {
background-color: #e0a800;
transform: translateY(-2px);
}
.btn-danger {
background-color: var(--danger);
color: white;
}
.btn-danger:hover {
background-color: #c82333;
transform: translateY(-2px);
}
.btn-secondary {
background-color: var(--secondary);
color: white;
}
.btn-secondary:hover {
background-color: #5a6268;
transform: translateY(-2px);
}
.btn:disabled {
opacity: 0.6;
cursor: not-allowed;
transform: none !important;
}
.progress-container {
margin: 20px 0;
}
.progress-info {
display: flex;
justify-content: space-between;
margin-bottom: 8px;
font-size: 14px;
color: var(--secondary);
}
.progress-bar {
height: 20px;
background-color: #e9ecef;
border-radius: 10px;
overflow: hidden;
}
.progress {
height: 100%;
background: linear-gradient(90deg, var(--primary), var(--info));
border-radius: 10px;
transition: width 0.3s ease;
display: flex;
align-items: center;
justify-content: center;
color: white;
font-size: 12px;
font-weight: bold;
}
.status-card {
background-color: #f8f9fa;
border-left: 4px solid var(--info);
padding: 15px;
border-radius: 8px;
margin: 15px 0;
font-size: 14px;
}
.status-success {
border-left-color: var(--success);
background-color: rgba(40, 167, 69, 0.1);
}
.status-error {
border-left-color: var(--danger);
background-color: rgba(220, 53, 69, 0.1);
}
.status-warning {
border-left-color: var(--warning);
background-color: rgba(255, 193, 7, 0.1);
}
.connection-status {
display: flex;
align-items: center;
justify-content: center;
padding: 10px;
border-radius: 6px;
margin-bottom: 20px;
background-color: #f8f9fa;
font-weight: 500;
}
.connected {
color: var(--success);
background-color: rgba(40, 167, 69, 0.1);
}
.disconnected {
color: var(--danger);
background-color: rgba(220, 53, 69, 0.1);
}
.status-indicator {
width: 12px;
height: 12px;
border-radius: 50%;
margin-right: 8px;
}
.connected .status-indicator {
background-color: var(--success);
}
.disconnected .status-indicator {
background-color: var(--danger);
}
.file-info {
background-color: #e9ecef;
padding: 12px;
border-radius: 8px;
margin: 15px 0;
font-size: 14px;
display: flex;
align-items: center;
gap: 10px;
}
.file-info i {
color: var(--primary);
}
.section-title {
font-size: 18px;
margin-bottom: 15px;
color: var(--dark);
display: flex;
align-items: center;
gap: 10px;
}
.hidden {
display: none;
}
.footer {
text-align: center;
margin-top: auto;
padding: 20px;
color: var(--secondary);
font-size: 0.9rem;
}
@media (min-width: 768px) {
.btn-container {
display: flex;
gap: 12px;
}
.btn-container .btn {
width: 50%;
margin-bottom: 0;
}
.header h1 {
font-size: 2.2rem;
}
}
</style>
</head>
<body>
<div class="container">
<div class="header">
<h1><i class="fas fa-sync-alt"></i> OTA UPDATE</h1>
<p>Обновление прошивки устройства по воздуху</p>
</div>
<div class="connection-status disconnected">
<span class="status-indicator"></span>
<span id="status-text">Не подключено</span>
</div>
<div id="rollback" class="card hidden">
<h2 class="section-title"><i class="fas fa-exclamation-triangle"></i> Подтверждение обновления</h2>
<div class="status-card status-warning">
<p>Обновление было загружено, но не подтверждено. Пожалуйста, подтвердите установку новой прошивки или откатитесь к предыдущей версии.</p>
</div>
<button class="btn btn-success" id="otaVerifyApp">
<i class="fas fa-check-circle"></i> Подтвердить и установить обновление
</button>
<button class="btn btn-danger" id="otaRollback">
<i class="fas fa-undo"></i> Отменить обновление и откатиться
</button>
</div>
<div id="update" class="card">
<h2 class="section-title"><i class="fas fa-file-upload"></i> Загрузка прошивки</h2>
<input type="file" id="otaFile" class="hidden" accept=".bin" onchange="readOtaFile(this)">
<button class="btn btn-primary" id="otaFileSelect" onclick="document.getElementById('otaFile').click()">
<i class="fas fa-folder-open"></i> Выбрать файл прошивки
</button>
<div id="fileInfo" class="file-info hidden">
<i class="fas fa-file-code"></i>
<span id="fileName">Файл не выбран</span>
</div>
<div id="otaProgressVisible" class="hidden">
<div class="progress-container">
<div class="progress-info">
<span id="progressStatus">Загрузка...</span>
<span id="progressPercent">0%</span>
</div>
<div class="progress-bar">
<div class="progress" id="otaProgress" style="width: 0%">0%</div>
</div>
</div>
</div>
<div class="btn-container">
<button class="btn btn-warning" id="otaStartCancel">
<i class="fas fa-play-circle"></i> Начать обновление
</button>
<button class="btn btn-success hidden" id="otaReStart">
<i class="fas fa-power-off"></i> Перезагрузить с новой прошивкой
</button>
</div>
</div>
<button class="btn btn-secondary" id="goHome">
<i class="fas fa-home"></i> Вернуться на главную
</button>
</div>
<div class="footer">
<p>NVS Device Management &copy; 2023</p>
</div>
<script>
let otaData;
let otaSetChunkSize = 0;
let otaStartsegment = 0;
let otaStarted = 0;
function readOtaFile(input) {
if (!input.files.length) return;
let reader = new FileReader();
let file = input.files[0];
// Показываем информацию о файле
document.getElementById('fileInfo').classList.remove('hidden');
document.getElementById('fileName').textContent = `${file.name} (${formatFileSize(file.size)})`;
document.getElementById('otaFileSelect').innerHTML = `<i class="fas fa-check-circle"></i> Файл выбран: ${file.name}`;
reader.readAsArrayBuffer(file);
input.value = null;
reader.onload = function () {
otaData = new Uint8Array(reader.result);
document.getElementById("otaStartCancel").classList.remove('hidden');
document.getElementById("otaProgressVisible").classList.add('hidden');
document.getElementById("otaReStart").classList.add('hidden');
};
reader.onerror = function () {
console.log(reader.error);
showStatus("Ошибка чтения файла", "error");
};
}
function formatFileSize(bytes) {
if (bytes < 1024) return bytes + " байт";
else if (bytes < 1048576) return (bytes / 1024).toFixed(2) + " КБ";
else return (bytes / 1048576).toFixed(2) + " МБ";
}
function updateProgress(value, max) {
const percent = Math.round((value / max) * 100);
const progressBar = document.getElementById("otaProgress");
progressBar.style.width = percent + "%";
progressBar.textContent = percent + "%";
document.getElementById("progressPercent").textContent = percent + "%";
document.getElementById("progressStatus").textContent = `Загружено: ${formatFileSize(value)} из ${formatFileSize(max)}`;
}
function showStatus(message, type = "info") {
// Здесь можно реализовать отображение статусных сообщений
console.log(`${type}: ${message}`);
}
// Обработчики событий
document.getElementById("otaStartCancel").addEventListener("click", function (e) {
if (otaData && otaData.length > 0 && otaStarted == 0) {
socket.send(JSON.stringify({ name: "otaSize", value: otaData.length }));
otaStarted = 1;
this.innerHTML = "<i class='fas fa-times-circle'></i> Отменить загрузку";
document.getElementById("otaFileSelect").disabled = true;
document.getElementById("otaProgressVisible").classList.remove("hidden");
updateProgress(0, otaData.length);
} else {
otaStarted = 0;
socket.send(JSON.stringify({ name: "otaCancel", value: "Cancel" }));
this.innerHTML = "<i class='fas fa-play-circle'></i> Начать обновление";
document.getElementById("otaFileSelect").disabled = false;
}
});
document.getElementById("goHome").addEventListener("click", function (e) {
socket.close();
window.location.href = '/';
});
document.getElementById("otaReStart").addEventListener("click", function (e) {
socket.send(JSON.stringify({ name: "otaRestartEsp", value: "restart" }));
});
// Rollback handlers
document.getElementById("otaVerifyApp").addEventListener("click", function (e) {
socket.send(JSON.stringify({ name: "otaProcessRollback", value: "false" }));
document.getElementById("rollback").classList.add("hidden");
document.getElementById("update").classList.remove("hidden");
});
document.getElementById("otaRollback").addEventListener("click", function (e) {
socket.send(JSON.stringify({ name: "otaProcessRollback", value: "true" }));
document.getElementById("rollback").classList.add("hidden");
document.getElementById("update").classList.remove("hidden");
});
function receiveWsData(data) {
try {
let obj = JSON.parse(data);
switch (obj.name) {
case "otaSetChunkSize":
otaSetChunkSize = obj.value;
break;
case "otaGetChunk":
let otaDataSend = otaData.subarray(obj.value, obj.value + otaSetChunkSize);
updateProgress(obj.value, otaData.length);
document.getElementById("otaStartCancel").innerHTML = `<i class='fas fa-times-circle'></i> Загрузка: ${formatFileSize(obj.value)} из ${formatFileSize(otaData.length)}`;
socket.send(otaDataSend);
break;
case "otaEnd":
otaStartsegment = 0;
otaStarted = 0;
document.getElementById("otaStartCancel").classList.add("hidden");
document.getElementById("otaStartCancel").innerHTML = "<i class='fas fa-play-circle'></i> Начать обновление";
updateProgress(otaData.length, otaData.length);
document.getElementById("otaFileSelect").disabled = false;
document.getElementById("otaReStart").classList.remove("hidden");
document.getElementById("otaReStart").innerHTML = "<i class='fas fa-power-off'></i> Прошивка загружена. Перезагрузить устройство";
document.getElementById("otaReStart").disabled = false;
showStatus("Прошивка успешно загружена", "success");
break;
case "otaError":
case "otaCancel":
otaStartsegment = 0;
otaStarted = 0;
document.getElementById("otaStartCancel").classList.remove("hidden");
document.getElementById("otaStartCancel").innerHTML = "<i class='fas fa-play-circle'></i> Начать обновление";
document.getElementById("otaFileSelect").disabled = false;
document.getElementById("otaReStart").classList.remove("hidden");
document.getElementById("otaReStart").innerHTML = "Загрузка отменена: " + obj.value;
document.getElementById("otaReStart").disabled = true;
showStatus("Загрузка отменена: " + obj.value, "error");
break;
case "otaCheckRollback":
document.getElementById("rollback").classList.remove("hidden");
document.getElementById("update").classList.add("hidden");
break;
}
} catch {
console.log(data + "Error msg");
}
}
// WebSocket connection
let protocol = "ws:";
if (document.location.protocol == "https:") protocol = "wss:";
let wsHostStr = protocol + "//" + document.location.host + document.location.pathname;
wsHostStr += (document.location.pathname == '/') ? "ws" : "/ws";
var socket = new WebSocket(wsHostStr);
socket.binaryType = "arraybuffer";
// WebSocket events
socket.onopen = function () {
console.log("connect ws");
document.getElementById("status-text").textContent = "Подключено";
document.querySelector(".connection-status").classList.remove("disconnected");
document.querySelector(".connection-status").classList.add("connected");
};
socket.onclose = function (event) {
console.log("close ws - reload");
document.getElementById("status-text").textContent = "Не подключено";
document.querySelector(".connection-status").classList.remove("connected");
document.querySelector(".connection-status").classList.add("disconnected");
setTimeout(() => document.location.reload(), 2000);
};
socket.onerror = function () {
console.log("error ws");
document.getElementById("status-text").textContent = "Ошибка подключения";
document.querySelector(".connection-status").classList.remove("connected");
document.querySelector(".connection-status").classList.add("disconnected");
setTimeout(() => document.location.reload(), 2000);
};
socket.onmessage = function (event) {
receiveWsData(event.data);
};
</script>
</body>
</html>