9#include <ArduinoJson.h>
35 webServer.on(
"/api/ota/check", HTTP_POST, []()
40 webServer.send(403,
"application/json",
"{\"error\":\"unavailable\"}");
44 webServer.send(200,
"application/json",
"{\"ok\":true}");
48 webServer.on(
"/api/ota/install", HTTP_POST, []()
53 webServer.send(403,
"application/json",
"{\"error\":\"unavailable\"}");
59 webServer.send(200,
"application/json",
"{\"ok\":true}");
75 html +=
"<h1>🚀 Обновления прошивки</h1>";
78 html +=
"<div class='info-block' style='background:#f8f9fa;padding:15px;border-radius:8px;margin-bottom:20px;'>";
79 html +=
"<div style='display:flex;justify-content:space-between;align-items:center;'>";
81 html +=
"<div id='otaStatus' style='color:#666;font-style:italic;'>Загрузка статуса...</div>";
82 html +=
"</div></div>";
85 html +=
"<div id='progressContainer' style='display:none;margin-bottom:20px;'>";
86 html +=
"<div style='margin-bottom:8px;'><span id='progressText'>Прогресс</span></div>";
87 html +=
"<div style='width:100%;background:#e9ecef;border-radius:6px;overflow:hidden;height:24px;'>";
88 html +=
"<div id='progressFill' style='width:0%;height:100%;background:linear-gradient(90deg,#28a745,#20c997);transition:width 0.3s;display:flex;align-items:center;justify-content:center;color:white;font-weight:bold;font-size:12px;'></div>";
89 html +=
"</div></div>";
92 html +=
"<div class='section' style='background:white;padding:20px;border-radius:8px;box-shadow:0 2px 4px rgba(0,0,0,0.1);margin-bottom:20px;'>";
93 html +=
"<h3 style='margin-top:0;color:#495057;'>🌐 Обновление с сервера</h3>";
96 html +=
"<div style='display:flex;gap:10px;flex-wrap:wrap;'>";
99 btnCheck.replace(
"<button ",
"<button id='btnCheck' ");
102 html +=
"<button id='btnInstall' style='display:none;' class='btn btn-success'>⬇️ Скачать и установить</button>";
103 html +=
"</div></div>";
106 html +=
"<div class='section' style='background:white;padding:20px;border-radius:8px;box-shadow:0 2px 4px rgba(0,0,0,0.1);'>";
107 html +=
"<h3 style='margin-top:0;color:#495057;'>📁 Локальное обновление</h3>";
108 html +=
"<p style='color:#6c757d;margin-bottom:15px;'>Загрузите файл прошивки (.bin) с вашего компьютера</p>";
109 html +=
"<div style='margin-bottom:15px;'>";
110 html +=
"<input type='file' name='firmware' accept='.bin' required style='width:100%;padding:10px;border:2px dashed #dee2e6;border-radius:6px;background:#f8f9fa;'>";
112 html +=
"<form id='uploadForm' enctype='multipart/form-data'>";
117 html +=
"</form></div>";
120 html +=
"<script>\n";
121 html +=
"let isOtaActive = false;\n";
122 html +=
"let updateAvailable = false;\n";
124 html +=
"function showProgress(text, percent) {\n";
125 html +=
" const container = document.getElementById('progressContainer');\n";
126 html +=
" const fill = document.getElementById('progressFill');\n";
127 html +=
" const textEl = document.getElementById('progressText');\n";
128 html +=
" container.style.display = 'block';\n";
129 html +=
" textEl.textContent = text;\n";
130 html +=
" fill.style.width = percent + '%';\n";
131 html +=
" fill.textContent = percent + '%';\n";
134 html +=
"function hideProgress() {\n";
135 html +=
" document.getElementById('progressContainer').style.display = 'none';\n";
138 html +=
"function updateStatus() {\n";
139 html +=
" fetch('/api/ota/status').then(r=>r.json()).then(j=>{\n";
140 html +=
" const status = j.status;\n";
141 html +=
" const statusEl = document.getElementById('otaStatus');\n";
142 html +=
" statusEl.textContent = status;\n";
143 html +=
" // Сброс цвета: по-умолчанию серый, перезаписываем только при ошибке\n";
144 html +=
" statusEl.style.color = '#666';\n";
146 html +=
" // Обработка прогресса\n";
147 html +=
" if (j.localUpload) {\n";
148 html +=
" // Локальная загрузка\n";
149 html +=
" if (j.total > 0) {\n";
150 html +=
" const percent = Math.round((j.progress * 100) / j.total);\n";
151 html +=
" showProgress('Загрузка файла: ' + Math.round(j.progress/1024) + ' КБ из ' + Math.round(j.total/1024) + ' КБ', percent);\n";
152 html +=
" } else {\n";
153 html +=
" showProgress('Загрузка файла: ' + Math.round(j.progress/1024) + ' КБ', 50);\n";
155 html +=
" isOtaActive = true;\n";
156 html +=
" } else if (status.includes('Загружено') && status.includes('%')) {\n";
157 html +=
" // Удаленная загрузка с процентами\n";
158 html +=
" const percent = parseInt(status.match(/\\d+/)[0]);\n";
159 html +=
" showProgress('Загрузка обновления', percent);\n";
160 html +=
" isOtaActive = true;\n";
161 html +=
" } else if (status.includes('Загружено') && status.includes('КБ')) {\n";
162 html +=
" // Удаленная загрузка без размера\n";
163 html +=
" showProgress('Загрузка обновления: ' + status, 50);\n";
164 html +=
" isOtaActive = true;\n";
165 html +=
" } else if (['Подключение', 'Загрузка', 'Проверка', 'Завершение', 'Завершение установки', 'Проверка обновлений'].includes(status)) {\n";
166 html +=
" // Этапы OTA\n";
167 html +=
" const stages = {'Подключение': 25, 'Загрузка': 50, 'Проверка': 75, 'Завершение': 90, 'Завершение установки': 95, 'Проверка обновлений': 30};\n";
168 html +=
" showProgress(status + '...', stages[status] || 25);\n";
169 html +=
" isOtaActive = true;\n";
170 html +=
" } else {\n";
171 html +=
" // Завершенные состояния\n";
172 html +=
" hideProgress();\n";
174 html +=
" if (isOtaActive) {\n";
175 html +=
" if (status.includes('Ошибка') || status.includes('Таймаут')) {\n";
176 html +=
" showToast('❌ ' + status, 'error');\n";
177 html +=
" } else if (status.includes('Успешно') || status.includes('завершено') || status.includes('✅')) {\n";
178 html +=
" showToast('✅ Обновление успешно завершено!', 'success');\n";
179 html +=
" } else if (status.includes('Перезагрузка') || status.includes('🔄')) {\n";
180 html +=
" showToast('🔄 Система перезагружается...', 'info');\n";
182 html +=
" isOtaActive = false;\n";
185 html +=
" // Показ кнопки установки\n";
186 html +=
" const installBtn = document.getElementById('btnInstall');\n";
187 html +=
" if (status.includes('Доступно обновление')) {\n";
188 html +=
" updateAvailable = true;\n";
189 html +=
" installBtn.style.display = 'inline-block';\n";
190 html +=
" if (!installBtn.hasAttribute('data-shown')) {\n";
191 html +=
" showToast('🎉 Найдено обновление! Нажмите \"Установить\"', 'info');\n";
192 html +=
" installBtn.setAttribute('data-shown', 'true');\n";
194 html +=
" } else {\n";
195 html +=
" updateAvailable = false;\n";
196 html +=
" installBtn.style.display = 'none';\n";
197 html +=
" installBtn.removeAttribute('data-shown');\n";
200 html +=
" }).catch(e => {\n";
201 html +=
" document.getElementById('otaStatus').textContent = 'Ошибка связи';\n";
202 html +=
" document.getElementById('otaStatus').style.color = '#dc3545';\n";
206 html +=
"function installUpdate() {\n";
207 html +=
" if (!updateAvailable) return;\n";
208 html +=
" const btn = document.getElementById('btnInstall');\n";
209 html +=
" btn.disabled = true;\n";
210 html +=
" btn.textContent = '⏳ Устанавливаем...';\n";
212 html +=
" fetch('/api/ota/install', {method: 'POST'})\n";
213 html +=
" .then(() => {\n";
214 html +=
" showToast('🚀 Установка началась', 'info');\n";
215 html +=
" isOtaActive = true;\n";
217 html +=
" .catch(e => {\n";
218 html +=
" showToast('❌ Ошибка установки', 'error');\n";
219 html +=
" btn.disabled = false;\n";
220 html +=
" btn.textContent = '⬇️ Скачать и установить';\n";
224 html +=
"// Обработчики событий\n";
225 html +=
"document.getElementById('btnCheck').addEventListener('click', () => {\n";
226 html +=
" const btn = document.getElementById('btnCheck');\n";
227 html +=
" btn.disabled = true;\n";
228 html +=
" btn.textContent = '⏳ Проверяем...';\n";
230 html +=
" fetch('/api/ota/check', {method: 'POST'})\n";
231 html +=
" .then(() => {\n";
232 html +=
" showToast('🔍 Проверка обновлений запущена', 'info');\n";
233 html +=
" isOtaActive = true;\n";
235 html +=
" .catch(e => {\n";
236 html +=
" showToast('❌ Ошибка запуска проверки', 'error');\n";
238 html +=
" .finally(() => {\n";
239 html +=
" setTimeout(() => {\n";
240 html +=
" btn.disabled = false;\n";
241 html +=
" btn.textContent = '🔍 Проверить обновления';\n";
242 html +=
" }, 3000);\n";
246 html +=
"document.getElementById('btnInstall').addEventListener('click', installUpdate);\n";
248 html +=
"document.getElementById('uploadForm').addEventListener('submit', e => {\n";
249 html +=
" e.preventDefault();\n";
250 html +=
" const fileInput = document.querySelector('input[name=\"firmware\"]');\n";
251 html +=
" const file = fileInput.files[0];\n";
253 html +=
" if (!file) {\n";
254 html +=
" showToast('❌ Выберите файл прошивки', 'error');\n";
255 html +=
" return;\n";
258 html +=
" if (!file.name.endsWith('.bin')) {\n";
259 html +=
" showToast('❌ Файл должен иметь расширение .bin', 'error');\n";
260 html +=
" return;\n";
263 html +=
" const submitBtn = e.target.querySelector('button[type=submit]');\n";
264 html +=
" submitBtn.disabled = true;\n";
265 html +=
" submitBtn.textContent = '⏳ Загружаем...';\n";
267 html +=
" const formData = new FormData();\n";
268 html +=
" formData.append('firmware', file);\n";
270 html +=
" showToast('📤 Начинаем загрузку прошивки...', 'info');\n";
271 html +=
" isOtaActive = true;\n";
273 html +=
" fetch('/ota/upload', {\n";
274 html +=
" method: 'POST',\n";
275 html +=
" body: formData\n";
277 html +=
" .then(r => r.json())\n";
278 html +=
" .then(j => {\n";
279 html +=
" if (j.ok) {\n";
280 html +=
" showToast('✅ Прошивка загружена успешно!', 'success');\n";
281 html +=
" } else {\n";
282 html +=
" showToast('❌ Ошибка: ' + (j.error || 'неизвестная ошибка'), 'error');\n";
285 html +=
" .catch(e => {\n";
286 html +=
" showToast('❌ Ошибка соединения', 'error');\n";
288 html +=
" .finally(() => {\n";
289 html +=
" submitBtn.disabled = false;\n";
290 html +=
" submitBtn.textContent = '⬆️ Загрузить прошивку';\n";
291 html +=
" fileInput.value = '';\n";
292 html +=
" isOtaActive = false;\n";
296 html +=
"// Автообновление статуса\n";
297 html +=
"setInterval(updateStatus, 1000);\n";
298 html +=
"updateStatus();\n";
301 webServer.send(200,
"text/html; charset=utf-8", html);
312 StaticJsonDocument<256> doc;
320 doc[
"status"] =
"local " + String(percent) +
"%";
326 doc[
"localUpload"] =
true;
333 doc[
"localUpload"] =
false;
338 serializeJson(doc, json);
339 webServer.send(200,
"application/json", json);
346 if (upload.status == UPLOAD_FILE_START)
353 logSystem(
"[OTA] Приём файла %s (%u байт)", upload.filename.c_str(), upload.totalSize);
355 if (!Update.begin(upload.totalSize == 0 ? UPDATE_SIZE_UNKNOWN : upload.totalSize))
357 logError(
"[OTA] Update.begin() failed");
358 Update.printError(Serial);
364 else if (upload.status == UPLOAD_FILE_WRITE)
366 if (Update.write(upload.buf, upload.currentSize) != upload.currentSize)
368 logError(
"[OTA] Write error: %d байт", upload.currentSize);
369 Update.printError(Serial);
378 static size_t lastLogged = 0;
386 else if (upload.status == UPLOAD_FILE_END)
391 if (Update.end(
true))
393 if (Update.isFinished())
395 logSuccess(
"[OTA] Файл принят успешно, перезагрузка через 2 сек");
398 webServer.send(200,
"application/json",
"{\"ok\":true}");
404 logError(
"[OTA] Update не завершён");
405 Update.printError(Serial);
408 webServer.send(200,
"application/json",
"{\"ok\":false,\"error\":\"not_finished\"}");
413 logError(
"[OTA] Update.end() failed");
414 Update.printError(Serial);
417 webServer.send(200,
"application/json",
"{\"ok\":false,\"error\":\"end_failed\"}");
420 else if (upload.status == UPLOAD_FILE_ABORTED)
422 logError(
"[OTA] Загрузка прервана");
426 webServer.send(200,
"application/json",
"{\"ok\":false,\"error\":\"aborted\"}");
void logWebRequest(const String &method, const String &uri, const String &clientIP)
Логирование веб-запросов
String generateButton(ButtonType type, const char *icon, const char *text, const char *action)
void logDebug(const char *format,...)
void logSuccess(const char *format,...)
void logError(const char *format,...)
void logSystem(const char *format,...)
Система логгирования с красивым форматированием
const char * getOtaStatus()
static bool isLocalUploadActive
static size_t localUploadTotal
static size_t localUploadProgress
static String localUploadStatus
static void sendOtaStatusJson()
static void handleFirmwareUpload()
void setupOtaRoutes()
Настройка маршрутов OTA (/updates, /api/ota/*, /ota/*)
#define JXCT_VERSION_STRING
String generateApModeUnavailablePage(const String &title, const String &icon)
Генерация страницы "Недоступно в AP режиме".
String generatePageHeader(const String &title, const String &icon)
Генерация заголовка HTML страницы
String generatePageFooter()
Генерация футера HTML страницы