Tacto convierte el teléfono (iPhone o Android, solo con el navegador, sin instalar nada) en un control remoto táctil para una experiencia web con audio e imagen. El teléfono mide qué hace tu mano: dónde toca, con cuántos dedos, con qué presión y diámetro, y cómo gira el aparato. Esos datos viajan por MQTT hasta una página en el computador, donde un panel de conexiones los enlaza con variables de sonido e imagen.
Está pensado como herramienta de curso: cada estudiante levanta su propio sistema en su computador, sin hosting especial, y puede después copiar la librería tacto.js a sus propias plataformas.
- Cómo funciona
- Puesta en marcha
- Archivos del proyecto
- La página del teléfono
- Mensajes MQTT
- Canales: lo que ve la experiencia
- La experiencia y el panel de conexiones
- Variables de salida
- Dos formas de montarlo
- Usar Tacto en tus propias experiencias
- Otras herramientas MQTT
- Variables de entorno del servidor
- Limitaciones conocidas
- Solución de problemas
Teléfono (iOS / Android, solo navegador) Computador
┌───────────────────────────────┐ MQTT ┌────────────────────────────────────────────┐
│ tacto: X, Y, dedos, presión, │ sobre │ Broker MQTT ──► Experiencia web (/exp) │
│ diámetro · giro 3 ejes · │ ─────────► │ (embebido) audio + imagen + │
│ micrófono · cámara (opcional) │ WebSocket │ │ panel de conexiones │
└───────────────────────────────┘ │ └──► otras herramientas (TCP 1883) │
└────────────────────────────────────────────┘
Tres piezas:
| Pieza | Qué es | Dónde vive |
|---|---|---|
| Página del teléfono | Lee el tacto y los sensores y los publica por MQTT. | public/index.html |
| Broker MQTT | El "cartero": recibe lo que publica el teléfono y se lo entrega a quien esté suscrito. | Dentro de server.js (Aedes) |
| Experiencia | Se suscribe a los datos, los convierte en canales y los lleva a audio e imagen según el panel. | public/exp.html + exp.js |
Por qué MQTT: es un protocolo ligero de publicar/suscribir, pensado para flujos de datos pequeños y frecuentes. El teléfono no necesita saber quién escucha, y varias herramientas pueden escuchar a la vez (la experiencia, un Arduino, Node-RED, un script de Python). Además queda como herramienta nueva en el repertorio de los estudiantes.
Salas. Cada conexión usa un código de sala (por ejemplo k7m2xq). Todos los temas MQTT cuelgan de tacto/<sala>/…, así que varios estudiantes pueden compartir un broker sin mezclarse. La experiencia genera la sala y la muestra con un QR.
Recorrido de un toque, de punta a punta:
- El dedo toca la superficie del teléfono. El navegador entrega un
TouchEventcon posición,force(presión) yradiusX/radiusY(tamaño del contacto). index.htmllo normaliza (X e Y entre 0 y 1) y lo publica entacto/<sala>/<dispositivo>/touch, como mucho una vez por cuadro de pantalla (~60 por segundo).- El broker lo reenvía a la experiencia.
tacto.jslo descompone en canales planos (touch.x,touch.f,t2.y…).- Cada conexión del panel toma un canal, lo reescala (rango, curva, inversión, suavizado) y lo escribe en una variable de salida (filtro, volumen, zoom, tono de color…).
- La variable cambia el audio (Web Audio) o la imagen (filtros y transformaciones CSS) en ese mismo cuadro.
Requisitos: Node.js 18 o superior, y que computador y teléfono estén en la misma red (el hotspot del teléfono, conexión USB compartida o el WiFi de la casa). La red de la universidad suele bloquear la conexión entre dispositivos; usa el hotspot.
npm install # solo la primera vez
node server.jsEn PowerShell,
npm startpuede fallar por la política de ejecución de scripts. Usanode server.js(onpm.cmd start); no hace falta cambiar ninguna política.
El servidor imprime algo así:
Tacto
Experiencia (computador): https://localhost:8443/exp
Teléfonos entran a: https://172.20.10.2:8443
Broker MQTT: wss://localhost:8443/mqtt · tcp :1883
En el computador
- Abre
https://localhost:8443/exp. El navegador avisa que el certificado no es de confianza: es normal, lo generó tu propio computador. Acepta continuar. - Pulsa Activar audio (los navegadores exigen un gesto del usuario para sonar).
En el teléfono
- Escanea el QR de la experiencia (o escribe la dirección que aparece bajo "Conectar el teléfono").
- Acepta el aviso del certificado: iPhone, Mostrar detalles → visitar este sitio web; Android, Avanzado → Continuar.
- Escribe tu nombre y pulsa Conectar. Acepta el permiso de movimiento cuando lo pida.
- Arriba debe decir ● conectado. En el computador, el teléfono aparece en el selector "Teléfono".
- Toca la pantalla: el panel de la derecha ya trae conexiones de ejemplo (el tono, el filtro y el zoom responden).
Sin teléfono: arrastra el ratón sobre la imagen de la experiencia. Actúa como un teléfono virtual llamado "ratón" (casilla "Ratón como teléfono").
Para una prueba guiada con teléfonos reales, mira PRUEBA_1.md.
Por qué HTTPS. Los sensores de movimiento y el micrófono solo funcionan en "contexto seguro". El servidor crea un certificado autofirmado (carpeta .cert/) y lo regenera solo si cambian las direcciones IP del computador. El tacto funciona igual sin HTTPS, pero el giro no.
05_TACTO/
├─ server.js Servidor: sirve las páginas, el broker MQTT, el QR y /info
├─ package.json Dependencias: express, aedes, mqtt, ws, qrcode, selfsigned
├─ README.md Este documento
├─ .github/workflows/pages.yml Publica public/ en GitHub Pages
├─ .gitignore Excluye node_modules, .cert y el prototipo _v1
├─ PRUEBA_1.md Guía de prueba con teléfonos reales y tabla de resultados
├─ .cert/ Certificado autofirmado (se crea solo)
├─ _v1/ Prototipo anterior (WebSocket + instrumento sonoro), archivado
└─ public/
├─ index.html Página del teléfono
├─ exp.html Estructura de la experiencia
├─ exp.js Lógica de la experiencia: salidas de audio/imagen, panel de conexiones
├─ fisica.js Física de materiales (tela, agua, squishy, remolino) y dibujo WebGL
├─ tacto.js Librería compartida: conexión MQTT, salas, catálogo de canales
├─ ui.css Estilos compartidos (variables de color, controles, foco)
├─ config.js Configuración opcional (broker propio, dirección de la página del teléfono)
├─ vendor/mqtt.min.js Cliente MQTT local (sin depender de CDN)
├─ .nojekyll Evita que Pages procese los archivos con Jekyll
├─ manifest.webmanifest, icon.svg Para "Añadir a pantalla de inicio"
server.js hace cuatro cosas:
- Sirve
public/con Express (/expentregaexp.html). - Sirve
mqtt.min.jsdesdenode_modulesen/vendor, para que funcione sin internet. - Corre un broker Aedes: MQTT sobre WebSocket en
/mqtt(navegadores) y MQTT sobre TCP en el puerto 1883 (herramientas externas). - Responde
/info(dirección base, estado del broker) y/qr.svg?u=…(genera el QR).
En el terminal registra + mqtt <id> y - mqtt <id> cada vez que un cliente entra o sale: sirve para comprobar que el teléfono llegó.
public/index.html tiene dos estados: formulario de entrada (nombre y sala) y vista de control.
Se leen con Touch Events (targetTouches), que son los que traen presión y radio de contacto. Para probar desde un computador cae a Pointer Events (ratón o lápiz).
| Dato | Origen en el navegador | Resultado |
|---|---|---|
| Posición X, Y | clientX/Y relativo a la superficie |
0–1 |
| Cantidad de dedos | targetTouches.length |
0–5 |
| Presión | Touch.force |
0–1 |
| Diámetro del contacto | radiusX + radiusY (el diámetro medio de la elipse) |
píxeles |
Cada dedo apoyado recibe una casilla estable de 1 a 5 que conserva mientras siga tocando. Así t1 es siempre el primer dedo que apoyaste, aunque levantes otro.
Envío: el movimiento se publica una vez por cuadro (≈60/s, calidad de servicio 0). Cuando cambia la cantidad de dedos se publica de inmediato con entrega garantizada (QoS 1), para no perder nunca un "apoyó" o un "soltó".
Presión y diámetro dependen del teléfono. El navegador solo los entrega si el hardware los mide. Muchos teléfonos Android dan ambos; algunos iPhone no entregan presión útil. Por eso la página mide, en vivo, la calidad de cada dato y la muestra abajo de la superficie:
- variable: cambia al apretar; sirve como control.
- constante: el teléfono entrega un valor fijo; no sirve.
- sin dato: siempre cero.
Si la presión no sirve en tu teléfono, el diámetro del contacto (que crece al apretar más) es el sustituto más cercano. Esta información viaja también en el estado del dispositivo (caps).
Con el botón Giro (se activa solo al conectar si das permiso):
deviceorientation: ángulos alfa, beta y gamma en grados.devicemotion: velocidad de giro (°/s) y aceleración con gravedad (m/s²).
iOS exige un permiso explícito, pedido desde un toque del usuario. Si lo negaste, cierra Safari por completo y vuelve a entrar. Se publica cada 33 ms (≈30/s).
- Micrófono: se mide el nivel (RMS) y tres bandas de frecuencia (graves 40–250 Hz, medios 250–2000 Hz, agudos 2–8 kHz) cada 50 ms. El audio no sale del teléfono: solo viajan los números.
- Cámara: se reduce la imagen a 64×36, se compara con el cuadro anterior y se calcula la fracción de píxeles que cambió, el centro de ese movimiento y el brillo medio, cada 80 ms. El video no sale del teléfono.
- Mantiene la pantalla encendida (Wake Lock). Si bloqueas la pantalla, el navegador suspende los sensores y el envío se detiene.
- Publica su estado como mensaje retained con último testamento (ver sección 5): si el teléfono se cae, la experiencia se entera y no deja nada "apretado".
- Responde a los ping de la experiencia para medir la latencia.
- Guarda nombre, sala e identificador de dispositivo en el navegador, para no repetirlos.
- En iPhone, "Añadir a pantalla de inicio" evita los gestos de borde de Safari.
Todos los mensajes son JSON, bajo tacto/<sala>/<dispositivo>/…. <dispositivo> es un identificador aleatorio de 6 caracteres que cada teléfono guarda.
| Tema | Retenido | Contenido |
|---|---|---|
…/status |
sí | { online, name, caps }. caps indica la calidad de presión y diámetro y qué módulos están activos. |
…/touch |
no | { t, n, ar, p:[{ i, x, y, f, d }] } |
…/motion |
no | { t, a:[alfa,beta,gamma], r:[x,y,z], c:[x,y,z] } |
…/mic |
no | { t, l, b:[graves,medios,agudos] } |
…/cam |
no | { t, m, x, y, b } |
…/cmd |
no | Lo publica la experiencia: { ping } |
…/pong |
no | Respuesta del teléfono: { t } |
Campos de touch: t marca de tiempo en ms; n cantidad de dedos; ar proporción ancho/alto de la superficie (se usa para medir distancias entre dedos sin deformarlas); p lista de dedos con i casilla 1–5, x/y posición 0–1, f presión 0–1 y d diámetro en px.
Campos de motion: a orientación en grados; r velocidad de giro en °/s, en orden [X, Y, Z]; c aceleración en m/s².
Último testamento (LWT). Al conectarse, el teléfono le deja al broker un mensaje de despedida: "publica status {online:false} si me caigo". Si la conexión se corta sin aviso (batería, túnel, pantalla bloqueada), el broker lo publica por él. La experiencia, al ver online:false, pone en cero dedos, presión y diámetro, así que ningún sonido queda sonando por un teléfono que ya no está.
Latencia. Cada 2 segundos la experiencia manda un ping con su reloj; el teléfono lo devuelve en pong. La diferencia es el tiempo de ida y vuelta pasando por el broker. La cabecera muestra la mediana de las últimas 40 mediciones. En red local típicamente son pocos milisegundos.
tacto.js toma cada mensaje y lo aplana en canales, números con nombre fijo y un rango habitual. Son las "entradas" del panel.
| Canal | Qué es | Rango |
|---|---|---|
touch.n |
dedos apoyados | 0–5 |
touch.down |
hay al menos un dedo (0/1) | 0–1 |
touch.x, touch.y |
promedio de la posición de todos los dedos | 0–1 |
touch.f |
presión máxima entre los dedos | 0–1 |
touch.d |
diámetro medio entre los dedos | 0–120 px |
touch.dist |
distancia entre los dedos 1 y 2 (en la unidad del ancho; corregida por proporción) | 0–1.5 |
touch.ang |
ángulo de la línea dedo 1 → dedo 2 | −180–180° |
t1.x … t5.x, .y, .f, .d |
cada dedo por separado | 0–1, px |
ang.a, ang.b, ang.g |
orientación alfa, beta, gamma | 0–360°, ±180°, ±90° |
rot.x, rot.y, rot.z |
velocidad de giro | ±360 °/s |
acc.x, acc.y, acc.z |
aceleración con gravedad | ±20 m/s² |
mic.level, mic.low, mic.mid, mic.high |
nivel y bandas | 0–1 |
cam.motion, cam.x, cam.y, cam.bright |
movimiento, su centro y brillo | 0–1 |
Detalles útiles:
- Un canal vale
nullhasta que llega su primer dato. El panel muestra "sin datos". - Al soltar un dedo, su presión y diámetro vuelven a 0, pero su X e Y conservan la última posición, para que un sonido no salte al soltar.
- Con dos dedos o más,
touch.x/yson el punto medio, útil como "centro de la mano". - Hay ejes y signos que dependen del teléfono; por eso
PRUEBA_1.mdpide anotar cómo se comportanang.*yrot.*en iPhone y en Android.
exp.html + exp.js, en /exp. Tres columnas:
- Entradas (izquierda): monitor en vivo de todos los canales, agrupados, con barra de valor. Sirve para ver qué entrega tu teléfono.
- Escenario (centro): la imagen, con los toques del teléfono dibujados encima; las fuentes de audio e imagen; el QR para conectar.
- Conexiones (derecha): el panel de mapeo.
Estado de la conexión, broker usado, selector de teléfono (automático usa el que más recientemente envió datos), latencia, sala (editable; Nueva crea otra) y Activar audio.
- Audio: oscilador (onda a elección: sierra, triángulo, seno, cuadrada) o un archivo de audio que se repite en bucle. Un osciloscopio muestra la salida.
- Imagen: una animación generada, o un archivo de video. Casilla para mostrar u ocultar los toques del teléfono.
La imagen (animación, video o cámara web) se dibuja en WebGL sobre una grilla de 96×54 puntos que los dedos deforman, como si tocaras la imagen. La deformación es temporal: se apaga sola en unos segundos ("recuperación", 2,5 s por defecto).
| Material | Qué hace | Cómo responde al tacto |
|---|---|---|
| Tela | Se arrastra y se estira, ondula y vuelve. | El dedo arrastra la tela; la presión la hunde (sombra). Con varios dedos se estira entre ellos. |
| Agua | Ondas que se expanden y se reflejan en los bordes. | Cada apoyo es una gota; al presionar emite anillos; al mover deja estela. |
| Squishy | Gelatina: se abulta bajo el dedo y rebota. | Cuanto más presión, más se hunde; al soltar oscila y se asienta. |
| Remolino | La imagen gira alrededor del dedo. | Mantener el toque aumenta el giro; mover arrastra el remolino. |
Se elige con los botones de la tarjeta "Física del material" o con las teclas 1–5 (también en pantalla completa). Cuatro deslizadores ajustan el efecto: intensidad, recuperación (segundos), tamaño del toque y relieve (luz y sombra). Los cuatro aparecen como variables de salida en el grupo Física del panel de conexiones, así que se pueden controlar con un sensor (por ejemplo, touch.f → fisica.intensidad, o ang.g → fisica.tamano).
La física usa por dedo: posición, desplazamiento y velocidad, presión (si el teléfono no la entrega, se asume 0,5) y diámetro del contacto (define el tamaño de la zona afectada). Si el navegador no permite WebGL, la imagen se muestra sin deformar.
Para crear un material nuevo, edita public/fisica.js: agrega su apariencia en MATERIALES y su física en step(), y un botón en exp.html.
Cada fila lleva una entrada a una variable de salida:
| Control | Qué hace |
|---|---|
| Casilla | Activa o desactiva la conexión. |
| Entrada → Salida | Qué canal controla qué variable. |
| Aprender | Pulsa y mueve el teléfono (u opera el control): elige la entrada que más cambie durante 8 segundos. |
| Rango de entrada | Qué parte del canal cuenta. Por ejemplo, touch.d de 10 a 90 px. |
| Rango de salida | Entre qué valores se mueve la variable. |
| Curva | lineal, suave (en S), acelerada (cuadrática) o rápida (raíz). |
| Invertir | Da vuelta la relación (arriba = poco). |
| Suavizado | Promedio con el valor anterior; 0 = instantáneo, cerca de 1 = muy lento. Quita el temblor. |
| Medidor | Barra violeta (entrada normalizada), barra lima (salida), y los valores entrada → salida. |
| ✕ | Elimina la conexión. |
Cómo se calcula (en cada cuadro de pantalla, para cada conexión activa):
- Lee el canal. Si no hay dato, la conexión no actúa.
- Normaliza:
(valor − mínimo de entrada) / (máximo − mínimo), limitado a 0–1. - Invierte si corresponde y aplica la curva.
- Suaviza con el valor del cuadro anterior.
- Escala al rango de salida. Las variables marcadas como logarítmicas (frecuencias) se escalan en proporción geométrica, que suena más natural.
- Escribe el resultado en la variable.
Reglas de comportamiento:
- Si dos conexiones apuntan a la misma variable, gana la última de la lista.
- Cuando una variable deja de recibir control (se desactiva la conexión o se suelta el control), vuelve a su valor base.
- Las variables tipo disparador (
audio.gate) se comportan como interruptor: la entrada normalizada ≥ 0,5 las abre. Si el teléfono desaparece, el disparador se cierra.
Ejemplo incluido (botón Ejemplo; se carga la primera vez):
| Entrada | Salida | Efecto |
|---|---|---|
touch.down |
audio.gate |
Suena solo mientras hay un dedo apoyado |
touch.x |
audio.tono (−12…24 semitonos) |
Izquierda–derecha cambia la nota |
touch.y |
audio.filtro (200…9000 Hz, invertido) |
Arriba abre el filtro |
touch.n |
audio.delay.mezcla |
Más dedos, más eco |
touch.d |
video.zoom (1…2,2) |
Un contacto más grande acerca la imagen |
ang.g |
video.tono (0…360°) |
Inclinar el teléfono rota el color |
touch.f |
video.brillo (1…2) |
Apretar ilumina |
Las conexiones se guardan solas en el navegador (localStorage), así que al volver siguen ahí. Exportar descarga un archivo tacto-conexiones.json; Importar lo carga en otro computador o navegador. El audio y el video que cargas no se guardan: hay que volver a elegirlos.
Todas aparecen en el panel de forma automática.
Audio (Web Audio). Cadena: fuente → envolvente (disparador) → filtro pasa-bajos → [directo | delay | reverb] → paneo → volumen → compresor → salida.
| Variable | Rango | Notas |
|---|---|---|
audio.volumen |
0–1 | |
audio.frecuencia |
60–2000 Hz | Solo con oscilador; logarítmica |
audio.tono |
−24 a 24 semitonos | Afecta oscilador y archivo |
audio.filtro |
80–12000 Hz | Frecuencia de corte; logarítmica |
audio.resonancia |
0,1–20 | |
audio.delay.mezcla |
0–1 | Cantidad de eco |
audio.delay.tiempo |
0,02–1 s | |
audio.delay.feedback |
0–0,9 | Repeticiones del eco |
audio.reverb.mezcla |
0–1 | Reverberación sintética de 2,4 s |
audio.pan |
−1 a 1 | Izquierda–derecha |
audio.gate |
0/1 | Suena / calla |
Imagen (filtros y transformaciones CSS sobre el escenario).
| Variable | Rango |
|---|---|
video.brillo, video.contraste |
0,2–3 |
video.saturacion |
0–4 |
video.tono |
0–360° |
video.desenfoque |
0–20 px |
video.invertir |
0–1 |
video.zoom |
0,5–4 |
video.rotacion |
±180° |
video.posx, video.posy |
±50 % |
video.opacidad |
0–1 |
video.velocidad |
0,1–4 (animación y video) |
video.posicion |
0–1 (punto del video; solo con video) |
Agregar una variable propia: en exp.js, sección SALIDAS, una línea basta:
tgt('audio.mi.variable', 'Audio', 'etiqueta visible', min, max, valorBase, { apply: v => /* lo que haga */ });Aparece sola en el panel. Para imagen existe el atajo vt(id, etiqueta, min, max, base).
| Local (por defecto) | Público (GitHub Pages) | |
|---|---|---|
| Quién sirve las páginas | node server.js |
Las publicas en GitHub Pages |
| Broker | El embebido (/mqtt) |
Uno público de prueba (wss://broker.emqx.io:8084/mqtt) o uno tuyo |
| Latencia | Baja (misma red) | La del servidor del broker |
| Aviso de certificado | Sí (autofirmado) | No |
| iPhone | Por verificar el WebSocket seguro con certificado autofirmado | Sin avisos |
| Requiere | Node.js | Nada que ejecutar |
Cómo elige el broker tacto.js, en este orden:
- Parámetro
?broker=wss://…en la dirección. TACTO_CONFIG.brokerenconfig.js.- Si la página la sirve el servidor de Tacto (
/inforesponde), su propio/mqtt. - El broker público de prueba.
Con GitHub Pages no hace falta configurar nada (usa el broker público). Para usar tu propio broker, o si la página del teléfono vive en otro sitio, edita public/config.js:
window.TACTO_CONFIG = {
broker: 'wss://mi-broker.example.com:8884/mqtt', // opcional
phoneUrl: 'https://tu-usuario.github.io/tacto/', // opcional: por defecto, la carpeta donde está exp.html
};En este modo la experiencia genera el QR en el navegador y muestra la dirección completa, con la sala y el broker incluidos.
Ojo con el broker público: cualquiera que conozca (o adivine) tu sala puede leer y publicar en ella. Usa una sala larga y no mandes nada sensible. Si el curso lo necesita, un broker propio (Mosquitto, EMQX, HiveMQ Cloud) elimina el problema.
El modo público es más lento porque cada mensaje viaja hasta el servidor del broker y vuelve. Medido desde Chile (ida y vuelta de un mensaje, mediana de 20):
| Broker | Ida y vuelta |
|---|---|
Local (node server.js, misma red) |
~7 ms |
broker.emqx.io (público, usado por Pages) |
~210 ms |
broker.hivemq.com (público) |
~240 ms |
Esa diferencia se nota al tocar y oír el sonido, y no se corrige con código: depende de la distancia al servidor.
- Para trabajar, ensayar y presentar en vivo: modo local. Es unas 30 veces más rápido. Usa
node server.jscon el teléfono en el hotspot. - Para compartir, mostrar el proyecto o probar desde cualquier lugar: GitHub Pages. Funciona sin instalar nada, pero con ~200 ms de retraso.
- Si necesitas Pages y menos retraso: crea un broker propio en la región más cercana (HiveMQ Cloud, EMQX Cloud…) y ponlo en
config.js. Mide el resultado con la latencia que muestra la cabecera de la experiencia.
Pages y el broker local no se pueden mezclar para el teléfono: Pages usa HTTPS, y el navegador bloquea las conexiones inseguras desde una página HTTPS; un broker local con certificado autofirmado genera avisos y puede fallar en iPhone.
El repositorio ya trae todo lo necesario: la carpeta public/ es el sitio completo (incluye vendor/mqtt.min.js, así que no depende de internet para cargar MQTT), y .github/workflows/pages.yml lo publica solo.
- Crea un repositorio en GitHub y sube el proyecto (la rama debe llamarse
main). - En el repositorio: Settings → Pages → Build and deployment → Source: GitHub Actions.
- Cada vez que subas cambios a
main, la acción Publicar en GitHub Pages publicapublic/. Su estado se ve en la pestaña Actions. - El sitio queda en
https://<usuario>.github.io/<repositorio>/:- Teléfono:
https://<usuario>.github.io/<repositorio>/(esindex.html). - Experiencia (computador):
https://<usuario>.github.io/<repositorio>/exp.html.
- Teléfono:
- Abre la experiencia en el computador. Muestra la sala, el QR y la dirección para el teléfono (con la sala y el broker ya incluidos). Escanea el QR y listo: no hay avisos de certificado, porque GitHub Pages tiene HTTPS válido.
En este modo no corre node server.js; el intermediario es el broker público (broker.emqx.io) o el que pongas en config.js. El QR se genera en el navegador (carga una pequeña librería desde internet; sin internet queda la dirección escrita). Cada estudiante puede hacer un fork del repositorio y publicar el suyo.
Diferencias con el modo local:
- No hay QR por
/qr.svgni puerto TCP 1883; las otras herramientas MQTT deben conectarse al broker público (por WebSocket o por el puerto que ese broker ofrezca). - Cualquiera que adivine tu sala puede leer y escribir en ella. Evítalo con salas largas, o usando un broker propio.
- Si el sitio estuviera en un subdirectorio distinto, todas las rutas son relativas y siguen funcionando.
Copia public/tacto.js (y config.js si usas broker propio) a tu proyecto. Funciona en cualquier página, sin librerías extra (carga mqtt.js por sí sola).
<script src="config.js"></script>
<script src="tacto.js"></script>
<script>
const t = new Tacto(); // sala guardada, o la de ?sala=abc123
t.on('estado', s => console.log('conexión:', s));
t.connect().then(() => {
(function loop() {
const x = t.get('touch.x'); // último valor del canal, o null si aún no hay datos
const d = t.get('touch.d');
if (x != null) miSonido.frecuencia = 200 + x * 800;
requestAnimationFrame(loop);
})();
});
</script>API de la clase Tacto:
| Miembro | Descripción |
|---|---|
new Tacto({ sala }) |
Crea el cliente. Sin sala, usa ?sala=, la guardada o una nueva. |
connect() |
Se conecta al broker y se suscribe a la sala. Devuelve una promesa. |
get(canal, [dispositivo]) |
Último valor del canal (del teléfono elegido, o el más activo). |
on(evento, fn) |
Eventos: datos (cada mensaje), estado, dispositivo, rtt, error. |
cambiarSala(código) |
Cambia de sala sin recargar. |
devices |
Diccionario de teléfonos vistos: nombre, conexión, canales, capacidades, latencias. |
sel, pick() |
Teléfono elegido ('auto' por defecto) y el dispositivo resultante. |
Tacto.CHANNELS |
Catálogo de canales (nombre, etiqueta, rango, grupo). |
exp.html + exp.js son un ejemplo completo y legible: puedes partir de ahí.
El broker embebido también escucha por TCP en el puerto 1883, así que cualquier cliente MQTT puede leer (o publicar) los mismos temas:
mosquitto_sub -h <ip-del-computador> -t "tacto/#" -vTambién sirven MQTT Explorer (interfaz gráfica para ver los temas en vivo), Node-RED, scripts en Python (paho-mqtt), o un ESP32/Arduino suscrito a tacto/<sala>/+/touch: el teléfono puede mover un motor o una luz. Es una buena forma de mostrar a los estudiantes que el tacto es solo datos que cualquiera puede consumir.
| Variable | Por defecto | Para qué |
|---|---|---|
PORT |
8443 |
Puerto de las páginas y del WebSocket MQTT |
HOST_IP |
primera IP de la red | Fuerza qué IP se muestra en el QR (útil con varios adaptadores) |
MQTT_TCP |
1883 |
Puerto TCP del broker; 0 lo desactiva |
HTTP |
— | HTTP=1 sirve sin HTTPS (para túneles o hosting que ya ponen el certificado) |
PUBLIC_URL |
— | Dirección pública que se usa en el QR (túneles) |
En PowerShell: $env:HOST_IP="172.20.10.2"; node server.js. En bash: HOST_IP=172.20.10.2 node server.js.
- Presión y diámetro dependen del hardware. Algunos iPhone no entregan presión útil; ver sección 4.
- Pantalla bloqueada = sin datos. Los navegadores móviles suspenden los sensores y los eventos de la página en segundo plano.
- iOS: el permiso de movimiento se pide una sola vez por sesión de Safari y debe venir de un toque del usuario. Hay que usar HTTPS para el giro.
- Certificado autofirmado: genera un aviso en cada dispositivo la primera vez. Si Safari en iPhone rechaza el WebSocket seguro con ese certificado, el camino alternativo es GitHub Pages con broker público (sección 9). Esto está por confirmar con teléfonos reales.
- No se graba ni reproduce la señal. Encaja bien como un cliente MQTT más (grabar los temas y volver a publicarlos); no está en esta versión.
- Lo que la web no entrega en iOS: LiDAR, AirPods y Apple Watch no son accesibles desde el navegador.
- Cámara: solo movimiento y brillo; no hay visión de manos ni cuerpo en esta versión.
| Síntoma | Causa probable y solución |
|---|---|
| El teléfono no carga la página | No está en la misma red que el computador, o el cortafuegos bloquea Node. Prueba con el hotspot del teléfono; la regla de Windows para Node.js debe permitir redes privadas/públicas. |
| La página carga pero dice "Reconectando" | Safari rechaza el WebSocket con certificado autofirmado. Abre primero https://<ip>:8443 y acepta el aviso; si persiste, usa el modo GitHub Pages con broker público. |
| Conecta pero la experiencia no ve el teléfono | Las salas no coinciden. Escanea el QR otra vez, o copia el código de la cabecera de la experiencia. |
| No suena | Falta Activar audio, o ninguna conexión abre audio.gate: toca la pantalla del teléfono (el ejemplo solo suena con un dedo apoyado). |
| El giro no cambia | No se aceptó el permiso de movimiento, o la página no está en HTTPS. En iPhone, cierra Safari por completo y entra de nuevo. |
| La presión dice "sin dato" o "constante" | El teléfono no la mide. Usa touch.d (diámetro) como sustituto. |
npm start falla en PowerShell |
Política de ejecución de scripts. Usa node server.js. |
| Varias IP en la lista y el QR apunta a la equivocada | Fuerza la correcta con HOST_IP. |
| El puerto 8443 o 1883 está ocupado | Hay otra copia del servidor corriendo. Ciérrala, o cambia PORT / MQTT_TCP. |
| Mucha latencia | Revisa la cifra en la cabecera. Con el broker público son ~200 ms; en modo local, ~7 ms. Ver "Velocidad: cuándo usar cada modo". |