const calibrationProfiles = { rtd: { description: "Calibración de un punto contra una temperatura de referencia.", note: "Espere a que la lectura se estabilice antes de enviar Cal,.", groups: [ { title: "Punto de referencia", help: "Use una referencia térmica trazable en grados Celsius.", value: "25.00", step: "0.01", button: "Calibrar temperatura", command(value) { return `Cal,${value}`; } } ] }, ph: { description: "Calibración de uno, dos o tres puntos. El punto medio siempre va primero.", note: "Orden Atlas: medio (pH 7), bajo (pH 4), alto (pH 10). Cal,mid borra los otros puntos.", groups: [ { title: "Punto medio", help: "Enjuague la sonda, colóquela en buffer pH 7 y espere estabilidad.", value: "7.00", step: "0.01", button: "Calibrar medio", command(value) { return `Cal,mid,${value}`; } }, { title: "Punto bajo", help: "Después del punto medio, use buffer ácido, normalmente pH 4.", value: "4.00", step: "0.01", button: "Calibrar bajo", command(value) { return `Cal,low,${value}`; } }, { title: "Punto alto", help: "Después del punto bajo, use buffer básico, normalmente pH 10.", value: "10.00", step: "0.01", button: "Calibrar alto", command(value) { return `Cal,high,${value}`; } }, { title: "Diagnóstico de pendiente", help: "Muestra pendiente ácida, básica y desplazamiento del punto neutro.", button: "Consultar pendiente", command() { return "Slope,?"; } }, { title: "Compensación de temperatura", help: "La compensación es temporal y siempre se expresa en grados Celsius.", value: "25.0", step: "0.1", button: "Aplicar temperatura", command(value) { return `T,${value}`; }, secondaryButton: "Consultar temperatura", secondaryCommand: "T,?" } ] }, do: { description: "Calibración atmosférica de un punto o calibración de dos puntos con cero.", note: "Para dos puntos, Atlas indica calibrar primero cero (Cal,0) y después aire (Cal).", groups: [ { title: "Cero oxígeno", help: "Use solución de cero O₂, elimine burbujas y espere una lectura estable.", button: "Calibrar cero", command() { return "Cal,0"; } }, { title: "Oxígeno atmosférico", help: "Deje la sonda expuesta al aire hasta que la lectura se estabilice.", button: "Calibrar aire", command() { return "Cal"; } }, { title: "Compensaciones", help: "Temperatura en °C, salinidad en µS/cm y presión atmosférica en kPa.", fields: [ { label: "Temperatura", value: "20.0", step: "0.1", command: "T" }, { label: "Salinidad", value: "0", step: "1", command: "S" }, { label: "Presión", value: "101.3", step: "0.1", command: "P" } ] } ] }, ec: { description: "Calibración de dos o tres puntos; la calibración en seco siempre va primero.", note: "Configure primero la constante K de la sonda. Nunca calibre EC a cero con Cal,0.", groups: [ { title: "Constante de la sonda", help: "Valores habituales: K 0.1, K 1.0 o K 10.", value: "1.0", step: "0.1", button: "Configurar K", command(value) { return `K,${value}`; }, secondaryButton: "Consultar K", secondaryCommand: "K,?" }, { title: "Calibración en seco", help: "Conecte la sonda completamente seca. Este paso siempre debe ser primero.", button: "Calibrar seco", command() { return "Cal,dry"; } }, { title: "Segundo punto", help: "Para calibración de dos puntos use Cal,, por ejemplo 1413.", value: "1413", step: "1", button: "Calibrar punto único", command(value) { return `Cal,${value}`; } }, { title: "Puntos bajo y alto", help: "Para tres puntos use primero bajo y finalmente alto.", fields: [ { label: "Bajo", value: "12880", step: "1", command: "Cal,low" }, { label: "Alto", value: "80000", step: "1", command: "Cal,high" } ] }, { title: "Compensación de temperatura", help: "La compensación se expresa en °C y no se conserva al apagar.", value: "25.0", step: "0.1", button: "Aplicar temperatura", command(value) { return `T,${value}`; }, secondaryButton: "Consultar temperatura", secondaryCommand: "T,?" } ] } }; async function fetchHistoricalData(sensorType) { try { const response = await fetch(`/api/sensors/${sensorType}`); if (!response.ok) { throw new Error(`HTTP ${response.status}`); } return await response.json(); } catch (error) { console.error("[ERROR] Fallo al conectar con el backend:", error); return []; } } async function loadEzoTransportStatus() { const statusElement = document.getElementById("ezo-transport-status"); try { const response = await fetch("/api/system/ezo", { cache: "no-store" }); const data = await response.json(); statusElement.textContent = data.mode === "hardware" ? "HARDWARE I2C" : data.mode === "demo" ? "DEMO" : "NO DISPONIBLE"; statusElement.className = `transport-${data.mode}`; appendTerminalLine( document.getElementById("terminal-output"), `[SISTEMA] ${data.message}`, data.mode === "hardware" ? "#63e6be" : "#ffd166" ); } catch (error) { statusElement.textContent = "NO DISPONIBLE"; statusElement.className = "transport-unavailable"; } } async function sendCommand(command, options = {}) { const sensorType = options.sensor || document.getElementById("terminal-sensor-select").value; const terminalOutput = document.getElementById("terminal-output"); const dangerous = ["factory", "i2c", "baud", "sleep"] .includes(String(command).split(",")[0].toLowerCase()); let dangerousConfirmed = false; if (dangerous) { dangerousConfirmed = confirm( `El comando ${command} puede reiniciar, dormir o cambiar la comunicación del circuito. ¿Desea enviarlo?` ); if (!dangerousConfirmed) return null; } const timeString = new Date().toLocaleTimeString(); appendTerminalLine(terminalOutput, `[${timeString}] TX (${sensorType}): ${command}`, "#ffffff"); try { const response = await fetch(`/api/sensors/${sensorType}/command`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ command, dangerousConfirmed }) }); const data = await response.json(); if (!response.ok) { throw new Error(data.error || `HTTP ${response.status}`); } const mode = data.mode === "hardware" ? "I2C" : "DEMO"; appendTerminalLine( terminalOutput, `[${timeString}] RX (${mode}/${sensorType}): ${data.response}`, data.response === "*ER" ? "#ff6b6b" : "#63e6be" ); terminalOutput.scrollTop = terminalOutput.scrollHeight; return data; } catch (error) { appendTerminalLine( terminalOutput, `[${timeString}] ERROR: ${error.message}`, "#ff6b6b" ); return null; } } async function sendCalibrationCommand(command) { const sensor = document.getElementById("cal-sensor-select").value; document.getElementById("terminal-sensor-select").value = sensor; return sendCommand(command, { sensor }); } async function clearCalibration() { const sensor = document.getElementById("cal-sensor-select").value; const confirmed = confirm( `Se borrarán todos los puntos de calibración del sensor ${sensor.toUpperCase()}. ¿Continuar?` ); if (confirmed) { await sendCalibrationCommand("Cal,clear"); } } function renderCalibrationPanel() { const sensor = document.getElementById("cal-sensor-select").value; const profile = calibrationProfiles[sensor]; const controls = document.getElementById("calibration-controls"); document.getElementById("calibration-description").textContent = profile.description; document.getElementById("calibration-note").textContent = profile.note; controls.replaceChildren(); profile.groups.forEach((group, groupIndex) => { const container = document.createElement("div"); container.className = "calibration-group"; const title = document.createElement("h4"); title.textContent = group.title; const help = document.createElement("p"); help.textContent = group.help; container.append(title, help); if (group.fields) { group.fields.forEach((field, fieldIndex) => { container.appendChild(createCalibrationAction( `${sensor}-${groupIndex}-${fieldIndex}`, field.label, field.value, field.step, (value) => `${field.command},${value}` )); }); } else { container.appendChild(createCalibrationAction( `${sensor}-${groupIndex}`, null, group.value, group.step, group.command, group.button )); } if (group.secondaryCommand) { const secondary = document.createElement("button"); secondary.className = "btn-command"; secondary.textContent = group.secondaryButton; secondary.addEventListener("click", () => sendCalibrationCommand(group.secondaryCommand) ); container.appendChild(secondary); } controls.appendChild(container); }); } function createCalibrationAction(id, label, value, step, commandBuilder, buttonText) { const row = document.createElement("div"); row.className = "calibration-action"; let input = null; if (value !== undefined) { input = document.createElement("input"); input.type = "number"; input.id = `cal-${id}`; input.value = value; input.step = step || "0.01"; input.setAttribute("aria-label", label || buttonText || "Valor de calibración"); row.appendChild(input); } const button = document.createElement("button"); button.className = "btn-command"; button.textContent = buttonText || label; button.addEventListener("click", async () => { const command = commandBuilder(input ? input.value : undefined); if (!command || (input && input.value === "")) return; await sendCalibrationCommand(command); }); row.appendChild(button); return row; } function sendCustomCommand() { const input = document.getElementById("custom-command"); const command = input.value.trim(); if (command) { sendCommand(command); input.value = ""; } } function appendTerminalLine(container, message, color) { const line = document.createElement("div"); line.textContent = message; line.style.color = color; container.appendChild(line); } document.addEventListener("DOMContentLoaded", () => { loadEzoTransportStatus(); renderCalibrationPanel(); });