Descripción: Juguete infantil con forma de caja mágica que se abre y reacciona a los golpes que se den en la parte superior. Los golpes deben ser una secuencia exacta previamente programada. Al acertar la secuencia sale el ratón que «habita» en su interior; y al fallar la secuencia, la tapadera se abre suavemente  y se cierra espasmódicamente reproduciendo con el sonido de los golpes la secuencia correcta hasta que es acertada y dicho esquema rítmico es reemplazado por otro distinto.


Componentes:

  • 2 servo motores SG90 (los pequeños azules).
  • 1 Arduino Nano
  • 1 Módulo de cargador de batería de litio tipo TP4056.
  • 1 DC-DC Step up boost
  • 1 batería 18650
  • 1 sensor piezoeléctrico de vibración.
  • Tornillos 2,5x10mm.
  • 2 leds WS2812b (opcional para iluminar los ojos).
  • Tira led 5V de 40cm aproximadamente. (Opcional para iluminar el interior de la caja).
  • 1 transistor 2N2222 opcional (control de la tira led).
  • 1 Resistencia 10k (protección para el transistor).
  • 1 Caja de madera (opcional).

La caja de madera da un toque muy artesanal, pero no debes preocuparte pues en el apartado de descargas tendrás el STL de la caja completa si lo deseas así que podrás imprimirla tú mismo.


Esquema de conexiones

La fuente será la batería 18650 por lo que para controlarla y cargarla la conectaremos a un TP4056 o similar. Debemos tener en cuenta que la pila ofrece unos 3.7V así que tendremos que añadir un Step up Boost para conseguir unos 5V.

Los dos servos se conectarán directamente a los pines digitales D3 y D5 y se alimentarán de los 5V que salen del Step up Boost y no de la salida de alimentación del Arduino que solo podrá administrarnos 400mA como máximo y es posible que en algún momento necesitemos más corriente para los motores y los leds.

La tira led de 5 voltios es un factor opcional para iluminar el interior de la caja y dar un efecto más misterioso que irá conectado mediante un transistor 2N2222A al pin D11 pues se trata de una salida PWM que nos permitirá controlar la intensidad de la luz. En mi caso la he colocado en la parte oculta del interior de la tapadera superior.

Los dos leds WS2812B irán en el interior de los ojos del «ratón» para dar un efecto más dramático. La razón de usar estos leds es que podemos controlar el color de los mismos aunque se pueden usar también dos leds RGB o prescindir de ellos (con la modificación adecuada del código).


Sensor piezoeléctrico.

Es el componente principal del juguete. Es el encargado de recibir la información de los golpes que se efectúen en la tapa de la caja.

Seguro que has visto alguno en juguetes infantiles. Se trata de un par de chapas metálicas que contienen un cristal en su interior que reacciona generando electricidad al recibir una vibración, y viceversa, posee la facultad de producir vibración al serle aplicada una corriente eléctrica. Por lo cual se trata de un elemento que puede funcionar como sensor de vibraciones o como elemento emisor de sonido (parlante). Así seguro que los has visto como elemento en el interior de las tarjetas navideñas que producen sonidos al abrirlas. Además, se trata de un componente muy barato.

Suelen venderlo con una PCB que contiene un diodo y una resistencia (hay información en internet para construir un sustituto), aunque se puede comprar solo. Si lo has adquirido sin la PCB, no te preocupes, no complica demasiado su incorporación en el proyecto.

En el archivo de descarga de los ficheros STL se incluye una pieza que podrás usar para sujetar el sensor piezoeléctrico, aunque como ves en la imagen superior, también se puede usar cinta adhesiva.

Conectaremos el sensor a la entrada analógica A0 del microcontrolador que nos permitirá evaluar cada uno de los golpes en valores de entre 0 y 1023. En el código asignaremos un valor de threshold para graduar el valor mínimo de la excitabilidad.


Los servomotores

En el prototipo usé un servo de mejores características y mayor tamaño para levantar la tapa pues la caja era de madera y el peso de la misma pensé que podría darme problemas. Así que empleé un HS-311 para tal efecto y garantizar una apertura adecuada. En el modelo definitivo se emplean 2 servos SG90.

El servo «tapa» se conectará en el pin D3 y el servo «ojos» en el D5. Recuerda alimentaros de la fuente para evitar sobrecargar la salida de Arduino.

Una vez colocados es importante calibrarlos modificando las variables del código que aparecen antes del setup (tapa_max, tapa_med …), que deben ajustarse a la altura máxima, mínima e intermedia en grados pues estos valores dependen de cada servo.

Los motores encajan justos y no es imprescindible atornillarlos.


Los ojos

Al ser una orientación estética, no se incluyen en los archivos STL. Puedes pegar algún muñeco, pegatina cartón con un dibujo. En mi caso usé unos ojos que me sobraron de una prueba para una cabeza robótica.

En el interior hay dos leds multicolor que son opcionales y conectados al pin D6.


La caja

La pila encajará en la cavidad asignada abrazada por un perno atornillado.

Se adjuntan las palancas accionadoras de los servos y la pieza para acoplar el sensor piezoeléctrico.

Unas bisagras pegadas en la parte posterior permitirán la apertura de la tapadera.


Esquemas rítmicos

La utilidad didáctica del juguete es complementada con la notación musical que se adjunta, pues representa los toques mágicos que hay programados en origen.

En el código de programación se omite el último toque que corresponde a la última figura blanca.

Tener en cuenta que el primer código es simplemente un golpe.

int secretCode[maximumKnocks] = {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0};

Como todos los toques son 0 (el último es omitido), significa que el código secreto es un solo golpe (una blanca).

El Código

Inicio, librerías y variables

Las librerías usadas son <Servo.h> para manejar los dos motores que llamaremos «tapa» y «ojos» y <Adafruit_NeoPixel.h> para los leds de los ojos que no son necesarios y podrías prescindir de este complemento. Aún así se incluyen en el archivo adjunto.

Antes del setup se calibran los servomotores para determinar la posición mínima, media y máxima. Estos valores varían dependiendo del servo y las peculiaridades de cada uno.

El pinluz=11 hace referencia a una tira led de 5v que he colocado alrededor de la tapadera pero si no quieres usarla puedes prescindir de ella. Añade un efecto mágico a la apertura de la caja. Esta luz será controlada más adelante con las rutinas sistole y diastole que aparecen al final del código.

Los toques estarán en la variable int secretCode[maximumKnocks] que tendrá como máximo 20. El último toque no debe aparecer en la variable y se supone que es una blanco. El primero de los códigos secretos los he dejado a 0, así que habrá que aplicar un sólo toque para abrir la caja.

Setup

Simplemente se inician los servos y se ponen en la posición oculta. A tener en cuenta que la librería Servo.h anula la propiedad de salida PWM de los pines D9 y D10.

Loop

Es muy simple, pues cuenta 5 minutos desde que no se realiza ninguna intervención y realiza la rutina espasmo, que hace que la caja se abra despacio y cierre rápido, para llamar la atención cada determinado tiempo. Esta función se puede suprimir también pues es un complemento para darle mayor atractivo. 

Luego, se limita a esperar hasta recibir una interactuación por medio del sensor piezoeléctrico.

#include <Servo.h>
Servo myservo;

#include <Servo.h>
#include <Adafruit_NeoPixel.h>
#ifdef __AVR__
#include <avr/power.h> // Required for 16 MHz Adafruit Trinket
#endif

#define PIN 6
#define NUMPIXELS 2
Adafruit_NeoPixel pixels(NUMPIXELS, PIN, NEO_GRB + NEO_KHZ800);

Servo tapa;
Servo ojos;
int tapa_max = 25;
int tapa_med = 50;
int tapa_min = 68;            //65

int ojos_max = 0;
int ojos_med = 132;
int ojos_min = 170;

int pinluz = 11;
const int knockSensor = 0;
const int threshold = 50;
const int rejectValue = 25;
const int averageRejectValue = 15;
const int knockFadeTime = 150;
const int maximumKnocks = 20;
const int knockComplete = 1200;
int codigo = 0;

//int secretCode[maximumKnocks] = {50, 25, 25, 50, 100, 50, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0};  // Initial setup   "Shave and a Hair Cut, two bits."
//int secretCode[maximumKnocks] = {50, 50, 100, 50, 50, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0};
int secretCode[maximumKnocks] = {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0};
int knockReadings[maximumKnocks];
int knockSensorValue = 0;

unsigned long previousMillis = 0;
unsigned long currentMillis = millis();
const long interval = 300000;

void setup() {
  Serial.begin (9600);
  pinMode (pinluz, OUTPUT);
  pixels.begin();
  tapa_abajo(0);
  delay (500);
  ojos_abajo(0);
  delay (500);
  Serial.println("Program start.");
}

void loop() {
  currentMillis = millis();
  if (currentMillis - previousMillis >= interval) {
    previousMillis = currentMillis;
    espasmo();
  }

  knockSensorValue = analogRead(knockSensor);
  if (knockSensorValue >= threshold) {
    listenToSecretKnock();
  }
}

void espasmo() {
  tapa_medio(100);
  delay (50);
  sistole (4);
  diastole (50);
  delay (100);
  tapa_abajo(0);
  delay (500);

}

void listenToSecretKnock() {
  Serial.println("knock starting");
  int i = 0;
  for (i = 0; i < maximumKnocks; i++) {
    knockReadings[i] = 0;
  }
  int currentKnockNumber = 0;
  int startTime = millis();
  int now;

  delay(knockFadeTime);
  do {
    knockSensorValue = analogRead(knockSensor);
    if (knockSensorValue >= threshold) {
      Serial.println("knock.");
      now = millis();
      knockReadings[currentKnockNumber] = now - startTime;
      currentKnockNumber ++;
      startTime = now;
      delay(knockFadeTime);
    }

    now = millis();
  } while ((now - startTime < knockComplete) && (currentKnockNumber < maximumKnocks));

  if (validateKnock() == true) {
    triggerDoorUnlock();
  } else {
    Serial.println("Secret knock failed.");
    cancion1();
  }

}

void triggerDoorUnlock() {
  Serial.println("Door unlocked!");
  codigo++;
  if (codigo >= 4) {
    codigo = 1;
  }
  Serial.print ("Código: ");
  Serial.println (codigo);

  tapa_arriba(0);
  delay (300);
  pixels.setPixelColor(1, pixels.Color(255, 0, 255));
  pixels.setPixelColor(0, pixels.Color(255, 0, 255));
  pixels.show();

  ojos_arriba(0);
  sistole (50);
  iris (13, 255, 0, 255, 60);   // nVeces, r, g, b, delay

  pixels.setPixelColor(1, pixels.Color(255, 0, 255));
  pixels.setPixelColor(0, pixels.Color(255, 0, 255));
  pixels.show();

  ojos_abajo(0);
  tapa_abajo(0);

  pixels.setPixelColor(1, pixels.Color(0, 0, 0));
  pixels.setPixelColor(0, pixels.Color(0, 0, 0));
  pixels.show();
  digitalWrite (pinluz, LOW);
  delay (300);


  if (codigo == 1) {
    int aa[maximumKnocks] = {50, 25, 25, 50, 100, 50, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0};
    for (int i = 0; i < maximumKnocks; i++) {
      secretCode[i] = aa[i];
    }

    validateKnock ();
  }
  if (codigo == 2) {
    int bb[maximumKnocks] = {50, 50, 25, 25, 25, 25, 100, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0};
    for (int x = 0; x < maximumKnocks; x++) {
      secretCode[x] = bb[x];
    }
    validateKnock ();
  }

  if (codigo == 3) {
    int cc[maximumKnocks] = {50, 100, 25, 25, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0};
    for (int x = 0; x < maximumKnocks; x++) {
      secretCode[x] = cc[x];
    }
    validateKnock ();
  }

  previousMillis = currentMillis;                                       //RELOJ
}


boolean validateKnock() {
  int i = 0;
  int currentKnockCount = 0;
  int secretKnockCount = 0;
  int maxKnockInterval = 0;

  for (i = 0; i < maximumKnocks; i++) {
    if (knockReadings[i] > 0) {
      currentKnockCount++;
    }
    if (secretCode[i] > 0) {
      secretKnockCount++;
    }

    if (knockReadings[i] > maxKnockInterval) {
      maxKnockInterval = knockReadings[i];
    }
  }



  int totaltimeDifferences = 0;
  int timeDiff = 0;
  for (i = 0; i < maximumKnocks; i++) {
    knockReadings[i] = map(knockReadings[i], 0, maxKnockInterval, 0, 100);
    timeDiff = abs(knockReadings[i] - secretCode[i]);
    if (timeDiff > rejectValue) {
      return false;
    }
    totaltimeDifferences += timeDiff;
  }

  if (totaltimeDifferences / secretKnockCount > averageRejectValue) {
    return false;
  }

  return true;

}



void cancion1() {
  int secretKnockCount = 0;
  for (int i = 0; i < maximumKnocks; i++) {
    if (secretCode[i] > 0) {
      secretKnockCount++;
    }
  }
  digitalWrite (pinluz, HIGH);
  tapa_medio(45);
  diastole (20);
  sistole (20);
  diastole (20);
  sistole (20);
  diastole (20);
 // sistole (20);


  for (int i = 0; i < secretKnockCount ; i++) {
    tapa_abajo (0);
    tapa_medio(0);
    if (secretCode[i] == 25) {
      delay (10);
    }
    if (secretCode[i] == 50) {
      delay (250);
    }
    if (secretCode[i] == 100) {
      delay (700);
    }
  }


  tapa_abajo (0);                     //el último toque.
  digitalWrite (pinluz, LOW);
  previousMillis = currentMillis;                                       //RELOJ
  delay (400);
}

void iris(int a, int b, int c, int d, int e) {                     // nVeces, r, g, b, delay
  for (int i = 0; i < a; i += 1) {
    pixels.setPixelColor(0, pixels.Color(b, c, d));
    pixels.setPixelColor(1, pixels.Color(b, c, d));
    pixels.show();
    delay (e);
    pixels.setPixelColor(0, pixels.Color(0, 0, 0));
    pixels.setPixelColor(1, pixels.Color(0, 0, 0));
    pixels.show();
    delay (e);
  }
}

void ojos_medio (int pausa) {
  ojos.attach(5);
  int pos_ojos = ojos.read();
  if (pausa == 0) {
    ojos.write (ojos_med);
    delay (250);
    ojos.detach();
  }
  if (pausa != 0) {
    if ( (pos_ojos - ojos_med) > 0) {
      for (int i = pos_ojos; i >= ojos_med; i -= 1) {
        ojos.write (i);
        delay (pausa);
      }
    }

    if ( (pos_ojos - ojos_med) <= 0) {
      for (int i = pos_ojos; i <= ojos_med; i += 1) {
        ojos.write (i);
        delay (pausa);
      }
    }
  }
  ojos.detach();                                                              //parece duplicado en el if del inicio
}

void ojos_abajo (int pausa) {
  ojos.attach(5);
  int pos_ojos = ojos.read();
  if (pausa == 0) {
    ojos.write (ojos_min);
    delay (250);
    ojos.detach();
  }
  if (pausa != 0) {
    if ( (pos_ojos - ojos_min) > 0) {
      for (int i = pos_ojos; i >= ojos_min; i -= 1) {
        ojos.write (i);
        delay (pausa);
      }
    }

    if ( (pos_ojos - ojos_min) <= 0) {
      for (int i = pos_ojos; i <= ojos_min; i += 1) {
        ojos.write (i);
        delay (pausa);
      }
    }
  }
  ojos.detach();
}

void ojos_arriba (int pausa) {
  ojos.attach(5);
  int pos_ojos = ojos.read();
  if (pausa == 0) {
    ojos.write (ojos_max);
    delay (250);
    ojos.detach();
  }
  if (pausa != 0) {
    if ( (pos_ojos - ojos_max) > 0) {
      for (int i = pos_ojos; i >= ojos_max; i -= 1) {
        ojos.write (i);
        delay (pausa);
      }
    }

    if ( (pos_ojos - ojos_max) <= 0) {
      for (int i = pos_ojos; i <= ojos_max; i += 1) {
        ojos.write (i);
        delay (pausa);
      }
    }
  }
  ojos.detach();
}


void tapa_abajo(int pausa) {
  tapa.attach(3);
  int pos_tapa = tapa.read();
  if (pausa == 0) {
    tapa.write (tapa_min);
    delay (130);
    tapa.detach();
  }
  if (pausa != 0) {
    if ( (pos_tapa - tapa_min) > 0) {
      for (int i = pos_tapa; i >= tapa_min; i -= 1) {
        tapa.write (i);
        delay (pausa);
      }
    }

    if ( (pos_tapa - tapa_min) <= 0) {
      for (int i = pos_tapa; i <= tapa_min; i += 1) {
        tapa.write (i);
        delay (pausa);
      }
    }
  }
  tapa.detach();
}

void tapa_medio(int pausa) {
  tapa.attach(3);
  int pos_tapa = tapa.read();
  if (pausa == 0) {
    tapa.write (tapa_med);
    delay (110);
    tapa.detach();
  }

  if (pausa != 0) {
    if ( (pos_tapa - tapa_med) > 0) {
      for (int i = pos_tapa; i >= tapa_med; i -= 1) {
        tapa.write (i);
        delay (pausa);
      }
    }

    if ( (pos_tapa - tapa_med) <= 0) {
      for (int i = pos_tapa; i <= tapa_med; i += 1) {
        tapa.write (i);
        delay (pausa);
      }
    }
  }
  tapa.detach();
}

void tapa_arriba(int pausa) {
  tapa.attach(3);
  int pos_tapa = tapa.read();
  if (pausa == 0) {
    tapa.write (tapa_max);
    delay (250);
    tapa.detach();
  }
  if (pausa != 0) {
    if ( (pos_tapa - tapa_max) > 0) {
      for (int i = pos_tapa; i >= tapa_max; i -= 1) {
        tapa.write (i);
        delay (pausa);
      }
    }

    if ( (pos_tapa - tapa_max) <= 0) {
      for (int i = pos_tapa; i <= tapa_max; i += 1) {
        tapa.write (i);
        delay (pausa);
      }
    }
  }
  tapa.detach();
}


void sistole(int intervalo) {
  for (int i = 0; i <= 255; i += intervalo) {
    analogWrite (pinluz, i);
    delay (20);
  }
  digitalWrite (pinluz, HIGH);
}

void diastole(int intervalo) {
  for (int i = 255; i >= 0; i -= intervalo) {
    analogWrite (pinluz, i);
    delay (20);
  }
  digitalWrite (pinluz, LOW);
}