Personal project

Multi-app control hub

A layered C++ framework for ESP32 devices that abstracts hardware logic, letting developers build self-contained knob-driven apps with just four callbacks.

3 min read

Shipped
The prototype on protoboard, a round display running a Pomodoro timer beside an ESP32, knob and buzzer

Abstract

This project is a knob-driven, multi-app device powered by an ESP32 and a round screen. Rather than hardcoding a single tool, I built a modular C++ framework to make the implementation of new apps easier, providing utilities for Bluetooth, display rendering, buzzer control, and LED animations. The architecture strictly separates hardware from software. Writing a new application requires just one class and four functions, with zero knowledge of the underlying pins or protocols. The prototype currently runs three apps: a Pomodoro timer, a Bluetooth media controller, and a home app-launcher interface.

Introduction

I originally set out to build a Pomodoro timer. However, knob-driven gadgets are a common maker staple, and building just another timer felt redundant.

The goal shifted to building a reusable, scalable base for any knob-driven device, where the engineering challenge is the software architecture rather than the end feature.

  • Hardware abstraction. Applications must not know about SPI, PWM, or encoder pins.
  • Developer experience. Adding a new application should require only one class, a standard lifecycle, and a registration step.

The development sections below detail the hardware platform, the software layer architecture, and the required application contract.

Hardware architecture

The physical prototype is built around an ESP32, chosen for its Bluetooth stack and ample PWM channels.

  • Display. A 240 by 240 pixel round TFT screen driven over SPI using Direct Memory Access (DMA) and double buffering.
  • Input. A standard rotary encoder, quadrature decoded via an IRAM interrupt with software debouncing.
  • Outputs. A common-cathode RGB LED and a passive buzzer, both controlled via PWM. The LED outputs use gamma correction for accurate color blending.
  • Build system. The entire firmware is managed through PlatformIO.

The software layers

The firmware stack isolates applications from hardware changes. Swapping the round display for a square panel only requires writing one new driver.

  • Hardware Abstraction Layer (HAL). Defines pure virtual interfaces for the display, encoder, LED, and buzzer.
  • Services. High-level managers for display rendering, LED patterns, audio playback, and Bluetooth HID.
  • Handlers. Interrupt managers that translate hardware events into software application calls.
  • Service registry. A dependency injection container. An app requests a shared service rather than constructing its own.Dependency injection keeps memory-heavy objects like the display service shared globally rather than duplicated per app.
  • Applications. Self-contained logic modules conforming to a standard lifecycle.

The app contract

An application inherits from a base class and implements four core callbacks:

class BaseApp {
  virtual void onEnter() = 0;          // Acquire services and draw the first frame
  virtual void onEncoderUpdate() = 0;  // Handle rotation and click events
  virtual void onTimerUpdate() = 0;    // Process periodic ticks from the timer ISR
  virtual void onExit() = 0;           // Persist state and release services
};

Once a class implements this contract and registers with the handlers, it becomes a live application on the device.

Results

The framework currently runs three distinct applications reliably on the ESP32 prototype.

  • Pomodoro timer. A state machine drives the settings UI, and the timer state persists cleanly across reboots.
  • Volume controller. The device acts as a Bluetooth Low Energy (BLE) HID peripheral, turning the physical knob into a media controller for any paired host device.
  • Home navigator. A circular app launcher specifically designed to suit the round screen.

Limits and next steps

The framework is stable but has room for expansion.

  • Application ecosystem. The primary next step is writing more diverse apps to stress-test the contract boundaries.
  • Unused hardware protocols. I2C is exposed through the HAL but currently remains unused by any sensors.
  • Community use. If you build your own knob-driven device using this framework, I would love to hear about it.