Instrumentos y Mediciones 2 · UNPSJB · Instructivo Técnico v1.0

Instrumentos Virtuales
Arduino UNO + HMI Web

Material de desarrollo para la cátedra Instrumentos y Mediciones 2 — Facultad de Ingeniería, UNPSJB. Guía para implementar instrumentos de medición y control con firmware C++ en Arduino UNO y panel de visualización HTML/JS/CSS conectado vía USB serial.

// 01

Arquitectura del sistema

El sistema se compone de tres capas: el hardware de adquisición (Arduino UNO con sus periféricos), la capa de comunicación (protocolo serial sobre USB), y la interfaz HMI (aplicación web en el navegador). Esta arquitectura es el punto de partida para los trabajos prácticos de la cátedra.

⚙ Arduino UNO ←→ ⚡ USB Serial ←→ 🖥 Navegador (HMI)
Pines analógicos (A0–A5) ──→ ADC 10-bit ──→ UART TX ──→ USB CDC ──→ Web Serial API ──→ Canvas/SVG
HMI ──→ Comando texto ──→ USB CDC ──→ UART RX ──→ Parser ──→ digitalWrite / PWM
⚙️

Arduino UNO

ATmega328P a 16 MHz. ADC de 10 bits, UART a 9600–115200 bps, pines PWM y GPIO.

🔌

USB Serial

El chip CH340/ATmega16U2 convierte UART a USB CDC. No requiere driver adicional en Linux/macOS.

🌐

Web Serial API

API nativa de Chrome/Edge que permite acceso al puerto COM desde JavaScript sin instalación.

📊

HMI Web

HTML + CSS + Canvas/SVG para visualización. Sin frameworks externos requeridos.

ℹ️
Compatibilidad de navegadores Web Serial API funciona en Google Chrome 89+ y Microsoft Edge 89+. No es compatible con Firefox ni Safari actualmente.
// 02

Requisitos y herramientas

ComponenteDescripciónNecesario
Arduino UNO R3Microcontrolador ATmega328P con USB integrado✅ Sí
Arduino IDEv2.x recomendado — arduino.cc/en/software✅ Sí
Google Chrome / Edge89+ para Web Serial API✅ Sí
Editor de códigoVS Code, Sublime Text u otroRecomendado
Cable USB-A a USB-BComunicación y alimentación del Arduino✅ Sí
MultímetroPara verificar señales de entradaRecomendado

Instalación rápida

  1. Instalar Arduino IDE desde arduino.cc/en/software y verificar que reconoce el Arduino UNO al conectarlo.
  2. En el IDE, ir a Herramientas → Puerto y seleccionar el puerto COM asignado (ej: COM3 en Windows, /dev/ttyACM0 en Linux).
  3. Crear un archivo HTML local (o servidor HTTP simple) para la interfaz HMI. No se requieren dependencias npm.
  4. Abrir el archivo HTML en Chrome. Al conectar el Arduino, el navegador pedirá permiso para acceder al puerto serial.
⚠️
Conflicto de puertos El Arduino IDE y la HMI web no pueden usar el puerto serial simultáneamente. Cerrar el Monitor Serie del IDE antes de abrir la conexión desde el navegador.
// 03

Protocolo de comunicación serial

Se utiliza un protocolo de texto simple, legible y fácil de depurar. Cada mensaje es una línea terminada en \n. El diseño prioriza la simplicidad sobre la eficiencia, lo que facilita el aprendizaje y la verificación de señales — criterio clave en el contexto de la cátedra.

Formato de trama

Arduino → HMI CLAVE:VALOR\n  —  ejemplo: V0:512\n
HMI → Arduino CMD:PARAM\n  —  ejemplo: LED:1\n
Multi-valor DATA:512,301,89\n  —  separador coma
JSON (avanzado) {"V0":512,"V1":301,"T":25.3}\n
💡
Recomendación de baud rate Usar 115200 bps para instrumentos que envían datos rápido (ej: osciloscopio). Para sensores lentos (temperatura, humedad) 9600 bps es suficiente y más robusto.

Tabla de comandos estándar

Comando (HMI→ARD)DescripciónEjemplo
STARTInicia envío continuo de datosSTART\n
STOPPausa el envío de datosSTOP\n
RATE:NEstablece intervalo de muestreo en msRATE:100\n
LED:0|1Controla LED de salidaLED:1\n
PWM:NAjusta duty cycle PWM (0–255)PWM:128\n
MODE:NCambia modo de operación del instrumentoMODE:2\n
PIN?:NSolicita lectura puntual de pin analógicoPIN?:A0\n
Dato (ARD→HMI)DescripciónEjemplo
V0:NValor ADC canal 0 (0–1023)V0:512\n
V1:NValor ADC canal 1V1:301\n
TEMP:N.NTemperatura con decimalTEMP:25.4\n
OKConfirmación de comando recibidoOK\n
ERR:MSGError con descripciónERR:CMD_UNKNOWN\n
HELLO:v1.0Identificación al conectarHELLO:v1.0\n
// 04

Firmware Arduino (C++)

La estructura del sketch sigue un patrón no bloqueante basado en temporización con millis(), permitiendo leer sensores, enviar datos y responder comandos de forma concurrente sin usar delay(). Este enfoque es fundamental en instrumentación ya que garantiza la integridad temporal de las mediciones.

Estructura principal — setup() y loop()

C++ / Arduino — sketch_instrumento.ino
// =============================================
// Instrumento Virtual — Firmware base
// Arduino UNO + HMI Web via USB Serial
// =============================================

const int BAUD_RATE   = 115200;
const int SAMPLE_PIN  = A0;       // Pin analógico de entrada
const int LED_PIN     = 13;       // LED integrado

unsigned long lastSample   = 0;
unsigned long sampleRate   = 100;  // ms entre muestras
bool          streaming    = false;

String        inputBuffer  = "";   // Buffer para comandos entrantes

void setup() {
  Serial.begin(BAUD_RATE);
  pinMode(LED_PIN, OUTPUT);
  
  // Esperar a que el puerto serie esté listo
  while (!Serial) { delay(10); }
  
  // Mensaje de identificación al conectar
  Serial.println("HELLO:v1.0");
}

void loop() {
  readCommands();   // Procesar comandos entrantes
  sendSamples();    // Enviar datos si streaming activo
}

Envío de datos de sensores

C++ / Arduino — función sendSamples()
void sendSamples() {
  if (!streaming) return;
  
  unsigned long now = millis();
  if (now - lastSample < sampleRate) return;
  lastSample = now;

  // Leer canal analógico (0 a 1023)
  int raw = analogRead(SAMPLE_PIN);

  // Convertir a tensión (0.0 – 5.0 V)
  float voltage = raw * (5.0 / 1023.0);
  
  // Enviar en formato CLAVE:VALOR
  Serial.print("V0:");   Serial.println(raw);
  Serial.print("VV:");   Serial.println(voltage, 3);

  // Para enviar múltiples canales en una línea:
  // Serial.print("DATA:"); Serial.print(ch0); Serial.print(",");
  // Serial.print(ch1); Serial.println();
}

Intérprete de comandos desde la HMI

C++ / Arduino — readCommands() + interpretCommand()
void readCommands() {
  while (Serial.available() > 0) {
    char c = (char) Serial.read();
    
    if (c == '\n') {
      inputBuffer.trim();          // Eliminar \r y espacios
      if (inputBuffer.length() > 0) {
        interpretCommand(inputBuffer);
      }
      inputBuffer = "";            // Limpiar buffer
    } else {
      inputBuffer += c;           // Acumular caracteres
    }
  }
}

void interpretCommand(String cmd) {
  // Separar CLAVE y PARAM por el delimitador ':'
  int sep = cmd.indexOf(':');
  String key   = (sep > 0) ? cmd.substring(0, sep) : cmd;
  String param = (sep > 0) ? cmd.substring(sep + 1) : "";

  if (key == "START") {
    streaming = true;
    Serial.println("OK");

  } else if (key == "STOP") {
    streaming = false;
    Serial.println("OK");

  } else if (key == "RATE") {
    sampleRate = param.toInt();
    Serial.println("OK");

  } else if (key == "LED") {
    digitalWrite(LED_PIN, param.toInt() ? HIGH : LOW);
    Serial.println("OK");

  } else if (key == "PWM") {
    analogWrite(9, param.toInt());  // Pin 9 con PWM
    Serial.println("OK");

  } else {
    Serial.print("ERR:CMD_UNKNOWN:");
    Serial.println(key);
  }
}
💡
Evitar delay() siempre El uso de delay() bloquea el procesamiento de comandos entrantes. Siempre usar el patrón millis() - lastTime >= interval para temporización no bloqueante.
// 05

Interfaz HMI en HTML/JS

Conexión con Web Serial API

La Web Serial API permite al navegador comunicarse con dispositivos serie (incluyendo Arduino) sin instalar drivers ni extensiones. El flujo es: solicitar permiso → abrir puerto → leer/escribir mediante streams. Es la tecnología que utilizaremos en los trabajos prácticos de IM2 para construir los paneles de visualización.

Clic "Conectar" → navigator.serial.requestPort() → port.open() → ReadableStream loop → parsear líneas → actualizar UI
JavaScript — serial.js — conexión completa
class ArduinoSerial {
  constructor(onData) {
    this.port     = null;
    this.reader   = null;
    this.writer   = null;
    this.onData   = onData;    // Callback intérprete (línea: string)
    this.buffer   = "";
    this.running  = false;
  }

  async connect(baudRate = 115200) {
    try {
      // Solicitar puerto al usuario
      this.port = await navigator.serial.requestPort();
      await this.port.open({ baudRate });

      // Configurar writer (HMI → Arduino)
      const textEncoder = new TextEncoderStream();
      textEncoder.readable.pipeTo(this.port.writable);
      this.writer = textEncoder.writable.getWriter();

      // Iniciar loop de lectura
      this.running = true;
      this._readLoop();
      return true;
    } catch (err) {
      console.error("Error al conectar:", err);
      return false;
    }
  }

  async _readLoop() {
    const decoder = new TextDecoderStream();
    this.port.readable.pipeTo(decoder.writable);
    this.reader = decoder.readable.getReader();

    while (this.running) {
      const { value, done } = await this.reader.read();
      if (done) break;

      this.buffer += value;
      const lines = this.buffer.split('\n');
      this.buffer = lines.pop();  // Retener fragmento incompleto

      for (const line of lines) {
        const trimmed = line.trim();
        if (trimmed) this.onData(trimmed);  // Llamar intérprete
      }
    }
  }

  async send(message) {
    if (this.writer) {
      await this.writer.write(message + '\n');
    }
  }

  async disconnect() {
    this.running = false;
    await this.reader?.cancel();
    await this.writer?.close();
    await this.port?.close();
  }
}

Intérprete de datos en la HMI

JavaScript — interpretación de líneas del Arduino
// Instanciar la clase con un callback de datos
const arduino = new ArduinoSerial(interpretarLinea);

function interpretarLinea(line) {
  const [key, val] = line.split(':');

  switch (key) {
    case 'HELLO':
      updateStatus('Conectado — firmware ' + val);
      arduino.send('START');  // Iniciar streaming automáticamente
      break;

    case 'V0':
      updateGauge('voltaje', parseInt(val) * 5.0 / 1023);
      break;

    case 'TEMP':
      updateDisplay('temperatura', parseFloat(val));
      break;

    case 'DATA':
      // Múltiples valores separados por coma
      const values = val.split(',').map(Number);
      updateOscilloscope(values);
      break;

    case 'ERR':
      console.warn('Arduino error:', val);
      break;
  }
}

Renderizado en Canvas — forma de onda básica

JavaScript — osciloscopio simple con Canvas 2D
const canvas  = document.getElementById('scope');
const ctx     = canvas.getContext('2d');
const buffer  = [];  // Historial de muestras
const MAX_PTS = 200;

function updateOscilloscope(sample) {
  buffer.push(sample);
  if (buffer.length > MAX_PTS) buffer.shift();

  const W = canvas.width;
  const H = canvas.height;
  const step = W / MAX_PTS;

  // Fondo
  ctx.fillStyle = '#0d0f14';
  ctx.fillRect(0, 0, W, H);

  // Grilla
  ctx.strokeStyle = '#1a2a1a';
  ctx.lineWidth = 1;
  for (let i = 0; i <= 4; i++) {
    const y = (H / 4) * i;
    ctx.beginPath(); ctx.moveTo(0, y); ctx.lineTo(W, y); ctx.stroke();
  }

  // Señal
  ctx.strokeStyle = '#00e5a0';
  ctx.lineWidth = 2;
  ctx.beginPath();
  buffer.forEach((v, i) => {
    const x = i * step;
    const y = H - (v / 1023) * H;
    i === 0 ? ctx.moveTo(x, y) : ctx.lineTo(x, y);
  });
  ctx.stroke();
}
// 06

Ejemplos de instrumentos completos

Ejemplo 1 — Voltímetro digital

Mide la tensión en el pin A0 (0–5 V), calcula promedio de 16 muestras para reducir ruido, y envía el resultado cada 200 ms.

C++ / Arduino — voltimetro.ino
// Voltímetro con promediado de muestras
const int   N_SAMPLES  = 16;
unsigned long lastMs    = 0;
bool          streaming  = false;
String        cmdBuf     = "";

void setup() {
  Serial.begin(115200);
  while (!Serial);
  Serial.println("HELLO:voltimetro-v1");
}

float readVoltage() {
  long sum = 0;
  for (int i = 0; i < N_SAMPLES; i++) {
    sum += analogRead(A0);
    delayMicroseconds(200);  // Estabilizar ADC
  }
  return (sum / N_SAMPLES) * (5.0 / 1023.0);
}

void loop() {
  // Leer comandos
  while (Serial.available()) {
    char c = Serial.read();
    if (c == '\n') {
      cmdBuf.trim();
      if (cmdBuf == "START") { streaming = true;  Serial.println("OK"); }
      if (cmdBuf == "STOP")  { streaming = false; Serial.println("OK"); }
      cmdBuf = "";
    } else { cmdBuf += c; }
  }

  // Enviar medición
  if (streaming && millis() - lastMs >= 200) {
    lastMs = millis();
    float v = readVoltage();
    Serial.print("VOLT:"); Serial.println(v, 4);
  }
}

Ejemplo 2 — Osciloscopio de baja frecuencia

Captura 50 muestras del pin A0 lo más rápido posible (sin delays) y las envía como paquete DATA: con valores separados por coma.

C++ / Arduino — osciloscopio.ino
const int   BLOCK  = 50;   // Muestras por paquete
bool          run    = false;
String        buf    = "";

void setup() {
  Serial.begin(115200);
  while (!Serial);
  Serial.println("HELLO:scope-v1");
}

void loop() {
  while (Serial.available()) {
    char c = Serial.read();
    if (c == '\n') {
      buf.trim();
      if (buf == "START") run = true;
      if (buf == "STOP")  run = false;
      buf = "";
    } else { buf += c; }
  }

  if (!run) return;

  // Capturar bloque de muestras
  int samples[BLOCK];
  for (int i = 0; i < BLOCK; i++) {
    samples[i] = analogRead(A0);
    delayMicroseconds(500); // ~2 kHz de muestreo
  }

  // Enviar paquete: DATA:val1,val2,...,valN
  Serial.print("DATA:");
  for (int i = 0; i < BLOCK; i++) {
    Serial.print(samples[i]);
    if (i < BLOCK - 1) Serial.print(',');
  }
  Serial.println();
}
📐
Frecuencia de muestreo máxima El ADC del ATmega328P tarda ~104 µs por conversión (sin configuración adicional). Con delayMicroseconds(500) se logran aproximadamente 1.6 kHz. Para señales de mayor frecuencia es necesario ajustar el prescaler del ADC.
// 07

Debug y resolución de errores

SíntomaCausa probableSolución
El botón Conectar no aparece o no abre diálogo Navegador no soporta Web Serial Usar Chrome 89+ o Edge 89+. Verificar con navigator.serial en consola.
La HMI no recibe datos aunque el Arduino envía Monitor Serie del IDE abierto Cerrar el Monitor Serie antes de conectar desde el navegador.
Datos corruptos o líneas mezcladas Baud rate incorrecto Verificar que el baud rate en Serial.begin() y en port.open() sean iguales.
Comando enviado desde HMI no responde Buffer de entrada sin procesar Asegurarse de que el loop() llama a readCommands() frecuentemente.
Lecturas ADC muy ruidosas Sin promediado o referencia flotante Promediar N muestras. Conectar AREF a 3.3 V o usar analogReference(INTERNAL).
Puerto COM no aparece en Windows Driver CH340 faltante Instalar driver CH340 desde el fabricante o usar Arduino UNO original (ATmega16U2).

Verificación rápida del protocolo

Antes de abrir la HMI, usar el Monitor Serie del Arduino IDE para verificar que el firmware envía los datos correctamente:

Monitor Serie — salida esperada tras enviar "START"
← (escribir en Monitor Serie):  START
→ (respuesta Arduino):          OK
→                                V0:512
→                                VV:2.502
→                                V0:498
→                                VV:2.434
← (escribir):                    STOP
→                                OK
🛠️
Consejo de desarrollo iterativo Desarrollar y probar el firmware primero con el Monitor Serie, sin involucrar la HMI. Una vez confirmado que el protocolo funciona correctamente, conectar la interfaz web. Esto aísla problemas y acelera el desarrollo.