Envio de notificaciones desde NodeMcu con Pushbullet

Este es un tema que ya había tratado previamente cuando estábamos trabajando con MicroPython, pero en esta ocasión quiero abordar el tema si estamos trabajando directamente con el IDE de Arduino.

Veremos como enviar mensajes simples desde NodeMCU a un telefono por medio de la API de PushBullet.

En primer lugar comenzaremos instalando la aplicacion Pushbullet en el teléfono, luego que se encuentre instalada y se cree una cuenta, debemos obtener el API Key para poder conectarnos a los servidores de Pushbullet:

Primero vamos a Setting, y luego hacemos clic en el botón «Create Access Token», nos va a mostrar una caja de texto con una cadena de caracteres que será nuestro token para poder conectarnos.

Cada vez que ingresemos en esa pantalla Pushbullet crea un nuevo Token y se descarta el anterior, por lo que es conveniente copiarlo en un bloc de notas. No compartan el token.

Construiremos un sencillo circuito para enviar mensajes cada vez que presionamos un botón:

El botón tiene conectado un pin a GND y el otro a D2.

Respecto al código del ejemplo, simplemente debemos realizar una petición a la API de Pushbullet de la siguiente manera:

const char* host = "api.pushbullet.com"; 
const char* apiKey = "TOKEN";
void enviarMensaje(String titulo,String mensaje) {
  
  WiFiClientSecure client;
  client.setInsecure();
  if(!client.connect(host,443)) {
    Serial.println("No se pudo conectar con el servidor");
    return;
  }

String url = "/v2/pushes";
  String message = "{\"type\": \"note\", \"title\": \""+titulo+"\", \"body\": \""+mensaje+"\"}\r\n";
  Serial.print("requesting URL: ");
  Serial.println(url);
  //send a simple note
  client.print(String("POST ") + url + " HTTP/1.1\r\n" +
               "Host: " + host + "\r\n" +
               "Authorization: Bearer " + apiKey + "\r\n" +
               "Content-Type: application/json\r\n" +
               "Content-Length: " +
               String(message.length()) + "\r\n\r\n");
  client.print(message);

  delay(2000);
  while (client.available() == 0);

  while (client.available()) {
    String line = client.readStringUntil('\n');
    Serial.println(line);
  }  
}

Luego desde loop() invocamos a la función definida de la siguiente manera:

void loop() {

  if(digitalRead(pinBoton) == LOW) {
    Serial.println("Presionado");

    enviarMensaje("NodeMCU","Mensaje enviado desde NodeMCU");
    delay(2000);
    
  }

  delay(100);
}

Cada vez que presionemos el botón, el dispositivo enviara un mensaje. Es recomendable implementar algún mecanismo/procedimiento para que no se envíen múltiples mensajes al mismo tiempo; para el ejemplo simplemente hice esperar al dispositivo por 2 segundos.

Aquí pueden encontrar el ejemplo del código.

Juego de plataformas en Java (14): Mapas

En este punto se habrán dado cuenta que resulta muy difícil armar un mapa; puesto que tenemos que instanciar uno a uno los elementos del mapa (un mapa por nivel); y quedarían de forma estática.

Buscaremos resolver este problema para hacer mas flexible al juego y que pueda cargar la distribución de elementos (Box, Fruit, Enemys) desde un archivo, de manera que al tener varios archivos podamos manejar diferentes niveles.

Nos va a ayudar también a tener que modificar menos código cada vez que deseemos probar un nuevo elemento.

Para ir por partes el archivo solo contendrá el tipo de elemento (BOX, ENEMY, FRUIT), el tipo de cada uno y los puntos cartersianos (x,y); por ejemplo:

BOX,2,250,428
TRAPS,0,280,420

Nos tiene que devolver lo siguiente:

Una de las condiciones de la clase que se ocupe de leer el archivo y construir el mapa, es solo instanciar los elementos que esten dentro de la parte visible del mapa.

Comenzaremos definiendo una clase llamada Maps, que contendra lo siguiente:

package gsampallo;

import java.io.BufferedReader;
import java.io.FileReader;
import java.util.ArrayList;
import java.awt.Point;

public class Maps {

    private int mapNumber;

    private int lastX = 0;
    private int lastY = 0;

    public Maps(int mapNumber) {
        this.mapNumber = mapNumber; 
    }
}

El constructor de la clase recibe un valor entero que nos indicara cual es el archivo que debemos cargar. lastX y lastY son los últimos valores del mapa que fueron cargados, todo lo menor a lastX y lastY no debe ser considerado al momento de volver a cargar el mapa.

Con esto establecido, definimos un nuevo método llamado loadMap(), que va a recibir como parámetros las listas de elementos de cada tipo, de manera de añadir a la lista lo que corresponda.

    public void loadMap(ArrayList<Box> listBox,ArrayList<Fruit> listFruit,ArrayList<Enemy> listEnemy,ArrayList<Traps> listTraps) {
        String path = "maps/map."+mapNumber;

        try {
            BufferedReader br = new BufferedReader(new FileReader(path));

            String line = br.readLine();
    
            while (line != null) {

                String[] data = line.split(",");
                int type = (int)Integer.valueOf(data[1]).intValue();
                int x = (int)Integer.valueOf(data[2]).intValue();
                int y = (int)Integer.valueOf(data[3]).intValue();

                if((x > lastX) || (x == lastX && y < lastY)) {

                    lastX = x;
                    lastY = y;

                    if(data[0].equals("BOX")) {
                        //System.out.println("BOX");

                        Box box = new Box(type,new Point(x,y));
                        listBox.add(box);

                    }

                }
                
                line = br.readLine();
            }
            
        } catch(Exception e) {

            System.err.println(e.getMessage());
        }
    }

En el método anterior solo especificamos para el tipo de elemento BOX, para hacerlo más sencillo de comprender. Leemos cada linea del archivo de texto plano; lo dividimos según donde estén ubicadas las comas; esto nos devuelve un array y lusto convertimos cada parte del array en su correspondiente elemento.

Finalmente solamente evaluamos si data[0] el primer elemento del array es igual a BOX, TRAPS, etc. y en consecuencia instanciamos el elemento correspondiente y lo integramos a la lista.

Creamos una carpeta llamada maps y dentro un archivo llamada map.1:

BOX,1,200,428
BOX,1,228,428
BOX,1,256,428
BOX,1,284,428
BOX,1,704,428
BOX,1,732,428
BOX,1,760,428

Por el momento solo trabajaremos con BOX, pero podemos extender al resto de los elementos sin problemas. Antes de seguir viendo como lo extendemos a los otros componentes (en particular Enemy) veremos como integrarlo con RunnerOne.

Incorporaremos algunas variables a RunnerOne para el manejo del mapa:

    private int increaseMap = 10;
    private Maps maps;
    private int mapNumber = 1;
    private int relativeGamePosition = 0;

Separaremos del constructor de RunnerOne, donde definimos las dimensiones del Frame y la posicion de los elementos del juego, creando un metodo llamada initGame():

    public void initGame() {
        /*
         * Background
         */
        background = new Background("image/bgd1.png");

        /*
         * Player
         */
        player = new Player(Player.MASK_DUDE,new Point(50,410));

	/*
	 * MAPS
	 */
	map = new Maps(mapNumber);
	increaseMap = player.width;
        
        /*
         * Credits
         */
        showCredits = new Credits();

        /*
         * FRUIT
         */
        listFruit = new ArrayList<Fruit>();

        /*
         * BOX
         */
        listBox = new ArrayList<Box>();

        /*
         * Weapons
         */
        listWeapon = new ArrayList<Weapon>();

        /*
         * Traps
         */
        listTraps = new ArrayList<Trap>();

        /*
         * Enemys
         */
        listEnemy = new ArrayList<Enemy>();

		//Timer
		timer = new Timer(DELAY, this);
		timer.start();
    }

Dentro de RunnerOne también incorporaremos un nuevo metodo llamado loadGameItems(), será el encargado de llamar a Maps y solicitarle que cargue los elementos del juego:

	private void loadGameItems() {

		map.loadMap(listBox,listFruit,listEnemy,listTraps);

	}

Uno de las especificaciones que hicimos fue que el mapa se cargue en memoria de manera gradual; para ello haremos uso de las variables que definimos previamente relativeGamePosition y increaseMap; basicamente cada vez que lleguemos a un punto determinado de avance establecido por increaseMap invocaremos al metodo loadGameItems().

Esta tarea se realiza desde actionPerformed, lo modificamos de la siguiente manera:

    public void actionPerformed(ActionEvent e) {

        if(relativeGamePosition < increaseMap) {
			relativeGamePosition++;
		} else {
			relativeGamePosition = 0;
			loadGameItems();
		}

        updateGame();

        repaint();
        
    }

Modificaremos el constructor para que llame a initGame() e inicialice los parámetros del juego:

public RunnerOne() {

  /** resto del codigo **/
  initGame();

  setVisible(true);

}

Si todo sale como espero, cuando compilemos y ejecutemos el juego veremos lo siguiente: