ESP32 OLED Battery Thing – Getting Started

ESP32 OLED Battery Thing

Getting Started

Als ich voriges Jahr im Krankenhaus war, hat mein Arbeitskollege mir ein WEMOS TTGO ESP32 Board geschenkt. Coole Sache!

Es kommt mit den folgenden Features:

  • ESP-WROOM-32 Module, inkl. WiFi und Bluetooth
  • USB to Serial Bridge, Silicon Labs CP210X Chip
  • Ladeschaltkreis für eine 18650 Zelle, inkl. Halterung auf der Rückseite
  • OLED Display, SSD1306, I²C Version
  • LED an GPIO 16

PinOut

Um euch das basteln zu erleichtern habe ich mal die Pinbelegung illustriert.

ESP-WROOM-32 WiFi & Bluetooth Battery OLED Board – PinOut

Arduino IDE

In der Arduino IDE müsst ihr in die Einstellungen gehen (Strg + ,) und bei den Zusätzlichen Boardmanager Links die URL von Espressif einfügen. Habt ihr hier schon andere Quellen eingetragen, können diese einfach mit Komma getrennt werden.

Der Link lautet: https://dl.espressif.com/dl/package_esp32_index.json

Danach geht es in den Boardverwalter, wo wir das Paket esp32 von Espressif installieren. Danach sollte bei Boards die Liste mit den neuen ESP32 Boards auftauchen. Auf dem linken Bild seht ihr bei mir, dass ich das WeMos Wifi&BluetoothBattery Board ausgewählt habe.

Damit können wir das Board programmieren. Jedoch gibt es eine Besonderheit: Man muss leider den Boot-Knopf gedrückt halten, wenn man Sketches hochladen will. Wenn ihr also eine Fehlermeldung bekommt, wie “Failed to connect to ESP32: Timed out waiting for packet header” müsst ihr den Boot-Knopf drücken. Eine dauerhaftere Lösung ist verlinkt.

Sollte sich das OLED-Display nicht aktualisieren, bzw der neue Sketch nicht Starten, drückt den EN-Knopf. Achtet beim Hochladen natürlich auch darauf, den Kippschalter auf ON zu haben.

OLED Display

Zunächst installieren wir uns die Bibliothek für das SSD1306 Display. Hier gibt es ein spezielles Treiberpaket, extra für ESP32 Boards.

Beim OLED Display auf dieser Variante gibt es eine kleine Abweichung bei der Verbindung mit dem ESP32. Anders als in den Beispielen sind die I²C SDA und SCL Pins wie folgt angeschlossen:

  • SCL – Pin 4 (SCL bedeutet Serial Clock – Taktleitung)
  • SDA – Pin 5 (SDA bedeutet Serial Data – Datenleitung)

Das bedeutet in den Beispielen, oder auch in eurem Eigenen Programm müsst ihr in der Zeile wo SSD1306Wire display(0x3c, SDA, SCL); steht einfach SSD1306Wire display(0x3c, 5, 4); eintragen.

In Manchen Beispielen werden die Pins mit D4 oder D5 geschrieben, das funktioniert aber nicht. Spätestens beim Kompilieren wird die IDE meckern.

Ein Beispiel wäre wie folgt:

//ESP32 OLED Learning

//----Includes für die Bibliotheken für das kleine OLED-Display
#include <Wire.h>  
#include "SSD1306Wire.h"

// Initialize the OLED display using Wire library
SSD1306Wire  display(0x3c, 5, 4);

void printBuffer(void) {
  // Initialisiert eine Funktion für Textausgabe
  // allocate memory to store 5 lines of text and 30 chars per line.
  display.setLogBuffer(5, 30);

  // Hier kommt der Text rein
  const char* test[] = {
      "Moin",
      "Wie geht es dir?",
      "Wir spielen endlich mit dem",
      "ESP32 Ding herum",
      "Ist ziemlich cool!",
      
  };

  for (uint8_t i = 0; i < 5; i++) {
    display.clear();
 //Display flush
    // Ausgabe (wie man es auch vom Seriellen Monitor kennt
    display.println(test[i]);
    // Draw it to the internal screen buffer
    display.drawLogBuffer(0, 0);
    // Display it on the screen
    display.display();
    delay(500);
  }
}

void setup() {
  display.init();

  //display.flipScreenVertically();
 //falls notwendig
  display.setContrast(255);
  
}

void loop() {
  printBuffer(); //Ruft die Funktion mit dem text ab.
  delay(1000);
 //warte 1 sekunde
  display.clear();}

Jetzt sollte der Text auf dem kleinen Display erscheinen.

Solltet ihr noch andere Probleme haben gibt es <hier/> einen guten Troubleshooting Guide.

BLE

Als nächstes versuche ich die LED an GPIO 16 über BLE anzusteuern. Dazu brauchen wir eine Firmware und eine App. Ich bin jetzt kein guter Programmierer also greife ich für die App auf den MIT App-Inventor zurück.

Dazu müssen die entsprechenden Bibliotheken eingebunden werden und natürlich das Bluetooth-Gebimmel definiert werden.

#include <BLEDevice.h>
#include <BLEServer.h>
#include <BLEUtils.h>
#include <BLE2902.h>
BLEServer *pServer = NULL;
BLECharacteristic * pTxCharacteristic;
#define SERVICE_UUID           "6E400001-B5A3-F393-E0A9-E50E24DCCA9E" 
// UART service UUID
#define CHARACTERISTIC_UUID_RX "6E400002-B5A3-F393-E0A9-E50E24DCCA9E"
#define CHARACTERISTIC_UUID_TX "6E400003-B5A3-F393-E0A9-E50E24DCCA9E"

Bluetooth services brauchen immer eine Einzigartige ID (UUID = Universally Unique Identifier). Diese kann man hier einfach übernehmen oder lässt sie sich durch einen Zufallsgenerator wie <hier/> erstellen.

class MyServerCallbacks: public BLEServerCallbacks {
    void onConnect(BLEServer* pServer) {
      deviceConnected = true;
    };

    void onDisconnect(BLEServer* pServer) {
      deviceConnected = false;
    }
};

class MyCallbacks: public BLECharacteristicCallbacks {
    void onWrite(BLECharacteristic *pCharacteristic) {
      std::string rxValue = pCharacteristic->getValue();

      if (rxValue.length() > 0) {
        Serial.println("*********");
        Serial.print("Received Value: ");
        for (int i = 0; i < rxValue.length(); i++)
          Serial.print(rxValue[i]);
        Serial.println();
        Serial.println("*********");
      }
    }
};

In die Void SetUp kommt folgender Code:

// Create the BLE Device
  BLEDevice::init("UART Service");

  // Create the BLE Server
  pServer = BLEDevice::createServer();
  pServer->setCallbacks(new MyServerCallbacks());

  // Create the BLE Service
  BLEService *pService = pServer->createService(SERVICE_UUID);

  // Create a BLE Characteristic
  pTxCharacteristic = pService->createCharacteristic(
                        CHARACTERISTIC_UUID_TX,
                        BLECharacteristic::PROPERTY_NOTIFY
                      );

  pTxCharacteristic->addDescriptor(new BLE2902());

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

  pRxCharacteristic->setCallbacks(new MyCallbacks());

  // Start the service
  pService->start();

  // Start advertising
  pServer->getAdvertising()->start();
  Serial.println("Waiting a client connection to notify...");

In die Loop Funktion kommt der folgende Code:

if (deviceConnected) {
    pTxCharacteristic->setValue(&txValue, 1);
    pTxCharacteristic->notify();
    txValue++;
    delay(10); // bluetooth stack will go into congestion, if too many packets are sent
  }

  // disconnecting
  if (!deviceConnected && oldDeviceConnected) {
    delay(500); // give the bluetooth stack the chance to get things ready
    pServer->startAdvertising(); // restart advertising
    Serial.println("restart advertising");
    oldDeviceConnected = deviceConnected;
  }
  // connecting
  if (deviceConnected && !oldDeviceConnected) {
    // do stuff here on connecting
    oldDeviceConnected = deviceConnected;
  }

Jetzt fehlt nur noch die Passende App auf dem Smartphone: TBA

Touch

Ein weiteres cooles Feature vom ESP32 ist die direkte integration von Touchfunktionen. An insgesamt 9 GPIO Pins lassen sich mit touchRead(); direkt im statischen Feld ausgeben. Wenn man mit einem Grenzwert (Threshold) Arbeitet, kann man so ziemlich leicht kleine Touch-Steuer-Elemente einbauen.

Mit einem Jumper-Kabel jeweils an Pin 32 und 33 kann man sich auf den Seriellen Monitor den wert Ausgeben lassen um die Funktion zu kalibrieren. (Bei mir etwa 130). Wenn ich das Kabel berühre sinkt der Wert auf etwa 20.

Für eine entprellte Toucheingabe des UI füge ich also folgenden Code hinzu::

//Pins Festlegen
#define touchPinLeft 32
#define touchPinRight 33


//Grenzwert Festlegen
#define thresholdTouchLeft 30
#define thresholdTouchRight 30

//Entprell-Variablen
byte touchLeftDurationMax = 40;
byte touchRightDurationMax = 40;
byte touchRightDuration = 0;
byte touchLeftDuration = 0;

//Zeitintervall zum Messen
const unsigned long touchIntervalMS = 50;

//Statusüberprüfung
byte prevTouchLState = LOW;
byte prevTouchRState = LOW;
byte currentTouchLState = LOW;
byte currentTouchRState = LOW;

//In welche Richtung soll die Anzeige "swipen"
bool swipeleft = false;
bool swiperight = false;

//Wie viele Displays will ich darstellen
int displayNum = 0; //Verschiedene Displays mit Taste zum Switchen
int maxDisplays = 5; //Wie viele max?

Dann brauche ich eine Reihe von funktionen, die für die verschiedenen Zustände etwas tun:

void touchLTouched() {
  Serial.println("Left Touchfield is touched");
  touchLeftDuration = 0; //Resets the Touch-Timer
}
void touchRTouched() {
  Serial.println("Right Touchfield is touched");
  touchRightDuration = 0; //Resets the Touch-Timer
}

//this is what we do when we release the touchfield
void touchLReleased() {
  Serial.println("Left Touchfield is released");

  //if it was pressed for longer than our max time
  if (touchLeftDuration >= touchLeftDurationMax ) {
    longTouchLeft();
  }
  else {
    shortTouchLeft();
  }
}

void touchRReleased() {
  Serial.println("Right Touchfield is released");

  //if it was pressed for longer than our max time
  if (touchRightDuration >= touchRightDurationMax ) {
    longTouchRight();
  }
  else {
    shortTouchRight();
  }
}

//this is what we do if the touchfield  was touched long
void longTouchLeft() {
  Serial.println("Left Touchfield was touched long");
}
void longTouchRight() {
  Serial.println("Right Touchfield was touched long");
}

//this is what we do if the touchfield left was touched shortly
void shortTouchLeft() {
  Serial.println("Left Touchfield was touched short");
  swipeleft = true;
}
void shortTouchRight() {
  Serial.println("Right Touchfield was touched short");
  swiperight = true;
}

void checkLeftTouch() {

  if ( touchRead(touchPinLeft) <= thresholdTouchLeft ) {
    currentTouchLState = HIGH;
    Serial.print("Left treshold in is: ");
    Serial.println(touchRead(touchPinLeft));
  }
  else if ( touchRead(touchPinLeft) >= thresholdTouchLeft ) {
    currentTouchLState = LOW;
  }
  //does it go from not touched to touched?
  if ((prevTouchLState == LOW) && (currentTouchLState == HIGH)) {
    touchLTouched();
  }
  //or does it go the other way?
  if ((prevTouchLState == HIGH) && (currentTouchLState == LOW)) {
    touchLReleased();
  }
  //if it is pressed, we count the time - we check in ms intervall
  else if (currentTouchLState == HIGH && prevTouchLState == HIGH ) {
    touchLeftDuration++;
    Serial.println("TouchDuration is: " + String(touchLeftDuration));
  }
  // so next round the new one is the old
  prevTouchLState = currentTouchLState;
}

void checkRightTouch() {

  if ( touchRead(touchPinRight) <= thresholdTouchRight ) {
    currentTouchRState = HIGH;
    Serial.print("Right treshold in is: ");
    Serial.println(touchRead(touchPinLeft));
  }
  else if ( touchRead(touchPinRight) >= thresholdTouchRight ) {
    currentTouchRState = LOW;
  }
  //does it go from not touched to touched?
  if ((prevTouchRState == LOW) && (currentTouchRState == HIGH)) {
    touchRTouched();
  }
  //or does it go the other way?
  if ((prevTouchRState == HIGH) && (currentTouchRState == LOW)) {
    touchRReleased();
  }
  //if it is pressed, we count the time - we check in ms intervall
  else if (currentTouchRState == HIGH && prevTouchRState == HIGH ) {
    touchRightDuration++;
    Serial.println("TouchDuration is: " + String(touchRightDuration));
  }
  // so next round the new one is the old
  prevTouchRState = currentTouchRState;
}

int getDisplayNum() {
  unsigned long touchTimePrev = 0;
  //do this every x milliseconds (defined within touchIntervalMS)
  if (millis() - touchTimePrev >= touchIntervalMS ) {
    touchTimePrev = millis();

    //Check if Left touchfield is touched or not
    checkLeftTouch();
    checkRightTouch();
  }

  if (swipeleft) {
    if (displayNum == 0) {
      displayNum = maxDisplays;

    }
    else {
      displayNum--;
    }
    swipeleft = false;
  }
  if (swiperight) {
    if (displayNum == maxDisplays) {
      displayNum = 0;
    }
    else {
      displayNum++;
    }
    swiperight = false;
  }

  return displayNum;
}

Außerdem eine Funktion die das eigentliche Display-Rendern übernimmt:

void printDisplay(int displayNum) {
  switch (displayNum)
  {
    case 0:
      display.clear();
      timeOverlay();
      statusOverlay();
      break;
    case 1:
      display.clear();
      timeOverlay();
      statusOverlay();
      display.setFont(ArialMT_Plain_24);
      display.drawString(20, 31, String(millis()));
      break;
    case 2:
      display.clear();
      timeOverlay();
      statusOverlay();
      display.setFont(ArialMT_Plain_16);
      display.drawString(0, 10, "Hello world");
      break;
    case 3:
      display.clear();
      timeOverlay();
      statusOverlay();
      display.setFont(ArialMT_Plain_10);
      display.setTextAlignment(TEXT_ALIGN_LEFT);
      display.drawStringMaxWidth(0, 10, 128,
                                 "Moin \n Dies ist ein Fließtext test. ABCDEFGHIJKLMNOPQRSTUVWXYZ" );
      break;
    case 4:
      display.clear();
      timeOverlay();
      statusOverlay();
      // The coordinates define the right end of the text
      display.setTextAlignment(TEXT_ALIGN_RIGHT);
      display.drawString(128, 33, "Right aligned (128,33)");
      break;
    case 5:
      display.clear();
      timeOverlay();
      statusOverlay();
      for (int i = 1; i < 8; i++) {
        display.setColor(WHITE);
        display.drawCircle(32, 32, i * 3);
        if (i % 2 == 0) {
          display.setColor(BLACK);
        }
        display.fillCircle(96, 32, 32 - i * 3);
      }
      break;

  }
  display.display();
  delay(10);
}

Im Setup darf natürlich nicht die Display Initialisierung fehlen:

display.init();
display.clear();
display.flipScreenVertically();
display.setContrast(255);

Und im Loop rufen wir einfach folgendes auf:

printDisplay(getDisplayNum());

Schreibe einen Kommentar