Small enclosure with 0.96 inch OLED display and rotary encoder knob, ESP32-S3 and AHT20 sensor on breadboard

Intermediate

Build a Compact Sensor Dashboard

DisplaysSensorsIoT

Create a small dashboard that combines multiple sensor readings on one display and introduces structured data, screen layout and periodic updates.

8 real parts included

Total for this kit: 1,546.00 ฿

Doesn't include any order discount you may get at checkout.

What you'll need

Project Goal

This project brings several sensor readings together onto a single display at once, moving beyond a single-value readout into structured layout, multiple data sources, and a real (if simple) user interface.

Gathering Multiple Sensor Values

With more than one sensor connected, read each one in turn every cycle and hold its value in a small, clearly-named set of variables before touching the display at all -- mixing reading and drawing logic together tends to make both harder to debug.

Display Layout

Decide on a fixed layout -- which value goes where on the screen -- before writing the drawing code. A simple grid (one region per value) is far easier to get right on a first attempt than trying to lay things out dynamically.

Refresh Intervals

Redraw only as often as the values actually change meaningfully. A full-screen redraw on every single loop iteration, many times a second, is usually unnecessary and can visibly flicker depending on the display type.

Avoiding Screen Flicker

Where possible, redraw only the specific regions of the screen whose values actually changed since the last update, rather than clearing and redrawing the entire display every cycle. Not every display library supports partial updates equally well -- check what your specific display and library support before assuming this optimization is available.

Data Formatting

Decide on consistent formatting for each value (decimal places, units, a fixed maximum width) before display, so the layout doesn't visibly shift around as numbers change length from one reading to the next.

Input Validation

The same sanity-checking principle from single-sensor projects applies here across every value: don't display (or act on) an implausible reading from any one sensor without at least flagging it, since a bad value from one sensor shouldn't be presented with the same confidence as the working ones.

Optional Expansion: Buttons and Menus

Once the fixed dashboard layout works, adding one or two physical buttons to switch between views (or drill into a single value's recent history) is a natural next step that turns a passive readout into something a little more interactive.

Optional Upgrade: Network-Connected Version

The dashboard concepts here combine directly with the wireless logging project -- the same multi-sensor gathering approach can feed a remote log or dashboard instead of, or alongside, the local display.

Wiring

  • OLED VCC → 3.3V, GND → GND, SDA → GPIO8, SCL → GPIO9 (ESP32-S3 default I2C pins)
  • AHT20 VIN → 3.3V, GND → GND, SDA → GPIO8, SCL → GPIO9 — same I2C bus as the OLED (OLED at 0x3C, AHT20 at 0x38, no conflict)
  • Rotary encoder VCC → 3.3V, GND → GND
  • Rotary encoder CLK → ESP32-S3 GPIO4 (`ENC_CLK_PIN`)
  • Rotary encoder DT → ESP32-S3 GPIO5 (`ENC_DT_PIN`)
  • Rotary encoder SW → ESP32-S3 GPIO6 (`ENC_SW_PIN`, uses `INPUT_PULLUP`)

Both I2C devices share one bus fine at their default addresses — just double check your OLED module isn't one of the rarer 0x3D-address variants. Avoid routing anything to GPIO0 on the S3 (it's the BOOT/strapping pin); the pins above are ordinary GPIOs on most ESP32-S3 dev boards but confirm against your specific board's pinout diagram. The encoder is read by simple polling here for clarity — if you see missed detents at faster spins, switching CLK/DT to interrupt-driven reads is the usual fix.

Wiring Diagram

VCC GND SDA → GPIO8 SCL → GPIO9 VIN GND SDA → GPIO8 SCL → GPIO9 VCC GND CLK → GPIO4 DT → GPIO5 SW → GPIO6 ESP32-S3 OLED AHT20 Rotary encoder 3.3V GND

Starter Code

#include <Wire.h>
#include <Adafruit_GFX.h>
#include <Adafruit_SSD1306.h>
#include <Adafruit_AHTX0.h>

#define SDA_PIN 8
#define SCL_PIN 9

#define SCREEN_WIDTH 128
#define SCREEN_HEIGHT 64
#define OLED_RESET -1
#define OLED_ADDR 0x3C

#define ENC_CLK_PIN 4
#define ENC_DT_PIN 5
#define ENC_SW_PIN 6

Adafruit_SSD1306 display(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, OLED_RESET);
Adafruit_AHTX0 aht;
sensors_event_t humidityEvent, tempEvent;

int screenIndex = 0;
const int NUM_SCREENS = 3;

int lastClkState;
unsigned long lastButtonPress = 0;

void readSensor() {
  aht.getEvent(&humidityEvent, &tempEvent);
}

void drawScreen() {
  display.clearDisplay();
  display.setTextSize(1);
  display.setTextColor(SSD1306_WHITE);
  display.setCursor(0, 0);

  switch (screenIndex) {
    case 0:
      display.println("TEMPERATURE");
      display.setTextSize(2);
      display.setCursor(0, 20);
      display.print(tempEvent.temperature, 1);
      display.println(" C");
      break;
    case 1:
      display.println("HUMIDITY");
      display.setTextSize(2);
      display.setCursor(0, 20);
      display.print(humidityEvent.relative_humidity, 1);
      display.println(" %");
      break;
    case 2:
      display.println("SUMMARY");
      display.setCursor(0, 20);
      display.print("Temp: ");
      display.print(tempEvent.temperature, 1);
      display.println(" C");
      display.print("Hum:  ");
      display.print(humidityEvent.relative_humidity, 1);
      display.println(" %");
      break;
  }
  display.display();
}

void setup() {
  Serial.begin(115200);
  Wire.begin(SDA_PIN, SCL_PIN);

  pinMode(ENC_CLK_PIN, INPUT);
  pinMode(ENC_DT_PIN, INPUT);
  pinMode(ENC_SW_PIN, INPUT_PULLUP);
  lastClkState = digitalRead(ENC_CLK_PIN);

  if (!display.begin(SSD1306_SWITCHCAPVCC, OLED_ADDR)) {
    Serial.println("OLED not found — check wiring.");
    while (1) delay(1000);
  }
  if (!aht.begin()) {
    Serial.println("AHT20 not found — check wiring.");
    while (1) delay(1000);
  }

  readSensor();
  drawScreen();
}

void loop() {
  // Rotary encoder rotation: compare CLK against DT on each CLK edge
  int clkState = digitalRead(ENC_CLK_PIN);
  if (clkState != lastClkState && clkState == HIGH) {
    if (digitalRead(ENC_DT_PIN) != clkState) {
      screenIndex = (screenIndex + 1) % NUM_SCREENS;      // clockwise
    } else {
      screenIndex = (screenIndex - 1 + NUM_SCREENS) % NUM_SCREENS; // counter-clockwise
    }
    drawScreen();
  }
  lastClkState = clkState;

  // Push button: force a sensor refresh
  if (digitalRead(ENC_SW_PIN) == LOW && millis() - lastButtonPress > 250) {
    lastButtonPress = millis();
    readSensor();
    drawScreen();
  }

  static unsigned long lastAutoRead = 0;
  if (millis() - lastAutoRead > 2000) {
    lastAutoRead = millis();
    readSensor();
    drawScreen();
  }
}

Not sure about a step? Ask us