Los botones IoT no son ninguna novedad, usándolos podemos controlar cualquier elemento de un sistema domótico IoT o hacer una petición a cualquier sistema conectado a internet.

Seguro que recuerdas el famoso a la vez que fallido intento de Amazon de integrar en nuestros hogares el Dash Button.

Este es sin duda el primer intento masivo de botones conectados. Un botón con forma alargada que permite pedir a traves de Amazon aquel producto al cual está asociado.

¿Te has quedado sin café?

No pasa nada, solo pulsa el Dash Button que se encuentra junto a la cafetera y recibieras el pedido de café lo antes posible.

Dash Button de café
Dash Button de café

¿Qué te parece si creamos nuestra propia versión de Dash Button? Pero lo programamos para algo más útil: guardar en la lista de favoritos de Spotify la canción que en está sonando en el momento de pulsarlo.

Directos al grano

Puedes descargar el código y los modelos 3D del botón desde el repositorio del proyecto o descargando el proyecto directamente.

El botón IoT de me gusta con ESP8266

La electrónica del botón IoT

Este botón IoT se puede implementar tan solo con un pulsador y con un ESP8266. De manera más concreta, los modelos 3D están pensados para:

Pulsador de agujero

Pulsador de 12×12 mm y 5 mm de vástago.

Wemos D1 mini

Wemos D1 mini.

Por supuesto no podemos olvidar el cable micro USB.

Conexionado del pulsador

Para conectar el pulsador a la placa Wemos D1 mini soldamos un par de cables a los terminales del pulsador y los conectamos de manera indistinta a GND y al pin digital D3. Estos pines se encuentran marcados en la placa como G y 3 respectivamente.

Pinout de Wemos D1 mini

Sin resistencia pullup

Hemos conectado el pulsador directamente a la placa sin usar una resistencia pullup o pulldown. Pero esto no quiere decir que no exista, de echo, está.

Muchos microcontroladores disponen de resistencias pullup o pulldown internas, dentro del propio circuito integrado. Esto facilita la tarea ya que en algunos casos podemos omitir colocarla de forma externa.

Para usar la resistencia pullup disponible en el pin D3 del ESP8266 debemos configurar la entrada digital usando una instrucción como la siguiente.

  pinMode(button,INPUT_PULLUP);

El firmware del botón de Spotify

El firmware de este proyecto está desarrollado con Arduino y como tal se compone de las cuatro secciones clásicas.

Comenzamos por la inclusión de las librerías necesarias, las relacionadas con la conectividad WiFi del ESP8266 y ArduinoJson, una librería magnífica que nos va a permitir manipular JSON de manera sencilla.

#include <Arduino.h>
#include <ESP8266WiFi.h>
#include <ESP8266WiFiMulti.h>
#include <ESP8266HTTPClient.h>
#include <WiFiClientSecureBearSSL.h>
#include <ArduinoJson.h>

A continuación encontramos la definición de variables, constantes y la instanciación del objeto WiFiMulti.

Cabe destacar que debemos configurar algunos parámetros de esta sección:

  1. const char* ssid y const char* pass corresponden con le nombre y la contraseña de la red WiFi a la que el botón ha de conectarse.
  2. String refresh y String auth corresponden a dos parámetros que nos autentican como usuario y como desarrollador en Spotify. Estos parámetros los veremos más adelante.
ESP8266WiFiMulti WiFiMulti;

// Set this section
const char* ssid = "";
const char* pass = "";

// Spotify authentication
String refresh = "";
String auth = "";

//Constants
const int button = D3;

// Variables
String token = "";

No es de extrañar que a continuación nos encontramos con la función setup clásica de cualquier programa Arduino.

Esta parte del código se encarga de configurar el pin del pulsador como entrada, activar la resistencia pullup interna, configurar el puerto serie y conectarse a la red WiFi.

void setup() {
  pinMode(button,INPUT_PULLUP);
  Serial.begin(115200);

  WiFi.mode(WIFI_STA);
  WiFiMulti.addAP(ssid, pass);
  while (WiFiMulti.run() != WL_CONNECTED){
    Serial.print(".");
    delay(500);
  }
}

Hasta aquí todo normal, es ahora cuando comienza la magia.

void loop() {
  if (!digitalRead(button)) {
      token = getNewToken();
      String id = getTrackId();
      saveTrack(id);
  }
}

En el loop disponemos de un condicional que realiza la lectura del pulsador y lo hace buscando cuando la entrada digital está en estado bajo. En este caso concreto evaluamos en estado bajo ya que al usar una resistencia pullup en estado de reposo del pulsador leemos 3.3v en la entrada.

Usar ! delante de la función digitalRead(button) es el equivalente a usar digitalRead(button) == false solo que de forma más corta y menos amigable.

Una vez detectado el accionamiento del pulsador paramos a realizar la tarea de comunicarnos con Spotify.

Comunicación con Spotify

La comunicación con Spotify la realizamos interactuando con su API en tres pasos distintos:

  1. Obtener token.
  2. Obtener el identificador de la canción que se está reproduciendo con la cuenta de Spotify vinculada.
  3. Dar la orden para que se guarde la canción en la lista de favoritos.

El token de acceso en Spotify

Para que la API sepa sobre que cuenta tiene que realizar las operaciones del segundo y tercer paso tenemos que contar un token de acceso. Este es una cadena de caracteres que nos identifica y que a la vez actúa como una especie de contraseña.

¡Ojo!

Los tokens son secretos, no los debemos compartir. De esta forma nos aseguramos que solo nuestro botón IoT puede comunicarse con nuestra cuenta de Spotify.

El token de acceso solo es útil durante los 60 minutos posteriores a su generación. Es por eso que debe ser el botón IoT el que se encarga de pedírselo a la API de Spotify cuando lo necesita para identificarse y realizar otra tarea.

La línea token = getNewToken(); se encarga de conseguir ese preciado token de acceso y para ello, por detrás, el firmware usa las constante String refresh y String auth que vimos antes. Estos dos valores son claves similares al token de acceso pero permanentes, no varían en el tiempo y solo sirven para obtener nuevos tokens de acceso. Por supuesto, también debemos mantenerlos en secreto.

Guardando la canción en favoritos

Ahora que disponemos de un token de acceso que nos identifica podemos comenzar a pedirle cosas a la API.

Primero averiguamos que canción se está reproduciendo y para ello usamos la línea:

String id = getTrackId();

La función getTrackId() va a preguntar a la API y a devolver el identificador de la canción.

Después le pedimos a Spotify que guarde esa canción en la lista de favoritos usando:

saveTrack(id);

Y ya está. Si todo a salido bien, la canción debe aparecer en la lista de favoritos.

Detrás de las funciones mágicas

Las funciones getNewToken, getTrackId y saveTrack están definidas al final del archivo del firmware. Se trata de tres peticiones POST, GET y POST respectivamente que interactúan con la API para enviar y recibir la información necesaria.

Si tienes curiosidad puedes leerlas y visitar la documentación de la API de Spotify para comprender su funcionamiento.

String refresh y String auth

Hasta el momento hemos visto que son necesarios tanto un token de refresco (refresh) como un código de autenticación (auth).

El token de refresco nos identifica como usuarios de Spotify y le sirve al botón IoT para obtener los tokens de acceso.

El código de autenticación nos identifica como desarrolladores de Spotify y es que para poder implementar este botón tenemos que ser desarrolladores.

Crear una cuenta de desarrollador en Spotify

Estamos de enhorabona, si tienes una cuenta de Spotify te puede resgistrar como desarrollador usando la misma y siguiendo los siguiente pasos.

Acceder a la página de desarrolladores, pulsar en el botón LOG IN e iniciar sesión usando la cuenta de Spotify.

Crear una cuenta de desarrollador en Spotify

Pulsar en CREATE AN APP para crear una nueva aplicación.

Dashboard de aplicaciones de desarrolladores de Spotify

Rellenar el nombre de la aplicación, descripción y marcar las casillas de verificación.

Asegúrate de leer y comprender los términos de uso, las líneas de marca y de no usar la app de forma comercial. Cualquier daño o incumplimiento de las políticas o normas es tu responsabilidad.

Crear una aplicación en Spotify

La web de desarrolladores nos va a proporcionar algunos datos de autenticación pero no los que necesitamos para el código. Para obtener refresh y auth he desarrollado una herramienta web a la que podemos acceder desde https://experiments.rinconingenieril.es/boton-spotify/.

Esta herramienta realiza todo el proceso en el navegador web, por tanto los datos van desde Spotify a tu navegador y no pasan por ninguno de mis servidores. Puedes revisar el código de la app en el respositorio del proyecto.

Para poder usar esta herramienta tenemos que autorizarla dentro del proyecto.

Acceder a la configuración del proyecto.

Panel de aplicación en la plataforma de desarrolladores de Spotify

Añadir la URL de redirección de la herramienta y pulsar en el botón ADD.

URL de redirección de la aplicación de Spotify

Bajar hasta el final de la página y pulsar el botón SAVE.

Botón de guardar de configuraciones de aplicación en Spotify

Desde la página del proyecto podemos obtener el Client ID y el Client Secret.

Par ver el Client Secret pulsa sobre SHOW CLIENT SECRET.

Client Id en el panel de la aplicación de desarrollador de Spotify

Estos dos últimos datos los necesitaremos más adelante.

Obtener refresh y auth

Para obtener los valores de refresh y auth necesarios para del botón IoT, acceder a https://experiments.rinconingenieril.es/boton-spotify/ e introducir el Client ID y el Client Secret obtenidos con la cuenta de desarrollador.

Herramienta para obtener los tokens de Spotify

Después de presionar el botón Obtener token la página nos redirigirá a la web de Spotify para que autoricemos el acceso a nuestra cuenta.

Por último, de manera automática el navegador web volverá a la aplicación inicial y nos mostrará refresh y auth en la sección Datos de autenticación de tu cuenta de Spotify.

Solo queda introducir estos datos en el código del firmware y cargarlo en el ESP8266.

#include <Arduino.h>
#include <ESP8266WiFi.h>
#include <ESP8266WiFiMulti.h>
#include <ESP8266HTTPClient.h>
#include <WiFiClientSecureBearSSL.h>
#include <ArduinoJson.h>

ESP8266WiFiMulti WiFiMulti;

// Set this section
const char* ssid = "";
const char* pass = "";

// Spotify authentication
String refresh = "";
String auth = "";

//Constants
const int button = D3;

// Variables
String token = "";

void setup() {
  pinMode(button,INPUT_PULLUP);
  Serial.begin(115200);

  WiFi.mode(WIFI_STA);
  WiFiMulti.addAP(ssid, pass);

  while (WiFiMulti.run() != WL_CONNECTED){
    Serial.print(".");
    delay(500);
  }
}

void loop() {
  if (!digitalRead(button)) {
      token = getNewToken();
      String id = getTrackId();
      saveTrack(id);
  }
}

String getTrackId(){
  std::unique_ptr<BearSSL::WiFiClientSecure>client(new BearSSL::WiFiClientSecure);
  client->setInsecure();
  HTTPClient https;

  if (https.begin(*client, "https://api.spotify.com/v1/me/player/currently-playing")) {
    https.addHeader("Authorization","Bearer " + token);
    int httpCode = https.GET();
    if (httpCode > 0) {
      Serial.printf("Track request code: %d\n", httpCode);
      // Successful
      if (httpCode == HTTP_CODE_OK || httpCode == HTTP_CODE_MOVED_PERMANENTLY) {
        String payload = https.getString();
        int index = payload.lastIndexOf("\"id\" : \"") + 8;
        String id;
        for (int i = 0; i < 22; i++)
          id += payload[index+i];
        Serial.println(id);
        https.end();
        return id;
      } else if (httpCode == HTTP_CODE_NO_CONTENT) {
        Serial.print("There aren't any track playing\n");
      }
    } else {
      Serial.printf("Track request failed, error: %s\n", https.errorToString(httpCode).c_str());
    }
    https.end();
  } else {
    Serial.print("Unable to connect with track endpoint\n");
  }
}

bool saveTrack(String id){
  std::unique_ptr<BearSSL::WiFiClientSecure>client(new https://pixabay.com/es/users/mohamed_hassan-5229782/BearSSL::WiFiClientSecure);
  client->setInsecure();
  HTTPClient https;

  if (https.begin(*client, "https://api.spotify.com/v1/me/tracks/?ids=" + id)) {
    https.addHeader("Authorization","Bearer " + token);
    int httpCode = https.PUT("");
    if (httpCode > 0) {
      Serial.printf("Save request code: %d\n", httpCode);
      String payload = https.getString();
      Serial.print(payload);
      // Successful
      if (httpCode == HTTP_CODE_OK || httpCode == HTTP_CODE_MOVED_PERMANENTLY) {
        Serial.print("Hurray");
        https.end();
        return false;
      }
    } else {
      Serial.printf("Save request failed, error: %s\n", https.errorToString(httpCode).c_str());
      return true;
    }
    https.end();
  } else {
    Serial.print("Unable to connect with save endpoint\n");
    return true;
  }
}


String getNewToken(){
  std::unique_ptr<BearSSL::WiFiClientSecure>client(new BearSSL::WiFiClientSecure);
  client->setInsecure();
  HTTPClient https;

  if (https.begin(*client, "https://accounts.spotify.com/api/token")) {
    https.addHeader("Authorization","Basic " + auth); // +
    https.addHeader("Content-Type","application/x-www-form-urlencoded");
    int httpCode = https.POST("grant_type=refresh_token&refresh_token=" + refresh);
    if (httpCode > 0) {
      Serial.printf("Refresh token request code: %d\n", httpCode);
      String payload = https.getString();
      // Successful
      if (httpCode == HTTP_CODE_OK || httpCode == HTTP_CODE_MOVED_PERMANENTLY) {
        Serial.print("Hurray");
        https.end();
        StaticJsonDocument<200> doc;
        DeserializationError error = deserializeJson(doc, payload);
        return doc["access_token"];
      }
    } else {
      Serial.printf("Refresh token request failed, error: %s\n", https.errorToString(httpCode).c_str());
    }
    https.end();
  } else {
    Serial.print("Unable to connect with refresh token endpoint\n");
  }
}

Atribuciones

Silueta silencio: mohamed_hassan

Silueta de una persona

Enrique Gómez

Ingeniero Electrónico Industrial y Automático, amante de la comunicación audiovisual y de la divulgación ingenieril y científica. Friki de Star Wars, cuenta más de lo que parece. Puedes saber más sobre mi y sobre mis trabajos en www.enriquegomez.me