// ESP32-C3 SuperMini + Polar H10 (BLE) + anillo WS2812 de 16 LEDs // Librerias: NimBLE-Arduino 2.x, Adafruit NeoPixel #include #include #define LED_PIN 10 #define NUM_LEDS 16 #define MAX_BRI 60 // 0-255; bajo para cuidar la bateria Adafruit_NeoPixel ring(NUM_LEDS, LED_PIN, NEO_GRB + NEO_KHZ800); static const NimBLEUUID HR_SERVICE("180D"); static const NimBLEUUID HR_CHAR("2A37"); NimBLEClient* client = nullptr; volatile uint16_t bpm = 0; volatile uint32_t lastData = 0; uint32_t nextBeat = 0; float level = 0; // Notificacion Heart Rate Measurement (0x2A37) void onHR(NimBLERemoteCharacteristic*, uint8_t* d, size_t len, bool) { if (len < 2) return; if (d[0] & 0x01) { // formato 16 bits if (len < 3) return; bpm = d[1] | (d[2] << 8); } else { // formato 8 bits bpm = d[1]; } lastData = millis(); Serial.printf("BPM: %u\n", bpm); } void fillColor(uint8_t r, uint8_t g, uint8_t b) { for (int i = 0; i < NUM_LEDS; i++) ring.setPixelColor(i, ring.Color(r, g, b)); ring.show(); } // Modo Ironman: reactor arco cian/blanco respirando void ironman(uint32_t now) { float t = (now % 4000) / 4000.0f; float lvl = 0.15f + 0.85f * (0.5f - 0.5f * cosf(t * 2 * PI)); fillColor(lvl * MAX_BRI * 0.35f, lvl * MAX_BRI * 0.85f, lvl * MAX_BRI); } bool connectPolar(const NimBLEScanResults& res) { for (int i = 0; i < res.getCount(); i++) { const NimBLEAdvertisedDevice* dev = res.getDevice(i); if (dev->getName().find("Polar H10") == std::string::npos) continue; Serial.printf("Encontrado: %s\n", dev->getName().c_str()); if (!client) client = NimBLEDevice::createClient(); if (!client->connect(dev)) return false; NimBLERemoteService* svc = client->getService(HR_SERVICE); if (!svc) { client->disconnect(); return false; } NimBLERemoteCharacteristic* ch = svc->getCharacteristic(HR_CHAR); if (!ch || !ch->canNotify() || !ch->subscribe(true, onHR)) { client->disconnect(); return false; } Serial.println("Conectado al Polar H10"); return true; } return false; } void setup() { Serial.begin(115200); ring.begin(); ring.clear(); ring.show(); NimBLEDevice::init(""); NimBLEDevice::getScan()->setActiveScan(true); } void loop() { uint32_t now = millis(); if (!client || !client->isConnected()) { bpm = 0; NimBLEScan* scan = NimBLEDevice::getScan(); if (!scan->isScanning()) { // escaneo asincrono: la animacion no se bloquea if (!connectPolar(scan->getResults())) scan->start(5000, false, true); } ironman(now); delay(10); return; } if (bpm > 30 && bpm < 230 && now - lastData < 5000) { if ((int32_t)(now - nextBeat) >= 0) { level = 1.0f; // latido nextBeat = now + 60000UL / bpm; } } level *= 0.92f; // desvanecimiento uint8_t b = (uint8_t)(level * level * MAX_BRI); fillColor(b, 0, 0); delay(10); }