🧐 Round Display w/TouchRing BLE Xiao C6 & S3-Plus

Hi there,

Something for the C & P cowboys out there…:v:

This is the S3 Plus (server) peripheral end…

// =======================================================================================
// Project:     TouchRing_BLE_S3 – BLE Peripheral Touch Ring Meter
// Platform:    Seeed Studio XIAO ESP32S3-Plus + 1.28" GC9A01 Round Display + CST816S Touch
// BLE Role:    Peripheral / Server
// BLE Service: 128-bit UUID: 00002B01-0000-1000-8000-00805F9B34FB
// BLE TX Char: Notify   – UUID: 00002B01-0000-1000-8000-00805F9B34FC
// BLE RX Char: Writable – UUID: 00002B01-0000-1000-8000-00805F9B34FD
// Features:
//   - Smooth, colorful ring meter UI with touch up/down gesture control
//   - Center-tap toggles BLE advertising ON/OFF
//   - Sends value changes via Notify
//   - Accepts remote value writes via BLE Write
//   - BLE status shown as BLUE (connected) or RED (off)
//   - Start-up info screen with MCU and version
// Libraries:   TFT_eSPI, lv_xiao_round_screen, BLEDevice (ESP32 BLE)
// Notes:       Compatible with TouchRing_RE_C6 (ESP32C6 Central)
// Revision:    v1.4 // 2025-06-12 11:47 PM
// Authors  : Pj Glasso, & Embedded AI Assistant (ESP32 Expert Mode) , https://wiki.seeedstudio.com/
// Code generated by AI_Code_Gen (LLM-based embedded assistant)
// Finalized with real-time hardware validation
// =======================================================================================
#include <TFT_eSPI.h>
#include <SPI.h>
#define USE_TFT_ESPI_LIBRARY
#define CST816S_NO_TOUCH_PIN_CHECK
#include "lv_xiao_round_screen.h"
#include <BLEDevice.h>
#include <BLEUtils.h>
#include <BLEServer.h>

// UUIDs for BLE Percentage8 Control
#define SERVICE_UUID            "00002B01-0000-1000-8000-00805F9B34FB"    // this is a more open Service than Volume Control " 2A07"
#define CHARACTERISTIC_UUID_TX  "00002B01-0000-1000-8000-00805F9B34FC"    // I wanted one that allowed Full duplex , P8 does that automatically 
#define CHARACTERISTIC_UUID_RX  "00002B01-0000-1000-8000-00805F9B34FD"    // with TX and RX in the same Char. 

// for just a simple BLE volume control you can use the Service for that too. This Tech is more for control and display(indicator) Hense
// under BLE details it reports as " Luminous Intensity " LOL go figure.

#define RADIUS 120
#define SEEED_GREEN 0x07E0        // this allows a color pallete of your choosing
#define SEEED_BLUE  0x001F
#define YELLOW      0xFFE0
#define ORANGE      0xFD20
#define RED         0xF800
#define GREY        0x2108
#define WHITE       0xFFFF
#define BLACK       0x0000
#define SKY_BLUE    0x867D  // Nice blue shade for BLE indicator

int currentValue = 50;
lv_coord_t touchX, touchY;
BLECharacteristic *pTxCharacteristic;
bool deviceConnected = false;
int ringValue = 50;  // or whatever your default value is

// Forward declarations , are exactly as required when the callbacks are before the function call, what a thing LOL
int ringMeter(int value, int vmin, int vmax, int x, int y, int r);
void drawText(String text, int x, int y, int value);
void drawBLEIndicator(bool connected);

// -----------------------------------------------------------------------------------
class MyServerCallbacks : public BLEServerCallbacks {
  void onConnect(BLEServer* pServer) {
    deviceConnected = true;
    Serial.println("BLE device connected");
    drawBLEIndicator(true);
  }

  void onDisconnect(BLEServer* pServer) {
    deviceConnected = false;
    Serial.println("BLE device disconnected");
    drawBLEIndicator(false);
    BLEDevice::startAdvertising();
  }
};

class MyCallbacks : public BLECharacteristicCallbacks {
  void onWrite(BLECharacteristic* pChar) {
    String rxVal = pChar->getValue().c_str();
    if (rxVal.length() > 0) {
      int newValue = atoi(rxVal.c_str());
      if (newValue >= 0 && newValue <= 99) {
        currentValue = newValue;
        ringMeter(currentValue, 0, 99, 0, 0, RADIUS);
        drawText(String(currentValue), 120, 125, currentValue);
        Serial.printf("BLE> %d\n", currentValue);
      }
    }
  }
};

// -----------------------------------------------------------------------------------
void setup() {
  Serial.begin(115200);
  delay(500);
  Serial.println();
Serial.println("=== Touch_Ring_BLE Demo ===");
Serial.println("Rev: v1.4");
Serial.println("MCU: ESP32-S3-Plus");
Serial.println("===========================");


  pinMode(TOUCH_INT, INPUT_PULLUP);
  Wire.begin(); // SDA=D4, SCL=D5

  tft.begin();
  tft.setRotation(0);
  tft.fillScreen(BLACK);
  tft.setTextDatum(MC_DATUM);
  tft.setTextColor(WHITE, BLACK);
  showMainScreen();
  delay(500);
  tft.fillScreen(TFT_BLACK);

  drawPlusMinus();
  ringMeter(currentValue, 0, 99, 0, 0, RADIUS);
  drawText(String(currentValue), 120, 125, currentValue);
  drawBLEIndicator(false); // Start disconnected

  // BLE setup
  BLEDevice::init("TouchRingBLE");
  BLEServer *pServer = BLEDevice::createServer();
  pServer->setCallbacks(new MyServerCallbacks());

  BLEService *pService = pServer->createService(SERVICE_UUID);

  pTxCharacteristic = pService->createCharacteristic(
  CHARACTERISTIC_UUID_TX,
  BLECharacteristic::PROPERTY_NOTIFY | BLECharacteristic::PROPERTY_READ
);
  // Set initial value (important!)
pTxCharacteristic->setValue(String(ringValue).c_str());

  BLECharacteristic *pRxCharacteristic = pService->createCharacteristic(
    CHARACTERISTIC_UUID_RX,
    BLECharacteristic::PROPERTY_WRITE
  );

  pRxCharacteristic->setCallbacks(new MyCallbacks());
  pService->start();

  BLEAdvertising *pAdvertising = BLEDevice::getAdvertising();
  pAdvertising->addServiceUUID(SERVICE_UUID);
  pAdvertising->start();
  Serial.println("BLE advertising started as TouchRingBLE...");
}

// -----------------------------------------------------------------------------------
void loop() {                     // Basically it's maintaining the display's Value, and Touch interrupts while monitoring the connection if any.
  static int lastValue = -1;
  if (chsc6x_is_pressed()) {
    chsc6x_get_xy(&touchX, &touchY);
    int dx = touchX - 120;
    int dy = touchY - 120;
    float dist = sqrt(dx * dx + dy * dy);

    if (dist > 20) {
      if (abs(dy) > abs(dx)) {
        if (dy < -10 && currentValue < 99) currentValue++;
        if (dy > 10 && currentValue > 0) currentValue--;
      }
    }

    if (currentValue != lastValue) {
      ringMeter(currentValue, 0, 99, 0, 0, RADIUS);
      drawText(String(currentValue), 120, 125, currentValue);
      Serial.printf("%d\n", currentValue);

      if (deviceConnected) {
        char buf[4];
        snprintf(buf, sizeof(buf), "%d", currentValue);
        pTxCharacteristic->setValue((uint8_t*)buf, strlen(buf));
        pTxCharacteristic->notify();
      }

      lastValue = currentValue;
    }
  }

  delay(30);
}

// -----------------------------------------------------------------------------------
int ringMeter(int value, int vmin, int vmax, int x, int y, int r) {
  x += r;
  y += r;
  int w = r / 3;
  int angle = 150;
  int v = map(value, vmin, vmax, -angle, angle);
  byte seg = 3;
  byte inc = 6;

  for (int i = -angle + inc / 2; i < angle - inc / 2; i += inc) {
    float sx = cos((i - 90) * DEG_TO_RAD);
    float sy = sin((i - 90) * DEG_TO_RAD);
    uint16_t x0 = sx * (r - w) + x;
    uint16_t y0 = sy * (r - w) + y;
    uint16_t x1 = sx * r + x;
    uint16_t y1 = sy * r + y;

    float sx2 = cos((i + seg - 90) * DEG_TO_RAD);
    float sy2 = sin((i + seg - 90) * DEG_TO_RAD);
    int x2 = sx2 * (r - w) + x;
    int y2 = sy2 * (r - w) + y;
    int x3 = sx2 * r + x;
    int y3 = sy2 * r + y;

    uint16_t color = GREY;
    if (i < v) {
      int mapped = map(i, -angle, angle, 0, 99);
      if (mapped <= 24) color = SEEED_GREEN;
      else if (mapped <= 50) color = SEEED_BLUE;
      else if (mapped <= 75) color = YELLOW;
      else if (mapped <= 86) color = ORANGE;
      else color = RED;
    }

    tft.fillTriangle(x0, y0, x1, y1, x2, y2, color);
    tft.fillTriangle(x1, y1, x2, y2, x3, y3, color);
  }

  return x + r;
}

// -----------------------------------------------------------------------------------
void drawText(String text, int x, int y, int value) {
  uint16_t color = (value > 86) ? RED : WHITE;
  tft.setTextColor(color, BLACK);
  tft.setTextDatum(MC_DATUM);
  tft.setTextSize(3);
  tft.fillRect(x - 30, y - 20, 60, 40, BLACK);  // Clear previous digits
  tft.drawString(text, x, y);
}

// -----------------------------------------------------------------------------------
void drawPlusMinus() {
  tft.setTextDatum(MC_DATUM);
  tft.setTextSize(3);
  tft.setTextColor(WHITE, BLACK);
  tft.drawString("+", 120, 30);   // Top center
  tft.drawString("-", 120, 210);  // Bottom center
}

// -----------------------------------------------------------------------------------
void showMainScreen() {
  String mcuType = "Unknown";

#if CONFIG_IDF_TARGET_ESP32C6
  mcuType = "ESP32-C6";
#elif CONFIG_IDF_TARGET_ESP32C3
  mcuType = "ESP32-C3";
#elif CONFIG_IDF_TARGET_ESP32S3
  mcuType = "ESP32-S3";
#elif CONFIG_IDF_TARGET_ESP32
  mcuType = "ESP32";
#endif

#ifdef ARDUINO_XIAO_ESP32S3
  mcuType = "ESP32-S3-Plus";
#endif

  tft.fillScreen(TFT_BLACK);
  tft.setTextColor(SEEED_GREEN);
  tft.setTextSize(2);
  tft.setCursor(40, 40);
  tft.println("Seeed Studio");
  tft.setTextColor(WHITE);
  tft.setCursor(30, 90);
  tft.print("MCU: ");
  tft.println(mcuType);
  tft.setTextColor(SKY_BLUE);
  tft.setCursor(40, 135);
  tft.println("Touch_Ring_BLE");
  tft.setTextColor(WHITE);
  tft.setCursor(70, 170);
  tft.print("Rev: v1.4");
  delay(5000);
}

// -----------------------------------------------------------------------------------
// Creates a "BLE" indicator on the display middle dial tiny ;-p 
void drawBLEIndicator(bool connected) {
  tft.setTextDatum(MC_DATUM);
  tft.setTextSize(1);
  tft.setTextColor(connected ? SKY_BLUE : GREY, BLACK);
  tft.drawString("BLE", 120, 80);
}

HTH
GL :slight_smile: PJ :+1:

2 Likes