🎡 Multi-concurrent BLE connections with Seeed ROUND display

Hi there,

SO, it’s been asked many times if more than one BLE connection can be made at a time with Peripherals to a Central and is the Seeed Studio Round Display up to the effort. YES!

Albeit NOT a standard method but uses another BLE LIB available in the Arduino tool box(works in ESP-IDF) also This is the NimBLE LIB
it is somewhat better for multiple peripherals and with some effort and understanding , 3 concurrent BLE connections can be made with 6 Notifications possible. How , Watch the video and Examine the code.
It’s a two part Series , with this being the SensorNode (peripheral) portion first.

ask any question you might have here, I’ll do my best to answer them.

HTH

GL :slight_smile: PJ :v:

/* =====================================================================================
   Project    : SensorNode BLE Peripheral with OLED & RGB Status
   Platform   : Seeed Studio XIAO nRF52840 Sense with Xiao DEV/expansion Base.
   BSP        : Seeeduino mbed @ 2.9.3
   Core       : Arduino mbed (nRF52840)
   MCU        : Nordic nRF52840 (Cortex-M4F @ 64MHz, 1MB Flash, 256KB RAM)
   Board FQBN : Seeeduino:mbed:xiaonRF52840Sense
   IDE        : Arduino IDE 2.x
   
      Libraries  : 
      - ArduinoBLE v1.4.0 (BLE Peripheral GATT Profile)
      - Wire (I2C OLED via Dev Expansion Board)
      - U8x8lib or U8g2lib (for SSD1306 OLED on Expansion Base)
   Peripherals:
      - OLED Display (via I2C)
      - RGB LED (Builtin)
      - USER Button (D1)
      - Buzzer (A3)
   Features   :
      - BLE advertising as SensorNodeX (custom 181A service) "B" and "M" OLED indicators for Notify states,
      - OLED Display showing device name and BLE status
      - RGB LED feedback (Yellow=Advertising, Green=Connected, Red=Disconnected)
      - Animated "..." progress indicator
      - Placeholder for future sensor expansion (Temp, Humidity, Motion, Battery)

   Author     : Pj Glasso, & Embedded AI Assistant (ESP32 Expert Mode) , wiki.seeedstudio.com/
   Version    : REV 1.0 // 2025-05-26 02:00 EDT
===================================================================================== */
// ===================================================================================
// Features : BLE 0x181A service, "B" and "M" OLED indicators for Notify states,
//            RGB LED, buzzer feedback, dynamic re-advertising after disconnect
// Version  : REV 1.6d // 2025-05-29 08:50 PM
// Added Vertual Battery Level Notify (changes the Level cuases the notify to fire)
// accepts Subscribes to both Notify's Beep on Connect/Disconnect.
// ===================================================================================

#include <ArduinoBLE.h>
#include <U8x8lib.h>
#define REVISION "REV 1.6d"

#define SENSOR_NODE_NUMBER 4    // each node number should be unique, It is posssible to use the MAC address to derive it also.
#define BUZZER_PIN A3           // Buzzer on Expansion Base

U8X8_SSD1306_128X64_NONAME_HW_I2C oled(U8X8_PIN_NONE);    // OLED on DEV Base

BLEService envService("181A");
BLEUnsignedCharCharacteristic tempChar("2A6E", BLERead);
BLEUnsignedCharCharacteristic humidityChar("2A6F", BLERead);
BLEUnsignedCharCharacteristic batteryChar("2A19", BLERead | BLENotify);
BLEUnsignedCharCharacteristic motionChar("2A56", BLERead | BLENotify);

int batteryLevel = 99;
unsigned long lastBatteryUpdate = 0;
bool batteryNotifySubscribed = false;
bool motionNotifySubscribed = false;

int scrollOffset = 0;
unsigned long lastScrollUpdate = 0;
unsigned long lastFlashTime = 0;
bool flashState = false;

void initdisplay() {
  pinMode(LEDR, OUTPUT);
  pinMode(LEDG, OUTPUT);
  pinMode(LEDB, OUTPUT);
  pinMode(LED_BUILTIN, OUTPUT);
  pinMode(BUZZER_PIN, OUTPUT);
  digitalWrite(LEDR, HIGH);
  digitalWrite(LEDG, HIGH);
  digitalWrite(LEDB, HIGH);
  digitalWrite(BUZZER_PIN, LOW);
}

void setLedRGB(bool red, bool green, bool blue) {
  digitalWrite(LEDR, red ? LOW : HIGH);
  digitalWrite(LEDG, green ? LOW : HIGH);
  digitalWrite(LEDB, blue ? LOW : HIGH);
}

void drawCenteredNodeNumber() {
  oled.setFont(u8x8_font_inb33_3x6_r);
  String numStr = String(SENSOR_NODE_NUMBER);
  uint8_t col = (numStr.length() == 1) ? 5 : 4;
  oled.drawString(col, 2, numStr.c_str());

  oled.setFont(u8x8_font_7x14_1x2_r);
  oled.drawString(9, 2, batteryNotifySubscribed ? "B" : " ");
  oled.setFont(u8x8_font_7x14_1x2_r);
  oled.drawString(9, 5, motionNotifySubscribed ? "M" : " ");
  oled.setFont(u8x8_font_chroma48medium8_r);
  oled.drawString(2, 7, "Connected     ");

}

void setup() {
  Serial.begin(115200);
  delay(2000);
  printBuildInfo();

  Serial.println("\nPower ON\n");
  startsound();

  oled.begin();
  oled.setFlipMode(1);
  oled.clear();
  oled.setFont(u8x8_font_chroma48medium8_r);
  oled.drawString(0, 0, "Power ON");

  initdisplay();
  setLedRGB(false, true, false);  // GREEN
  delay(1500);

  oled.clear();
  oled.setFont(u8x8_font_7x14_1x2_r);
  oled.drawString(2, 0, "SensorNode");
  drawCenteredNodeNumber();
  oled.setFont(u8x8_font_chroma48medium8_r);
  oled.drawString(2, 7, "Advertising");
  setLedRGB(false, false, true);  // BLUE

  if (!BLE.begin()) {
    oled.drawString(0, 6, "BLE Failed");
    while (1);
  }

  String nodeName = "SensorNode" + String(SENSOR_NODE_NUMBER);
  BLE.setLocalName(nodeName.c_str());
  BLE.setDeviceName(nodeName.c_str());
  BLE.setAdvertisedService(envService);

  envService.addCharacteristic(tempChar);
  envService.addCharacteristic(humidityChar);
  envService.addCharacteristic(batteryChar);
  envService.addCharacteristic(motionChar);
  BLE.addService(envService);

  tempChar.writeValue(77);
  humidityChar.writeValue(55);
  batteryChar.writeValue(batteryLevel);
  motionChar.writeValue(0);

  BLE.advertise();
  Serial.println(nodeName + " advertising...");
}

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

  if (central) {
    oled.drawString(0, 7, "               ");
    oled.drawString(2, 7, "Connected     ");
    Serial.print("Connected to: ");
    Serial.println(central.address());

    beepConnected();

    while (central.connected()) {
      unsigned long now = millis();

      // Battery Notify + Simulate Drop every 25 sec
      if (batteryChar.subscribed() && now - lastBatteryUpdate > 25000) {
        lastBatteryUpdate = now;
        batteryLevel -= random(1, 3);
        if (batteryLevel < 70) batteryLevel = 99;
        batteryChar.writeValue(batteryLevel);
        Serial.print("Battery Level: ");
        Serial.println(batteryLevel);
      }

      // Detect notify subscription state changes
      bool currentBatterySubscribed = batteryChar.subscribed();
      bool currentMotionSubscribed = motionChar.subscribed();

      if (currentBatterySubscribed != batteryNotifySubscribed ||
          currentMotionSubscribed != motionNotifySubscribed) {
        batteryNotifySubscribed = currentBatterySubscribed;
        motionNotifySubscribed = currentMotionSubscribed;
        drawCenteredNodeNumber();
      }

      if (now - lastFlashTime > 15000) {
        lastFlashTime = now;
        setLedRGB(false, true, true);
        delay(300);
        setLedRGB(false, false, false);
      }

      delay(100);
    }

    // --- DISCONNECT HANDLING + BLE RE-INIT ---
    oled.setFont(u8x8_font_7x14_1x2_r);
    oled.drawString(9, 2, " ");  // Clear B
    oled.setFont(u8x8_font_chroma48medium8_r);
    oled.drawString(9, 5, " ");  // Clear M
    oled.drawString(0, 7, "Disconnected  ");

    setLedRGB(true, false, false);
    beepDisconnected();
    delay(10000);

    batteryNotifySubscribed = false;
    motionNotifySubscribed = false;
    batteryChar.unsubscribe();
    motionChar.unsubscribe();
    BLE.stopAdvertise();
    delay(200);

    String nodeName = "SensorNode" + String(SENSOR_NODE_NUMBER);
    BLE.setLocalName(nodeName.c_str());
    BLE.setDeviceName(nodeName.c_str());
    BLE.setAdvertisedService(envService);
    BLE.advertise();

    oled.drawString(0, 7, "               ");
    scrollOffset = 0;
    lastScrollUpdate = millis();

    Serial.println("Disconnected.");
    Serial.println("Re-advertising as: " + nodeName);
  }

  unsigned long now = millis();
  if (!BLE.connected() && (now - lastScrollUpdate > 200)) {
    lastScrollUpdate = now;
    oled.drawString(0, 7, "               ");
    String scrollText = "     Advertising     ";
    int windowSize = 11;

    if (scrollOffset >= scrollText.length()) scrollOffset = 0;

    String view;
    if (scrollOffset + windowSize <= scrollText.length()) {
      view = scrollText.substring(scrollOffset, scrollOffset + windowSize);
    } else {
      int firstPart = scrollText.length() - scrollOffset;
      view = scrollText.substring(scrollOffset);
      view += scrollText.substring(0, windowSize - firstPart);
    }

    oled.drawString(0, 7, view.c_str());
    scrollOffset++;
    flashState = !flashState;
    setLedRGB(false, false, flashState);
  }
}

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);
}

void beepConnected() {
  tone(BUZZER_PIN, 880); delay(250); noTone(BUZZER_PIN);
}

void beepDisconnected() {
  delay(200);
  tone(BUZZER_PIN, 480); delay(300); noTone(BUZZER_PIN);
}

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__) + " at " + String(__TIME__));
  Serial.println("==================\n");
}

Full Screen, you can see the serial port messages from the SensorNode_4 in the IDE.

Hi There,

Here’s PART 2 of my project, this time using the Seeed Studio Round Display as the Central.

  • It starts by scanning and building a list of SensorNodes it finds.
  • It then connects to up to 3 SensorNodes concurrently, reading their Temperature & Humidity characteristics and subscribing to Battery and/or Motion notifications.

For extra fun, I added a More I/O B2B hat and hooked up an 8-pixel NeoPixel strip. Each pixel represents a SensorNode (up to 8), and the colors show its state:

  • Blue – Advertising in scan
  • Green – Connected
  • Purple – Notification received (1 of 3)
  • Red – Disconnected
  • Off – Not present in scan

Each SensorNode gets connected, read, and subscribed. After receiving 3 Battery notifications, it disconnects and moves to the next Node until all scanned devices are serviced.

And just for style points, I added some watch faces—it’s the classic demo with a few tweaks. :smile: (Hopefully Rolex doesn’t come knocking… LOL :+1:)

Feel free to drop questions HERE —I’ll do my best to answer.

Enjoy, and remember: projects should always be fun!

GL :slight_smile: PJ :v:

BLE_ROUND_MASTER_FINAL.zip (112.3 KB)

Hi there,

As requested, the Break down video.

Enjoy

PJ :v: