JXCT Soil Sensor 7-in-1 v3.4.9 (June 2025)
Professional IoT soil monitoring system with ESP32, Modbus RTU, MQTT, and advanced compensation algorithms
Загрузка...
Поиск...
Не найдено
routes_ota.cpp
См. документацию.
7#include "../wifi_manager.h"
8#include "ota_manager.h"
9#include <ArduinoJson.h>
10#include <Update.h>
11#include <Arduino.h>
12
13extern WebServer webServer;
15extern String navHtml();
16
17// Глобальные переменные для отслеживания прогресса локальной загрузки
18static bool isLocalUploadActive = false;
19static size_t localUploadProgress = 0;
20static size_t localUploadTotal = 0;
21static String localUploadStatus = "idle";
22
23// --- ВСПОМОГАТЕЛЬНЫЕ ---
24static void sendOtaStatusJson();
25static void handleFirmwareUpload();
26
28{
29 logDebug("Настройка OTA маршрутов");
30
31 // API: статус OTA
32 webServer.on("/api/ota/status", HTTP_GET, sendOtaStatusJson);
33
34 // API: ручная проверка
35 webServer.on("/api/ota/check", HTTP_POST, []()
36 {
37 logWebRequest("POST", "/api/ota/check", webServer.client().remoteIP().toString());
39 {
40 webServer.send(403, "application/json", "{\"error\":\"unavailable\"}");
41 return;
42 }
43 triggerOtaCheck(); // уже включает handleOTA()
44 webServer.send(200, "application/json", "{\"ok\":true}");
45 });
46
47 // API: установить найденное обновление
48 webServer.on("/api/ota/install", HTTP_POST, []()
49 {
50 logWebRequest("POST", "/api/ota/install", webServer.client().remoteIP().toString());
52 {
53 webServer.send(403, "application/json", "{\"error\":\"unavailable\"}");
54 return;
55 }
56
57 // Запускаем принудительную установку
59 webServer.send(200, "application/json", "{\"ok\":true}");
60 });
61
62 // HTML страница обновлений
63 webServer.on("/updates", HTTP_GET, []()
64 {
65 logWebRequest("GET", "/updates", webServer.client().remoteIP().toString());
66
68 {
69 webServer.send(200, "text/html; charset=utf-8", generateApModeUnavailablePage("Обновления", "🚀"));
70 return;
71 }
72
73 String html = generatePageHeader("Обновления", "🚀");
74 html += navHtml();
75 html += "<h1>🚀 Обновления прошивки</h1>";
76
77 // Информация о версии в красивом блоке
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;'>";
80 html += "<div><b>📱 Текущая версия:</b> " + String(JXCT_VERSION_STRING) + "</div>";
81 html += "<div id='otaStatus' style='color:#666;font-style:italic;'>Загрузка статуса...</div>";
82 html += "</div></div>";
83
84 // Единый прогресс-бар для всех операций
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>";
90
91 // Блок удаленного обновления
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>";
94
95 // Кнопки проверки и установки
96 html += "<div style='display:flex;gap:10px;flex-wrap:wrap;'>";
97 {
98 String btnCheck = generateButton(ButtonType::OUTLINE, "🔍", "Проверить обновления", "");
99 btnCheck.replace("<button ", "<button id='btnCheck' ");
100 html += btnCheck;
101 }
102 html += "<button id='btnInstall' style='display:none;' class='btn btn-success'>⬇️ Скачать и установить</button>";
103 html += "</div></div>";
104
105 // Блок локального обновления
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;'>";
111 html += "</div>";
112 html += "<form id='uploadForm' enctype='multipart/form-data'>";
113 {
114 String uploadBtn = generateButton(ButtonType::SECONDARY, "⬆️", "Загрузить прошивку", "");
115 html += uploadBtn;
116 }
117 html += "</form></div>";
118
119 // JavaScript для управления интерфейсом
120 html += "<script>\n";
121 html += "let isOtaActive = false;\n";
122 html += "let updateAvailable = false;\n";
123 html += "\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";
132 html += "}\n";
133 html += "\n";
134 html += "function hideProgress() {\n";
135 html += " document.getElementById('progressContainer').style.display = 'none';\n";
136 html += "}\n";
137 html += "\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";
145 html += " \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";
154 html += " }\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";
173 html += " \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";
181 html += " }\n";
182 html += " isOtaActive = false;\n";
183 html += " }\n";
184 html += " \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";
193 html += " }\n";
194 html += " } else {\n";
195 html += " updateAvailable = false;\n";
196 html += " installBtn.style.display = 'none';\n";
197 html += " installBtn.removeAttribute('data-shown');\n";
198 html += " }\n";
199 html += " }\n";
200 html += " }).catch(e => {\n";
201 html += " document.getElementById('otaStatus').textContent = 'Ошибка связи';\n";
202 html += " document.getElementById('otaStatus').style.color = '#dc3545';\n";
203 html += " });\n";
204 html += "}\n";
205 html += "\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";
211 html += " \n";
212 html += " fetch('/api/ota/install', {method: 'POST'})\n";
213 html += " .then(() => {\n";
214 html += " showToast('🚀 Установка началась', 'info');\n";
215 html += " isOtaActive = true;\n";
216 html += " })\n";
217 html += " .catch(e => {\n";
218 html += " showToast('❌ Ошибка установки', 'error');\n";
219 html += " btn.disabled = false;\n";
220 html += " btn.textContent = '⬇️ Скачать и установить';\n";
221 html += " });\n";
222 html += "}\n";
223 html += "\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";
229 html += " \n";
230 html += " fetch('/api/ota/check', {method: 'POST'})\n";
231 html += " .then(() => {\n";
232 html += " showToast('🔍 Проверка обновлений запущена', 'info');\n";
233 html += " isOtaActive = true;\n";
234 html += " })\n";
235 html += " .catch(e => {\n";
236 html += " showToast('❌ Ошибка запуска проверки', 'error');\n";
237 html += " })\n";
238 html += " .finally(() => {\n";
239 html += " setTimeout(() => {\n";
240 html += " btn.disabled = false;\n";
241 html += " btn.textContent = '🔍 Проверить обновления';\n";
242 html += " }, 3000);\n";
243 html += " });\n";
244 html += "});\n";
245 html += "\n";
246 html += "document.getElementById('btnInstall').addEventListener('click', installUpdate);\n";
247 html += "\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";
252 html += " \n";
253 html += " if (!file) {\n";
254 html += " showToast('❌ Выберите файл прошивки', 'error');\n";
255 html += " return;\n";
256 html += " }\n";
257 html += " \n";
258 html += " if (!file.name.endsWith('.bin')) {\n";
259 html += " showToast('❌ Файл должен иметь расширение .bin', 'error');\n";
260 html += " return;\n";
261 html += " }\n";
262 html += " \n";
263 html += " const submitBtn = e.target.querySelector('button[type=submit]');\n";
264 html += " submitBtn.disabled = true;\n";
265 html += " submitBtn.textContent = '⏳ Загружаем...';\n";
266 html += " \n";
267 html += " const formData = new FormData();\n";
268 html += " formData.append('firmware', file);\n";
269 html += " \n";
270 html += " showToast('📤 Начинаем загрузку прошивки...', 'info');\n";
271 html += " isOtaActive = true;\n";
272 html += " \n";
273 html += " fetch('/ota/upload', {\n";
274 html += " method: 'POST',\n";
275 html += " body: formData\n";
276 html += " })\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";
283 html += " }\n";
284 html += " })\n";
285 html += " .catch(e => {\n";
286 html += " showToast('❌ Ошибка соединения', 'error');\n";
287 html += " })\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";
293 html += " });\n";
294 html += "});\n";
295 html += "\n";
296 html += "// Автообновление статуса\n";
297 html += "setInterval(updateStatus, 1000);\n";
298 html += "updateStatus();\n";
299 html += "</script>";
300 html += generatePageFooter();
301 webServer.send(200, "text/html; charset=utf-8", html);
302 });
303
304 // Upload маршрут
305 webServer.on("/ota/upload", HTTP_POST, []() { /* ответ отправится в обработчике */ }, handleFirmwareUpload);
306
307 logSuccess("Маршруты OTA настроены");
308}
309
310static void sendOtaStatusJson()
311{
312 StaticJsonDocument<256> doc;
313
314 // Если идёт локальная загрузка, показываем её статус
316 {
317 if (localUploadTotal > 0)
318 {
319 int percent = (localUploadProgress * 100) / localUploadTotal;
320 doc["status"] = "local " + String(percent) + "%";
321 }
322 else
323 {
324 doc["status"] = "local " + String(localUploadProgress / 1024) + "KB";
325 }
326 doc["localUpload"] = true;
327 doc["progress"] = localUploadProgress;
328 doc["total"] = localUploadTotal;
329 }
330 else
331 {
332 doc["status"] = getOtaStatus();
333 doc["localUpload"] = false;
334 }
335
336 doc["version"] = JXCT_VERSION_STRING;
337 String json;
338 serializeJson(doc, json);
339 webServer.send(200, "application/json", json);
340}
341
343{
344 HTTPUpload& upload = webServer.upload();
345
346 if (upload.status == UPLOAD_FILE_START)
347 {
348 isLocalUploadActive = true;
350 localUploadTotal = upload.totalSize;
351 localUploadStatus = "uploading";
352
353 logSystem("[OTA] Приём файла %s (%u байт)", upload.filename.c_str(), upload.totalSize);
354
355 if (!Update.begin(upload.totalSize == 0 ? UPDATE_SIZE_UNKNOWN : upload.totalSize))
356 {
357 logError("[OTA] Update.begin() failed");
358 Update.printError(Serial);
359 isLocalUploadActive = false;
360 localUploadStatus = "error";
361 // НЕ отправляем ответ здесь - это приведёт к обрыву загрузки
362 }
363 }
364 else if (upload.status == UPLOAD_FILE_WRITE)
365 {
366 if (Update.write(upload.buf, upload.currentSize) != upload.currentSize)
367 {
368 logError("[OTA] Write error: %d байт", upload.currentSize);
369 Update.printError(Serial);
370 isLocalUploadActive = false;
371 localUploadStatus = "error";
372 }
373 else
374 {
375 localUploadProgress += upload.currentSize;
376
377 // Логируем прогресс каждые 64KB
378 static size_t lastLogged = 0;
379 if (localUploadProgress - lastLogged > 65536)
380 {
381 logSystem("[OTA] Загружено: %u байт", localUploadProgress);
382 lastLogged = localUploadProgress;
383 }
384 }
385 }
386 else if (upload.status == UPLOAD_FILE_END)
387 {
388 logSystem("[OTA] Загрузка завершена: %u байт", localUploadProgress);
389 localUploadStatus = "verifying";
390
391 if (Update.end(true)) // true = устанавливать как boot partition
392 {
393 if (Update.isFinished())
394 {
395 logSuccess("[OTA] Файл принят успешно, перезагрузка через 2 сек");
396 localUploadStatus = "success";
397 isLocalUploadActive = false;
398 webServer.send(200, "application/json", "{\"ok\":true}");
399 delay(2000);
400 ESP.restart();
401 }
402 else
403 {
404 logError("[OTA] Update не завершён");
405 Update.printError(Serial);
406 isLocalUploadActive = false;
407 localUploadStatus = "error";
408 webServer.send(200, "application/json", "{\"ok\":false,\"error\":\"not_finished\"}");
409 }
410 }
411 else
412 {
413 logError("[OTA] Update.end() failed");
414 Update.printError(Serial);
415 isLocalUploadActive = false;
416 localUploadStatus = "error";
417 webServer.send(200, "application/json", "{\"ok\":false,\"error\":\"end_failed\"}");
418 }
419 }
420 else if (upload.status == UPLOAD_FILE_ABORTED)
421 {
422 logError("[OTA] Загрузка прервана");
423 Update.abort();
424 isLocalUploadActive = false;
425 localUploadStatus = "aborted";
426 webServer.send(200, "application/json", "{\"ok\":false,\"error\":\"aborted\"}");
427 }
428}
void logWebRequest(const String &method, const String &uri, const String &clientIP)
Логирование веб-запросов
Определения error_handlers.cpp:152
String generateButton(ButtonType type, const char *icon, const char *text, const char *action)
Определения jxct_ui_system.cpp:286
@ SECONDARY
Определения jxct_ui_system.h:66
@ OUTLINE
Определения jxct_ui_system.h:69
void logDebug(const char *format,...)
Определения logger.cpp:112
void logSuccess(const char *format,...)
Определения logger.cpp:129
void logError(const char *format,...)
Определения logger.cpp:61
void logSystem(const char *format,...)
Определения logger.cpp:213
Система логгирования с красивым форматированием
void triggerOtaInstall()
Определения ota_manager.cpp:423
const char * getOtaStatus()
Определения ota_manager.cpp:45
void triggerOtaCheck()
Определения ota_manager.cpp:407
WebServer webServer
WiFiMode currentWiFiMode
Определения wifi_manager.cpp:26
static bool isLocalUploadActive
Определения routes_ota.cpp:18
static size_t localUploadTotal
Определения routes_ota.cpp:20
static size_t localUploadProgress
Определения routes_ota.cpp:19
static String localUploadStatus
Определения routes_ota.cpp:21
static void sendOtaStatusJson()
Определения routes_ota.cpp:310
static void handleFirmwareUpload()
Определения routes_ota.cpp:342
String navHtml()
Определения wifi_manager.cpp:82
void setupOtaRoutes()
Настройка маршрутов OTA (/updates, /api/ota/*, /ota/*)
Определения routes_ota.cpp:27
#define JXCT_VERSION_STRING
Определения version.h:12
String generateApModeUnavailablePage(const String &title, const String &icon)
Генерация страницы "Недоступно в AP режиме".
Определения web_templates.cpp:175
String generatePageHeader(const String &title, const String &icon)
Генерация заголовка HTML страницы
Определения web_templates.cpp:8
String generatePageFooter()
Генерация футера HTML страницы
Определения web_templates.cpp:19
WiFiMode
Определения wifi_manager.h:12
@ AP
Определения wifi_manager.h:13
@ STA
Определения wifi_manager.h:14