Skip to content

Networking and Communication

During Communication Week, we aimed to enable our custom development boards to communicate with each other. Since the ATTiny does not support wireless protocols, we decided to use UART for communication. To achieve this, we built a voltage divider to step down the ATTiny’s 5V signals to 3.3V signals compatible with the ESP32. Sending 5V signals directly could have damaged the ESP32 in the long run. In hindsight, we realized that the ATTiny can also operate at 3.3V, meaning that at this voltage level, a logic level converter would not have been necessary. Unfortunately, while wiring the ATTiny, a critical mistake occurred: we swapped GND and VCC on the ATTiny development board. This destroyed both the MCU and, during testing, the onboard LED. Implementing a protective mechanism or clearer labeling on the board could have helped prevent such usability issues.

As a result, we had to fall back on boards from other weeks, since we did not have spare ATTiny chips available in the lab. Consequently, we are now using Nico’s Input Week board and William’s Input Week board. Since neither of these boards has additional pins exposed, UART communication is no longer feasible. William’s Input Board uses an ESP32 H2, which does not support Wi-Fi. This excludes both Wi-Fi and ESP-NOW as communication protocols. Therefore, Bluetooth Low Energy (BLE) remains the only practical option for communication.

In this setup, we configured Nico’s board as a BLE server and William’s board as the client. The server broadcasts BLE advertisements, similar to the way standard Bluetooth devices advertise themselves. On a smartphone, these advertisements are typically visible when Bluetooth is enabled and devices are being scanned. While BLE uses a different protocol that is not always listed in standard device scans, most smartphones offer apps that support BLE, allowing these devices to be detected and interacted with. In our case, we used the LightBlue app from the App Store.

The smartphone could then connect to the advertising server and act as a client in this relationship. In this setup, the server’s service can now be subscribed to, allowing the transmitted packages to be displayed. To read these packages, the output can be converted to a UTF-8 string in the top-right corner. This enables optimal debugging of the transmitted values.

For the ESP32, we use the same mechanism. However, instead of simply viewing the values, we use them directly for actions. The control logic remains similar to the Week project: 0–100% controls the Hue or brightness, toggled via a button press. The 0–100% range is now mapped to the distance measured by the ToF sensor: 0 mm corresponds to 0%, and 500 mm corresponds to 100%. When a new measurement is received, the display remains on. If no new data arrives for six seconds, the display turns off automatically. The display now shows not only the current settings at the bottom but also the measured distance.

BLE Server Code:

#include <Wire.h>
#include <VL53L0X.h>
#include <BLEDevice.h>
#include <BLEServer.h>
#include <BLE2902.h>

#define DEVICE_NAME         "VL53L0X-C3"
#define SERVICE_UUID        "6E400001-B5A3-F393-E0A9-E50E24DCCA9E"
#define CHARACTERISTIC_UUID "6E400003-B5A3-F393-E0A9-E50E24DCCA9E"

VL53L0X sensor;
BLECharacteristic *pCharacteristic;
bool deviceConnected = false;

class ServerCallbacks : public BLEServerCallbacks {
  void onConnect(BLEServer* pServer) {
    deviceConnected = true;
    Serial.println("[BLE] Client verbunden");
  }
  void onDisconnect(BLEServer* pServer) {
    deviceConnected = false;
    Serial.println("[BLE] Client getrennt – re-advertise");
    BLEDevice::startAdvertising();
  }
};

void setup() {
  Serial.begin(115200);
  delay(1000);

  Wire.begin(9, 8);
  Wire.setClock(100000);
  if (!sensor.init()) { Serial.println("Sensor init failed!"); while (1); }
  sensor.setTimeout(2000);
  sensor.startContinuous();

  BLEDevice::init(DEVICE_NAME);
  BLEServer *pServer = BLEDevice::createServer();
  pServer->setCallbacks(new ServerCallbacks());

  BLEService *pService = pServer->createService(SERVICE_UUID);
  pCharacteristic = pService->createCharacteristic(
    CHARACTERISTIC_UUID,
    BLECharacteristic::PROPERTY_NOTIFY
  );
  pCharacteristic->addDescriptor(new BLE2902());
  pService->start();

  BLEAdvertising *pAdv = BLEDevice::getAdvertising();
  pAdv->addServiceUUID(SERVICE_UUID);
  pAdv->start();

  Serial.println("[BLE] Advertising gestartet");
}

void loop() {
  int distance = sensor.readRangeContinuousMillimeters();

  if (!sensor.timeoutOccurred()) {
    int d = distance - 15;
    Serial.print("Distance: "); Serial.print(d); Serial.println(" mm");

    if (deviceConnected) {
      String val = String(d);
      pCharacteristic->setValue(val.c_str());
      pCharacteristic->notify();
    }
  } else {
    if (deviceConnected) {
      pCharacteristic->setValue("-1");
      pCharacteristic->notify();
    }
  }

  delay(500);
}

BLE Client Code:

#include <FastLED.h>
#include <Wire.h>
#include <U8g2lib.h>
#include <BLEDevice.h>
#include <BLEClient.h>
#include <BLEScan.h>
#include <BLEAdvertisedDevice.h>

// --- PINS -------------------------------------------------------
#define NUM_LEDS        1
#define DATA_PIN        8
#define POTI_PIN        4
#define BUTTON_PIN      11
#define SDA_PIN         10
#define SCL_PIN         12

// --- BLE --------------------------------------------------------
#define TARGET_NAME         "VL53L0X-C3"
#define SERVICE_UUID        "6E400001-B5A3-F393-E0A9-E50E24DCCA9E"
#define CHARACTERISTIC_UUID "6E400003-B5A3-F393-E0A9-E50E24DCCA9E"

static BLEAddress    *pServerAddress = nullptr;
static BLEClient     *pClient        = nullptr;
static bool           bleConnected   = false;
static bool           doConnect      = false;
static bool           doScan         = true;

// --- DISPLAY ----------------------------------------------------
U8G2_SSD1306_128X64_NONAME_F_HW_I2C u8g2(U8G2_R0, U8X8_PIN_NONE);

// --- LED --------------------------------------------------------
CRGB leds[NUM_LEDS];

// --- SMOOTHING --------------------------------------------------
#define SMOOTHING_SAMPLES 10
int readings[SMOOTHING_SAMPLES], readIndex = 0, smoothTotal = 0;

// --- MODES ------------------------------------------------------
enum Mode { MODE_HUE, MODE_BRIGHTNESS };
Mode currentMode = MODE_HUE;

// --- STATE ------------------------------------------------------
bool    ledEnabled    = true;
bool    displayOn     = true;
uint8_t hue           = 0;
uint8_t brightness    = 128;
int     lastPotiValue = -1;
int     distanceMM    = -1;

// --- DISPLAY TIMEOUT --------------------------------------------
#define DISPLAY_TIMEOUT_MS 8000
unsigned long lastActivityTime = 0;

// --- BUTTON -----------------------------------------------------
bool          lastButtonState  = HIGH;
unsigned long buttonPressTime  = 0;
bool          longPressHandled = false;

// ---------------------------------------------------------------

// BLE Notification Callback
void notifyCallback(BLERemoteCharacteristic* pChar, uint8_t* pData, size_t length, bool isNotify) {
  String s = "";
  for (int i = 0; i < length; i++) s += (char)pData[i];
  int d = s.toInt();

  if (d != distanceMM) {
    distanceMM = d;
    Serial.print("[BLE] Distanz: "); Serial.print(distanceMM); Serial.println(" mm");

    // Distanz auf 0–255 mappen (0mm = 0, 500mm = 255)
    if (distanceMM >= 0) {
      uint8_t mappedValue = (uint8_t) constrain(map(distanceMM, 0, 500, 0, 255), 0, 255);

      if (currentMode == MODE_HUE)        hue        = mappedValue;
      else                                brightness = mappedValue;

      updateLED();
    }

    wakeDisplay();   // Display bei jeder Änderung aufwecken
    drawDisplay();
  }
}

// BLE Scan: merke Adresse wenn Target gefunden
class AdvertisedDeviceCallbacks : public BLEAdvertisedDeviceCallbacks {
  void onResult(BLEAdvertisedDevice advertisedDevice) {
    if (advertisedDevice.getName() == TARGET_NAME) {
      Serial.println("[BLE] Server gefunden!");
      pServerAddress = new BLEAddress(advertisedDevice.getAddress());
      BLEDevice::getScan()->stop();
      doConnect = true;
      doScan    = false;
    }
  }
};

bool connectToServer() {
  Serial.print("[BLE] Verbinde mit "); Serial.println(pServerAddress->toString().c_str());
  pClient = BLEDevice::createClient();
  if (!pClient->connect(*pServerAddress)) { Serial.println("[BLE] Verbindung fehlgeschlagen"); return false; }

  BLERemoteService *pService = pClient->getService(SERVICE_UUID);
  if (!pService) { Serial.println("[BLE] Service nicht gefunden"); pClient->disconnect(); return false; }

  BLERemoteCharacteristic *pChar = pService->getCharacteristic(CHARACTERISTIC_UUID);
  if (!pChar) { Serial.println("[BLE] Characteristic nicht gefunden"); pClient->disconnect(); return false; }

  if (pChar->canNotify()) pChar->registerForNotify(notifyCallback);

  bleConnected = true;
  Serial.println("[BLE] Verbunden ✓");
  return true;
}

// ---------------------------------------------------------------
void wakeDisplay() {
  lastActivityTime = millis();
  if (!displayOn) { displayOn = true; u8g2.setPowerSave(0); }
}

void updateLED() {
  if (ledEnabled) {
    leds[0] = CHSV(hue, 255, brightness);
  } else {
    leds[0] = CRGB::Black;
  }
  FastLED.show();
}

void drawDisplay() {
  if (!displayOn) return;

  int displayValue = (currentMode == MODE_HUE) ? hue : brightness;
  int barWidth = map(displayValue, 0, 255, 0, 118);
  int percent  = map(displayValue, 0, 255, 0, 100);

  u8g2.clearBuffer();

  u8g2.setFont(u8g2_font_6x10_tf);
  u8g2.drawStr(0, 10, currentMode == MODE_HUE ? "[ HUE ]   Brightness" : "  Hue   [ BRIGHTNESS ]");
  u8g2.drawHLine(0, 13, 128);

  // Wert (kleiner, Platz für Distanzzeile)
  u8g2.setFont(u8g2_font_logisoso22_tf);
  u8g2.setCursor(0, 38);
  u8g2.print(percent); u8g2.print("%");

  u8g2.setFont(u8g2_font_6x10_tf);
  u8g2.drawStr(100, 38, ledEnabled ? "ON" : "OFF");

  // Distanzzeile
  char buf[24];
  if (!bleConnected) {
    snprintf(buf, sizeof(buf), "BLE: suche...");
  } else if (distanceMM < 0) {
    snprintf(buf, sizeof(buf), "Dist: --- mm");
  } else {
    snprintf(buf, sizeof(buf), "Dist: %d mm", distanceMM);
  }
  u8g2.drawStr(0, 51, buf);

  u8g2.drawFrame(0, 55, 128, 9);
  if (barWidth > 0) u8g2.drawBox(1, 56, barWidth, 7);

  u8g2.sendBuffer();
}

// ---------------------------------------------------------------
void setup() {
  Serial.begin(115200);
  delay(500);

  analogReadResolution(12);
  analogSetAttenuation(ADC_11db);
  for (int i = 0; i < SMOOTHING_SAMPLES; i++) readings[i] = 0;

  pinMode(BUTTON_PIN, INPUT_PULLUP);
  FastLED.addLeds<NEOPIXEL, DATA_PIN>(leds, NUM_LEDS);

  Wire.begin(SDA_PIN, SCL_PIN);
  u8g2.begin();
  u8g2.setPowerSave(0);

  // BLE init
  BLEDevice::init("");
  BLEScan *pScan = BLEDevice::getScan();
  pScan->setAdvertisedDeviceCallbacks(new AdvertisedDeviceCallbacks());
  pScan->setActiveScan(true);
  pScan->start(5, false);

  lastActivityTime = millis();
  updateLED();
  drawDisplay();
}

// ---------------------------------------------------------------
void loop() {

  // == BLE State Machine ========================================
  if (doConnect) {
    connectToServer();
    doConnect = false;
    drawDisplay();
  }

  // Verbindung verloren -> neu scannen (alle 10 s)
  static unsigned long lastScanAttempt = 0;
  if (!bleConnected && !doScan && millis() - lastScanAttempt > 10000) {
    doScan = true;
  }
  if (doScan) {
    lastScanAttempt = millis();
    doScan = false;
    Serial.println("[BLE] Scan starte...");
    BLEDevice::getScan()->start(5, false);
  }
  if (bleConnected && pClient && !pClient->isConnected()) {
    bleConnected = false;
    distanceMM   = -1;
    doScan       = true;
    Serial.println("[BLE] Verbindung verloren");
    drawDisplay();
  }

  // == POTI =====================================================
  int raw = analogRead(POTI_PIN);
  smoothTotal -= readings[readIndex];
  readings[readIndex] = raw;
  smoothTotal += readings[readIndex];
  readIndex = (readIndex + 1) % SMOOTHING_SAMPLES;

  int potiValue = constrain(map(smoothTotal / SMOOTHING_SAMPLES, 0, 3504, 0, 255), 0, 255);
  if (abs(potiValue - lastPotiValue) >= 3) {
    wakeDisplay();
    lastPotiValue = potiValue;
    if (currentMode == MODE_HUE) hue = potiValue;
    else brightness = potiValue;
    updateLED();
    drawDisplay();
  }

  // == BUTTON ===================================================
  bool buttonState = digitalRead(BUTTON_PIN);

  if (buttonState == LOW && lastButtonState == HIGH) {
    buttonPressTime = millis(); longPressHandled = false;
  }
  if (buttonState == LOW && !longPressHandled && millis() - buttonPressTime >= 3000) {
    ledEnabled = !ledEnabled; longPressHandled = true;
    wakeDisplay(); updateLED(); drawDisplay();
  }
  if (buttonState == HIGH && lastButtonState == LOW && !longPressHandled) {
    if (!displayOn) { wakeDisplay(); drawDisplay(); }
    else {
      currentMode = (currentMode == MODE_HUE) ? MODE_BRIGHTNESS : MODE_HUE;
      wakeDisplay(); drawDisplay();
    }
  }
  lastButtonState = buttonState;

  // == DISPLAY TIMEOUT ==========================================
  if (displayOn && millis() - lastActivityTime >= DISPLAY_TIMEOUT_MS) {
    displayOn = false;
    u8g2.setPowerSave(1);
  }

  delay(10);
}