Protege tu Arduino con desbloqueo por patrón web: configuración fácil y control remoto
Componentes y materiales
| Componente | Cantidad |
|---|---|
| Arduino UNO | 1 |
| PHPoC WiFi Shield para Arduino | 1 |
| Servomotor Seeed SG92R | 1 |
| Cables jumper | 1 |
Sobre este proyecto
Introducción
Si eres principiante, te recomiendo leer primero los siguientes tutoriales:
- Arduino - Motor
- Arduino - Servomotor
- Arduino - WiFi
Seguramente conoces el desbloqueo por patrón que usan los teléfonos móviles. Ahora esta funcionalidad está disponible para Arduino, impidiendo que personas no autorizadas controlen o monitoricen la placa.
El código de este proyecto se puede reutilizar libremente en otras aplicaciones. Para simplificar, se usa el control de un servomotor como ejemplo.
La idea proviene de dos proyectos escritos para la plataforma PHPoC:
- Desbloqueo por patrón basado en web para dispositivo IoT
- Control de servomotor vía WebSocket con interfaz gráfica
Los he adaptado para Arduino.
¿Por qué PHPoC WiFi Shield?
- Soporte nativo para WebSocket, ideal para este proyecto.
- Servidor web embebido dedicado que permite almacenar la aplicación web en el propio shield.
El shield PHPoC incluye aplicaciones web integradas que permiten controlar/monitorizar Arduino sin conocimientos de programación web. Además, los usuarios que saben programar aplicaciones web pueden desarrollar las suyas propias y almacenarlas en el shield.
Demostración
El usuario dibuja su patrón en el navegador web, el cual se envía al Arduino a través de WebSocket (vía PHPoC Shield). Si el patrón coincide con el predefinido, Arduino envía un código ACEPTADO y permite el control del servomotor. De lo contrario, envía DENEGADO.
Flujo de datos
Navegador web ↔ PHPoC WiFi Shield ↔ Arduino
Funcionamiento
Cuando el usuario dibuja su patrón en el navegador, este se mapea a una cadena (ejemplo: "1,4,8,6,3"). Esta cadena se envía al Arduino vía WebSocket. Arduino la compara con el patrón almacenado:
- Si coinciden: envía código 202 (ACEPTADO) y establece authenticated = true.
- Si no coinciden: envía código 401 (DENEGADO) y establece authenticated = false.
Al recibir un comando de control, Arduino verifica primero la variable authenticated. Si es true, ejecuta la acción; si es false, envía DENEGADO.
Mapeo del patrón
El patrón se convierte en una cadena numérica. En la imagen, el patrón corresponde a "1, 4, 8, 6, 3".
Se configura un tiempo de espera (timeout). Tras un periodo de inactividad, la autenticación expira y el usuario debe volver a introducir el patrón.
Código fuente
- ArduinoUnlockExample.ino: se compila y sube a Arduino mediante Arduino IDE.
- unlock.php: aplicación web que se sube al PHPoC Shield usando PHPoC Debugger.
Pasos a seguir
- Configurar la información WiFi en el PHPoC Shield (SSID y contraseña).
- Subir la nueva interfaz web (UI) al PHPoC Shield.
- Escribir y subir el código de Arduino.
Configurar WiFi en PHPoC Shield
Consulta esta guía.
Subir la UI web al PHPoC Shield
- Descarga el código fuente
unlock.php(sección de código). - Descarga las dos imágenes para el control del servomotor:
- Súbelas al PHPoC Shield usando PHPoC Debugger según esta instrucción (no elimines los archivos existentes en el shield).
Escribir el código de Arduino
- Instala la librería PHPoC para Arduino en Arduino IDE (ver instrucción).
- Consulta el código fuente en la sección de código.
- Compila y sube a Arduino vía Arduino IDE.
Prueba
- Abre el monitor serie en Arduino IDE para ver la dirección IP.
- En el navegador, escribe:
https://dirección_IP/unlock.php - Haz clic en "Connect" y prueba el desbloqueo y control del servomotor.
Referencias de funciones
- Arduino - Librería Servo
- Servo.attach()
- Servo.write()
- Servo.writeMicroseconds()
- Servo.read()
- Servo.attached()
- Servo.detach()
- Serial.begin()
- Serial.println()
- delay()
- millis()
- for loop
- while loop
- if else
- loop()
- setup()
- String.toInt()
- String.substring()
- String.indexOf()
- String.remove()
- String.equals()
Código
unlock.php (PHP)
Interfaz de usuario web
<!DOCTYPE html> <html> <head> <title>Arduino - PHPoC Shield</title> <meta name="viewport" content="width=device-width, initial-scale=0.7, maximum-scale=0.7"> <meta charset="utf-8"> <style> body { text-align: center; font-size: width/2pt; } h1 { font-weight: bold; font-size: width/2pt; } h2 { font-weight: bold; font-size: width/2pt; } button { font-weight: bold; font-size: width/2pt; } </style> <script> var CMD_AUTH = 0; var CMD_CTRL = 1; var ws; var authorized = false; /* variable de desbloqueo */ var unlock_width = 400, unlock_height = 400; var unlock_inner_radius = 14; var unlock_middle_radius = 22; var unlock_outer_radius = 34; var unlock_gap = 140; var unlock_touch_state = 0; var unlock_touch_x = 0, unlock_touch_y = 0; var unlock_touch_list = new Array(); var unlock_ratio = 1; /* variable de control: cámbiala según tu aplicación */ var servo_width = 401, servo_height = 466; var servo_pivot_x = 200, servo_pivot_y = 200; var servo_bracket_radius = 160, servo_bracket_angle = 0; var servo_bracket_img = new Image(); var servo_click_state = 0; var servo_last_angle = 0; var servo_mouse_xyra = {x:0, y:0, r:0.0, a:0.0}; servo_bracket_img.src = "servo_bracket.png"; function init() { /* inicializar parte de desbloqueo */ var unlock = document.getElementById("unlock"); unlock.width = unlock_width; unlock.height = unlock_height; unlock.addEventListener("touchstart", unlock_mouse_down); unlock.addEventListener("touchend", unlock_mouse_up); unlock.addEventListener("touchmove", unlock_mouse_move); unlock.addEventListener("mousedown", unlock_mouse_down); unlock.addEventListener("mouseup", unlock_mouse_up); unlock.addEventListener("mousemove", unlock_mouse_move); var ctx = unlock.getContext("2d"); ctx.translate(unlock_width/2, unlock_height/2); ctx.shadowBlur = 20; ctx.shadowColor = "LightGray"; ctx.lineCap="round"; ctx.lineJoin="round"; /* inicializar parte de control */ var servo = document.getElementById("servo"); servo.width = servo_width; servo.height = servo_height; servo.style.backgroundImage = "url('servo_body.png')"; servo.addEventListener("touchstart", servo_mouse_down); servo.addEventListener("touchend", servo_mouse_up); servo.addEventListener("touchmove", servo_mouse_move); servo.addEventListener("mousedown", servo_mouse_down); servo.addEventListener("mouseup", servo_mouse_up); servo.addEventListener("mousemove", servo_mouse_move); ctx = servo.getContext("2d"); ctx.translate(servo_pivot_x, servo_pivot_y); update_view(); } function connect_onclick() { if(ws == null) { var ws_host_addr = "<?echo _SERVER("HTTP_HOST")?>"; if((navigator.platform.indexOf("Win") != -1) && (ws_host_addr.charAt(0) == "[")) { ws_host_addr = ws_host_addr.replace(/[\[\]]/g, ''); ws_host_addr = ws_host_addr.replace(/:/g, "-"); ws_host_addr += ".ipv6-literal.net"; } ws = new WebSocket("ws://" + ws_host_addr + "/web_pattern", "text.phpoc"); document.getElementById("ws_state").innerHTML = "CONNECTING"; ws.onopen = ws_onopen; ws.onclose = ws_onclose; ws.onmessage = ws_onmessage; } else ws.close(); } function ws_onopen() { document.getElementById("ws_state").innerHTML = "<font color='blue'>CONNECTED</font>"; document.getElementById("bt_connect").innerHTML = "Disconnect"; update_view(); } function ws_onclose() { document.getElementById("ws_state").innerHTML = "<font color='gray'>CLOSED</font>"; document.getElementById("bt_connect").innerHTML = "Connect"; ws.onopen = null; ws.onclose = null; ws.onmessage = null; ws = null; authorized = false; update_view(); } function ws_onmessage(e_msg) { e_msg = e_msg || window.event; var resp = parseInt(e_msg.data); if(resp == 202) authorized = true; else if(resp == 401) authorized = false; else console.log("unknown:" + resp); update_view(); } function update_view() { if(!authorized) unlock_update_view(); else servo_update_view(); } function unlock_update_view() { document.body.style.backgroundColor = "black"; document.body.style.color = "white"; var unlock_area = document.getElementById('unlock_area'); var control_area = document.getElementById('control_area'); unlock_area.style.display = 'block'; control_area.style.display = 'none'; var unlock = document.getElementById("unlock"); var ctx = unlock.getContext("2d"); ctx.clearRect(-unlock_width/2, -unlock_height/2, unlock_width, unlock_height); // dibujar puntos y líneas tocados ctx.lineWidth = 10; ctx.strokeStyle="white"; ctx.globalAlpha=1; ctx.beginPath(); for (var i = 0; i < unlock_touch_list.length; i++) { var temp = unlock_touch_list[i] - 1; var x = temp % 3 - 1; var y = Math.floor(temp / 3) - 1; ctx.lineTo(x*unlock_gap, y*unlock_gap); } if(unlock_touch_state) ctx.lineTo(unlock_touch_x, unlock_touch_y); ctx.stroke(); for (var i = 0; i < unlock_touch_list.length; i++) { var temp = unlock_touch_list[i] - 1; var x = temp % 3 - 1; var y = Math.floor(temp / 3) - 1; ctx.globalAlpha=0.2; ctx.fillStyle = "white"; ctx.beginPath(); ctx.arc(x*unlock_gap, y*unlock_gap, unlock_outer_radius, 0, 2 * Math.PI); ctx.fill(); } // dibujar base for(var y = -1; y <= 1; y++) { for(var x = -1; x <= 1; x++) { ctx.globalAlpha=0.5; ctx.fillStyle = "white"; ctx.beginPath(); ctx.arc(x*unlock_gap, y*unlock_gap, unlock_middle_radius, 0, 2 * Math.PI); ctx.fill(); ctx.globalAlpha=1; ctx.fillStyle = "Cyan"; ctx.beginPath(); ctx.arc(x*unlock_gap, y*unlock_gap, unlock_inner_radius, 0, 2 * Math.PI); ctx.fill(); } } } function unlock_process_event(event) { if(event.offsetX) { unlock_touch_x = event.offsetX - unlock_width/2; unlock_touch_y = event.offsetY - unlock_height/2; } else if(event.layerX) { unlock_touch_x = event.layerX - unlock_width/2; unlock_touch_y = event.layerY - unlock_height/2; } else { unlock_touch_x = (Math.round(event.touches[0].pageX - event.touches[0].target.offsetLeft)) - unlock_width/2; unlock_touch_y = (Math.round(event.touches[0].pageY - event.touches[0].target.offsetTop)) - unlock_height/2; } for(var i = 1; i <= 9; i++) { if(i == unlock_touch_list[unlock_touch_list.length - 1]) continue; var idx_x = (i-1)%3 - 1; var idx_y = Math.floor((i-1)/3) - 1; var center_x = idx_x*unlock_gap; var center_y = idx_y*unlock_gap; var dist = Math.sqrt( (unlock_touch_x - center_x)*(unlock_touch_x - center_x) + (unlock_touch_y - center_y)*(unlock_touch_y - center_y) ); if(dist < unlock_outer_radius) { unlock_touch_list.push(i); unlock_touch_state = 1; break; } } update_view(); } function unlock_mouse_down() { if(ws == null || authorized) return; event.preventDefault(); unlock_process_event(event); } function unlock_mouse_up() { if(ws == null || authorized) return; event.preventDefault(); if(ws != null && authorized == false) send_to_Arduino(CMD_AUTH, unlock_touch_list.toString()); unlock_touch_state = 0; unlock_touch_list.splice(0, unlock_touch_list.length); update_view(); } function unlock_mouse_move() { if(ws == null || authorized) return; event.preventDefault(); if(authorized) return; unlock_process_event(event); } function servo_update_view() { document.body.style.backgroundColor = "white"; document.body.style.color = "black"; var unlock_area = document.getElementById('unlock_area'); var control_area = document.getElementById('control_area'); unlock_area.style.display = 'none'; control_area.style.display = 'block'; /* modificar área de control aquí */ var servo = document.getElementById("servo"); var ctx = servo.getContext("2d"); ctx.clearRect(-servo_pivot_x, -servo_pivot_y, servo_width, servo_height); ctx.rotate(servo_bracket_angle / 180 * Math.PI); ctx.drawImage(servo_bracket_img, -servo_pivot_x, -servo_pivot_y); ctx.rotate(-servo_bracket_angle / 180 * Math.PI); } function check_range_xyra(event, servo_mouse_xyra) { var x, y, r, a, rc_x, rc_y, radian; var min_r, max_r, width; if(event.touches) { var touches = event.touches; x = (touches[0].pageX - touches[0].target.offsetLeft) - servo_pivot_x; y = servo_pivot_y - (touches[0].pageY - touches[0].target.offsetTop); min_r = 60; max_r = servo_pivot_x; width = 40; } else { x = event.offsetX - servo_pivot_x; y = servo_pivot_y - event.offsetY; min_r = 60; max_r = servo_bracket_radius; width = 20; } /* conversión coordenadas cartesianas a polares */ r = Math.sqrt(x * x + y * y); a = Math.atan2(y, x); servo_mouse_xyra.x = x; servo_mouse_xyra.y = y; servo_mouse_xyra.r = r; servo_mouse_xyra.a = a; radian = servo_bracket_angle / 180 * Math.PI; /* rotar coordenadas */ rc_x = x * Math.cos(radian) - y * Math.sin(radian); rc_y = x * Math.sin(radian) + y * Math.cos(radian); if((r < min_r) || (r > max_r)) return false; if((rc_y < -width) || (rc_y > width)) return false; return true; } function servo_mouse_down() { if(event.touches && (event.touches.length > 1)) servo_click_state = event.touches.length; if(servo_click_state > 1) return; if(check_range_xyra(event, servo_mouse_xyra)) { servo_click_state = 1; servo_last_angle = servo_mouse_xyra.a / Math.PI * 180.0; } } function servo_mouse_up() { servo_click_state = 0; } function servo_mouse_move() { var angle; if(event.touches && (event.touches.length > 1)) servo_click_state = event.touches.length; if(servo_click_state > 1) return; if(!servo_click_state) return; if(!check_range_xyra(event, servo_mouse_xyra)) { servo_click_state = 0; return; } angle = servo_mouse_xyra.a / Math.PI * 180.0; if((Math.abs(angle) > 90) && (angle * servo_last_angle < 0)) { if(servo_last_angle > 0) servo_last_angle = -180; else servo_last_angle = 180; } servo_bracket_angle += (servo_last_angle - angle); servo_last_angle = angle; if(servo_bracket_angle > 90) servo_bracket_angle = 90; if(servo_bracket_angle < -90) servo_bracket_angle = -90; servo_update_view(); send_to_Arduino(CMD_CTRL, Math.floor(servo_bracket_angle)) debug = document.getElementById("debug"); debug.innerHTML = Math.floor(servo_bracket_angle); event.preventDefault(); } function send_to_Arduino(cmd, data) { if(ws.readyState == 1) { ws.send(cmd + ":" + data + "\r\n"); } } window.onload = init; </script> </head> <body> <p> <h1>Arduino - Web Pattern Unlock</h1> </p> <div id="unlock_area" style="display:block;"> <canvas id="unlock"></canvas> </div> <div id="control_area" style="display:none;"> <canvas id="servo"></canvas> <p>Angle : <span id="debug">0</span></p> </div> <h2> <p>WebSocket : <span id="ws_state">null</span></p> <button id="bt_connect" type="button" onclick="connect_onclick();">Connect</button> </h2> </body> </html>
ArduinoUnlockExample (Arduino)
/* arduino web server - pattern unlock */ #include "SPI.h" #include "Phpoc.h" #include <Servo.h> #define CMD_AUTH 0 #define CMD_CTRL 1 #define ACCEPTED "202" #define UNAUTHORIZED "401" PhpocServer server(80); Servo servo; String pattern; bool authenticated; unsigned long timeout; unsigned long lastActiveTime; void setup() { Serial.begin(9600); while(!Serial) ; Phpoc.begin(PF_LOG_SPI | PF_LOG_NET); //Phpoc.begin(); server.beginWebSocket("web_pattern"); Serial.print("WebSocket server address : "); Serial.println(Phpoc.localIP()); servo.attach(8); // conecta el servomotor en el pin 8 servo.write(90); pattern = String("1,4,8,6,3"); authenticated = false; timeout = 10000; // 10000 milisegundos lastActiveTime = 0; } void loop() { // esperar nuevo cliente: PhpocClient client = server.available(); if (client) { String data = client.readLine(); if(data) { int pos = data.indexOf(':'); int cmd = data.substring(0, pos).toInt(); if(cmd == CMD_AUTH) { String reqPattern = data.substring(pos+1); reqPattern.remove(reqPattern.indexOf(13)); reqPattern.remove(reqPattern.indexOf(10)); if(pattern.equals(reqPattern)) { authenticated = true; sendResponse(ACCEPTED, 3); lastActiveTime = millis(); } else { authenticated = false; sendResponse(UNAUTHORIZED, 3); } } else if(cmd == CMD_CTRL) { if(authenticated) { int angle = data.substring(pos+1).toInt(); angle = map(angle, 90, -90, 0, 180); servo.write(angle); lastActiveTime = millis(); Serial.println(angle); } else { sendResponse(UNAUTHORIZED, 3); } } } } if (authenticated && ((millis() - lastActiveTime) > timeout)){ authenticated = false; sendResponse(UNAUTHORIZED, 3); } } void sendResponse(char *data, int len) { server.write(data, len); }
Esquemáticos
Proceso de manufactura
- La Evolución y Producción de Patrones de Costura Industriales
- Sistema de Iluminación DMX Controlado por Web – Controlador Maestro Arduino
- Construye un Spybot controlado por Bluetooth con Arduino
- FlickMote: Mando IR por gestos para Smart Home
- TV‑B‑Gone DIY con Arduino: Mando universal para apagar cualquier TV
- Construye un Reloj Maestro LED Personalizado con Alarma – Visible desde 12 Metros
- Find Me: Localizador Inteligente de Objetos con Arduino y Bluetooth
- Soluciones de Alimentación Optimizadas para Proyectos Arduino
- Tres en raya con Arduino, matriz LED MAX7219 y carcasa de cartón
- Construye un Robot Cuadrúpedo Inteligente con Arduino: Guía Paso a Paso
- Arduino UNO + Shield WiFi: Visualización de Temperatura en Tiempo Real en Web vía Serial