You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

351 lines
13 KiB
JavaScript

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,<temperatura>.",
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,<valor>, 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;
const result = await sendCommand(command, { sensor });
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");
}
}
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();
});