Back to Works
U02  ·  NET-DSP Smart Home Display 2025

The KALM
Project

An ambient desk and wall calendar built on an ESP32-C3 driving a 7.5″ B/W e-paper display via SPI. The device has no always-on Wi-Fi, no local server, and no subscription. Its entire operational contract is four steps: wake → fetch → draw → sleep. Heavy UI rendering is offloaded entirely to Google Apps Script, which returns the calendar as one Base64 bitmap over HTTPS; the MCU decodes the stream in 24-row slices so a full framebuffer never sits in RAM.

MCU ESP32-C3 · RISC-V · 400 KB RAM
Display 7.5″ B/W e-paper · 800×480 · GxEPD2_750_T7
Driver GxEPD2 · SPI interface
Calendar render GAS bitmap · device 24-row decode
Weather render GAS JSON · local UI · Meteocons
Power Deep sleep · 00:01 & 12:00 + button
01 /

The Problem

An always-visible schedule should not require you to unlock a phone, run a server, or pay a cloud subscription to a third party.

Every existing solution for ambient calendar display forces a significant operational compromise. KALM was built to eliminate all three of the primary failure modes simultaneously.

  • Phone-based solutions require interaction — the act of checking defeats the purpose of ambient display.
  • Local server stacks (Home Assistant, custom Pi rigs) demand always-on infrastructure, maintenance, and updates.
  • Cloud SaaS subscriptions (smart displays, digital frames) introduce recurring cost, dependency on vendor uptime, and privacy exposure.
Design Decision

A single-purpose appliance that performs one loop flawlessly: wake, fetch, render to screen, sleep. No RTOS. No persistent Wi-Fi. No background processes. The device is either asleep or it is done.

KALM device on desk or wall mount
ESP32-C3 E-Paper GxEPD2 Deep Sleep 3D Printed Google Apps Script WiFiManager
02 /

Cloud-Rendered Architecture

Constraint
RAM Is the Bottleneck
The ESP32-C3 has roughly 400 KB of usable RAM. Rendering a 800×480 bitmap locally — even with simple 1-bit graphics — requires 48 KB just for the framebuffer. Add a font engine, layout logic, and HTTP stack, and the device runs out of heap before the first calendar entry is drawn. The render step is offloaded entirely.
Calendar Pipeline
Google Apps Script → Base64 Bitmap
Google Apps Script reads the calendar via CalendarApp, draws a 4-day grid (06:00–21:00) server-side, and returns one Base64-encoded 1-bit 800×480 buffer as plain text. The ESP32 streams that HTTP body, Base64-decodes into a small row buffer (24 scanlines at a time), and writes each slice to the panel over SPI — never allocating a full framebuffer on the MCU.
24-row decode on device
Weather Pipeline
GAS → Weather JSON → Local UI
For ?view=weather_json, the same Apps Script deployment calls OpenWeather and returns a compact JSON object. The ESP32 draws the weather screen locally (FreeSans + Meteocons icon font). The bitmap layout stays on the MCU; only the data fetch is cloud-side.
?view=weather_json · local draw
GCal
Source
Google Calendar · CalendarApp
Auth + fetch
GAS
Cloud Renderer
Google Apps Script
Single Base64 body · HTTPS (streamed)
MCU
Microcontroller
ESP32-C3
SPI · 24-row decode buffer
EPD
Output
7.5″ E-Paper · 800×480

Weather: GAS proxies OpenWeather → JSON → on-device UI. No server-side weather bitmap.

KALM Apps Script rendered calendar bitmap
03 /

Hardware & Enclosure

MCU
ESP32-C3 (RISC-V, 160 MHz, 400 KB RAM)
Display
7.5″ B/W e-paper · 800×480 (GDEW075T7 class)
Driver
GxEPD2_750_T7 · SPI (CS 7 · DC 1 · RST 3 · BUSY 0)
Enclosure
Custom FDM 3D-printed shell
Frame
Natural wood listel
Input
Refresh Button
Setup
WiFiManager · AP “KALM-Terminal” · script URL in NVS
Connectivity
802.11 b/g/n · Wi-Fi off before deep sleep
Bus
SPI display · GPIO deep-sleep wake on refresh pin
Display Driver
Calendar data is streamed into the panel with writeImagePart, then a single full refresh runs at the end of the update. The firmware does not use partial refresh for calendar mode. Once updated, the panel holds the image with no backlight and no ongoing power draw.
Enclosure Design
FDM-printed in matte white PLA. A routed groove accepts a solid-wood listel strip, providing a visual anchor. Inputs can be hidden behind the shell (e.g. capacitive pads driving GPIO high); firmware uses pull-downs and treats a press as logic HIGH. No visible apertures in the intended build.
KALM PCB and display assembly
04 /

Power & State

Timer · GPIO refresh · or power-on
Wake
Deep sleep ends on esp timer (scheduled refresh, 5 min weather timeout, or computed delay until 00:01 / 12:00), GPIO HIGH on the refresh pin, or cold boot. Wi‑Fi connects; optional long-press on config at boot opens the captive portal.
~1.2 s
GAS returns Base64 (calendar) or JSON (weather)
Fetch & Decode
Calendar: stream one Base64 HTTP body, decode into a 24-row buffer, write each slice via SPI. Weather: parse compact JSON and draw the layout on-device. No full 48 KB framebuffer on the MCU.
~4–6 s
Stream complete or weather UI done
Draw
Calendar path ends with a full panel refresh; weather uses paged drawing. The e-paper image persists without power after the update finishes.
~2 s
Draw complete · sleep timer programmed
Deep Sleep
Wi‑Fi disconnected and radio off. esp_deep_sleep with timer until the next 00:01 or 12:00 (calendar), 300 s (weather glance), or after errors. Refresh pin alone can wake from sleep.
hours
001
No Always-On Wi-Fi
The radio is active for the duration of one fetch cycle only. No persistent MQTT connection. No mDNS broadcast. No OTA listener. The device is not addressable on the network outside of its own initiated connection window.
Wi‑Fi on only during setup() · off before sleep
002
Weather Mode via Button
A short press on the refresh input while showing the calendar fetches ?view=weather_json from the same script URL (GAS calls OpenWeather), draws the screen on-device, and sleeps for 300 s. A timer wake then reloads the calendar and returns to the normal twice-daily schedule. Press again during weather to switch back immediately.
300 s timer wake → calendar · or manual toggle
003
Scheduled Refresh Cadence
After a calendar refresh, firmware computes seconds until the next 00:01 or 12:00 (Europe/Warsaw) and sets one deep-sleep timer — not two fixed hardware alarms. NTP sync runs on the scheduled calendar path. GPIO refresh and config long-press at boot are additional wake paths.
Target times: 00:01 · 12:00 · refresh GPIO · config hold
004
E-Paper Bistability
The 7.5″ bistable e-paper panel retains its pixel state without power after a refresh. Once drawn, the image stays visible whether the MCU is asleep or unpowered — the ambient “always there” effect does not require a backlight or active display driver.
Display quiescent current: 0 µA