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: "Calibre con T=20 °C, S=0 y P=101.3 kPa; aplique compensaciones reales después.", 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", query: "T,?" }, { label: "Salinidad", value: "0", step: "1", command: "S", query: "S,?" }, { label: "Presión", value: "101.3", step: "0.1", command: "P", query: "P,?" } ] } ] }, ec: { description: "Calibración de dos o tres puntos; la calibración en seco siempre va primero.", note: "Configure K y mantenga T=25 °C durante toda la calibración. Nunca use Cal,0.", groups: [ { title: "Constante de la sonda", help: "Use el valor real de la sonda, entre K 0.01 y K 10.2.", 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 K 1.0 Atlas recomienda Cal,12880 después de Cal,dry.", value: "12880", 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: "Aplíquela sólo después de calibrar; durante la calibración debe permanecer en 25 °C.", 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 normalizedCommand = String(command).trim().toLowerCase(); const dangerous = ["factory", "i2c", "baud", "sleep", "import"] .includes(normalizedCommand.split(",")[0]) || normalizedCommand === "cal,clear" || normalizedCommand === "m,clear"; let dangerousConfirmed = options.dangerousConfirmed === true; if (dangerous && !dangerousConfirmed) { dangerousConfirmed = confirm( `El comando ${command} modifica o borra configuración sensible 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: window.getApiHeaders({ "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, options = {}) { const sensor = document.getElementById("cal-sensor-select").value; document.getElementById("terminal-sensor-select").value = sensor; const result = await sendCommand(command, { sensor, ...options }); const normalized = String(command).toLowerCase(); if ( result && (normalized === "cal" || normalized.startsWith("cal,")) && normalized !== "cal,?" ) { await sendCommand("Cal,?", { sensor }); } return result; } 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", { dangerousConfirmed: true }); } } 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) => { const action = createCalibrationAction( `${sensor}-${groupIndex}-${fieldIndex}`, field.label, field.value, field.step, (value) => `${field.command},${value}` ); if (field.query) { const queryButton = document.createElement("button"); queryButton.className = "btn-command"; queryButton.textContent = "Consultar"; queryButton.addEventListener("click", () => sendCalibrationCommand(field.query) ); action.appendChild(queryButton); } container.appendChild(action); }); } 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(); });