474 lines
23 KiB
PHP
474 lines
23 KiB
PHP
<?php
|
|
$title = 'Flashear ESP8266 - ESP Admin';
|
|
ob_start();
|
|
?>
|
|
|
|
<!-- Header -->
|
|
<div class="mb-6">
|
|
<div class="flex flex-col lg:flex-row lg:items-center lg:justify-between gap-4">
|
|
<div>
|
|
<h1 class="text-3xl font-bold text-slate-900 flex items-center gap-2">
|
|
<svg class="w-8 h-8 text-emerald-600" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 10V3L4 14h7v7l9-11h-7z" />
|
|
</svg>
|
|
Flashear ESP8266
|
|
</h1>
|
|
<p class="text-slate-600 mt-1">Actualiza el firmware de tus dispositivos vía USB o OTA</p>
|
|
</div>
|
|
<a href="/" class="inline-flex items-center gap-2 px-4 py-2.5 text-sm font-semibold rounded-lg bg-white text-slate-700 hover:bg-slate-50 border-2 border-slate-200 shadow-sm transition-all">
|
|
<svg class="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 19l-7-7m0 0l7-7m-7 7h18" />
|
|
</svg>
|
|
Volver
|
|
</a>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Contenedor principal -->
|
|
<div class="max-w-5xl mx-auto space-y-6">
|
|
|
|
<!-- Flasheo USB -->
|
|
<div class="bg-white rounded-xl border-2 border-slate-200 shadow-sm hover:shadow-md hover:-translate-y-0.5 transition-all duration-300 overflow-hidden">
|
|
<div class="px-6 py-4 border-b border-slate-200 bg-gradient-to-r from-blue-50 to-blue-100">
|
|
<h2 class="text-lg font-bold text-slate-900 flex items-center gap-2">
|
|
<svg class="w-5 h-5 text-blue-600" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 9l3 3-3 3m5 0h3M5 20h14a2 2 0 002-2V6a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z" />
|
|
</svg>
|
|
Flasheo USB (Web Serial)
|
|
</h2>
|
|
<p class="text-xs text-slate-600 mt-1">Conecta tu ESP8266 vía USB y flashea directamente desde el navegador</p>
|
|
</div>
|
|
|
|
<div class="p-6 space-y-4">
|
|
<!-- Requisitos -->
|
|
<div class="bg-blue-50 border-2 border-blue-200 rounded-lg p-4">
|
|
<div class="flex items-start gap-3">
|
|
<svg class="w-5 h-5 text-blue-600 mt-0.5 flex-shrink-0" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
|
|
</svg>
|
|
<div class="flex-1">
|
|
<h3 class="font-semibold text-blue-900 mb-1">Requisitos</h3>
|
|
<ul class="text-sm text-blue-800 space-y-1">
|
|
<li>• Navegador Chrome/Edge (basado en Chromium)</li>
|
|
<li>• Ejecutar en <strong>HTTPS</strong> o <strong>localhost</strong></li>
|
|
<li>• Cable USB conectado al ESP8266</li>
|
|
</ul>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Drivers -->
|
|
<div class="bg-amber-50 border-2 border-amber-200 rounded-lg p-4">
|
|
<div class="flex items-start gap-3">
|
|
<svg class="w-5 h-5 text-amber-600 mt-0.5 flex-shrink-0" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z" />
|
|
</svg>
|
|
<div class="flex-1">
|
|
<h3 class="font-semibold text-amber-900 mb-2">Drivers Requeridos</h3>
|
|
<p class="text-sm text-amber-800 mb-3">Si tu ESP usa chip <strong>CH340</strong> (común en clones), instala el driver:</p>
|
|
<a href="http://www.wch.cn/downloads/CH341SER_EXE.html" target="_blank" class="inline-flex items-center gap-2 px-4 py-2 text-sm font-medium rounded-lg bg-amber-600 text-white hover:bg-amber-700 transition-all">
|
|
<svg class="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-4l-4 4m0 0l-4-4m4 4V4" />
|
|
</svg>
|
|
Descargar Driver CH340
|
|
</a>
|
|
<p class="text-xs text-amber-700 mt-2">
|
|
Para NodeMCU originales (CP2102):
|
|
<a class="font-medium underline hover:text-amber-900" href="https://www.silabs.com/developers/usb-to-uart-bridge-vcp-drivers" target="_blank">Silicon Labs</a>
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Info Firmware -->
|
|
<div class="bg-slate-50 border-2 border-slate-200 rounded-lg p-4">
|
|
<h3 class="font-semibold text-slate-900 mb-2">Firmware</h3>
|
|
<p class="text-sm text-slate-600 mb-2">Se usará el archivo definido en <code class="px-2 py-0.5 bg-slate-200 rounded text-xs font-mono">/assets/flash/esp8266-manifest.json</code></p>
|
|
<div id="otaFirmwareInfo_usb" class="text-xs text-slate-500">Cargando manifest...</div>
|
|
</div>
|
|
|
|
<!-- Botón de flasheo -->
|
|
<div class="flex flex-col items-center gap-3 py-4">
|
|
<esp-web-install-button id="installBtn" manifest="/assets/flash/esp8266-manifest.json"></esp-web-install-button>
|
|
<p class="text-xs text-slate-500 text-center max-w-md">
|
|
Si el botón aparece deshabilitado, verifica el navegador y el contexto seguro (HTTPS/localhost)
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- OTA (Over-The-Air) -->
|
|
<div class="bg-white rounded-xl border-2 border-slate-200 shadow-sm hover:shadow-md hover:-translate-y-0.5 transition-all duration-300 overflow-hidden">
|
|
<div class="px-6 py-4 border-b border-slate-200 bg-gradient-to-r from-emerald-50 to-emerald-100">
|
|
<h2 class="text-lg font-bold text-slate-900 flex items-center gap-2">
|
|
<svg class="w-5 h-5 text-emerald-600" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8.111 16.404a5.5 5.5 0 017.778 0M12 20h.01m-7.08-7.071c3.904-3.905 10.236-3.905 14.141 0M1.394 9.393c5.857-5.857 15.355-5.857 21.213 0" />
|
|
</svg>
|
|
OTA (Over-The-Air)
|
|
</h2>
|
|
<p class="text-xs text-slate-600 mt-1">Actualiza el firmware remotamente sin cables</p>
|
|
</div>
|
|
|
|
<div class="p-6 space-y-4">
|
|
<!-- Info OTA -->
|
|
<div class="bg-emerald-50 border-2 border-emerald-200 rounded-lg p-4">
|
|
<div class="flex items-start gap-3">
|
|
<svg class="w-5 h-5 text-emerald-600 mt-0.5 flex-shrink-0" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
|
|
</svg>
|
|
<div class="flex-1">
|
|
<h3 class="font-semibold text-emerald-900 mb-1">Cómo funciona</h3>
|
|
<p class="text-sm text-emerald-800">
|
|
El dispositivo descargará el firmware desde la URL especificada y se actualizará automáticamente.
|
|
Asegúrate de que la URL sea accesible desde la red del dispositivo.
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Selector de dispositivo -->
|
|
<div>
|
|
<label for="otaChipidSelect" class="block text-sm font-semibold text-slate-700 mb-2">
|
|
Dispositivo
|
|
</label>
|
|
<select id="otaChipidSelect" class="w-full px-4 py-2.5 border-2 border-slate-200 rounded-lg focus:outline-none focus:border-emerald-500 focus:ring-2 focus:ring-emerald-200 transition-all">
|
|
<option value="" disabled selected>Cargando dispositivos...</option>
|
|
</select>
|
|
<p class="text-xs text-slate-500 mt-2" id="otaOnlineHint">
|
|
Solo se muestran dispositivos <strong>online</strong> que <strong>necesitan actualización</strong> (se ocultan los que ya tienen FW OK)
|
|
</p>
|
|
<div id="otaDeviceStatus" class="mt-3 inline-flex items-center gap-2 px-3 py-1.5 text-xs font-semibold rounded-full bg-slate-100 text-slate-700 border border-slate-200">
|
|
<span class="w-2 h-2 rounded-full bg-slate-400"></span>
|
|
Selecciona un dispositivo
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Info Firmware OTA -->
|
|
<div class="bg-slate-50 border-2 border-slate-200 rounded-lg p-4">
|
|
<h3 class="font-semibold text-slate-900 mb-2">Firmware</h3>
|
|
<p class="text-sm text-slate-600 mb-2">Se usará el archivo definido en <code class="px-2 py-0.5 bg-slate-200 rounded text-xs font-mono">/assets/flash/esp8266-manifest.json</code></p>
|
|
<div id="otaFirmwareInfo_ota" class="text-xs text-slate-500">Cargando manifest...</div>
|
|
</div>
|
|
|
|
<!-- Botones de acción -->
|
|
<div class="flex flex-wrap items-center gap-3 pt-2">
|
|
<button id="btnStartOTA" class="inline-flex items-center gap-2 px-4 py-2.5 text-sm font-semibold rounded-lg bg-emerald-600 text-white hover:bg-emerald-700 shadow-sm transition-all">
|
|
<svg class="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-8l-4-4m0 0L8 8m4-4v12" />
|
|
</svg>
|
|
Iniciar OTA
|
|
</button>
|
|
<button id="btnRebootAfterOTA" class="inline-flex items-center gap-2 px-4 py-2.5 text-sm font-medium rounded-lg bg-white text-slate-700 hover:bg-slate-50 border-2 border-slate-200 transition-all" title="Reiniciar dispositivo después de la actualización">
|
|
<svg class="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15" />
|
|
</svg>
|
|
Reiniciar Después
|
|
</button>
|
|
</div>
|
|
|
|
<!-- Indicador de progreso OTA -->
|
|
<div id="otaProgress" class="hidden mt-4 p-4 bg-blue-50 border-2 border-blue-200 rounded-lg">
|
|
<div class="flex items-center gap-3">
|
|
<div class="flex-shrink-0">
|
|
<svg class="animate-spin h-6 w-6 text-blue-600" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
|
|
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
|
|
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
|
|
</svg>
|
|
</div>
|
|
<div class="flex-1">
|
|
<p id="otaProgressText" class="text-sm font-semibold text-blue-900">Procesando actualización OTA...</p>
|
|
<p class="text-xs text-blue-700 mt-1">El dispositivo descargará e instalará el firmware. Esto puede tomar hasta 3 minutos.</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
</div>
|
|
|
|
<!-- ESP Web Tools: botón de instalación/flasheo -->
|
|
<script type="module" src="https://unpkg.com/esp-web-tools@latest/dist/web/install-button.js?module"></script>
|
|
<script>
|
|
// OTA logic
|
|
(function(){
|
|
const MANIFEST_URL = '/assets/flash/esp8266-manifest.json';
|
|
let firmwareUrl = '';
|
|
let firmwareVersion = '';
|
|
let lastOtaChipid = '';
|
|
let otaTimeoutId = null;
|
|
const OTA_INACTIVITY_TIMEOUT_MS = 300000;
|
|
|
|
// Datos de dispositivos
|
|
let dispositivos = {}; // chipid -> {chipid, tag, nombre, ...}
|
|
let dispositivosLoadError = false;
|
|
|
|
const $chipSel = document.getElementById('otaChipidSelect');
|
|
const $btn = document.getElementById('btnStartOTA');
|
|
const $reboot = document.getElementById('btnRebootAfterOTA');
|
|
const $fwInfo_usb = document.getElementById('otaFirmwareInfo_usb');
|
|
const $fwInfo_ota = document.getElementById('otaFirmwareInfo_ota');
|
|
const $otaProgress = document.getElementById('otaProgress');
|
|
const $otaProgressText = document.getElementById('otaProgressText');
|
|
const $otaDeviceStatus = document.getElementById('otaDeviceStatus');
|
|
|
|
if (!$btn) return;
|
|
try { if (window.applyIcons) window.applyIcons(document); } catch {}
|
|
|
|
function escapeHtml(value) {
|
|
return String(value ?? '').replace(/[&<>"']/g, ch => ({
|
|
'&': '&',
|
|
'<': '<',
|
|
'>': '>',
|
|
'"': '"',
|
|
"'": '''
|
|
}[ch]));
|
|
}
|
|
|
|
function normalizeChip(value) {
|
|
return String(value || '').trim().toUpperCase();
|
|
}
|
|
|
|
function findDevice(chipid) {
|
|
const normalized = normalizeChip(chipid);
|
|
const key = Object.keys(dispositivos || {}).find(item => normalizeChip(item) === normalized || normalizeChip(dispositivos[item]?.chipid) === normalized);
|
|
return key ? dispositivos[key] : null;
|
|
}
|
|
|
|
function setDeviceOnline(chipid, online) {
|
|
const normalized = normalizeChip(chipid);
|
|
const key = Object.keys(dispositivos || {}).find(item => normalizeChip(item) === normalized || normalizeChip(dispositivos[item]?.chipid) === normalized);
|
|
if (key) dispositivos[key].online = !!online;
|
|
}
|
|
|
|
function updateSelectedDeviceStatus() {
|
|
if (!$otaDeviceStatus) return;
|
|
const selectedChipid = normalizeChip($chipSel?.value);
|
|
if (!selectedChipid) {
|
|
$otaDeviceStatus.className = 'mt-3 inline-flex items-center gap-2 px-3 py-1.5 text-xs font-semibold rounded-full bg-slate-100 text-slate-700 border border-slate-200';
|
|
$otaDeviceStatus.innerHTML = '<span class="w-2 h-2 rounded-full bg-slate-400"></span> Selecciona un dispositivo';
|
|
return;
|
|
}
|
|
|
|
const device = findDevice(selectedChipid);
|
|
const online = device && (device.online === true || String(device.online || '').toLowerCase() === 'online');
|
|
$otaDeviceStatus.className = online
|
|
? 'mt-3 inline-flex items-center gap-2 px-3 py-1.5 text-xs font-semibold rounded-full bg-emerald-100 text-emerald-700 border border-emerald-200'
|
|
: 'mt-3 inline-flex items-center gap-2 px-3 py-1.5 text-xs font-semibold rounded-full bg-red-100 text-red-700 border border-red-200';
|
|
$otaDeviceStatus.innerHTML = online
|
|
? '<span class="w-2 h-2 rounded-full bg-emerald-500 animate-pulse"></span> Online ahora'
|
|
: '<span class="w-2 h-2 rounded-full bg-red-500"></span> Offline ahora';
|
|
}
|
|
|
|
function clearOtaTimeout() {
|
|
if (otaTimeoutId) clearTimeout(otaTimeoutId);
|
|
otaTimeoutId = null;
|
|
}
|
|
|
|
function armOtaTimeout() {
|
|
clearOtaTimeout();
|
|
otaTimeoutId = setTimeout(function(){
|
|
if ($otaProgressText) $otaProgressText.textContent = 'OTA sin actividad reciente. Verifica el estado del dispositivo.';
|
|
try { mostrarToast('No se detecto actividad OTA reciente. Verifica manualmente.', { type: 'warning' }); } catch{}
|
|
lastOtaChipid = '';
|
|
if ($btn) $btn.disabled = false;
|
|
}, OTA_INACTIVITY_TIMEOUT_MS);
|
|
}
|
|
|
|
// Cargar manifest para obtener la URL del .bin
|
|
function cargarManifest() {
|
|
if ($fwInfo_ota) $fwInfo_ota.textContent = 'Cargando manifest...';
|
|
if ($fwInfo_usb) $fwInfo_usb.textContent = 'Cargando manifest...';
|
|
return fetch(MANIFEST_URL, { cache: 'no-store' })
|
|
.then(r => r.json())
|
|
.then(json => {
|
|
try {
|
|
firmwareVersion = (typeof json.version === 'string') ? json.version : '';
|
|
const builds = Array.isArray(json.builds) ? json.builds : [];
|
|
const first = builds[0];
|
|
const parts = Array.isArray(first?.parts) ? first.parts : [];
|
|
const part = parts.find(p => typeof p?.path === 'string') || null;
|
|
if (part) {
|
|
const base = new URL(MANIFEST_URL, window.location.origin);
|
|
firmwareUrl = new URL(part.path, base).href;
|
|
const infoText = firmwareVersion ? `Firmware: ${firmwareUrl} (v${firmwareVersion})` : `Firmware: ${firmwareUrl}`;
|
|
if ($fwInfo_ota) $fwInfo_ota.textContent = infoText;
|
|
if ($fwInfo_usb) $fwInfo_usb.textContent = infoText;
|
|
return;
|
|
}
|
|
} catch {}
|
|
firmwareUrl = '';
|
|
if ($fwInfo_ota) $fwInfo_ota.textContent = 'No se pudo determinar el firmware desde el manifest.';
|
|
if ($fwInfo_usb) $fwInfo_usb.textContent = 'No se pudo determinar el firmware desde el manifest.';
|
|
})
|
|
.catch(() => {
|
|
firmwareUrl = '';
|
|
if ($fwInfo_ota) $fwInfo_ota.textContent = 'No se pudo cargar el manifest.';
|
|
if ($fwInfo_usb) $fwInfo_usb.textContent = 'No se pudo cargar el manifest.';
|
|
});
|
|
}
|
|
|
|
// Cargar dispositivos
|
|
function cargarDispositivos() {
|
|
return fetch('/api/get_dispositivos', { credentials: 'same-origin' })
|
|
.then(r => {
|
|
if (!r.ok) throw new Error('HTTP ' + r.status);
|
|
return r.json();
|
|
})
|
|
.then(data => {
|
|
dispositivosLoadError = false;
|
|
dispositivos = data || {};
|
|
})
|
|
.catch(() => {
|
|
dispositivosLoadError = true;
|
|
dispositivos = {};
|
|
try { mostrarToast('No se pudo cargar la lista de dispositivos.', { type: 'danger' }); } catch{}
|
|
});
|
|
}
|
|
|
|
function renderSelect() {
|
|
if (!$chipSel) return;
|
|
if (dispositivosLoadError) {
|
|
$chipSel.innerHTML = '<option value="" disabled selected>Error al cargar dispositivos</option>';
|
|
return;
|
|
}
|
|
|
|
const opciones = [];
|
|
let totalOnline = 0;
|
|
let filtradosPorFW = 0;
|
|
|
|
for (const chip in dispositivos) {
|
|
const d = dispositivos[chip];
|
|
const isOnline = d.online === true || String(d.online || '').toLowerCase() === 'online';
|
|
if (isOnline) {
|
|
totalOnline++;
|
|
// Filtrar dispositivos que ya tienen el firmware actualizado
|
|
const deviceFW = (d.firmware || '').trim();
|
|
const isFWUpdated = firmwareVersion && deviceFW && deviceFW === firmwareVersion;
|
|
|
|
if (!isFWUpdated) {
|
|
const fwInfo = deviceFW ? ` - FW: ${deviceFW}` : '';
|
|
const label = `${d.tag || chip} (${d.chipid || chip})${fwInfo}`;
|
|
opciones.push(`<option value="${escapeHtml(d.chipid || chip)}">${escapeHtml(label)}</option>`);
|
|
} else {
|
|
filtradosPorFW++;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (opciones.length === 0) {
|
|
if (totalOnline === 0) {
|
|
$chipSel.innerHTML = '<option value="" disabled selected>No hay dispositivos online</option>';
|
|
} else {
|
|
$chipSel.innerHTML = '<option value="" disabled selected>Todos los dispositivos tienen FW actualizado</option>';
|
|
}
|
|
} else {
|
|
const hint = filtradosPorFW > 0 ? ` (${filtradosPorFW} con FW OK ocultos)` : '';
|
|
$chipSel.innerHTML = `<option value="" disabled selected>Seleccione un dispositivo${hint}</option>` + opciones.join('');
|
|
}
|
|
|
|
// Preseleccionar desde query si existe y está online
|
|
try {
|
|
const qp = new URLSearchParams(window.location.search);
|
|
const qchip = normalizeChip(qp.get('chipid'));
|
|
if (qchip && opciones.length) {
|
|
const opt = Array.from($chipSel.options).find(o => normalizeChip(o.value) === qchip);
|
|
if (opt) $chipSel.value = opt.value;
|
|
}
|
|
} catch {}
|
|
updateSelectedDeviceStatus();
|
|
}
|
|
|
|
Promise.all([cargarManifest(), cargarDispositivos()]).then(renderSelect);
|
|
$chipSel?.addEventListener('change', updateSelectedDeviceStatus);
|
|
|
|
if (window.mqttRealtime) {
|
|
window.mqttRealtime.onMessage(function(evt) {
|
|
if (evt.type === 'state') {
|
|
setDeviceOnline(evt.chipid, evt.estado === 'online');
|
|
updateSelectedDeviceStatus();
|
|
}
|
|
|
|
if (evt.type !== 'ota' || normalizeChip(evt.chipid) !== normalizeChip(lastOtaChipid)) return;
|
|
|
|
const status = String(evt.status || '').trim();
|
|
if ($otaProgress) $otaProgress.classList.remove('hidden');
|
|
if ($otaProgressText) $otaProgressText.textContent = `OTA: ${status}`;
|
|
armOtaTimeout();
|
|
|
|
if (/^(success|failed|no_updates|error:)/i.test(status)) {
|
|
clearOtaTimeout();
|
|
lastOtaChipid = '';
|
|
if ($btn) $btn.disabled = false;
|
|
if (status === 'success') {
|
|
try { mostrarToast('OTA completada. El dispositivo se reiniciara.', { type: 'success' }); } catch{}
|
|
} else {
|
|
try { mostrarToast(`OTA finalizo con estado: ${status}`, { type: status.indexOf('error:') === 0 || status === 'failed' ? 'danger' : 'warning' }); } catch{}
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
$btn.addEventListener('click', function(){
|
|
const chipid = ($chipSel?.value || '').trim();
|
|
const fw = firmwareUrl;
|
|
if (!chipid) { try { mostrarToast('Selecciona un dispositivo online', { type: 'danger' }); } catch{} return; }
|
|
if (!fw || !fw.endsWith('.bin')) { try { mostrarToast('El manifest no define un .bin válido', { type: 'danger' }); } catch{} return; }
|
|
|
|
const topic = `dispositivo/${chipid}/comando/ota`;
|
|
const payload = { url: fw };
|
|
|
|
$btn.disabled = true;
|
|
clearOtaTimeout();
|
|
|
|
// Mostrar progreso
|
|
if ($otaProgress) $otaProgress.classList.remove('hidden');
|
|
if ($otaProgressText) $otaProgressText.textContent = 'Enviando comando OTA al dispositivo...';
|
|
|
|
lastOtaChipid = chipid;
|
|
|
|
const publisher = api.postJSON('/api/mqtt_publish', { topic, payload, qos: 0, retain: false });
|
|
|
|
publisher
|
|
.then(function(){
|
|
try { mostrarToast('Comando OTA enviado'); } catch{}
|
|
if ($otaProgressText) $otaProgressText.textContent = 'Esperando que el dispositivo descargue e instale el firmware...';
|
|
|
|
armOtaTimeout();
|
|
/*
|
|
if ($otaProgress) $otaProgress.classList.add('hidden');
|
|
try { mostrarToast('⏱️ No se detectó respuesta del dispositivo. Verifica manualmente.', { type: 'warning' }); } catch{}
|
|
lastOtaChipid = '';
|
|
$btn.disabled = false;
|
|
}, 180000); // 3 minutos
|
|
*/
|
|
})
|
|
.catch(function(err){
|
|
const msg = err.data?.message || err.data?.msg || err.message || 'Error al enviar OTA';
|
|
try { mostrarToast(msg, { type: 'danger' }); } catch{}
|
|
if ($otaProgress) $otaProgress.classList.add('hidden');
|
|
lastOtaChipid = '';
|
|
$btn.disabled = false;
|
|
});
|
|
});
|
|
|
|
$reboot?.addEventListener('click', function(){
|
|
const chipid = ($chipSel?.value || '').trim();
|
|
if (!chipid) { try { mostrarToast('Selecciona un dispositivo', { type: 'danger' }); } catch{} return; }
|
|
const topic = `dispositivo/${chipid}/comando/reboot`;
|
|
const payload = '1';
|
|
const publisher = window.mqttRealtime
|
|
? window.mqttRealtime.publish(topic, payload)
|
|
: api.postJSON('/api/mqtt_publish', { topic, payload });
|
|
|
|
publisher
|
|
.then(function(){ try { mostrarToast('Comando de reinicio enviado'); } catch{} })
|
|
.catch(function(){ try { mostrarToast('No se pudo enviar reinicio', { type: 'danger' }); } catch{} });
|
|
});
|
|
})();
|
|
</script>
|
|
<?php
|
|
$content = ob_get_clean();
|
|
if (!defined('RENDER_PARTIAL')) {
|
|
include __DIR__ . '/../layouts/layout.php';
|
|
}
|