Controlar un brazo robótico con Arduino usando visión por computador

Ya hemos controlado Arduino y el brazo robótico desde una web y con la voz, ahora toca hacerlo con gestos usando la webcam. Eso significa que necesitaremos usar alguna forma de estimar la posición del brazo y de la mano.

Este proyecto se basa en p5.js junto con la libreria ML5js que encapsula varios modelos de redes neuronales de TensorFlow.js (¿Os acordáis cuando TensorFlow era lo más increíble de la IA?…pues no hace tanto) para usarlos de manera sencilla. Vamos a usar dos modelos distintos uno para el cuerpo posenet y otro para la mano handpose (efectivamente, los nombres no son los más originales del mundo).

En este articulo nos vamos a centrar solo en la parte de la visión por computador, para entender como funciona el software usado en Arduino o como se le envía datos de desde la web puedes consultar los enlaces: control del brazo y controlar Arduino desde la web

Modelos

Ambos modelos funcionan de una forma similar, marcando puntos clave en el esqueleto y la mano. Nosotros tomaremos solo unos pocos puntos que usaremos para calcular el movimiento del brazo y de la mano.

Mano

El modelo de la mano tiene los siguientes puntos:

IndiceParte
0muñeca
1,2,3,4pulgar
5,6,7,8índice
9,10,11,12corazón
13,14,15,16anular
17,18,19,20meñique

Usaremos los puntos asociados a la punta del pulgar (4) y del corazón (12)

Si la distancia entre esos dos puntos es menor que un valor dado la mano del robot se cierra y si es mayo que ese valor la mano se abre.

//distancia entre dos puntos
function distance(x1,y1,x2,y2){
  return Math.sqrt(Math.pow(x1-x2, 2) + Math.pow(y1-y2, 2));  
}

Brazo

El modelo del cuerpo (posenet) tiene los siguientes puntos:

IndiceParte
0nariz
1ojo izquierdo
2ojo derecho
3oreja izquierda
4oreja derecha
5hombro izquierdo
6hombro derecho
7codo izquierdo
8codo derecho
9muñeca izquierda
10muñeca derecha
11cadera izquierda
12cadera derecha
13rodilla izquierda
14rodilla derecha
15tobillo izquierdo
16tobillo derecho

En mi caso usare los puntos asociados al hombro (6), codo (8) y muñeca (10) del brazo derecho. Con ellos trazaremos dos rectas una del hombro al codo y otra de la muñeca al codo. Usaremos el ángulo entre esas dos rectas para mover el brazo robótico. Moveremos los servos del codo y del hombro. Ignoraremos la base del brazo robótico, ese servo se va a quedar quieto

Pendiente de una recta definida por dos puntos

m = (y2-y1) / (x2-x1)

Ahora que tenemos la pendiente de cada recta (m1, m2) podemos calcular el angulo entre ellas:

g = arctg( ( m1-m2) / (1-m1m2) )

Como la función arcotangente de JS devuelve radianes tendremos que convertir a grados multiplicando por:

g * 180/PI

En código:

//pendiente de una recta definida por dos puntos
function pendiente(x1,y1, x2,y2){
  return (y1-y2)/(x1-x2);  
}

//angulo entre dos rectas  
function angulo(m1,m2){
  return (Math.atan((m1-m2)/(1-(m1*m2))) * 180/ Math.PI);  
}

Transladando movimientos

Seria genial simplemtne transladar los movimientos del brazo en la camara al robot, pero la cosa es un poco más complicada. Ya que no se parecen.

Si bien es fácil hacer una pinza con la mano es un poco más difícil adaptar el brazo humano al del robot:

Para evitar complicaciones innecesarias simplemente mediremos el angulo entre el brazo y el antebrazo y estiraremos o encogeremos el brazo robótico.

Otra cosa que es necesario hacer para que el movimiento sea estable y que no tiemble el brazo (el robótico) es que los movimientos no van a ser continuos, por ejemplo el brazo solo avanzara cuando el angulo del brazo humano se modifique en 10 grados. Y la mano tendrá un rango de distancia que no cambiara de estado si esta cerrada seguirá cerrada, si esta abierta seguirá abierta. Como ayuda visual estos cambios se indicaran con cambios de color en la imagen.

Código para la mano:

// marca los keypoints de la mano
function drawHandKeypoints() {
  if(predictions.length > 0) {
    const keypoint1 = predictions[0].landmarks[4];
    const keypoint2 = predictions[0].landmarks[12];
    
    const dist = distance(keypoint1[0], keypoint1[1], keypoint2[0], keypoint2[1]);
    //console.log(dist);
    
    //calcula la posicion del servo
    if(dist > distOpen){
      manoPos = manoPosMin;
      fill(0, 0, 255);
    } else if(dist < distOpen && dist > distClose){ 
      manoPos = manoPosMin;
      fill(0, 255, 255);
    } else {
      manoPos = manoPosMax;
      fill(255, 255, 255);
    }

    //dibuja los keypoints
    noStroke();
    ellipse(keypoint1[0], keypoint1[1], 10, 10);  
    ellipse(keypoint2[0], keypoint2[1], 10, 10);  
  }
}

Código para el brazo:

// marca los keypoints de la pose 
function drawPoseKeypoints()  {
  // Si ha detectado una pose
  if(poses.length > 0){
    let pose = poses[0].pose;
    
    //lee los keypoints de la pose que buscamos
    let manoKeypoint = pose.keypoints[6];
    let codoKeypoint = pose.keypoints[8];
    let hombroKeypoint = pose.keypoints[10];
    
    //dibuja los keypoints
    fill(255, 0, 0);
    noStroke();
    ellipse(manoKeypoint.position.x, manoKeypoint.position.y, 10, 10);
    ellipse(codoKeypoint.position.x, codoKeypoint.position.y, 10, 10);
    ellipse(hombroKeypoint.position.x, hombroKeypoint.position.y, 10, 10);
    
    let mManoCodo = pendiente(manoKeypoint.position.x, manoKeypoint.position.y, codoKeypoint.position.x, codoKeypoint.position.y);
    let mHombroCodo = pendiente(hombroKeypoint.position.x, hombroKeypoint.position.y,codoKeypoint.position.x, codoKeypoint.position.y);
    
    let ang = Math.abs(angulo(mManoCodo,mHombroCodo));
    //console.log("angulo "+ang);     
    
    //calcula la posicion de los servos
    if(ang > 80){            
      hombroPos = hombroPosMin;
      codoPos = codoPosMin;      
      stroke(255, 255, 255);
    } else if(ang > 70){  
      hombroPos = hombroPosMin+10;
      codoPos = codoPosMin+10;      
      stroke(255, 100, 100);    
    } else if(ang > 60){  
      hombroPos = hombroPosMin+20;
      codoPos = codoPosMin+20;      
      stroke(255, 50, 50);     
    } else if(ang > 50){  
      hombroPos = hombroPosMin+30;
      codoPos = codoPosMin+30;      
      stroke(200, 0, 0);         
    } else if(ang > 40){  
      hombroPos = hombroPosMin+40;
      codoPos = codoPosMin+40;      
      stroke(128, 0, 0);    
    } else if(ang > 30){  
      hombroPos = hombroPosMax;
      codoPos = codoPosMax;  
      stroke(0, 0, 0);      
    } else {
      stroke(0, 255, 0); 
    }
    
    //dibuja el esqueleto
    line(manoKeypoint.position.x, manoKeypoint.position.y, codoKeypoint.position.x, codoKeypoint.position.y);
    line(hombroKeypoint.position.x, hombroKeypoint.position.y,codoKeypoint.position.x, codoKeypoint.position.y);
  }
}

Enviar datos al Arduino

Una vez leídas las posiciones del brazo y la mano y convertidas a ángulos del brazo robótico se envían al Arduino.

//Actualizamos la posicion del brazo
function updatePosition(){  
  send("S2:"+codoPos);
  send("S3:"+hombroPos);
  send("S4:"+manoPos);
}

Enviar los datos demasiado rápido surgen dos problemas:

  • Saturar el puerto serie lo cual bloquearía la placa
  • No dar tiempo al servo a posicionarse antes de cambiar a una nueva posición lo cual hace que el brazo tiemble

Para evitar todo esto vamos a actualizar las posiciones de los servos cuarto de segundo, tiempo calculado a base de prueba y error, para ello usaremos setInterval(updatePosition, 250);

Código completo

Index.html

<html>
<head>
  <meta charset="UTF-8">
  <title>Mover brazo con CV</title>
  <script src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/0.9.0/p5.min.js"></script>
  <script src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/0.9.0/addons/p5.dom.min.js"></script>
  <script src="https://unpkg.com/p5-webserial@0.1.1/build/p5.webserial.js"></script>  
  <script src="https://unpkg.com/ml5@latest/dist/ml5.min.js"></script>
</head>
<body>
  <h1>CV control</h1>
  <p id='status'>Cagando modelos:</p>
  <script src="sketch.js"></script>
</head>
</body>
</html>

sketch.js:

let video;
let poseNet;
let poses = [];
let handpose;
let predictions = [];
let hombroPos = 90;
let codoPos = 120;
let manoPos = 10;
  
let hombroPosMax = 150;
let codoPosMax = 170;
let manoPosMax = 35;
let hombroPosMin = 80;
let codoPosMin = 90;
let manoPosMin = 10;
const distOpen = 70;
const distClose = 25;
let statusText = "";

//puerto serie
const serial = new p5.WebSerial();

function setup() {
  createCanvas(640, 480);
  portButton = createButton("Elegir puerto");
  portButton.position(5, 5);
  portButton.mousePressed(choosePort);
  let botonApagar = createButton("Pos. Inicial");
  botonApagar.position(300, 5);
  botonApagar.mousePressed(() => {    
    send("Q");   
    hombroPos = 90;
    codoPos = 120;
    manoPos = 10;
  });
  
  video = createCapture(VIDEO);
  video.size(width, height);
  inicializarSerial();
  setInterval(updatePosition, 250);
  // deteccion de la pose
  poseNet = ml5.poseNet(video, modelPoseReady);  
  poseNet.on('pose', function(results) {
    poses = results;
  });
  // deteccion de la mano
  handpose = ml5.handpose(video, modelHandReady);
  handpose.on("predict", results => {
    predictions = results;
  });  
  
  video.hide();
}

function modelPoseReady() {
  statusText += "Modelo para la pose cargado<br>"
  select('#status').html(statusText);
}

function modelHandReady() {
  statusText += "Modelo para la mano cargado<br>"
  select('#status').html(statusText);
}

function draw() {
  image(video, 0, 0, width, height);
  // Dibujamos los keypoints
  drawPoseKeypoints();
  drawHandKeypoints();
}

// marca los keypoints de la pose 
function drawPoseKeypoints()  {
  // Si ha detectado una pose
  if(poses.length > 0){
    let pose = poses[0].pose;
    
    //lee los keypoints de la pose que buscamos
    let manoKeypoint = pose.keypoints[6];
    let codoKeypoint = pose.keypoints[8];
    let hombroKeypoint = pose.keypoints[10];
    
    //dibuja los keypoints
    fill(255, 0, 0);
    noStroke();
    ellipse(manoKeypoint.position.x, manoKeypoint.position.y, 10, 10);
    ellipse(codoKeypoint.position.x, codoKeypoint.position.y, 10, 10);
    ellipse(hombroKeypoint.position.x, hombroKeypoint.position.y, 10, 10);
    
    let mManoCodo = pendiente(manoKeypoint.position.x, manoKeypoint.position.y, codoKeypoint.position.x, codoKeypoint.position.y);
    let mHombroCodo = pendiente(hombroKeypoint.position.x, hombroKeypoint.position.y,codoKeypoint.position.x, codoKeypoint.position.y);
    
    let ang = Math.abs(angulo(mManoCodo,mHombroCodo));
    //console.log("angulo "+ang);     
    
    //calcula la posicion de los servos
    if(ang > 80){            
      hombroPos = hombroPosMin;
      codoPos = codoPosMin;      
      stroke(255, 255, 255);
    } else if(ang > 70){  
      hombroPos = hombroPosMin+10;
      codoPos = codoPosMin+10;      
      stroke(255, 100, 100);    
    } else if(ang > 60){  
      hombroPos = hombroPosMin+20;
      codoPos = codoPosMin+20;      
      stroke(255, 50, 50);     
    } else if(ang > 50){  
      hombroPos = hombroPosMin+30;
      codoPos = codoPosMin+30;      
      stroke(200, 0, 0);         
    } else if(ang > 40){  
      hombroPos = hombroPosMin+40;
      codoPos = codoPosMin+40;      
      stroke(128, 0, 0);    
    } else if(ang > 30){  
      hombroPos = hombroPosMax;
      codoPos = codoPosMax;  
      stroke(0, 0, 0);      
    } else {
      stroke(0, 255, 0); 
    }
    
    //dibuja el esqueleto
    line(manoKeypoint.position.x, manoKeypoint.position.y, codoKeypoint.position.x, codoKeypoint.position.y);
    line(hombroKeypoint.position.x, hombroKeypoint.position.y,codoKeypoint.position.x, codoKeypoint.position.y);
  }
}

// marca los keypoints de la mano
function drawHandKeypoints() {
  if(predictions.length > 0) {
    const keypoint1 = predictions[0].landmarks[4];
    const keypoint2 = predictions[0].landmarks[12];
    
    const dist = distance(keypoint1[0], keypoint1[1], keypoint2[0], keypoint2[1]);
    //console.log(dist);
    
    //calcula la posicion del servo
    if(dist > distOpen){
      manoPos = manoPosMin;
      fill(0, 0, 255);
    } else if(dist < distOpen && dist > distClose){ 
      fill(0, 255, 255);
    } else {
      manoPos = manoPosMax;
      fill(255, 255, 255);
    }

    //dibuja los keypoints
    noStroke();
    ellipse(keypoint1[0], keypoint1[1], 10, 10);  
    ellipse(keypoint2[0], keypoint2[1], 10, 10);  
  }
}

//pendiente de una recta definida por dos puntos
function pendiente(x1,y1, x2,y2){
  return (y1-y2)/(x1-x2);  
}

//angulo entre dos rectas  
function angulo(m1,m2){
  return (Math.atan((m1-m2)/(1-(m1*m2))) * 180/ Math.PI);  
}

//distancia entre dos puntos
function distance(x1,y1,x2,y2){
  return Math.sqrt(Math.pow(x1-x2, 2) + Math.pow(y1-y2, 2));  
}

//Actualizamos la posicion del brazo
function updatePosition(){  
  send("S2:"+codoPos);
  send("S3:"+hombroPos);
  send("S4:"+manoPos);
}

//enviar datos al puerto serie
function send(cmd) {
    console.log(cmd);
    serial.write(cmd+"\n");
}

//leer datos del puerto serie
function serialEvent() {
    let readSerialStr = serial.readLine();
    trim(readSerialStr);
    if (readSerialStr) {
        console.log(readSerialStr);
    }
}

//incializar la conexion serie
function inicializarSerial() {
    if (!navigator.serial) {
        alert("WebSerial no esta sorportado en este navegador. Prueba en Chrome o Edge.");
    }
    serial.getPorts();
    serial.on("noport", showPortButton);
    serial.on("portavailable", openPort);
    serial.on("requesterror", portError);
    serial.on("data", serialEvent);
    serial.on("close", closePort);
    navigator.serial.addEventListener("connect", portConnect);
    navigator.serial.addEventListener("disconnect", portDisconnect);
    statusText += "Puerto serie incializado<br>"
    select('#status').html(statusText);
}

// Muestra la ventana de seldccion de puerto
function choosePort() {
    showPortButton();
    serial.requestPort();
}

//abrir conexion con puerto serie
function openPort() {
    console.log("Abriendo puerto serie");
    serial.open().then(initiateSerial);
    function initiateSerial() {
        console.log("Puerto serie abierto");
    }
    hidePortButton();
}

//Cerrar conexion con puerto serie
function closePort() {
    console.log("Puerto serie cerrado");
    serial.close();
    showPortButton();
}

//Error con el puerto serie
function portError(err) {
    alert("Serial port error: " + err);
    showPortButton();
}

//Evento puerto serie conectado
function portConnect() {
    console.log("Puerto serie conectado");
    serial.getPorts();
    hidePortButton()
}

//Evento puerto serie desconectado
function portDisconnect() {
    serial.close();
    console.log("Puerto serie desconectado");
    showPortButton();
}

function showPortButton() {
    portButton.show();
}

function hidePortButton() {
    portButton.hide();
}

Puede ver un vídeo sobre este tema en mi canal de Youtube:

Haz click para ver el vídeo en Youtube

Y si estas interesado en el tema puedes ver la lista de vídeos.

Controlar Arduino desde el navegador

Por sorprendente que parezca una web puede controlar un Arduino a través de su puerto serie para ello se puede usar la API WebSerial que por ahora solo funciona correctamente en Chrome y Edge. Para no montar todo desde cero usaremos el framework P5.js y su editor web que permite programar una web de forma muy parecida a programar en Arduino (ambos están inspirados en Processing y su entorno de desarrollo) y la librería p5.webserial. Eso lo podemos hacer incluyendo las siguientes librerías:

<!DOCTYPE html>
<html lang="en">
<head>
  <script src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/1.1.9/p5.min.js"
  integrity="sha512-WIklPM6qPCIp6d3fSSr90j+1unQHUOoWDS4sdTiR8gxUTnyZ8S2Mr8e10sKKJ/bhJgpAa/qG068RDkg6fIlNFA=="
  crossorigin="anonymous"></script>
  <script src="https://unpkg.com/p5-webserial@0.1.1/build/p5.webserial.js"></script>
 <script src="sketch.js"></script>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>p5.webserial</title>
</head>
</html>

Vamos a construir un control para mover el brazo robot que ya explique en este otro post. Para ellos crearemos una web con controles que nos permitan mover cada servo, llevarlo a la posición inicial y mandar el comando que queramos al brazo:

Vamos a centrarnos en la parte del código que controla la conexión con Arduino, que es el siguiente:

//Crear el objeto Serial
const serial = new p5.WebSerial();



//enviar datos al puerto serie
function send(cmd){
  console.log(cmd);
  serial.write(cmd);
}

//leer datos del puerto serie
function serialEvent() {
  let readSerialStr = serial.readLine(); 
  trim(readSerialStr);                   
  if (readSerialStr) {
    console.log(readSerialStr);
  }
}

//incializar la conexion serie
function inicializarSerial() {
  if (!navigator.serial) {
    alert("WebSerial no esta sorportado en este navegador. Prueba en Chrome o Edge.");
  }
  serial.getPorts();
  serial.on("noport", showPortButton);
  serial.on("portavailable", openPort);
  serial.on("requesterror", portError);
  serial.on("data", serialEvent);
  serial.on("close", closePort);
  navigator.serial.addEventListener("connect", portConnect);
  navigator.serial.addEventListener("disconnect", portDisconnect);
}

// Muestra la ventana de seldccion de puerto
function choosePort() {
  showPortButton();
  serial.requestPort();
}

//abrir conexion con puerto serie
function openPort() {
  console.log("Abriendo puerto serie");
  serial.open().then(initiateSerial);

  function initiateSerial() {
    console.log("Puerto serie abierto");
  }
  hidePortButton();
}

//Cerrar conexion con puerto serie
function closePort() {
  console.log("Puerto serie cerrado");
  serial.close();
  showPortButton();  
}

//Error con el puerto serie
function portError(err) {
  alert("Serial port error: " + err);
  showPortButton();
}

//Evento puerto serie conectado
function portConnect() {
  console.log("Puerto serie conectado");
  serial.getPorts();
  hidePortButton()
}

//Evento puerto serie desconectado
function portDisconnect() {
  serial.close();
  console.log("Puerto serie desconectado");
  showPortButton();
}

function showPortButton() {
  portButton.show();
}

function hidePortButton() {
  portButton.hide();
}

Lo primero es inicializar el objeto WebSerial para poder trabajar con él.

inicializarSerial(): Esta función se encarga de inicializar la conexión serie. Primero, verifica si el navegador admite la API Web Serial utilizando el objeto navigator.serial. Si el navegador no admite la API, muestra una alerta. Si la API está disponible, establece varios eventos para manejar la apertura, cierre, errores y desconexiones del puerto serie. Finalmente, agrega eventos para detectar cuándo se conecta o desconecta un puerto serie:

  • «noport»: Este evento se activa cuando no se detecta ningún puerto serial disponible en el sistema. En este caso, se llama a la función showPortButton() para mostrar un botón que permita al usuario seleccionar un puerto.
  • «portavailable»: Este evento se activa cuando se detecta un puerto serial disponible en el sistema. En este caso, se llama a la función openPort() para abrir la conexión con el puerto.
  • «requesterror»: Este evento se activa cuando se produce un error al intentar acceder al puerto serial. En este caso, se llama a la función portError() para mostrar un mensaje de error al usuario.
  • «data»: Este evento se activa cuando se recibe un dato del puerto serial. En este caso, se llama a la función serialEvent() para procesar el dato recibido.
  • «close»: Este evento se activa cuando se cierra la conexión con el puerto serial. En este caso, se llama a la función closePort() para cerrar la conexión.
  • «connect»: Este evento se activa cuando se establece una conexión con un puerto serial. En este caso, se llama a la función portConnect() para detectar los puertos disponibles.
  • «disconnect»: Este evento se activa cuando se pierde la conexión con un puerto serial. En este caso, se llama a la función portDisconnect() para cerrar la conexión y mostrar un botón para seleccionar otro puerto.

serialEvent(): Esta función se ejecuta cada vez que se recibe un mensaje del dispositivo conectado a través del puerto serie. Primero, lee la cadena recibida utilizando la función serial.readLine(). Si la cadena leída no está vacía, muestra la cadena en la consola del navegador utilizando console.log(readSerialStr). Lo usaremos para mostrar los datos enviados por el puerto serie.

choosePort(): Esta función muestra una ventana de selección de puerto para que el usuario pueda elegir un puerto serie disponible. La ventana se muestra llamando a la función showPortButton().

openPort(): Esta función se encarga de abrir la conexión con el puerto serie seleccionado. Utiliza la función serial.open() para abrir la conexión serie. Si la conexión se abre con éxito, se llama a la función initiateSerial() para inicializar la conexión serie.

closePort(): Esta función se encarga de cerrar la conexión con el puerto serie actual. Utiliza la función serial.close() para cerrar la conexión serie.

portError(err): Esta función se llama si se produce un error durante la conexión serie. Muestra una alerta con el mensaje de error

portConnect(): Esta función se llama cuando se detecta que un puerto serie se ha conectado. Llama a la función serial.getPorts() para obtener la lista actualizada de puertos serie disponibles.

portDisconnect(): Esta función se llama cuando se detecta que un puerto serie se ha desconectado. Cierra la conexión con el puerto serie actual llamando a la función serial.close().

send(cmd): Esta función toma un argumento cmd que representa los datos que se deben enviar al dispositivo conectado a través del puerto serie. Envía los datos al puerto serie utilizando la función serial.write(cmd).

Uniendo todas las piezas

Unamos todas las piezas, para ello usaremos la función send(cmd) para enviar los comando que controlan el funcionamiento del brazo (más info aqui) al pulsar cada botón.

Código JS (sketch.js):

//Crear el objeto Serial
const serial = new p5.WebSerial();
let portButton;
let cmdInput;

let posicionBase = 90;
let posicionHombro = 90;
let posicionCodo = 120;
let posicionMano = 10;

function setup() {
  createCanvas(400, 400);
  
  portButton = createButton("Elegir puerto");
  portButton.position(10, 10);
  portButton.mousePressed(choosePort);
    
  inicializarSerial();

  // Grupo Base
  crearGrupoBotones("Base", 1, 50, 50, () => {return posicionBase+=5;}, () => {return posicionBase-=5;});

  // Grupo Hombro
  crearGrupoBotones("Hombro", 3, 50, 120, () => {return posicionHombro+=5;}, () => {return posicionHombro-=5;});

  // Grupo Codo
  crearGrupoBotones("Codo", 2, 50, 190, () => {return posicionCodo+=5;}, () => {return posicionCodo-=5;});

  // Grupo Mano
  crearGrupoBotones("Mano", 4, 50, 260, () => {return posicionMano+=5;}, () => {return posicionMano-=5;});
  
  let botonApagar = createButton("Pos. Inicial");
  botonApagar.position(175, 300);
  botonApagar.mousePressed(() => {    
    send("Q");
    posicionBase = 90;
    posicionHombro = 90;
    posicionCodo = 120;
    posicionMano = 10;
  });
  
  let inputCmd = createInput();
  inputCmd.position(170, 360);
  inputCmd.size(60);

  botonCmd = createButton('->');
  botonCmd.position(240, 360);
  botonCmd.mousePressed(() => {send(inputCmd.value())});
  
}

function draw() {
  background(220);

  // Mostrar los valores actuales de las posiciones
  textSize(20);
  text("Base: " + posicionBase, 150, 60);
  text("Hombro: " + posicionHombro, 150, 130);
  text("Codo: " + posicionCodo, 150, 200);
  text("Mano: " + posicionMano, 150, 270);
}

function crearGrupoBotones(nombre, servo, x, y, funcSumar, funcRestar) {
  textSize(20);
  text(nombre, x, y);

  //Sumar
  let botonSumar = createButton("+");
  botonSumar.position(x + 75, y);
  botonSumar.mousePressed(() => {    
    send("S"+servo+":"+funcSumar());
  });

  //Restar
  let botonRestar = createButton("-");
  botonRestar.position(x + 225, y);
  botonRestar.mousePressed(() => {    
    send("S"+servo+":"+funcRestar());
  });
}

//enviar datos al puerto serie
function send(cmd){
  console.log(cmd);
  serial.write(cmd+"\n");
}

//leer datos del puerto serie
function serialEvent() {
  let readSerialStr = serial.readLine(); 
  trim(readSerialStr);                   
  if (readSerialStr) {
    console.log(readSerialStr);
  }
}

//incializar la conexion serie
function inicializarSerial() {
  if (!navigator.serial) {
    alert("WebSerial no esta sorportado en este navegador. Prueba en Chrome o Edge.");
  }
  serial.getPorts();
  serial.on("noport", showPortButton);
  serial.on("portavailable", openPort);
  serial.on("requesterror", portError);
  serial.on("data", serialEvent);
  serial.on("close", closePort);
  navigator.serial.addEventListener("connect", portConnect);
  navigator.serial.addEventListener("disconnect", portDisconnect);
}

// Muestra la ventana de seldccion de puerto
function choosePort() {
  showPortButton();
  serial.requestPort();
}

//abrir conexion con puerto serie
function openPort() {
  console.log("Abriendo puerto serie");
  serial.open().then(initiateSerial);

  function initiateSerial() {
    console.log("Puerto serie abierto");
  }
  hidePortButton();
}

//Cerrar conexion con puerto serie
function closePort() {
  console.log("Puerto serie cerrado");
  serial.close();
  showPortButton();  
}

//Error con el puerto serie
function portError(err) {
  alert("Serial port error: " + err);
  showPortButton();
}

//Evento puerto serie conectado
function portConnect() {
  console.log("Puerto serie conectado");
  serial.getPorts();
  hidePortButton()
}

//Evento puerto serie desconectado
function portDisconnect() {
  serial.close();
  console.log("Puerto serie desconectado");
  showPortButton();
}

function showPortButton() {
  portButton.show();
}

function hidePortButton() {
  portButton.hide();
}

Puedes ver la demostración de este artículo en el siguiente vídeo de mi canal de Youtube:

Haz click para ver el vídeo en mi canal de Youtube

Y si estas interesado en el tema puedes ver la lista de vídeos.

Controlar un brazo robótico de 4 motores con Arduino

Vamos a ver como controlar desde el ordenador un brazo robotico con cuatro motores. Aunque realmente del movimiento se ocupan tres, el cuarto es solo para abrir y cerrar «la pinza».Vamos a nombrar los servos de la siguiente manera:

  • base: se encuentra en la base del robot y controla el giro del brazo
  • hombro: controla «el hombro» del brazo
  • codo: controla «el codo» del brazo
  • mano: controla la apertura y cierre de la pinza

En las siguientes fotos puede verse el brazo que voy a usar. El servo «base» se encuentra en la parte inferior del brazo. El servo «hombro» en la parte superior de la fotografía (en la foto vista desde arriba), el servo «codo» en la inferior y el servo «mano» en la mano (este era sencillo).

Para controlar el movimiento de estos servos usaremos dos variables, una guardará el valor en grados actual, la otra el valor en grados al que debe de moverse. En cada iteración de la función loop() se llamara a la función ActualizarServos() que se ocupa de actualizar el valor del servo. Si ambas variables coinciden no se hace nada, si son distintas cada iteración se sumará o restará un grado para acercarse al valor deseado. Esto se hace así para evitar movimientos demasiado rápidos que puedan dañar el robot o golpear alguna cosa del entorno. La velocidad se puede controlar con el delay al final de la función loop()

Cada servo tendrá unos grados mínimos y máximos delos que no podrá pasar, delimitados por la estructura del brazo (por ejemplo, que choque con la propia estructura del brazo o que la articulación no «da más de si»). Se usaran dos variables para estar seguro que no pasamos esos limites. Por ejemplo para la base se usaran: basePosMax y basePosMin.

Si se intenta establecer un valor mayor o menor del fijado por esas dos variables se igualará al valor de estas. Asi evitamos situaciones que puedan dañar el brazo.

Para controlar el brazo usaremos comandos de texto enviados a través del puerto serie

Lo comandos son:

  • Sn:grados: n indica el servo y grados la posicion en grados a la que debe ir
  • Q: mueve cada servo a la «posición de apagado» / «posición de inicio»
  • W: escribe en consola la posición en grados de cada servo

El comando Q es necesario para poder «parar» el brazo. No se guarda el estado de los servos, por lo que para evitar que el brazo realice movimientos bruscos al iniciarse hay que llevarlo a la posición de «apagado» o de «arranque» (obviamente son la misma).

El comando S tiene las siguientes correspondencias con los servos:

  • S1 base
  • S2 codo
  • S3 hombro
  • S4 mano

Por ejemplo, para cerrar la mano se podria usar el comando S4:35

El código para el controlarlo es el siguiente:

#include <Servo.h> 

// Se definen los pines a los que están conectados los servos
const int basePin = 5;
const int hombroPin = 11;
const int codoPin = 3;
const int manoPin = 10;

// Se crean los objetos Servo correspondientes a cada servo
Servo baseServo;
Servo hombroServo;
Servo codoServo;
Servo manoServo;

// Variables para almacenar la posición actual y destino
int basePos = 90;
int hombroPos = 90;
int codoPos = 120;
int manoPos = 10;

int basePosDest = basePos;
int hombroPosDest = hombroPos;
int codoPosDest = codoPos;
int manoPosDest = manoPos;

int basePosMax = 180;
int hombroPosMax = 150;
int codoPosMax = 170;
int manoPosMax = 35;

int basePosMin = 0;
int hombroPosMin = 80;
int codoPosMin = 90;
int manoPosMin = 10;

void setup() {
  // Se inicializan los servos en la posición central
  baseServo.write(basePos);
  hombroServo.write(hombroPos);
  codoServo.write(codoPos);
  manoServo.write(manoPos);

  // Se inician los objetos Servo
  baseServo.attach(basePin);
  hombroServo.attach(hombroPin);
  codoServo.attach(codoPin);
  manoServo.attach(manoPin);

  // Se inicia la comunicación por el puerto serie
  Serial.begin(9600);
}

void loop() {
  // Se actualizan los servos
  ActualizarServos();

  // Se espera para que los servos alcancen su nueva posición
  delay(20);
}

// Función que se llama al final decada iteracion de loop
void serialEvent() {
  // Se lee la línea recibida del puerto serie
  String data = Serial.readStringUntil('\n');
  char command = data.charAt(0);
  if(command == 'Q'){
    basePosDest = 90;
    hombroPosDest = 90;
    codoPosDest = 120;
    manoPosDest = 10;
  }

  if(command == 'W'){
    Serial.print("S1 Base:   ");
    Serial.print(basePosDest);
    Serial.print("   [");
    Serial.print(basePosMin);
    Serial.print(" - ");
    Serial.print(basePosMax);
    Serial.println("]");

    Serial.print("S2 Codo: ");
    Serial.print(codoPosDest);
    Serial.print("   [");
    Serial.print(codoPosMin);
    Serial.print(" - ");
    Serial.print(codoPosMax);
    Serial.println("]");    

    Serial.print("S3 Hombro: ");
    Serial.print(hombroPosDest);
    Serial.print("   [");
    Serial.print(hombroPosMin);
    Serial.print(" - ");
    Serial.print(hombroPosMax);
    Serial.println("]");

    Serial.print("S4 Mano:   ");
    Serial.print(manoPosDest);
    Serial.print("   [");
    Serial.print(manoPosMin);
    Serial.print(" - ");
    Serial.print(manoPosMax);
    Serial.println("]");
  }
  if(command == 'S'){
    // Se separa la información de servo y ángulo
    // Se obtiene el índice del servo a partir del segundo caracter
    int servoIndex = data.charAt(1) - '0'; 
    // Se obtiene el ángulo a partir del cuarto caracter hasta el final
    int angle = data.substring(3).toInt(); 
    
    // Se actualiza la posición deseada del servo correspondiente
    switch (servoIndex) {
      case 1:
        basePosDest = angle;
        if(basePosDest > basePosMax){
          basePosDest = basePosMax;
        }
        if(basePosDest < basePosMin){
          basePosDest = basePosMin;
        }
        break;
      case 2:
        codoPosDest = angle;
        if(codoPosDest > codoPosMax){
          codoPosDest = codoPosMax;
        }
        if(codoPosDest < codoPosMin){
          codoPosDest = codoPosMin;
        }
        break;
      case 3:
        hombroPosDest = angle;
        if(hombroPosDest > hombroPosMax){
          hombroPosDest = hombroPosMax;
        }
        if(hombroPosDest < hombroPosMin){
          hombroPosDest = hombroPosMin;
        }
        break;
      case 4:
        manoPosDest = angle;
        if(manoPosDest > manoPosMax){
          manoPosDest = manoPosMax;
        }
        if(manoPosDest < manoPosMin){
          manoPosDest = manoPosMin;
        }
        break;
      default:
        break;
    }
  }
}

// Función que actualiza la posición de los servos
void ActualizarServos() {
  if (basePos < basePosDest) {
    basePos++;
  } else if (basePos > basePosDest) {
    basePos--;
  }
  baseServo.write(basePos);

  if (hombroPos < hombroPosDest) {
    hombroPos++;
  } else if (hombroPos > hombroPosDest) {
    hombroPos--;
  }
  hombroServo.write(hombroPos);

  if (codoPos < codoPosDest) {
    codoPos++;
  } else if (codoPos > codoPosDest) {
    codoPos--;
  }
  codoServo.write(codoPos);

  if (manoPos < manoPosDest) {
    manoPos++;
  } else if (manoPos > manoPosDest) {
    manoPos--;
  }
  manoServo.write(manoPos);
}

Puedes ver más detalles sobre este tema en el siguiente vídeo de mi canal deYoutube:

Haz click para ver el vídeo en mi canal deYoutube

Y si estas interesado en el tema puedes ver la lista de vídeos.

Crear un sonar en Arduino con un servo y un sensor de ultrasónico HC-SR04

Ya vimos como medir distancias con un sensor ultrasonico, ahora crearemos un sonar con él. Para ello lo pondremos encima de un servo que ira girando el sensor de 5 en 5 grados. Cada movimiento hará tres medidas con el sensor de la cual tomara la mediana (ya vimos que esto se hace para reducir el ruido). Esos datos serán enviados al puerto serie como «angulo, distancia». Para que luego los muestre un programa realizado en processing.

#include <Servo.h> // incluir la biblioteca para controlar el servo

Servo myservo; // declarar un objeto de tipo Servo

int trigPin = 12;  // pin para el trigger del sensor
int echoPin = 11; // pin para el echo del sensor
int angle = 0; // ángulo actual del servo

void setup() {
  Serial.begin(9600); // inicializar el puerto serie
  myservo.attach(9); // conectar el servo al pin 9
  pinMode(trigPin, OUTPUT); // configurar el pin trigger como salida
  pinMode(echoPin, INPUT); // configurar el pin echo como entrada
}

void loop() {
  for (angle = 0; angle <= 180; angle += 5) {
    myservo.write(angle); //movemos el servo
    delay(100); //para que el servo no este moviendose
    int distance = getMedianDistance(); // distancia medida por el sensor
    Serial.print(angle);
    Serial.print(", ");
    Serial.println(distance);
    delay(50);
  }
  for (angle = 180; angle >= 0; angle -= 5) {
    myservo.write(angle); //movemos el servo
    delay(100); //para que el servo no este moviendose
    int distance = getMedianDistance(); // distancia medida por el sensor
    Serial.print(angle);
    Serial.print(", ");
    Serial.println(distance);
    delay(50);
  }
}

int cmp_desc(const void *c1, const void *c2){  
  return *((int *)c2) - *((int *)c1);
}

int getMedianDistance() {
  int distances[3];
  for (int i = 0; i < 3; i++) { //3 mediciones
    long duration, distance;
    digitalWrite(trigPin, LOW);
    delayMicroseconds(2);
    digitalWrite(trigPin, HIGH);
    delayMicroseconds(10);
    digitalWrite(trigPin, LOW);
    duration = pulseIn(echoPin, HIGH);
    distance = (duration / 2) / 29.1;
    distances[i] = distance;
  }

  //ordenamos
  qsort(distances, 3, sizeof(int), cmp_desc);
  
  //tomamos la mediana (el central)
  return distances[1];
}

Para dibujar el resultado usaremos el siguiente programa en processing:

import processing.serial.*;

Serial port;
boolean drawLines = true; // indica si se deben dibujar las líneas
int x0, y0; // coordenadas del punto central
float r = 100; // radio
float angle, distance; // ángulo y distancia leídos desde el puerto serie
float zoom = 2.0; //multiplicamos la distancia para mejorar la visualizacion
int radio = 50; //se usa pra dibujar los circulos del fondo del radar

void setup() {
  size(400, 400);
  x0 = width / 2;
  y0 = height - 50;
  String portName = Serial.list()[0]; // elegir el primer puerto serie disponible
  port = new Serial(this, portName, 9600); // inicializar el puerto serie
  drawBackground();
}

void draw() {
  if (port.available() > 0) {
    String data = port.readStringUntil('\n'); // leer los datos desde el puerto serie
    if (data != null) {      
      String[] values = split(data, ','); // separar los datos en grados y distancia
      if(values.length == 2){ //tiene que haber dos datos
        angle = float(values[0]) / 180 * PI; // convertir los grados a radianes
        if (angle == 0.0) { //si angulo es 0 limpiamos el radar          
          drawBackground();
        }
        distance = float(values[1])*zoom;
        float x = x0 + cos(angle) * distance; // calcular las coordenadas x,y
        float y = y0 - sin(angle) * distance;
        println(distance, angle, x, y);
        stroke(0, 255, 0);
        fill(0, 255, 0);
        ellipse(x, y, 5, 5); // dibujar un punto en las coordenadas calculadas
      }
    }
  }
}

void drawBackground(){ //dibuja el fondo con forma de radar
  background(0);
  noFill();
  radio = 50;
  for (int i = 0; i < 10; i++) {
    stroke(100, 200, 100);
    strokeWeight(2);
    ellipse(x0, y0, 2 * radio, 2 * radio);
    radio += 50;
  }
}

Vamos a centrarnos en la parte que dibuja los datos

Primero vemos si el puerto está disponible, si hay datos en el puesto y si tenemos dos valores separados por una coma:

 if (port.available() > 0) {
    String data = port.readStringUntil('\n'); // leer los datos desde el puerto serie
    if (data != null) {      
      String[] values = split(data, ','); // separar los datos en grados y distancia
      if(values.length == 2){ //tiene que haber dos datos

Convertimos el angulo a radianes:

angle = float(values[0]) / 180 * PI; // convertir los grados a radianes

Ajustamos la distancia para que se vea bien en el canvas:

distance = float(values[1])*zoom;

Finalmente teniendo el angulo y la distancia proyectamos el punto usando, nuestra por todos querida (¿verdad?), trigonometría:

 float x = x0 + cos(angle) * distance; // calcular las coordenadas x,y
 float y = y0 - sin(angle) * distance;

La variables x0 y y0 son las coordenadas del centro de nuestro radar.

El resultado tiene este aspecto:

¡Ojo! Tener en cuenta que la orientación en el radar no tiene porque coincidir con el del sensor, puedes girar el sensor y la imagen de la pantalla no se girará. Seguramente ahora parezca una tontería pero cuando los datos salen invertidos horizontalmente puede resultar confuso.

Puedes ver todo esto en este vídeo de mi canal de Youtube:

Haz click para ver el vídeo en mi canal de Youtube

Medir distancias con Arduino y el sensor de ultrasónico HC-SR04

El sensor HC-SR04 utiliza el principio de medición de tiempo de eco para determinar la distancia a un objeto. Consiste en un emisor de ultrasonidos y un receptor.

Cuando el emisor de ultrasonidos es activado, envía un pulso de sonido de alta frecuencia. Este pulso se refleja en un objeto cercano y vuelve al receptor. El sensor mide el tiempo transcurrido desde que se envió el pulso hasta que se recibió el eco y utiliza esta información para calcular la distancia del objeto.

La distancia se calcula utilizando la fórmula: d = (t * v) / 2 donde t es el tiempo transcurrido desde el envío del pulso hasta la recepción del eco, y v es la velocidad del sonido (en cm por microsegundo). El 2 es porque el sonido tiene que recorrer la distancia 2 veces, una para ir y otra para volver al sensor.

El sensor HC-SR04 tiene dos pines, TRIG (disparador) y ECHO (eco), que se utilizan para enviar y recibir el pulso de sonido, respectivamente. El Arduino se comunica con el sensor mediante estos pines para activar el emisor, medir el tiempo de eco y calcular la distancia.

En nuestro ejemplo conectamos el pin TRIG del sensor al pin 9 del Arduino, el pin ECHO al pin 8, el pin Vcc al pin 5V y el pin GND al pin GND.

En el bucle principal loop(), se envía un pulso de 10 microsegundos al pin TRIG para iniciar la medición de distancia. Luego, se mide el tiempo que tarda el pulso en volver al pin ECHO utilizando la función pulseIn().

A continuación, se calcula la distancia en cm utilizando la fórmula antes vista.

const int TRIGGER_PIN = 8;
const int ECHO_PIN = 9;
const double VEL_SOUND = 34000.0 / 1000000.0;
void setup() {
  Serial.begin(9600);
  pinMode(ECHO_PIN, INPUT);
  pinMode(TRIGGER_PIN, OUTPUT); 
}
void loop() {
  // Envía un pulso de 10 microsegundos al TRIGGER_PIN
  digitalWrite(TRIGGER_PIN, HIGH);
  delayMicroseconds(10);
  // Detiene el pulso
  digitalWrite(TRIGGER_PIN, LOW);
  
  // Lee el tiempo que tarda el pulso en volver al ECHO_PIN
  long duration = pulseIn(ECHO_PIN, HIGH);  
  // Convierte el tiempo en distancia (en cm)
  double distance = (duration * VEL_SOUND ) / 2.0; 
  
  Serial.print("Duracion: ");
  Serial.print(duration);
  Serial.print(" Distancia: ");
  Serial.println(distance);
  delay(100);
}

Los sensores ultrasonidos son una forma barata de medir distancias. Por otro lado para distancias muy cercanas o muy lejanas el sensor puede no funcionar. Tampoco da buenos resultados para objetos en movimiento, o de según que materiales o formas.

Es un sensor expuesto a ruidos. Las mayores fuentes de ruido son los ruidos sonoros ambiente en la misma frecuencia y los ecos.

Para reducirlo vamos a usar un filtro de mediana tomando 3 muestras de cada medida. A mayor número de muestras más seguro estaremos pero más tiempo tardaremos en tener una medida lo cual reducirá la frecuencia de muestreo (no confundir con la frecuencia del sonido enviado)

Vamos a ver el código para implementar un filtro de mediana de en el ejemplo anterior:

const int TRIGGER_PIN = 8;
const int ECHO_PIN = 9;
const double VEL_SOUND = 34000.0 / 1000000.0;
void setup() {
  Serial.begin(9600);
  pinMode(ECHO_PIN, INPUT);
  pinMode(TRIGGER_PIN, OUTPUT); 
}
void loop() {
  // Convierte el tiempo en distancia (en cm)
  long duration = readDuration();
  double distance = (duration * VEL_SOUND) / 2.0; 
  
  Serial.print("Duracion: ");
  Serial.print(duration);
  Serial.print(" Distancia: ");
  Serial.println(distance);
}
long readDuration(){
  long a,b,c;
  a = readSensor();
  delay(50);
  b = readSensor();
  delay(50);
  c = readSensor();
  delay(50);
  return medianFilter(a,b,c);
}
long readSensor(){
  // Envía un pulso de 10 microsegundos al TRIGGER_PIN
  digitalWrite(TRIGGER_PIN, HIGH);
  delayMicroseconds(10);
  // Detiene el pulso
  digitalWrite(TRIGGER_PIN, LOW);  
  
  // Lee el tiempo que tarda el pulso en volver al ECHO_PIN
  long duration = pulseIn(ECHO_PIN, HIGH);  
}
long medianFilter(long a, long b, long c) {
    long temp;
    if (a > b) { // Ordenar a, b
        temp = a;
        a = b;
        b = temp;
    }
    if (b > c) { // Ordenar b, c
        temp = b;
        b = c;
        c = temp;
    }
    if (a > b) { // Ordenar a, b de nuevo
        temp = a;
        a = b;
        b = temp;
    }
    return b; // El valor medio es el segundo de los tres números ordenados
}

Puedes ver un vídeo sobre este tema en mi canal de Youtube:

Haz click para ver el vídeo en mi canal de Youtube

Dibujos en el osciloscopio con Arduino

Ya vimos cómo crear un conversor de digital a analógico barato. Luego con ese conversor vimos cómo crear un generador de funciones. Ahora vamos a tomar ese generador de funciones y a usarlo para crear nuestros dibujos en el osciloscopio.

Los dibujos son algo limitados, podemos simplemente elegir la altura de la linea que dibuja el osciloscopio. Así que no esperéis grandes obras de arte.

Antes de explicar como funciona os dejo el cogido para realizar dibujos:

float f = 100; //frecuencia en Hz
float p = 1/f; //periodo
float t = 0;
int i = 0;
unsigned long oldMicros = 0;
unsigned long nowMicros = 0;
const int WAVE_POINTS = 44; //puntos generados

byte wave[WAVE_POINTS]; //valores generados
double waveDt = p/WAVE_POINTS; //tiempo entre cada punto 

const byte PRESCALER2 = 0b001;

void setup() {
  //ajusta la frec. salida PWM pin 3
  TCCR2B = (TCCR2B & 0b11111000) | PRESCALER2;
  pinMode(3, OUTPUT);
  
  //Dibujamos la onda  
  wave[i++] = 0;
  wave[i++] = 250;
  wave[i++] = 250;
  wave[i++] = 200;
  wave[i++] = 200;
  wave[i++] = 250;
  wave[i++] = 250;
  wave[i++] = 200;
  wave[i++] = 200;
  wave[i++] = 250;
  wave[i++] = 250;
  wave[i++] = 100;  
  wave[i++] = 100;
  wave[i++] = 200;
  wave[i++] = 200;
  wave[i++] = 150;
  wave[i++] = 150;
  wave[i++] = 200;
  wave[i++] = 200;
  wave[i++] = 150;
  wave[i++] = 150;
  wave[i++] = 200;
  wave[i++] = 200;
  wave[i++] = 150;
  wave[i++] = 150;
  wave[i++] = 200;
  wave[i++] = 200;
  wave[i++] = 150;
  wave[i++] = 150;
  wave[i++] = 200;
  wave[i++] = 200;
  wave[i++] = 100;
  wave[i++] = 100;
  wave[i++] = 250;
  wave[i++] = 250;
  wave[i++] = 200;
  wave[i++] = 200;
  wave[i++] = 250;
  wave[i++] = 250;
  wave[i++] = 200;
  wave[i++] = 200;
  wave[i++] = 250;
  wave[i++] = 250;
  wave[i++] = 0;
    
}

double dt = 0;

void loop() {
  nowMicros = micros();
  dt = double(nowMicros - oldMicros)/2000000;

  t += dt/waveDt;
  analogWrite(3, wave[int(t)]);

  oldMicros = nowMicros;    

  //reinicia al recorrer todos los puntos
  if(t > WAVE_POINTS-1){ 
    t = 0;
  }
}

En este caso se dibuja un castillo, debajo podéis ver el resultado.

Señal con forma de castillo (más o menos)

Veamos paso a paso como

1 – Ajustar al frecuencia de nuestra señal, se hace en la línea:

float f = 100; //frecuencia en HZ

2 – Indicar el número de puntos que tendrá nuestro dibujo:

const int WAVE_POINTS = 44; //puntos generados

Mi consejo es que pongas el doble de puntos de los necesarios, es buena idea repetir el mismo punto dos veces seguidas, se debe a que el siguiente trozo de código hace que a veces se salte algún valor para mantener la frecuencia de la señal:

t += dt/waveDt;
analogWrite(3, wave[int(t)]);

3 – Rellenamos el array wave que es donde se indica la altura de cada uno de los trozos del dibujo con un valor entre 0 y 255 (0 – 5 V.)

wave[i++] = 0;
wave[i++] = 250;
wave[i++] = 250;
wave[i++] = 200;
wave[i++] = 200;

Es una buena idea que dejéis varios ceros antes del dibujo para separarlo claramente del anterior.

Con estos sencillos pasos ya podéis convertiros en artistas del osciloscopio y crear vuestras obras de arte.

Puede ver el proceso en vídeo en mi canal de Youtube:

Haz click para ver el vídeo en mi canal de Youtube

Clonar un mando de radio frecuencia con Arduino y un receptor/emisor de 433Mhz

Esta técnica es una forma rápida y sencilla de clonar un mando de radiofrecuencia, aunque desgraciadamente no funciona con todos los mandos. Ahora con los que funciona puedes duplicar su funcionamiento en pocos minutos. Además de un Arduino necesitaremos un receptor y un emisor de la frecuencia en la que funcione el mando. En nuestro caso 433 MHz.

Vamos a usar un receptor muy sencillo que tiene cuatro patillas  a parte de +5v y GND necesitamos conectar la patilla por la que recibiremos los datos (en mi caso la que está junto a la de +5v) al puerto digital número 2. Es la patilla cuya interrupción escuchará la librería que vamos a usar: rc-switch. La librería se puede encontrar desde la sección librerías del IDE e instalarla.

Una vez instalada la librería cargaremos el ejemplo ReceiveDemo_Advance.

Una vez cargado abriremos el serial monitor donde deberían aparecer los datos que envía el mando cuando pulsemos el botón. Copiaremos el código decimal, el numero de bits, el protocolo y la duración del pulso. estos datos vamos a meterlos en este programa:

#include <RCSwitch.h>
RCSwitch mySwitch = RCSwitch();

void setup() {
  Serial.begin(9600);
  
  // Pîn digital al que se conecta el emisor
  mySwitch.enableTransmit(10);
  
  // Protocolo
  mySwitch.setProtocol(1);

  // Duracion del pulso
  mySwitch.setPulseLength(503);
  
  // Cuantas veces se repite la transmision
  mySwitch.setRepeatTransmit(10);
  
}

void loop() {
  Serial.println("Enviando señal");
  //código decimal y nº bits
  mySwitch.send(2351425, 24); 
  delay(10000);  
}

Para ejecutarlo necesitaremos un emisor de 433 MHz. en este caso ademas de a 5V y GND conectaremos el pin por el que envía los datos al pin digital 10 de la placa de Arduino.

Puedes ver un vídeo con un ejemplo de su uso y más información en mi canal de Youtube:

Haz click para ver el vídeo en mi canal de Youtube

Comprobar lo rápido que ordena Arduino

Vamos a ver cómo medir lo rápido que es capaz de ordenar Arduino un listado de 400 enteros (con más qsort falla en Arduino UNO). Aprovecharemos esto para ver cómo medir el tiempo que le cuesta ejecutar el código en Arduino.

Evitar «interrupciones»

Lo primero es quitarnos de «en medio» cualquier molestia a nuestro código en Arduino

  • Procesos asíncronos como la comunicación por Serial o el Watchdog
  • Interrupciones

En el caso de tener el Watchdog activado debemos asegurarnos de que no salta mientras probamos el bloque de código que queremos medir. Podemos deactivarlo con wdt_disable();

Con Serial debemos vaciar la cache usando Serial.flush(); para volcarla

En el caso de las interrupciones se puede usar noInterrupts(); para evitar que se lancen.

Como medida de seguridad extra podemos añadir un delay para dar tiempo a que todo termine. Aunque es una medida más paranoica que otra cosa debido a que Arduino es una arquitectura de un solo hilo (a diferencia de los procesadores modernos que tienen múltiples hilos). No hay manera de que quede alguna «tarea pendiente». Pero, al menos yo, me quedo más tranquilo.

En el caso de nuestro ejemplo no tenemos ni watchdog ni interrupciones, pero si comunicación usando Serial.

Medir el tiempo

Para medir el tiempo debemos crear dos variables de tipo unsigned long una guardara el tiempo justo antes de ejecutar el código y otra justo después. Para saber el tiempo total de ejecución del código basta con restarlas. Si el codigo le cuesta ejcutarsemenos de 70 minutos podemos usar micros() si se cuesta mas millis().

Ejemplo

Veamos el ejemplo comentado al principio midiendo el coste de ordenar 400 elementos:

int cmp_desc(const void *c1, const void *c2){  
  return *((int *)c2) - *((int *)c1);
}
int cmp_asc(const void *c1, const void *c2){  
  return *((int *)c1) - *((int *)c2);
}
void setup() {
  Serial.begin(9600);
  Serial.println("Start");
  Serial.flush();
  int array[400] = {32, 162, 26, 82, 85, 220, 157, 149, 142, 255, 160, 66, 151, 37, 204, 0, 242, 92, 34, 24, 124, 14, 19, 173, 187, 160, 223, 225, 59, 151, 237, 245, 125, 35, 143, 125, 210, 53, 129, 136, 126, 174, 88, 138, 17, 132, 68, 116, 154, 7, 77, 31, 167, 59, 147, 165, 232, 59, 184, 169, 175, 116, 77, 110, 118, 211, 19, 111, 142, 247, 0, 120, 229, 138, 43, 173, 210, 98, 218, 75, 142, 101, 204, 14, 30, 130, 102, 142, 214, 176, 153, 178, 3, 199, 135, 191, 149, 22, 230, 64, 15, 165, 225, 187, 85, 204, 69, 144, 147, 9, 49, 134, 208, 171, 151, 231, 217, 37, 42, 68, 135, 102, 212, 216, 51, 177, 137, 242, 117, 154, 241, 32, 119, 210, 5, 180, 63, 209, 180, 39, 106, 11, 232, 6, 216, 249, 223, 113, 29, 78, 210, 138, 69, 2, 63, 37, 91, 146, 155, 243, 92, 174, 143, 122, 102, 130, 203, 168, 186, 255, 17, 167, 162, 41, 158, 81, 56, 213, 209, 11, 248, 108, 146, 82, 230, 159, 132, 17, 168, 165, 161, 190, 4, 53, 181, 149, 52, 64, 219, 215, 72, 4, 79, 186, 8, 16, 40, 32, 82, 115, 56, 138, 46, 126, 255, 124, 21, 85, 211, 13, 21, 21, 111, 227, 88, 128, 247, 158, 188, 210, 196, 190, 24, 38, 198, 81, 168, 245, 174, 40, 74, 236, 78, 68, 48, 44, 130, 34, 133, 118, 215, 242, 168, 21, 123, 84, 77, 140, 30, 83, 94, 29, 94, 138, 46, 223, 228, 13, 2, 70, 87, 74, 47, 100, 193, 86, 80, 237, 130, 142, 152, 239, 113, 114, 133, 160, 217, 34, 161, 214, 168, 92, 216, 178, 67, 188, 110, 136, 183, 147, 127, 209, 88, 102, 133, 196, 15, 66, 237, 189, 208, 0, 98, 147, 116, 130, 214, 231, 58, 150, 227, 155, 117, 133, 42, 98, 114, 254, 17, 80, 113, 63, 215, 190, 35, 171, 89, 180, 91, 26, 147, 39, 126, 66, 34, 1, 139, 87, 183, 129, 153, 106, 219, 245, 143, 182, 62, 99, 27, 82, 198, 234, 158, 122, 16, 119, 254, 241, 170, 186, 197, 192, 46, 133, 179, 54, 236, 35, 34, 97, 48, 150, 19, 26, 235, 17, 15, 182, 201, 151, 30, 40, 94, 188, 192, 149, 220, 250, 16, 35};
  unsigned long timeStart;
  unsigned long timeEnd;
  delay(500);
  timeStart = micros();    
  qsort(array, 400, sizeof(int), cmp_asc);
  timeEnd = micros();  
  Serial.print((timeEnd-timeStart));
  Serial.println();
  
}
void loop()
{
}

Puedes ver el vídeo donde ejecuto el ejemplo y muestro sus resultados:

Haz click para ver el vídeo en mi canal de Youtube

Como ordenar un array en Arduino

A veces tras la librerías oficiales de Arduino nos «tapan» utilidades que tienen las librerías avr-libc del fabricante del controlador. en este caso vamos a usar la función qsort() que implementar el algoritmo quicksort. No entraremos en detalle, simplemente aceptaremos que ordena y lo hace rápido.

qsort(void *base, size_t n_memb, size_t size, cmp_t *cmp)

En este caso la función qsort() requiere 4 parámetros:

  • *base : array de elementos a ordenar
  • n_memb : número de elementos del array
  • size : tamaño de cada elemento
  • cmp : función que realiza la comparación

Como qsort no sabe mágicamente como quieres ordenar lo elementos del array hay que pasar una función de comparación entre dos elementos que tiene que devolver los siguientes valores:

  • Si p1 == p2 devuelve 0
  • Si p1 va antes de p2 devuelve -1 (o culaquier número negativo)
  • Si p2 va después de p1 devuelve 1 (o culaquier número positivo)

La función comparación tiene la sigueinte firma:

int cmp_desc(const void *c1, const void *c2)

Como los parámetros se pasan como un puntero (a void) hay que realizar un cast al tipo de elemento de nuestro array. En nuestro ejemplo int, veamos el código de ejemplo de la función ordenar descendente (primero los números mayores)

int cmp_desc(const void *c1, const void *c2){  
  return *((int *)c2) - *((int *)c1);
}

Veamos un ejemplo completo con ordenación ascendente y descendente:

int cmp_desc(const void *c1, const void *c2){  
  return *((int *)c2) - *((int *)c1);
}

int cmp_asc(const void *c1, const void *c2){  
  return *((int *)c1) - *((int *)c2);
}


void setup() {
  Serial.begin(9600);

  int array[10] = {10, 5, 34, 76, 7, 6, 5, 23, 2, 42};
  
  qsort(array, 10, sizeof(int), cmp_asc);
  
  Serial.println("Resultado ascendente: ");
  for(int i = 0; i < 10; i++){
    Serial.print(array[i]);
    Serial.print(", ");
  } 
  Serial.println("");

  qsort(array, 10, sizeof(int), cmp_desc);

  Serial.println("Resultado descendente: ");
  for(int i = 0; i < 10; i++){
    Serial.print(array[i]);
    Serial.print(", ");
  } 
  Serial.println("");
}


void loop()
{
}

Y así de sencillo podemos ordenar cualquier array siempre que podemos hacer una función que compare valores. No necesitamos ningún tipo de librerías externas.

Puedes ver como funciona este código en el siguiente vídeo de mi canal de Youtube:

Haz click para ver el vídeo en mi canal de Youtube

Generador de funciones / señales barato y simple usando Arduino

Ya vimos como convertir de forma barata de PWM a analógico. Ahora que tenemos una salida analógica ¿Podemos usarla para generar distintos tipos de señales? Vamos aintentar generar señales de distinto tipo sinusoidal, triangular, cuadrada (aunque para eso tenemos PWM), triangular, ….

Sin embargo vamos a tener alguna limitaciones:

  • Las limitaciones son que el valor mínimo de salida es 0v y el máximo 5v.
  • La frecuencia no puede ser muy alta. La base de nuestro sistema es un señal cuadrada funciona a 32 kHz y convierte la anchura de la señal (0-100%) a voltaje (0-5v). Para que esto ocurra necesita algunos pulsos, ademas que para reconstruir la señal necesita varios puntos. ¿Cuántos? Depende del tipo de señal, pero tomando la sinusoidal como referencia en mis pruebas el limite es unos 500-600HZ con la primera versión del programa y unos 1500-2000Hz con la segunda versión.

Ya vimos como generar distintas funciones para las ondas, ahora hay que añadir un cambio, las generaremos devolviendo un valor entre 0 y 1. Las funciones modificadas son las siguientes:

//Distintas señales a elegir
double signal = tf; //dientes de sierra
double signal = (sin(tf*2*PI)+1)/2 ; //seno
double signal = sin(tf*PI); //seno positiva
double signal = (sin(tf*2*PI) > 0) ? 1: 0; //cuadrada
double signal = (sin(tf*2*PI) >= 0) ? 2*tf: 2-(2*tf); //triangular

Solo podemos usar una cada vez. Puedes definir tus propias funciones mientras que devuelvan un valor entre 0 y 1. Donde 0 representa 0v y 1 es igual a 5v, cualquier valor intermedio representa un voltaje proporcional. Por ejemplo 0.5 representa 2.5v

Veamos el código:

float t = 0;
float f = 100; //frecuencia
float p = 1/f; //periodo
unsigned long oldMicros = 0;
unsigned long nowMicros = 0;

const byte PRESCALER2 = 0b001;

void setup() {
  //ajusta la frec. salida PWM pin 3
  TCCR2B = (TCCR2B & 0b11111000) | PRESCALER2;
  pinMode(3, OUTPUT);
}

double dt = 0;
double tf = 0;
double signal = 0;

void loop() {
  nowMicros = micros();
  dt = double(nowMicros - oldMicros)/1000000;
  
  t += dt;
  tf = t*f;
  //Distintas señales a elegir
  //signal = tf; //dientes de sierra
  signal = (sin(tf*2*PI)+1)/2 ; //seno  
  //signal = sin(tf*PI); //seno positiva
  //signal = (sin(tf*2*PI) > 0) ? 1: 0; //cuadrada
  //signal = (sin(tf*2*PI) >= 0) ? 2*tf: 2-(2*tf); //triangular
  
  byte value = byte(255 * signal);
  analog(value);

  oldMicros = nowMicros;
  
  //cuando alcanza un periodo se reinicia
  if(t >= p){ 
    t = 0;
  }
}

//compensa el desajuste de la salida analogica
void analog(byte value){
  value += (255 - value) >> 4; 
  analogWrite(3, value); 
}

Con esta versión se puede alcanzar una frecuencia máxima (probada con una señal sinusoidal) de entre 500-600Hz.

Alcanzando mayores frecuencias

Para intentar exprimir al máximo nuestro Arduino vamos a optimizar el código todo lo posible. Para ello vamos a reducir los cálculos necesarios en cada iteración. Lo haremos precalculando en un array los valores de la onda en varios puntos. Luego en cada iteración calcularemos que punto nos toca mostrar del array. Esta forma da lugar a ondas más «sucias» con más ruido. Pero también permite mayores frecuencias.

float f = 1000; //frecuencia
float p = 1/f; //periodo
float t = 0;
int i = 0;
unsigned long oldMicros = 0;
unsigned long nowMicros = 0;
const int WAVE_POINTS = 2000; //puntos generados

byte wave[WAVE_POINTS]; //valores generados
double waveDt = p/WAVE_POINTS; //tiempo entre cada punto 

const byte PRESCALER2 = 0b001;

void setup() {
  //ajusta la frec. salida PWM pin 3
  TCCR2B = (TCCR2B & 0b11111000) | PRESCALER2;
  pinMode(3, OUTPUT);
  
  //Precalculamos la onda
  for(i = 0; i < WAVE_POINTS; i++){
    t += waveDt;
    double tf = t*f;
    //double signal = tf; //dientes de sierra
    //double signal = (sin(tf*2*PI)+1)/2 ; //seno  
    //double signal = sin(tf*PI); //seno positiva
    //double signal = (sin(tf*2*PI) > 0) ? 1: 0; //cuadrada
    double signal = (sin(tf*2*PI) >= 0) ? 2*tf: 2-(2*tf); //triangular
    wave[i] = analog(byte(255 * signal));
  }
}

double dt = 0;

void loop() {
  nowMicros = micros();
  dt = double(nowMicros - oldMicros)/1000000;

  t += dt/waveDt;
  analogWrite(3, wave[int(t)]);

  oldMicros = nowMicros;    

  //reinicia al recorrer todos los puntos
  if(t > WAVE_POINTS){ 
    t = 0;
  }
}

//compensa el desajuste de la salida analogica
byte analog(byte value){
  value += (255 - value) >> 4; 
  return value;
}

Con esta versión se puede alcanzar una frecuencia máxima (probada con una señal sinusoidal) de entre 1500-2000Hz usando 2000 puntos para la tabla donde se precalculan los valores.

Esta versión tiene el problema de que ocupa casi todas la memoria SRAM de la placa, aunque se pueden reducir el número de puntos se reduce la calidad de la señal.

Ventajas y desventajas

Frente a cualquier generador de funciones barato tiene la desventaja de que la señal es más ruidosa y es posible que no alcance frecuencias tan elevadas.

Por otro lado en la parte de las ventajas esta el precio y en que la forma de la onda y su frecuencia es completamente programable, pudiendo hacerlo que nosotros queremos.

Puede ver un vídeo explicativo donde profundizo más en el tema en mi canal de Youtube:

Haz click para ver el vídeo en Youtube