Con este post inauguro mi blog.

[Parte I] [Parte II]

Estuve trabajando en un pequeño proyecto en la NodeMCU (ESP8266 12-E) para conectarme con la API Twitter (más en un próximo post), aprovechando su capacidad para conectarse a una red WiFi.

NodeMCU

NodeMCU

Me encontré con un problema: cómo pasarle el nombre de la red y la clave para conectarse.

En principio la red puede elegirse de las disponibles usando botones, pero ingresar la contraseña es más complicado sin un teclado, porque son múltiples caracteres.

Algunas de las alternativas que se me ocurrieron:

  • Pasarla por bluetooth: requiere una app de smartphone (algún día voy a desarrollar una que lo haga).
  • Pasarla por QR: bastante más sencilla que el bluetooth pero requiere una cámara.
  • Pasarla por morse (audio): podría generar los tonos con un smartphone (y otra app…) pero sería mejor algo más discreto.
  • Pasarla por morse (luz): ídem anterior, pero usando el flash de la cámara del teléfono. Esta es más interesante porque no hace ruido y no es necesario un smartphone necesariamente. ¿Será necesaria una cámara?

Morse con luz

La idea del código morse es transmitir letras y números, en principio, usando combinaciones de “puntos y rayas” (pulsos cortos y largos, respectivamente). Esto se usa en forma de sonido en los telégrafos para mandar telegramas. La misma idea se usa, con luz, para comunicarse entre barcos o aviones, usando lámparas o linternas.

Por ejemplo, para mandar el clásico SOS (ayuda), los códigos son:

Letra Morse Luz
S . . . (100ms 100ms 100ms)
O - - - (300ms 300ms 300ms)
S . . . (100ms 100ms 100ms)

Pensando la idea

Existen varias formas de medir luz con una placa de desarrollo como la NodeMCU.

La primera que se nos ocurre es usar una cámara: se pueden recibir los pulsos y traducirlos a strings. El problema es que procesar una imagen completa es complicado, hay que procesar la imagen, compensar la iluminación, etc.

Una segunda alternativa es usar un fotorresistor.

Fotorresistor

Fotorresistor

Un fotorresistor produce un cambio en la electricidad que pasa por él cuando recibe luz, y esto se puede medir con un conversor analógico digital (ADC) como el que tiene la NodeMCU en el pin A0.

Para calcular los pulsos basta con medir la intensidad de la luz por encima de cierto umbral (cuando la luz está “prendida”), y calcular su duración para decidir si es un punto o una raya. La idea por ahora es solo detectar los pulsos y mostrarlos en el puerto serie.

Para empezar usé un Arduino UNO en lugar de la NodeMCU porque también tiene pines analógicos y no quería complicarme con el tema del WiFi. Creo que cualquier placa con pin analógico sirve.

Para enviar los pulsos usé la aplicación Morse Flashlight que está en el Play Store de Android. Permite configurar la frecuencia de los pulsos.

Circuito

Para conectar un fotorresistor a la placa hace falta también una resistencia de 1KΩ (Ohms). Hay que conectar una de las patas del fotorresistor a 3.3v, y la otra conectarla por un lado con una resistencia a tierra, y por otro lado al pin analógico (A0).

Materiales:

  • 3 cables de protoboard macho-macho
  • 1 protoboard, puede ser chiquita
  • 1 fotorresistor
  • 1 resistencia de 1KΩ
  • 1 Arduino UNO

Esquema:

Esquema

Circuito para conectar el Arduino UNO al fotorresistor

Código

Para leer desde el pin analógico, primero hay que configurar el pin como de lectura (INPUT):

const int pin_sensor = A0;

pinMode(pin_sensor, INPUT);

y después efectivamente leer el valor analógico que viene del fotorresistor:

int luz_sensor = analogRead(pin_sensor);

Como no estamos en completa oscuridad, hay que fijar primero la luz ambiente para diferenciarla del flash. Llevo esta variable como global para mantenerla en todas las iteraciones de loop().

int luz_ambiente = 0;

void fijar_luz_ambiente() {
  int max_luz = 0;
  for (int i = 0; i<100; i++) {
    int luz_sensor = analogRead(pin_sensor);
    if(luz_sensor > max_luz) {
      max_luz = luz_sensor;
    }
  }
  luz_ambiente = max_luz;
}

Para trabajar con el puerto serie hay que inicializar la comunicación, en este caso en 9600 baudios (el Monitor Serie que usemos tiene que estar a esa tasa también).

Serial.begin(9600);

Además hay dos funciones, void esperar_luz() y bool esperar_oscuridad(int, bool) que sirven para esperar un cambio de estado. La primera espera a que caiga la potencia del flash hasta la luz ambiente fijada anteriormente, mientras que la segunda espera a que aparezca un flash (salvo que se le indique que use el tiempo límite). El tiempo límite es configurable.

void esperar_oscuridad() {
  int luz_sensor = 0;
  do {
    luz_sensor = analogRead(pin_sensor);
  } while (luz_sensor > luz_ambiente);
}

void esperar_luz(bool con_tiempo) {
  int luz_sensor;
  int inicio = millis();
  do {
    luz_sensor = analogRead(pin_sensor);
    if(con_tiempo && (millis() > inicio + tiempo_maximo)) {
      return;
    }
  } while (luz_sensor < luz_ambiente + 10);
}

En la función loop() se calcula la duración de un pulso de luz y se guarda en un string global un punto o la raya. Luego se calcula la duración de la oscuridad para saber si la palabra continúa o no.

void loop() {
esperar_luz(false);  
  const int inicio_luz = millis();
  esperar_oscuridad();
  int duracion_luz = millis() - inicio_luz;
  if(duracion_luz > largo_punto) {
    palabra = palabra + raya;
  } else {
    palabra = palabra + punto;
  }
  Serial.print(".");
  const int inicio_oscuridad = millis();
  esperar_luz(true);
  int duracion_oscuridad = millis() - inicio_oscuridad;
  if(duracion_oscuridad >= largo_silencio) {
    Serial.println();
    Serial.print("La palabra es:");
    Serial.print("[");
    Serial.print(palabra);
    Serial.println("]");
    palabra = "";    
  }
}

Código completo

const int pin_sensor = A0;
const int largo_punto = 300;
const int tiempo_maximo =  1000;
const int largo_silencio = 750;
const String punto = ".";
const String raya = "-";
String palabra = "";
int luz_ambiente;

void fijar_luz_ambiente(){
  luz_ambiente = analogRead(pin_sensor);
}

void esperar_luz(bool con_tiempo) {
  int luz_sensor;
  int inicio = millis();
  do {
    luz_sensor = analogRead(pin_sensor);
    if(con_tiempo && (millis() > inicio + tiempo_maximo)) {
      return;
    }
  } while (luz_sensor < luz_ambiente + 10);
}

void esperar_oscuridad() {
  int luz_sensor;
  do {
    luz_sensor = analogRead(pin_sensor);
  } while (luz_sensor > luz_ambiente);
}

void setup() {
  Serial.begin(9600);
  pinMode(pin_sensor, INPUT);
  fijar_luz_ambiente();
  Serial.print("Luz ambiente: ");
  Serial.println(luz_ambiente);
}

void loop() {
  esperar_luz(false);  
  const int inicio_luz = millis();
  esperar_oscuridad();
  int duracion_luz = millis() - inicio_luz;
  if(duracion_luz > largo_punto) {
    palabra = palabra + raya;
  } else {
    palabra = palabra + punto;
  }
  Serial.print(".");
  const int inicio_oscuridad = millis();
  esperar_luz(true);
  int duracion_oscuridad = millis() - inicio_oscuridad;
  if(duracion_oscuridad >= largo_silencio) {
    Serial.println();
    Serial.print("La palabra es:");
    Serial.print("[");
    Serial.print(palabra);
    Serial.println("]");
    palabra = "";    
  }
}

Por hacer, limitaciones y divagaciones varias

¿En qué base está morse?

Hasta el momento este código solo se ocupa de la recepción e interpretación de los puntos y las rayas, pero no se encarga del parsing, es decir de la interpretación del código morse como letras.

Podría decirse que el código morse es ternario (base 3), porque tiene “puntos”, “rayas” y “silencios”. Pero ¿es viable para todas las combinaciones? ¿Cómo se distinguen dos puntos de una raya?

En este punto pensé que tal vez no es ternario sino cuaternario (base 4) porque tiene “pulso largo”, “pulso corto”, “silencio largo”, “silencio corto” (mismo problema de combinaciones). Tal vez “pulso largo+silencio corto”, “pulso corto+silencio largo”… (mismo problema).

Otro punto a considerar es la intensidad de la luz. Si defino “escalones” de intensidad tendría más combinaciones posibles. Habría que ver si Android me deja configurarlo sin root. Y tendría que hacer una app para eso.

A todo esto, ¿por qué me interesa qué base es?

Resulta que cuanto más grande es la base en la que se representa un número, menos dígitos tiene. Por ejemplo, el 255 necesita 3 digitos en decimal, 2 dígitos en base 16 (hexadecimal) y 8 en base 2 (binario). Como estoy pensando en transmisión en serie, menos dígitos implican transmitir más rápido (suponiendo que no pongo ceros a la izquierda para rellenar). Pero tal vez lo más fácil sea considerar simplemente binario.

Protocolos serie de caracteres

Una forma de transmitir caracteres en serie es el protocolo que usan los teclados PS/2. Tienen una línea de clock (reloj) y otra de datos, por la que mandan packs de 7 bits con un bit de inicio antes y uno de parada después del dato. Una ventaja que le veo al morse es que no necesita una línea de clock, porque se maneja por cambios de estado (prendido / apagado) y duración de los intervalos. El antecesor a PS/2 es el conector serie unidireccional, que también tiene bits de inicio y parada pero no usa clock.

Una alternativa interesante es sincronizar el teléfono y la placa y usar un protocolo de longitud fija (bits de la misma duración). Dicen por ahí (no encuentro el comentario en stackoverflow, pero por ahí está) que en los dispositivos Android sin rootear es difícil sincronizar el flash. Pero vale la pena intentarlo.

Tal vez estoy reinventando la rueda porque existen LiFi y VLC

¿Y cómo son las claves de WiFi?

El código morse, así como está, es case insensitive, es decir que no distingue minúsculas de mayúsculas. Las contraseñas que se usan para redes WiFi con seguridad WPA2 (recomendada) pueden tener entre 8 y 63 caracteres ASCII, es decir, letras minúsculas, mayúsculas, números y algunos símbolos (espero que sólo los caracteres imprimibles, y considerando ASCII 8bit) según mi router. Pero comprobé con horror que mi smartphone acepta caracteres con tilde y hasta ideogramas japoneses para la contraseña de su punto de acceso WiFi.

Considerar solo ASCII de 7 bits sigue siendo un ejercicio interesante, así que voy a seguir con esa suposición.

Conclusión

Tenemos un código relativamente sencillo para capturar morse-luz con una placa compatible con Arduino usando un fotorresistor.

Falta traducir los puntos y rayas a palabras. Y considerar números y símbolos.

Divagué un montón con lo de la luz pero también se puede jugar con sonido.

Próxima parada: ¿bluetooth?


Por comentarios o sugerencias mi correo está al final de la página. También pueden levantar un issue en el repo de GitHub del sitio.