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
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: 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) {
|
|
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();
|
|
});
|