// bbpod_arduino.ino
// -------------------------------------------------
// BBpod Simulator on ESP32 (Arduino + FreeRTOS)
// -------------------------------------------------
// - Conexión MQTT con Odradek https://wokwi.com/projects/430786177055207425
// - Envía keep-alive y maneja estrés
// - Minijuego de ritmo con 3 LEDs en niveles y botón
// - Comando "CALM" por serial para resetear estrés y enviar "receptor"
// - Quiero añadir la posibilidad de que solo mande receptor bajo un nivel de humedad alto,
// a referencia de que los BTs solo salen cuando hay declive (aumentan los niveles de quiralio)
// tal vez sea interesante tratar el boton con interrupciones y no sondeo!!
// -------------------------------------------------
#include <WiFi.h>
#include <PubSubClient.h>
#include <Adafruit_GFX.h>
#include <Adafruit_SSD1306.h>
#include "bitmaps.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "freertos/semphr.h"
// ------------------- CONFIGURACIÓN -------------------
// WiFi
const char* WIFI_SSID = "Wokwi-GUEST";
const char* WIFI_PASS = "";
// MQTT
const char* MQTT_BROKER = "broker.hivemq.com";
const int MQTT_PORT = 1883;
const char* TOPIC_STATE = "odradek/state";
const char* TOPIC_CMD = "odradek/cmd";
const char* RECEPTOR_MSG = "receptor";
const char* STRESS_MSG = "stress";
// Hardware
#define SCREEN_WIDTH 128
#define SCREEN_HEIGHT 64
const int LED_PINS[3] = {12, 14, 26};
const int BUZZER_PIN = 27;
const int BUTTON_PIN = 0;
// Juego
#define STRESS_THRESH 10 // ciclos para estresarse
#define MIN_DELAY_MS 800 // espera mínima antes de encender siguiente LED
#define MAX_DELAY_MS 2000 // espera máxima
#define HIT_WINDOW_MS 500 // ventana para pulsar
// Estados MQTT recibidos
enum {
STATE_DISCONNECTED = 0,
STATE_CONNECTED_IDLE,
STATE_EV_FAR,
STATE_EV_CLOSE,
STATE_EV_NEAR_CAUTIOUS,
STATE_EV_NEAR_ALERT,
STATE_BB_STRESSED
};
// ------------------- VARIABLES GLOBALES -------------------
WiFiClient wifiClient;
PubSubClient mqttClient(wifiClient);
Adafruit_SSD1306 display(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, -1);
volatile int odradekState = STATE_DISCONNECTED;
volatile bool gameActive = false;
SemaphoreHandle_t stateMutex;
// Estrés interno
static int stressCount = 0;
static bool selfStressed = false;
// ------------------- PROTOTIPOS -------------------
void initWiFi();
void initMQTT();
void mqttCallback(char* topic, uint8_t* payload, unsigned int length);
void taskMQTT(void* pv);
void taskGame(void* pv);
void drawFace();
void playMiniGame();
void beepTick();
void buzzFail();
void buzzSuccess();
// ------------------- SETUP -------------------
void setup() {
Serial.begin(115200);
randomSeed(esp_random());
stateMutex = xSemaphoreCreateMutex();
// OLED
Wire.begin(21, 22);
display.begin(SSD1306_SWITCHCAPVCC, 0x3C);
display.clearDisplay();
display.display();
// Pines
pinMode(BUZZER_PIN, OUTPUT);
pinMode(BUTTON_PIN, INPUT_PULLUP);
for (int i = 0; i < 3; i++) {
pinMode(LED_PINS[i], OUTPUT);
digitalWrite(LED_PINS[i], LOW);
}
initWiFi();
initMQTT();
xTaskCreatePinnedToCore(taskMQTT, "MQTT", 4096, NULL, 1, NULL, 1);
xTaskCreatePinnedToCore(taskGame, "Game", 8192, NULL, 1, NULL, 1);
}
void loop() {
// Debug “CALM”
if (Serial.available()) {
String cmd = Serial.readStringUntil('\n');
cmd.trim();
if (cmd.equalsIgnoreCase("CALM")) {
xSemaphoreTake(stateMutex, portMAX_DELAY);
stressCount = 0;
selfStressed = false;
xSemaphoreGive(stateMutex);
mqttClient.publish(TOPIC_CMD, RECEPTOR_MSG);
Serial.println(">> Estrés reseteado (DEBUG CALM)");
}
}
vTaskDelay(pdMS_TO_TICKS(100));
}
// ------------------- WIFI & MQTT -------------------
void initWiFi() {
WiFi.begin(WIFI_SSID, WIFI_PASS);
while (WiFi.status() != WL_CONNECTED) {
delay(100);
Serial.print(".");
}
Serial.println("\nConectado a WiFi");
}
void initMQTT() {
mqttClient.setServer(MQTT_BROKER, MQTT_PORT);
mqttClient.setCallback(mqttCallback);
}
void mqttCallback(char* topic, uint8_t* payload, unsigned int length) {
String msg;
for (unsigned int i = 0; i < length; i++) msg += (char)payload[i];
int idx = msg.indexOf("\"state\":");
if (idx >= 0) {
int val = msg.substring(idx + 8).toInt();
xSemaphoreTake(stateMutex, portMAX_DELAY);
odradekState = val;
xSemaphoreGive(stateMutex);
}
}
// ------------------- TAREA MQTT -------------------
void taskMQTT(void* pv) {
const TickType_t pollInterval = pdMS_TO_TICKS(100);
const TickType_t keepaliveInterval = pdMS_TO_TICKS(1000);
TickType_t lastPoll = xTaskGetTickCount();
TickType_t lastKeepalive = lastPoll;
for (;;) {
// 1) Reconexión si hace falta
if (!mqttClient.connected()) {
while (!mqttClient.connect("bbpod_arduino")) {
vTaskDelay(pdMS_TO_TICKS(2000));
}
mqttClient.subscribe(TOPIC_STATE);
mqttClient.publish(TOPIC_CMD, RECEPTOR_MSG);
xSemaphoreTake(stateMutex, portMAX_DELAY);
stressCount = 0;
selfStressed = false;
xSemaphoreGive(stateMutex);
}
// 2) Procesar callbacks
mqttClient.loop();
// 3) Keep-alive + cálculo de estrés cada 1 s
if ((xTaskGetTickCount() - lastKeepalive) >= keepaliveInterval) {
xSemaphoreTake(stateMutex, portMAX_DELAY);
int st = odradekState;
xSemaphoreGive(stateMutex);
if (st == STATE_EV_NEAR_CAUTIOUS) stressCount++;
else if (st == STATE_EV_NEAR_ALERT) stressCount += 2;
else if (stressCount > 0) stressCount--;
if (stressCount < 0) stressCount = 0;
if (stressCount >= STRESS_THRESH && !selfStressed) {
selfStressed = true;
Serial.println(">> Modo ESTRES activado");
}
mqttClient.publish(TOPIC_CMD, selfStressed ? STRESS_MSG : RECEPTOR_MSG);
// Avanzo el marcador para el siguiente keep-alive
lastKeepalive += keepaliveInterval;
}
// 4) Polling rápido para mqtt.loop() y refresco de estado
vTaskDelayUntil(&lastPoll, pollInterval);
}
}
// ------------------- TAREA JUEGO -------------------
void taskGame(void* pv) {
const TickType_t pollInterval = pdMS_TO_TICKS(100);
TickType_t lastWake = xTaskGetTickCount();
int lastState = -1;
bool lastStressed = false;
for (;;) {
xSemaphoreTake(stateMutex, portMAX_DELAY);
int st = odradekState;
bool stressed= selfStressed;
bool active = gameActive;
xSemaphoreGive(stateMutex);
if (!active && (st != lastState || stressed != lastStressed)) {
drawFace();
lastState = st;
lastStressed = stressed;
}
if (selfStressed && !active) {
xSemaphoreTake(stateMutex, portMAX_DELAY);
gameActive = true;
xSemaphoreGive(stateMutex);
Serial.println(">> Iniciando MiniGame");
playMiniGame();
buzzSuccess();
mqttClient.publish(TOPIC_CMD, RECEPTOR_MSG);
xSemaphoreTake(stateMutex, portMAX_DELAY);
selfStressed = false;
stressCount = 0;
gameActive = false;
xSemaphoreGive(stateMutex);
lastState = -1;
}
vTaskDelayUntil(&lastWake, pdMS_TO_TICKS(100));
}
}
// ------------------- CARA OLED -------------------
void drawFace() {
display.clearDisplay();
if (selfStressed) display.drawBitmap(0, 0, sad_bitmap, 128, 64, WHITE);
else if (odradekState < STATE_EV_NEAR_CAUTIOUS) // Quiero que dependa de su cantidad de estres, no del estado, además mutex.
display.drawBitmap(0, 0, happy_bitmap, 128, 64, WHITE);
else display.drawBitmap(0, 0, serious_bitmap, 128, 64, WHITE);
display.display();
}
// ------------------- MINI-JUEGO POR NIVELES -------------------
void playMiniGame() {
int nivel = 0;
Serial.println(">>> MiniGame arrancado");
while (nivel < 3) {
// 1) espera aleatoria
int espera = random(MIN_DELAY_MS, MAX_DELAY_MS);
Serial.printf("Nivel %d: espero %d ms antes de encender LED %d\n",
nivel, espera, nivel);
delay(espera);
// 2) enciende LED del nivel
int pin = LED_PINS[nivel];
Serial.printf("Nivel %d: LED pin %d ON\n", nivel, pin);
digitalWrite(pin, HIGH);
beepTick();
// 3) ventana de pulsación
bool acierto = false;
unsigned long inicio = millis();
while (millis() - inicio < HIT_WINDOW_MS) {
if (digitalRead(BUTTON_PIN) == LOW) { // si se mantiene pulsado el boton haces trampa asi que lo tengo que arreglar
acierto = true;
Serial.println("PULSACIÓN DETECTADA");
break;
}
vTaskDelay(pdMS_TO_TICKS(150));
}
// 4) apaga LED y decide
digitalWrite(pin, LOW);
Serial.printf("Nivel %d: LED pin %d OFF acierto=%s\n",
nivel, pin, acierto ? "sí" : "no");
if (acierto) {
nivel++;
} else {
buzzFail();
nivel = 0;
Serial.println("¡Fallaste! Reinicio al nivel 0");
}
}
Serial.println("¡Has completado los 3 niveles!");
}
// ------------------- AUXILIARES -------------------
void beepTick() {
tone(BUZZER_PIN, 800, 100);
noTone(BUZZER_PIN);
}
void buzzFail() {
for (int i = 0; i < 3; i++) {
tone(BUZZER_PIN, 500 + i*200, 100);
vTaskDelay(pdMS_TO_TICKS(150));
}
noTone(BUZZER_PIN);
}
void buzzSuccess() {
tone(BUZZER_PIN, 523, 133); delay(133);
tone(BUZZER_PIN, 523, 133); delay(133);
tone(BUZZER_PIN, 523, 133); delay(133);
tone(BUZZER_PIN, 523, 400); delay(400);
tone(BUZZER_PIN, 415, 400); delay(400);
tone(BUZZER_PIN, 466, 400); delay(400);
tone(BUZZER_PIN, 523, 133); delay(133);
delay(133);
tone(BUZZER_PIN, 466, 133); delay(133);
tone(BUZZER_PIN, 523, 1200); delay(1200);
}