XIAO nRF52840 driving Haptic motor with Adafruit DRV2605L

Hi there,

So, Thanks but I only added what was missing , may still need adjustments it was late so I pushed it out and wasn’t able to test it but i will today. :+1:

HTH
GL :slight_smile: PJ :v:

seems like a super fit for Xiao device for sure :cowboy_hat_face:

EDIT

Like this


and everything appears to be in order but you will need to test . :v:

this code (bypasses the halt if no DRV is connected)

// ===============================================================
// CalmBand — DRV2605 BLE Haptics with "SensorNode-style" UI
// Board   : Seeed XIAO nRF52840 Sense + Dev Expansion (SSD1306 I2C)
// Features: DRV2605 RTP metronome/heartbeat, BLE control,
//           OLED status (B=BLE, M=Motor active), RGB status LED (active-LOW),
//           buzzer beeps, auto re-advertise after disconnect.
// Rev     : 2025-10-09
// ===============================================================

#include <Arduino.h>
#include <Wire.h>
#include <ArduinoBLE.h>
#include <U8x8lib.h>
#include "Adafruit_DRV2605.h"

// ---------------- Build info banner ----------------
#define REVISION "CalmBand UI REV A"
static void printBuildInfo() {
  String filename = String(__FILE__);
  int lastSlash = filename.lastIndexOf('/');
  if (lastSlash == -1) lastSlash = filename.lastIndexOf('\\');
  String sketchName = filename.substring(lastSlash + 1);

  Serial.println("\n=== Build Info ===");
  Serial.println("Sketch   : " + sketchName);
  Serial.println("Version  : " + String(REVISION));
  Serial.println("Compiled : " + String(__DATE__) + " " + String(__TIME__));
  Serial.println("==================\n");
}

// ---------------- Pins & board setup ----------------
// Button on XIAO D1 (active-low, internal pullup)
#ifndef D1
  #define D1 1
#endif
#define BUTTON_PIN D1

// Buzzer on expansion base (adjust if different)
#define BUZZER_PIN A3

// Prefer core-defined RGB if available (active-LOW on XIAO nRF52840 Sense)
#ifndef LEDR
  // Fallback pins if your core doesn’t define LEDR/G/B:
  #define LEDR D10
  #define LEDG D9
  #define LEDB D8
#endif

// Active-LOW LEDs on XIAO
#define LED_ACTIVE_LOW 1

// Device name shown in BLE
#define BLE_NAME "CalmBand"

// ---------------- OLED ----------------
U8X8_SSD1306_128X64_NONAME_HW_I2C oled(U8X8_PIN_NONE);

// ---------------- DRV2605 ----------------
Adafruit_DRV2605 drv;

// ---------------- BLE service & characteristics ----------------
BLEService hapticsService("12345678-1234-1234-1234-1234567890ab");

BLEByteCharacteristic            modeChar ("12345678-1234-1234-1234-1234567890ac", BLERead | BLEWrite); // 0=metronome,1=heartbeat
BLEByteCharacteristic            powerChar("12345678-1234-1234-1234-1234567890ad", BLERead | BLEWrite); // 0..100
BLEUnsignedShortCharacteristic   onMsChar ("12345678-1234-1234-1234-1234567890ae", BLERead | BLEWrite); // ms
BLEUnsignedShortCharacteristic   offMsChar("12345678-1234-1234-1234-1234567890af", BLERead | BLEWrite); // ms
BLEByteCharacteristic            activeChar("12345678-1234-1234-1234-1234567890b0", BLERead | BLEWrite); // 0/1

// ---------------- App state ----------------
uint8_t  gMode   = 0;     // 0=metronome, 1=heartbeat
uint8_t  gPower  = 60;    // 0..100 (maps to RTP 0..127)
uint16_t gOnMs   = 150;   // metronome ON
uint16_t gOffMs  = 850;   // metronome OFF
bool     gActive = false; // running?

// Button debounce
bool lastBtn = HIGH;
bool toggled = false;
unsigned long lastDebounce = 0;
const unsigned long debounceMs = 30;

// Pattern scheduler
unsigned long tNow  = 0;
unsigned long tNext = 0;
enum Phase { IDLE, MET_ON, MET_OFF, HB_BEAT1_ON, HB_GAP, HB_BEAT2_ON, HB_REST };
Phase phase = IDLE;

enum BleState { BLE_ADV, BLE_CONN, BLE_DISC };
BleState bleState = BLE_ADV;
unsigned long discShownUntil = 0;
const unsigned long DISC_FLASH_MS = 1000;

// ---------------- Small helpers ----------------
static inline void ledWrite(uint8_t pin, bool on) {
  // `on` means "light should be ON"
  if (LED_ACTIVE_LOW) digitalWrite(pin, on ? LOW : HIGH);
  else                digitalWrite(pin, on ? HIGH : LOW);
}

static inline void setLED(bool r, bool g, bool b) {
  ledWrite(LEDR, r);
  ledWrite(LEDG, g);
  ledWrite(LEDB, b);
}

static inline void ledBlue()  { setLED(false, false, true);  }
static inline void ledGreen() { setLED(false, true,  false); }
static inline void ledRed()   { setLED(true,  false, false); }
static inline void ledOff()   { setLED(false, false, false); }

// Buzzer tones
static void startSound() {
  tone(BUZZER_PIN, 890); delay(220); noTone(BUZZER_PIN); delay(20);
  tone(BUZZER_PIN, 800); delay(220); noTone(BUZZER_PIN); delay(20);
  tone(BUZZER_PIN, 800); delay(220); noTone(BUZZER_PIN); delay(20);
  tone(BUZZER_PIN, 990); delay(420); noTone(BUZZER_PIN); delay(20);
}
static void beepConnected()    { tone(BUZZER_PIN, 880); delay(250); noTone(BUZZER_PIN); }
static void beepDisconnected() { delay(200); tone(BUZZER_PIN, 480); delay(300); noTone(BUZZER_PIN); }

// DRV helpers
static void drvBeginRTP() {
  drv.setMode(DRV2605_MODE_REALTIME);
  drv.selectLibrary(6);
  drv.useERM();
}
static inline uint8_t mapPower(uint8_t p) {
  if (p > 100) p = 100;
  return (uint8_t)((p * 127) / 100);
}
static inline void vibOn()  { drv.setRealtimeValue(mapPower(gPower)); }
static inline void vibOff() { drv.setRealtimeValue(0); }

static void startMetronome() { phase = MET_ON;      tNext = millis(); }
static void startHeartbeat() { phase = HB_BEAT1_ON; tNext = millis(); }
static void stopAll()        { vibOff(); phase = IDLE; }

// BLE writes
static void applyBLEWritesIfAny() {
  if (modeChar.written())   gMode   = modeChar.value();
  if (powerChar.written())  gPower  = powerChar.value();
  if (onMsChar.written())   gOnMs   = onMsChar.value();
  if (offMsChar.written())  gOffMs  = offMsChar.value();
  if (activeChar.written()) gActive = activeChar.value() != 0;
}
static void pushBLEStateToChars() {
  modeChar.writeValue(gMode);
  powerChar.writeValue(gPower);
  onMsChar.writeValue(gOnMs);
  offMsChar.writeValue(gOffMs);
  activeChar.writeValue(gActive ? 1 : 0);
}

// Button (active-low)
static void handleButtonToggle() {
  bool reading = digitalRead(BUTTON_PIN);
  if (reading != lastBtn) {
    lastDebounce = millis();
    lastBtn = reading;
  }
  if ((millis() - lastDebounce) > debounceMs) {
    if (reading == LOW && !toggled) {
      gActive = !gActive;
      activeChar.writeValue(gActive ? 1 : 0);
      toggled = true;
      if (!gActive) stopAll();
    } else if (reading == HIGH) {
      toggled = false;
    }
  }
}

// Pattern loops
const uint16_t HB_INTERBEAT_GAP_MS = 150;

static void loopMetronome() {
  tNow = millis();
  switch (phase) {
    case MET_ON:
      vibOn();
      tNext = tNow + gOnMs;
      phase = MET_OFF;
      break;
    case MET_OFF:
      if ((long)(tNow - tNext) >= 0) {
        vibOff();
        tNext = tNow + gOffMs;
        phase = MET_ON;
      }
      break;
    default: break;
  }
}

static void loopHeartbeat() {
  tNow = millis();
  switch (phase) {
    case HB_BEAT1_ON:
      vibOn();
      tNext = tNow + gOnMs;
      phase = HB_GAP;
      break;
    case HB_GAP:
      if ((long)(tNow - tNext) >= 0) {
        vibOff();
        tNext = tNow + HB_INTERBEAT_GAP_MS;
        phase = HB_BEAT2_ON;
      }
      break;
    case HB_BEAT2_ON:
      if ((long)(tNow - tNext) >= 0) {
        vibOn();
        tNext = tNow + gOnMs;
        phase = HB_REST;
      }
      break;
    case HB_REST:
      if ((long)(tNow - tNext) >= 0) {
        vibOff();
        tNext = tNow + gOffMs;
        phase = HB_BEAT1_ON;
      }
      break;
    default: break;
  }
}

// ---------------- OLED UI ----------------
static bool oledInited = false;
static bool flagBLE   = false; // shows "B"
static bool flagMOTOR = false; // shows "M"

static void oledBanner(bool connected, bool advertising) {
  if (!oledInited) return;

  oled.clear();

  // Title
  oled.setFont(u8x8_font_7x14_1x2_r);
  oled.drawString(2, 0, "CalmBand");

  // Big “label” in middle (like your node number)
  oled.setFont(u8x8_font_inb33_3x6_r);
  oled.drawString(5, 2, "H");   // just a big monogram “H” for Haptics

  // Flags column
  oled.setFont(u8x8_font_7x14_1x2_r);
  oled.drawString(9, 2, flagBLE   ? "B" : " ");
  oled.drawString(9, 5, flagMOTOR ? "M" : " ");

  // Status bottom line
  oled.setFont(u8x8_font_chroma48medium8_r);
  if (connected)      oled.drawString(2, 7, "Connected     ");
  else if (advertising) oled.drawString(2, 7, "Advertising   ");
  else                 oled.drawString(2, 7, "Idle          ");
}

static void oledRefreshFlags() {
  if (!oledInited) return;
  oled.setFont(u8x8_font_7x14_1x2_r);
  oled.drawString(9, 2, flagBLE   ? "B" : " ");
  oled.drawString(9, 5, flagMOTOR ? "M" : " ");
}

// ---------------- Setup / Loop ----------------
void setup() {
  Serial.begin(115200);
  delay(50);
  Serial.println();
  Serial.println(F("CalmBand power-up..."));
  printBuildInfo();

  pinMode(BUTTON_PIN, INPUT_PULLUP);
  pinMode(BUZZER_PIN, OUTPUT);

  pinMode(LEDR, OUTPUT);
  pinMode(LEDG, OUTPUT);
  pinMode(LEDB, OUTPUT);
  ledBlue(); // start as advertising look

  // OLED init
  oled.begin();
  oled.setFlipMode(1);
  oledInited = true;
  oled.clear();
  oled.setFont(u8x8_font_chroma48medium8_r);
  oled.drawString(0, 0, "Power ON");

  // Power on sound
  startSound();

  // DRV2605
  Wire.begin();
  if (!drv.begin()) {
    Serial.println(F("ERROR: DRV2605 not found! Halt."));
    oled.drawString(0, 6, "DRV2605 ERROR");
    //while (1) { delay(10); }
  }
  drvBeginRTP();
  vibOff();

  // BLE
  if (!BLE.begin()) {
    Serial.println(F("ERROR: BLE init failed"));
    oled.drawString(0, 6, "BLE ERROR");
    while (1) { delay(10); }
  }

  BLE.setLocalName(BLE_NAME);
  BLE.setDeviceName(BLE_NAME);
  BLE.setAdvertisedService(hapticsService);

  hapticsService.addCharacteristic(modeChar);
  hapticsService.addCharacteristic(powerChar);
  hapticsService.addCharacteristic(onMsChar);
  hapticsService.addCharacteristic(offMsChar);
  hapticsService.addCharacteristic(activeChar);
  BLE.addService(hapticsService);

  pushBLEStateToChars();
  BLE.advertise();

  bleState = BLE_ADV;
  flagBLE   = false;
  flagMOTOR = false;
  oledBanner(false, true);
  Serial.println(String("BLE advertising as \"") + BLE_NAME + "\"");
}

static void updateBleLedAndOled(bool connectedJustNow, bool disconnectedJustNow) {
  if (connectedJustNow) {
    bleState = BLE_CONN;
    flagBLE = true;
    ledGreen();
    beepConnected();
    oledBanner(true, false);
    return;
  }
  if (disconnectedJustNow) {
    bleState = BLE_DISC;
    flagBLE = false;
    ledRed();
    beepDisconnected();
    discShownUntil = millis() + DISC_FLASH_MS;
    oledBanner(false, false);
    return;
  }
  if (bleState == BLE_DISC) {
    if ((long)(millis() - discShownUntil) >= 0) {
      bleState = BLE_ADV;
      ledBlue();
      oledBanner(false, true);
    }
  }
}

void loop() {
  BLEDevice central = BLE.central();

  static bool wasConnected = false;
  bool isConnected = central && central.connected();

  bool connectedJustNow    = ( isConnected && !wasConnected);
  bool disconnectedJustNow = (!isConnected &&  wasConnected);

  applyBLEWritesIfAny();
  updateBleLedAndOled(connectedJustNow, disconnectedJustNow);
  wasConnected = isConnected;

  // Button toggles motor active
  handleButtonToggle();
  flagMOTOR = gActive;
  oledRefreshFlags();

  // Start/stop state machine
  if (!gActive) {
    stopAll();
  } else if (phase == IDLE) {
    if (gMode == 0) startMetronome();
    else            startHeartbeat();
  }

  // Run active pattern
  if (gActive) {
    if (gMode == 0) loopMetronome();
    else            loopHeartbeat();
  }

  // Re-advertise “heartbeat” already handled by BLE library; if you want
  // explicit stop/start after disconnect, you can do:
  // if (bleState == BLE_ADV && !BLE.connected()) { /* still advertising */ }
}

HTH
GL :slight_smile: PJ :v: