
Entrada 11: Centro de Control de Misión (Telemetry Hub) con Claude Artifacts: Versión Simulador y Versión Web Serial Real (2º Bachillerato) BermejaHab
Tras completar el soporte vital en el Iglú Habitacional con ESO (micro:bit) y la automatización mecatrónica de la Cúpula del Invernadero en Shackleton con 1º de Bachillerato (Arduino Mega 2560), alcanzamos la cúspide de la integración vertical del proyecto BermejaHab. Es el momento del alumnado de 2º de Bachillerato en la asignatura Programación y Computación.
Su objetivo en esta fase final es actuar como Ingenieros de Software de Vuelo, desarrollando la interfaz del Centro de Control de Misión (Telemetry Hub) mediante Claude Artifacts (HTML5, Tailwind CSS y JavaScript).
Para garantizar una progresión pedagógica idónea en el aula, el trabajo se divide en dos fases consecutivas:
- Fase 1 (Versión Simulador): Prototipado e interacción web con datos simulados para validar la interfaz y hacer pruebas de estrés sin necesidad de cableado.
- Fase 2 (Versión Real con Web Serial API): Conexión física directa entre el navegador web y el puerto USB del Arduino Mega para decodificar la telemetría en tiempo real.
Plaintext
FASE 1: SIMULADOR (Aula / Sin Hardware) FASE 2: TELEMETRÍA REAL (Conexión USB)
┌──────────────────────────────────────┐ ┌──────────────────────────────────────┐
│ Generador Interno de Datos (JS) │ │ Arduino Mega 2560 (Sensores Reales) │
└──────────────────┬───────────────────┘ └──────────────────┬───────────────────┘
│ │ [Puerto USB / COM]
▼ ▼
┌──────────────────────────────────────┐ ┌──────────────────────────────────────┐
│ Dashboard Web de Pruebas │ │ Web Serial API (Navegador Chrome) │
│ (Eventos Inyectados / Pruebas UI) │ │ Parser de Cadenas ASCII / Regex │
└──────────────────────────────────────┘ └──────────────────────────────────────┘
FASE 1: Versión Simulador (Prototipado en el Aula sin Hardware)
En esta primera fase, el alumnado diseña y prueba la experiencia de usuario (UI/UX) y la lógica de alertas utilizando un motor de simulación interna en JavaScript. Permite comprobar cómo responde la cúpula ante picos de radiación solar o noches artificiales sin depender de la placa física.
🤖 Prompt 1: Generación del Simulador en Claude
Plaintext
Actúa como un desarrollador frontend senior especializado en interfaces de control espacial (UI/UX para la ESA/NASA). Crea una aplicación web interactiva en un solo archivo (HTML + Tailwind CSS + JavaScript) que funcione como el SIMULADOR del Centro de Control de Misión de la base lunar BermejaHab.
Requisitos de la Versión Simulador:
1. Estética: Modo oscuro profesional estilo "Space Mission Control" (tonos slate-950, cyan, emerald y rose), con tipografía monospace para métricas.
2. Panel Módulo Habitacional (micro:bit - ESO): Temperatura interior y estado del soporte vital.
3. Panel Cúpula e Invernadero (Arduino Mega - 1º Bachillerato):
- Muestra la Hora Simulada en Shackleton (00:00 a 23:00).
- Indicador de Radiación UV (Sensor S12SD) y Clima Agrícola (AM2302).
- Estado visual de la Cúpula: Banner brillante que cambie entre "TRANSPARENTE (Fotosíntesis)" y "OPACA (Protección / Noche)".
4. Motor de Simulación e Inyección de Eventos:
- Botón "Iniciar Telemetría" que haga avanzar la hora y varíe los datos dinámicamente cada 2 segundos.
- Botones de inyección de eventos para pruebas en el aula: "Forzar UV > 8.0" y "Forzar Temp > 28 °C".
5. Terminal de Log: Consola estilo Linux en la parte inferior para imprimir las tramas generadas.
Genera el código completo en un único artifact listo para previsualizar.
💻 Código Web 1: Simulador de Misión (mission_control_simulator.html)
HTML
<!DOCTYPE html>
<html lang="es">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>BermejaHab - Simulador de Misión</title>
<script src="https://cdn.tailwindcss.com"></script>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
<style>
.font-mono-tech { font-family: 'Courier New', Courier, monospace; }
.glow-green { box-shadow: 0 0 15px rgba(16, 185, 129, 0.3); }
.glow-red { box-shadow: 0 0 15px rgba(244, 63, 94, 0.4); }
</style>
</head>
<body class="bg-slate-950 text-slate-100 min-h-screen p-4 md:p-6 font-sans">
<!-- HEADER / BARRA DE ESTADO SUPERIOR -->
<header class="max-w-7xl mx-auto mb-6 bg-slate-900 border border-slate-800 rounded-xl p-4 flex flex-col md:flex-row justify-between items-center gap-4 shadow-2xl">
<div class="flex items-center gap-3">
<div class="p-3 bg-cyan-500/10 border border-cyan-500/30 rounded-lg text-cyan-400">
<i class="fa-solid fa-shuttle-space text-2xl"></i>
</div>
<div>
<h1 class="text-xl font-bold tracking-wider text-slate-100">BERMEJAHAB <span class="text-xs px-2 py-0.5 rounded bg-cyan-950 text-cyan-400 border border-cyan-800">SIMULADOR</span></h1>
<p class="text-xs text-slate-400">Centro de Control de Misión | IES Monterroso (MonteSTEAM)</p>
</div>
</div>
<div class="flex items-center gap-6 bg-slate-950 px-6 py-2 rounded-lg border border-slate-800">
<div class="text-right">
<div class="text-xs text-slate-500 uppercase tracking-widest">Hora Shackleton</div>
<div id="clock-display" class="text-2xl font-bold font-mono-tech text-cyan-400">08:00 AM</div>
</div>
<div id="phase-badge" class="px-3 py-1 rounded text-xs font-bold uppercase bg-amber-500/20 text-amber-400 border border-amber-500/30">
☀️ Día Simulado
</div>
</div>
<button id="btn-toggle-sim" onclick="toggleSimulation()" class="bg-cyan-600 hover:bg-cyan-500 text-slate-950 font-bold px-4 py-2 rounded-lg transition-all flex items-center gap-2">
<i class="fa-solid fa-play" id="sim-icon"></i> <span id="sim-text">Iniciar Telemetría</span>
</button>
</header>
<!-- GRID PRINCIPAL -->
<main class="max-w-7xl mx-auto grid grid-cols-1 lg:grid-cols-3 gap-6">
<!-- NODO 1: HABITÁCULO (ESO) -->
<section class="bg-slate-900 border border-slate-800 rounded-xl p-5 flex flex-col justify-between shadow-lg">
<div>
<div class="flex justify-between items-center mb-4">
<h2 class="font-bold text-slate-300 flex items-center gap-2">
<i class="fa-solid fa-house-chimney-window text-indigo-400"></i> Módulo Habitacional (ESO)
</h2>
<span class="text-xs bg-emerald-500/10 text-emerald-400 px-2 py-1 rounded border border-emerald-500/20">VIRTUAL</span>
</div>
<div class="grid grid-cols-2 gap-4 my-4">
<div class="bg-slate-950 p-4 rounded-lg border border-slate-800">
<span class="text-xs text-slate-500 block mb-1">Temp. Interior</span>
<span id="hab-temp" class="text-3xl font-bold font-mono-tech text-slate-100">21.5</span>
<span class="text-sm text-slate-400">°C</span>
</div>
<div class="bg-slate-950 p-4 rounded-lg border border-slate-800">
<span class="text-xs text-slate-500 block mb-1">Presión Est.</span>
<span id="hab-press" class="text-3xl font-bold font-mono-tech text-slate-100">1013</span>
<span class="text-sm text-slate-400">hPa</span>
</div>
</div>
</div>
<div class="bg-slate-950 p-3 rounded-lg border border-slate-800 flex items-center justify-between">
<span class="text-xs text-slate-400">Soporte Vital:</span>
<span class="text-xs font-bold text-emerald-400 bg-emerald-950/50 px-2.5 py-1 rounded border border-emerald-800">
<i class="fa-solid fa-circle-check mr-1"></i> NOMINAL
</span>
</div>
</section>
<!-- NODO 2: INVERNADERO (1º BACH) -->
<section class="bg-slate-900 border border-slate-800 rounded-xl p-5 shadow-lg lg:col-span-2 flex flex-col justify-between">
<div>
<div class="flex justify-between items-center mb-4">
<h2 class="font-bold text-slate-300 flex items-center gap-2">
<i class="fa-solid fa-seedling text-emerald-400"></i> Cúpula y Cultivo Lunar (1º Bach)
</h2>
<span class="text-xs bg-cyan-500/10 text-cyan-400 px-2 py-1 rounded border border-cyan-500/20">Nodo Mega 2560</span>
</div>
<div id="dome-status-card" class="mb-6 p-4 rounded-xl bg-emerald-950/40 border border-emerald-500/40 glow-green transition-all flex items-center justify-between">
<div class="flex items-center gap-4">
<div id="dome-icon-bg" class="p-3 bg-emerald-500 text-slate-950 rounded-lg text-2xl">
<i class="fa-solid fa-sun" id="dome-icon"></i>
</div>
<div>
<span class="text-xs text-slate-400 uppercase tracking-wider block">Estado de Cúpula Electrócroma</span>
<h3 id="dome-status-text" class="text-xl font-extrabold text-emerald-400">TRANSPARENTE (Fotosíntesis Activa)</h3>
<p id="dome-cause-text" class="text-xs text-slate-300 mt-0.5">Captando luz solar natural de Shackleton</p>
</div>
</div>
<div id="dome-led-indicator" class="hidden md:flex items-center gap-2 bg-slate-900 px-3 py-1.5 rounded-lg border border-slate-800">
<span class="w-3 h-3 rounded-full bg-emerald-500 animate-pulse"></span>
<span class="text-xs font-mono-tech text-slate-300">LED_BUILTIN: LOW</span>
</div>
</div>
<div class="grid grid-cols-1 sm:grid-cols-3 gap-4">
<div class="bg-slate-950 p-4 rounded-lg border border-slate-800">
<span class="text-xs text-slate-500 block mb-1">Radiación UV (S12SD)</span>
<div class="flex items-baseline gap-2">
<span id="uv-value" class="text-3xl font-bold font-mono-tech text-slate-100">2.4</span>
<span class="text-xs text-slate-400">UVI</span>
</div>
<div class="w-full bg-slate-800 h-1.5 rounded-full mt-2 overflow-hidden">
<div id="uv-bar" class="bg-amber-400 h-full w-[24%] transition-all"></div>
</div>
</div>
<div class="bg-slate-950 p-4 rounded-lg border border-slate-800">
<span class="text-xs text-slate-500 block mb-1">Temp. Agrícola (AM2302)</span>
<div class="flex items-baseline gap-2">
<span id="crop-temp" class="text-3xl font-bold font-mono-tech text-slate-100">22.8</span>
<span class="text-xs text-slate-400">°C</span>
</div>
<div class="w-full bg-slate-800 h-1.5 rounded-full mt-2 overflow-hidden">
<div id="temp-bar" class="bg-rose-400 h-full w-[45%] transition-all"></div>
</div>
</div>
<div class="bg-slate-950 p-4 rounded-lg border border-slate-800">
<span class="text-xs text-slate-500 block mb-1">Humedad (AM2302)</span>
<div class="flex items-baseline gap-2">
<span id="crop-hum" class="text-3xl font-bold font-mono-tech text-slate-100">65.0</span>
<span class="text-xs text-slate-400">%</span>
</div>
<div class="w-full bg-slate-800 h-1.5 rounded-full mt-2 overflow-hidden">
<div id="hum-bar" class="bg-cyan-400 h-full w-[65%] transition-all"></div>
</div>
</div>
</div>
</div>
<!-- PRUEBAS EN EL AULA -->
<div class="mt-6 pt-4 border-t border-slate-800 flex flex-wrap items-center justify-between gap-4">
<span class="text-xs text-slate-400 font-semibold"><i class="fa-solid fa-sliders mr-1"></i> Inyección de Eventos de Prueba:</span>
<div class="flex gap-2">
<button onclick="injectEvent('uv')" class="text-xs bg-amber-500/20 hover:bg-amber-500/30 text-amber-300 border border-amber-500/30 px-3 py-1.5 rounded transition">⚡ Forzar UV > 8.0</button>
<button onclick="injectEvent('temp')" class="text-xs bg-rose-500/20 hover:bg-rose-500/30 text-rose-300 border border-rose-500/30 px-3 py-1.5 rounded transition">🔥 Forzar Temp > 28 °C</button>
<button onclick="resetEvents()" class="text-xs bg-slate-800 hover:bg-slate-700 text-slate-300 border border-slate-700 px-3 py-1.5 rounded transition">🔄 Normalizar</button>
</div>
</div>
</section>
<!-- TERMINAL DE CONSOLA -->
<section class="bg-slate-900 border border-slate-800 rounded-xl p-5 lg:col-span-3 shadow-lg">
<div class="flex justify-between items-center mb-3">
<h2 class="text-sm font-bold text-slate-400 flex items-center gap-2 font-mono-tech">
<i class="fa-solid fa-terminal text-cyan-400"></i> CONSOLA DE TELEMETRÍA SIMULADA
</h2>
<button onclick="clearLogs()" class="text-xs text-slate-500 hover:text-slate-300">Limpiar Consola</button>
</div>
<div id="console-log" class="bg-slate-950 border border-slate-800 rounded-lg p-4 font-mono-tech text-xs text-emerald-400 h-40 overflow-y-auto space-y-1">
<div>[SIMULATOR READY] Pulsa "Iniciar Telemetría" para arrancar el flujo de datos.</div>
</div>
</section>
</main>
<script>
let isRunning = false, timer = null, hour = 8, forceUV = false, forceTemp = false;
function toggleSimulation() {
isRunning = !isRunning;
document.getElementById('sim-text').innerText = isRunning ? "Pausar Telemetría" : "Iniciar Telemetría";
document.getElementById('sim-icon').className = isRunning ? "fa-solid fa-pause" : "fa-solid fa-play";
if (isRunning) timer = setInterval(updateCycle, 2000);
else clearInterval(timer);
}
function injectEvent(type) {
if (type === 'uv') forceUV = true;
if (type === 'temp') forceTemp = true;
updateCycle();
}
function resetEvents() { forceUV = false; forceTemp = false; updateCycle(); }
function clearLogs() { document.getElementById('console-log').innerHTML = '<div>[CONSOLE CLEARED]</div>'; }
function updateCycle() {
hour = (hour + 1) % 24;
const hourStr = hour < 10 ? '0' + hour : hour;
document.getElementById('clock-display').innerText = `${hourStr}:00`;
const isNight = (hour >= 16 || hour < 8);
const phaseBadge = document.getElementById('phase-badge');
phaseBadge.innerText = isNight ? "🌙 Noche Artificial" : "☀️ Día Simulado";
phaseBadge.className = isNight
? "px-3 py-1 rounded text-xs font-bold uppercase bg-indigo-500/20 text-indigo-400 border border-indigo-500/30"
: "px-3 py-1 rounded text-xs font-bold uppercase bg-amber-500/20 text-amber-400 border border-amber-500/30";
let uv = forceUV ? 9.5 : (isNight ? 0.0 : (Math.random() * 3 + 2).toFixed(1));
let temp = forceTemp ? 30.2 : (21.0 + Math.random() * 4).toFixed(1);
let hum = (60 + Math.random() * 10).toFixed(1);
document.getElementById('uv-value').innerText = uv;
document.getElementById('crop-temp').innerText = temp;
document.getElementById('crop-hum').innerText = hum;
document.getElementById('uv-bar').style.width = Math.min((uv/12)*100, 100) + '%';
document.getElementById('temp-bar').style.width = Math.min((temp/40)*100, 100) + '%';
document.getElementById('hum-bar').style.width = hum + '%';
let isOpaca = false, cause = "";
if (isNight) { isOpaca = true; cause = "🌙 Noche Artificial (Fotoperiodo)"; }
else if (uv > 8.0) { isOpaca = true; cause = "🚨 Alerta UV Extrema (> 8.0 UVI)"; }
else if (temp > 28.0) { isOpaca = true; cause = "🔥 Golpe de Calor (> 28.0 °C)"; }
else { isOpaca = false; cause = "Captando luz solar de Shackleton"; }
const card = document.getElementById('dome-status-card');
const statusText = document.getElementById('dome-status-text');
const causeText = document.getElementById('dome-cause-text');
const iconBg = document.getElementById('dome-icon-bg');
const icon = document.getElementById('dome-icon');
const led = document.getElementById('dome-led-indicator');
if (isOpaca) {
card.className = "mb-6 p-4 rounded-xl bg-rose-950/40 border border-rose-500/40 glow-red transition-all flex items-center justify-between";
statusText.className = "text-xl font-extrabold text-rose-400";
statusText.innerText = "OPACA (Protección Activa)";
causeText.innerText = cause;
iconBg.className = "p-3 bg-rose-500 text-slate-950 rounded-lg text-2xl";
icon.className = "fa-solid fa-shield-halved";
led.innerHTML = '<span class="w-3 h-3 rounded-full bg-rose-500 animate-pulse"></span><span class="text-xs font-mono-tech text-slate-300">LED_BUILTIN: HIGH</span>';
} else {
card.className = "mb-6 p-4 rounded-xl bg-emerald-950/40 border border-emerald-500/40 glow-green transition-all flex items-center justify-between";
statusText.className = "text-xl font-extrabold text-emerald-400";
statusText.innerText = "TRANSPARENTE (Fotosíntesis Activa)";
causeText.innerText = cause;
iconBg.className = "p-3 bg-emerald-500 text-slate-950 rounded-lg text-2xl";
icon.className = "fa-solid fa-sun";
led.innerHTML = '<span class="w-3 h-3 rounded-full bg-emerald-500 animate-pulse"></span><span class="text-xs font-mono-tech text-slate-300">LED_BUILTIN: LOW</span>';
}
const consoleLog = document.getElementById('console-log');
const logLine = document.createElement('div');
logLine.innerText = `[${hourStr}:00] UV: ${uv} UVI | Temp: ${temp}°C | Hum: ${hum}% | Cúpula: ${isOpaca ? '🔴 OPACA' : '🟢 TRANSPARENTE'} (${cause})`;
consoleLog.appendChild(logLine);
consoleLog.scrollTop = consoleLog.scrollHeight;
}
</script>
</body>
</html>

FASE 2: Versión Telemetría Real (Web Serial API con USB)
En esta segunda fase, el alumnado sustituye el generador aleatorio de JavaScript por la Web Serial API del navegador (compatible con Google Chrome y Microsoft Edge). La página web se conecta directamente al puerto COM / USB donde está enchufado el Arduino Mega 2560 a 9600 baudios, decodifica las líneas enviadas por el programa de la Entrada 10 y actualiza la interfaz en vivo.
Plaintext
ARDUINO MEGA 2560 NAVEGADOR WEB (CHROME)
┌──────────────────────┐ Cable USB ┌────────────────────────────────┐
│ Entrada 10 Firmware │ ────────────> │ Web Serial API (navigator.serial)│
│ Serial.print(...) │ 9600 Baud │ Decodificador Regex de Líneas │
└──────────────────────┘ └────────────────────────────────┘
🤖 Prompt 2: Generación de la Interfaz Real Web Serial API
Plaintext
Actúa como un programador frontend experto en IoT. Adapta el Dashboard de BermejaHab para que reciba TELEMETRÍA REAL mediante la Web Serial API de JavaScript desde una placa Arduino Mega 2560 conectada por USB a 9600 baudios.
Requisitos de la Versión Real:
1. Reemplaza los botones de simulación por un botón destacado: "🔌 Conectar Arduino (USB/COM)".
2. Utiliza navigator.serial.requestPort() y un TextDecoderStream() para leer la línea de texto que envía el Arduino cada 2 segundos.
3. El formato exacto emitido por el Arduino Mega en el código de la Entrada 10 es:
"[Hora: 08:00] UV: 2.4 UVI | Temp: 22.8 °C - Hum: 65.0 % | LED 13: 🟢 OFF (Transparente) --> ☀️ Día Simulado (Fotosíntesis Activa)"
4. Implementa Expresiones Regulares (Regex) en JavaScript para extraer:
- Hora simulada.
- Valor flotante de UV (UVI).
- Valor flotante de Temperatura (°C).
- Valor flotante de Humedad (%).
- Estado de la cúpula (OPACA u OFF/TRANSPARENTE) y el motivo del estado.
5. Muestra en vivo la línea cruda recibida en el terminal de consola inferior y actualiza todos los componentes de la interfaz.
6. Maneja errores de conexión o puerto no soportado adecuadamente.
💻 Código Web 2: Hub con Telemetría Serie Real (mission_control_webserial.html)
HTML
<!DOCTYPE html>
<html lang="es">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>BermejaHab - Hub de Telemetría Real (Web Serial)</title>
<script src="https://cdn.tailwindcss.com"></script>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
<style>
.font-mono-tech { font-family: 'Courier New', Courier, monospace; }
.glow-green { box-shadow: 0 0 15px rgba(16, 185, 129, 0.3); }
.glow-red { box-shadow: 0 0 15px rgba(244, 63, 94, 0.4); }
</style>
</head>
<body class="bg-slate-950 text-slate-100 min-h-screen p-4 md:p-6 font-sans">
<!-- HEADER -->
<header class="max-w-7xl mx-auto mb-6 bg-slate-900 border border-slate-800 rounded-xl p-4 flex flex-col md:flex-row justify-between items-center gap-4 shadow-2xl">
<div class="flex items-center gap-3">
<div class="p-3 bg-emerald-500/10 border border-emerald-500/30 rounded-lg text-emerald-400">
<i class="fa-solid fa-plug text-2xl"></i>
</div>
<div>
<h1 class="text-xl font-bold tracking-wider text-slate-100">BERMEJAHAB <span class="text-xs px-2 py-0.5 rounded bg-emerald-950 text-emerald-400 border border-emerald-800">WEB SERIAL LIVE</span></h1>
<p class="text-xs text-slate-400">Recepción Directa desde Arduino Mega 2560 | IES Monterroso</p>
</div>
</div>
<div class="flex items-center gap-6 bg-slate-950 px-6 py-2 rounded-lg border border-slate-800">
<div class="text-right">
<div class="text-xs text-slate-500 uppercase tracking-widest">Hora Recibida</div>
<div id="clock-display" class="text-2xl font-bold font-mono-tech text-cyan-400">--:--</div>
</div>
<div id="phase-badge" class="px-3 py-1 rounded text-xs font-bold uppercase bg-slate-800 text-slate-400 border border-slate-700">
Esperando Puerto...
</div>
</div>
<button id="btn-connect" onclick="connectSerial()" class="bg-emerald-500 hover:bg-emerald-400 text-slate-950 font-bold px-5 py-2.5 rounded-lg transition-all flex items-center gap-2 shadow-lg">
<i class="fa-solid fa-usb"></i> <span id="connect-text">Conectar Arduino USB</span>
</button>
</header>
<!-- MAIN PANEL -->
<main class="max-w-7xl mx-auto grid grid-cols-1 lg:grid-cols-3 gap-6">
<!-- NODO 1: HABITÁCULO -->
<section class="bg-slate-900 border border-slate-800 rounded-xl p-5 flex flex-col justify-between shadow-lg">
<div>
<div class="flex justify-between items-center mb-4">
<h2 class="font-bold text-slate-300 flex items-center gap-2">
<i class="fa-solid fa-house-chimney-window text-indigo-400"></i> Módulo Habitacional (ESO)
</h2>
<span id="hab-status-tag" class="text-xs bg-slate-800 text-slate-400 px-2 py-1 rounded border border-slate-700">STANDBY</span>
</div>
<div class="grid grid-cols-2 gap-4 my-4">
<div class="bg-slate-950 p-4 rounded-lg border border-slate-800">
<span class="text-xs text-slate-500 block mb-1">Temp. Interior</span>
<span id="hab-temp" class="text-3xl font-bold font-mono-tech text-slate-100">21.0</span>
<span class="text-sm text-slate-400">°C</span>
</div>
<div class="bg-slate-950 p-4 rounded-lg border border-slate-800">
<span class="text-xs text-slate-500 block mb-1">Presión Est.</span>
<span id="hab-press" class="text-3xl font-bold font-mono-tech text-slate-100">1013</span>
<span class="text-sm text-slate-400">hPa</span>
</div>
</div>
</div>
<div class="bg-slate-950 p-3 rounded-lg border border-slate-800 flex items-center justify-between">
<span class="text-xs text-slate-400">Estado de Enlace Serie:</span>
<span id="link-status" class="text-xs font-bold text-amber-400 bg-amber-950/50 px-2.5 py-1 rounded border border-amber-800">
<i class="fa-solid fa-triangle-exclamation mr-1"></i> DESCONECTADO
</span>
</div>
</section>
<!-- NODO 2: INVERNADERO (DATOS REALES ARDUINO) -->
<section class="bg-slate-900 border border-slate-800 rounded-xl p-5 shadow-lg lg:col-span-2 flex flex-col justify-between">
<div>
<div class="flex justify-between items-center mb-4">
<h2 class="font-bold text-slate-300 flex items-center gap-2">
<i class="fa-solid fa-seedling text-emerald-400"></i> Telemetría Real de Cúpula (1º Bach)
</h2>
<span class="text-xs bg-cyan-500/10 text-cyan-400 px-2 py-1 rounded border border-cyan-500/20">Arduino Mega 2560 (D4 + A0)</span>
</div>
<div id="dome-status-card" class="mb-6 p-4 rounded-xl bg-slate-950 border border-slate-800 transition-all flex items-center justify-between">
<div class="flex items-center gap-4">
<div id="dome-icon-bg" class="p-3 bg-slate-800 text-slate-400 rounded-lg text-2xl">
<i class="fa-solid fa-circle-question" id="dome-icon"></i>
</div>
<div>
<span class="text-xs text-slate-400 uppercase tracking-wider block">Estado de Cúpula (LED Integrado D13)</span>
<h3 id="dome-status-text" class="text-xl font-extrabold text-slate-300">SIN DATOS AÚN</h3>
<p id="dome-cause-text" class="text-xs text-slate-400 mt-0.5">Conecta el cable USB para recibir telemetría</p>
</div>
</div>
<div id="dome-led-indicator" class="hidden md:flex items-center gap-2 bg-slate-900 px-3 py-1.5 rounded-lg border border-slate-800">
<span class="w-3 h-3 rounded-full bg-slate-600"></span>
<span class="text-xs font-mono-tech text-slate-400">LED_BUILTIN: --</span>
</div>
</div>
<div class="grid grid-cols-1 sm:grid-cols-3 gap-4">
<div class="bg-slate-950 p-4 rounded-lg border border-slate-800">
<span class="text-xs text-slate-500 block mb-1">Radiación UV (S12SD)</span>
<div class="flex items-baseline gap-2">
<span id="uv-value" class="text-3xl font-bold font-mono-tech text-slate-100">0.0</span>
<span class="text-xs text-slate-400">UVI</span>
</div>
<div class="w-full bg-slate-800 h-1.5 rounded-full mt-2 overflow-hidden">
<div id="uv-bar" class="bg-amber-400 h-full w-[0%] transition-all"></div>
</div>
</div>
<div class="bg-slate-950 p-4 rounded-lg border border-slate-800">
<span class="text-xs text-slate-500 block mb-1">Temp. Agrícola (AM2302)</span>
<div class="flex items-baseline gap-2">
<span id="crop-temp" class="text-3xl font-bold font-mono-tech text-slate-100">0.0</span>
<span class="text-xs text-slate-400">°C</span>
</div>
<div class="w-full bg-slate-800 h-1.5 rounded-full mt-2 overflow-hidden">
<div id="temp-bar" class="bg-rose-400 h-full w-[0%] transition-all"></div>
</div>
</div>
<div class="bg-slate-950 p-4 rounded-lg border border-slate-800">
<span class="text-xs text-slate-500 block mb-1">Humedad (AM2302)</span>
<div class="flex items-baseline gap-2">
<span id="crop-hum" class="text-3xl font-bold font-mono-tech text-slate-100">0.0</span>
<span class="text-xs text-slate-400">%</span>
</div>
<div class="w-full bg-slate-800 h-1.5 rounded-full mt-2 overflow-hidden">
<div id="hum-bar" class="bg-cyan-400 h-full w-[0%] transition-all"></div>
</div>
</div>
</div>
</div>
<div class="mt-6 pt-4 border-t border-slate-800 text-xs text-slate-500">
<i class="fa-solid fa-circle-info mr-1"></i> Requiere navegador compatible con Web Serial API (Chrome/Edge en PC/Mac).
</div>
</section>
<!-- TERMINAL SERIE RECEPTOR -->
<section class="bg-slate-900 border border-slate-800 rounded-xl p-5 lg:col-span-3 shadow-lg">
<div class="flex justify-between items-center mb-3">
<h2 class="text-sm font-bold text-slate-400 flex items-center gap-2 font-mono-tech">
<i class="fa-solid fa-terminal text-emerald-400"></i> FLUJO SERIE EN DIRECTO [RAW ASCII STREAM - 9600 BAUD]
</h2>
<button onclick="clearLogs()" class="text-xs text-slate-500 hover:text-slate-300">Limpiar Terminal</button>
</div>
<div id="console-log" class="bg-slate-950 border border-slate-800 rounded-lg p-4 font-mono-tech text-xs text-emerald-400 h-44 overflow-y-auto space-y-1">
<div>[WEB SERIAL READY] Haz clic en "Conectar Arduino USB" y selecciona el puerto serie correspondiente.</div>
</div>
</section>
</main>
<script>
let port, reader;
function clearLogs() {
document.getElementById('console-log').innerHTML = '<div>[TERMINAL CLEARED]</div>';
}
async function connectSerial() {
if (!("serial" in navigator)) {
alert("Tu navegador no soporta la Web Serial API. Por favor, usa Google Chrome o Microsoft Edge en un ordenador.");
return;
}
try {
port = await navigator.serial.requestPort();
await port.open({ baudRate: 9600 });
document.getElementById('connect-text').innerText = "Conectado al Puerto";
document.getElementById('btn-connect').className = "bg-slate-800 text-emerald-400 font-bold px-5 py-2.5 rounded-lg border border-emerald-500/40 flex items-center gap-2";
document.getElementById('link-status').className = "text-xs font-bold text-emerald-400 bg-emerald-950/50 px-2.5 py-1 rounded border border-emerald-800";
document.getElementById('link-status').innerHTML = '<i class="fa-solid fa-link mr-1"></i> ENLACE ACTIVO';
document.getElementById('hab-status-tag').className = "text-xs bg-emerald-500/10 text-emerald-400 px-2 py-1 rounded border border-emerald-500/20";
document.getElementById('hab-status-tag').innerText = "RECEIVING";
const textDecoder = new TextDecoderStream();
const readableStreamClosed = port.readable.pipeTo(textDecoder.writable);
reader = textDecoder.readable.getReader();
let lineBuffer = "";
while (true) {
const { value, done } = await reader.read();
if (done) { reader.releaseLock(); break; }
lineBuffer += value;
let lines = lineBuffer.split('\n');
lineBuffer = lines.pop(); // Mantener línea incompleta en el buffer
for (let line of lines) {
if (line.trim().length > 0) {
processSerialLine(line.trim());
}
}
}
} catch (error) {
console.error("Error al conectar con el Puerto Serie:", error);
appendLog(`⚠️ Error de Conexión: ${error.message}`);
}
}
function appendLog(msg) {
const consoleLog = document.getElementById('console-log');
const logLine = document.createElement('div');
logLine.innerText = msg;
consoleLog.appendChild(logLine);
consoleLog.scrollTop = consoleLog.scrollHeight;
}
function processSerialLine(line) {
appendLog(`>> ${line}`);
// Expresiones Regulares para extraer datos del formato de Arduino Mega:
// Ej: [Hora: 08:00] UV: 2.4 UVI | Temp: 22.8 °C - Hum: 65.0 % | LED 13: 🔴 ON (Opaca) --> 🌙 Noche Artificial...
const matchHora = line.match(/\[Hora:\s*(\d{2}:\d{2})\]/);
const matchUV = line.match(/UV:\s*([\d\.]+)/);
const matchTemp = line.match(/Temp:\s*([\d\.]+)/);
const matchHum = line.match(/Hum:\s*([\d\.]+)/);
const isON = line.includes("🔴 ON") || line.includes("OPACA");
if (matchHora) {
const horaStr = matchHora[1];
document.getElementById('clock-display').innerText = `${horaStr}`;
const hourNum = parseInt(horaStr.split(':')[0]);
const isNight = (hourNum >= 16 || hourNum < 8);
const phaseBadge = document.getElementById('phase-badge');
phaseBadge.innerText = isNight ? "🌙 Noche Artificial" : "☀️ Día Simulado";
phaseBadge.className = isNight
? "px-3 py-1 rounded text-xs font-bold uppercase bg-indigo-500/20 text-indigo-400 border border-indigo-500/30"
: "px-3 py-1 rounded text-xs font-bold uppercase bg-amber-500/20 text-amber-400 border border-amber-500/30";
}
if (matchUV) {
const uv = parseFloat(matchUV[1]);
document.getElementById('uv-value').innerText = uv.toFixed(1);
document.getElementById('uv-bar').style.width = Math.min((uv/12)*100, 100) + '%';
}
if (matchTemp) {
const temp = parseFloat(matchTemp[1]);
document.getElementById('crop-temp').innerText = temp.toFixed(1);
document.getElementById('temp-bar').style.width = Math.min((temp/40)*100, 100) + '%';
}
if (matchHum) {
const hum = parseFloat(matchHum[1]);
document.getElementById('crop-hum').innerText = hum.toFixed(1);
document.getElementById('hum-bar').style.width = hum + '%';
}
// Extraer motivo tras la flecha "-->"
let cause = "Sincronizado con Arduino Mega";
if (line.includes("-->")) {
cause = line.split("-->")[1].trim();
}
// Actualizar interfaz según el estado del LED_BUILTIN
const card = document.getElementById('dome-status-card');
const statusText = document.getElementById('dome-status-text');
const causeText = document.getElementById('dome-cause-text');
const iconBg = document.getElementById('dome-icon-bg');
const icon = document.getElementById('dome-icon');
const led = document.getElementById('dome-led-indicator');
if (isON) {
card.className = "mb-6 p-4 rounded-xl bg-rose-950/40 border border-rose-500/40 glow-red transition-all flex items-center justify-between";
statusText.className = "text-xl font-extrabold text-rose-400";
statusText.innerText = "OPACA (Protección Activa)";
causeText.innerText = cause;
iconBg.className = "p-3 bg-rose-500 text-slate-950 rounded-lg text-2xl";
icon.className = "fa-solid fa-shield-halved";
led.innerHTML = '<span class="w-3 h-3 rounded-full bg-rose-500 animate-pulse"></span><span class="text-xs font-mono-tech text-slate-300">LED_BUILTIN: HIGH</span>';
} else {
card.className = "mb-6 p-4 rounded-xl bg-emerald-950/40 border border-emerald-500/40 glow-green transition-all flex items-center justify-between";
statusText.className = "text-xl font-extrabold text-emerald-400";
statusText.innerText = "TRANSPARENTE (Fotosíntesis Activa)";
causeText.innerText = cause;
iconBg.className = "p-3 bg-emerald-500 text-slate-950 rounded-lg text-2xl";
icon.className = "fa-solid fa-sun";
led.innerHTML = '<span class="w-3 h-3 rounded-full bg-emerald-500 animate-pulse"></span><span class="text-xs font-mono-tech text-slate-300">LED_BUILTIN: LOW</span>';
}
}
</script>
</body>
</html>
🔄 Secuencia Didáctica en el Taller de Programación
- 💻 Sesión 1 (Simulador): El alumnado utiliza la Fase 1 (Prompt 1) para generar el Artifact del simulador en Claude. Validan la usabilidad y realizan pruebas de estrés inyectando alertas ficticias.
- 🔌 Sesión 2 (Integración Web Serial): Pasan a la Fase 2 (Prompt 2). Conectan la placa Arduino Mega 2560 al ordenador mediante el cable USB, abren el archivo
mission_control_webserial.htmlen Chrome o Edge y pulsan “Conectar Arduino USB”. - 🧪 Verificación Multidisciplinar: Modifican físicamente los valores de radiación (tapando/iluminando el S12SD) o temperatura (soplando sobre el AM2302) y observan cómo el Dashboard web reacciona en tiempo real.
- ⚙️ Análisis de Software: Analizan cómo las Expresiones Regulares en JavaScript transforman cadenas de texto simples recibidas por el puerto serie en métricas visuales para el usuario.
🎓 Vinculación Curricular (2º de Bachillerato)
- Asignatura: Programación y Computación (2º Bachillerato).
- Saberes Básicos:
- Desarrollo Frontend e Interfaces IoT: Diseño semántico HTML5, maquetación adaptativa con Tailwind CSS y manipulación asíncrona del DOM.
- Comunicación Serie y Parsing de Datos: Uso de la Web Serial API (
navigator.serial), decodificación de flujos ASCII mediante Expresiones Regulares (Regex) y manejo de Buffers. - Ingeniería de Software Acelerada por IA: Uso iterativo de prompts avanzados con Claude Artifacts para desarrollo web modular.
- Competencias Específicas: Diseñar, desarrollar y validar sistemas informáticos distribuidos que conecten hardware físico con aplicaciones de supervisión web.
🏁 Cierre del Proyecto BermejaHab: Éxito de Integración Vertical
Con el Centro de Control de Misión operativo en sus dos versiones (Simulada y Real), el proyecto BermejaHab (MonteSTEAM) culmina su recorrido interdisciplinar entre etapas:
- ESO: Soporte vital y sensorización del hábitat (micro:bit).
- 1º Bachillerato: Control del fotoperiodo, clima y radiación UV en el Invernadero de Shackleton (Arduino Mega 2560 + AM2302 + S12SD).
- 2º Bachillerato: Desarrollo del software de misión, simulación de datos y conexión Web Serial en tiempo real mediante Claude Artifacts.



