How to Build a Morning Thermal Printer with ESP32

CSN-A2 on GPIO 4/5, separate 5 V printer supply, and a Wi-Fi daily briefing slip

ESP32Smart HomeBeginner45 minutes5 components

Updated

How to Build a Morning Thermal Printer with ESP32
For illustrative purposes only
On this page

What you'll build

This guide walks through a daily briefing printer: an ESP32 that connects to Wi-Fi once, syncs time, pulls today’s forecast from Open-Meteo (no API key), optionally reads Google Calendar events with an API key, and prints a receipt with the date, weather, appointments, and blank to-do lines. The hardware matches the Week 4 workshop build. You can watch the build on YouTube or read the workshop write-up (sign-in may be required if the video is private).

Gather these parts before you wire anything:

  • ESP32-WROOM-32 dev board (or equivalent ESP32 DevKit) — powered from USB while you flash and debug
  • CSN-A2 (or compatible) mini thermal receipt printer
  • 5 V power supply for the printer — a 5 V wall charger / adapter (phone-charger style) or bench supply rated for about 2 A. This feeds the printer VH pin only
  • Breadboard and jumper wires
  • Common ground between the ESP32 supply and the printer supply (jumper the GND rails together)

The printer and the ESP32 use two separate power sources. USB is enough for the microcontroller; the thermal head needs its own 5 V charger. Do not power the printer from the ESP32’s 5 V pin or 3.3 V rail — print current spikes will brown out the board and uploads will fail.

Wire the printer’s TTL RX (pin 5) to ESP32 GPIO 4 (UART2 TX) and, if you want status read-back, the printer TX (pin 6) to GPIO 5 — keeping GPIO1/GPIO3 free for USB upload and Serial at 115200 baud. Connect printer VH (power-side pin 3) to the +5 V output of your charger; tie printer GND (pins 1 and 4) to ESP32 ground. Many CSN-A2 units accept 5–9 V on VH, but a 5 V / 2 A adapter is the usual choice. The starter sketch uses 9600 baud on HardwareSerial(2) with the Adafruit Thermal Printer and ArduinoJson libraries. After one successful print it calls esp_deep_sleep_start(), so each power-on or reset produces a fresh slip — handy on a bedside outlet timer.

Fill in WIFI_SSID, WIFI_PASSWORD, GOOGLE_API_KEY, and CALENDAR_ID before flashing. Amsterdam coordinates and timezone defaults match the workshop; change WEATHER_LAT, WEATHER_LON, and TZ_OFFSET_SEC for your city. For another Wi-Fi desk display without paper, see the ESP32 Weather Station guide.

Wiring diagram

Wiring diagram

Interactive wiring diagram

Components needed

ComponentTypeQtyBuy
ESP32 development boardboard1
5 V power supply (2 A)other1€1.65
Male-to-male jumper wiresother6
USB data cable for ESP32other1
CSN-A2 Thermal Printeractuator1

Prices and availability are indicative and may have been updated by the supplier. Schematik may earn a commission from purchases made through affiliate links.

Assembly

1

Power the CSN-A2 safely

Connect the printer VH input (power-side pin 3) to a dedicated 5 V power supply rated for about 1.5–2 A during printing (a 5 V / 2 A wall adapter is a common choice). Tie the printer power GND (pin 1) and TTL GND (pin 4) to the same ground as your ESP32. Power the ESP32 separately over USB.

2

Wire TTL serial on UART2

Connect the printer TTL RX (pin 5) to ESP32 GPIO 4 (UART2 TX). Share ground between the printer TTL side and the ESP32. Leave the printer TX pin unwired unless you need status read-back (its 5 V level needs a divider or level shifter for an ESP32 RX).

3

Set Wi-Fi credentials and print time

Edit WIFI_SSID and WIFI_PASSWORD in the sketch, then adjust PRINT_HOUR and PRINT_MINUTE to the local clock time you want a ticket each day. Flash over USB from Schematik or the Arduino IDE.

4

Verify the morning slip

Open Serial Monitor at 115200 baud. After Wi-Fi and time sync you should see a test slip print once, then a matching slip each day at the configured hour. If nothing prints, re-check VH power, RX to GPIO 4, and shared ground.

Pin assignments

PinConnectionType
5V5v-printer-power-supply_0 +5V Printer VH (pin 3)POWER
GND5v-printer-power-supply_0 GND Printer GND and ESP32 GNDGROUND
5Vcsn-a2-thermal-printer_0 VHPOWER
GNDcsn-a2-thermal-printer_0 GNDGROUND
GPIO 4csn-a2-thermal-printer_0 RXUART

Code

// ESP32 Daily Briefing Printer
// Connects to Wi-Fi, fetches weather from Open-Meteo, fetches Google Calendar events,
// then prints a daily briefing with date, weather, appointments, and to-do lines.

#include <Arduino.h>
#include <WiFi.h>
#include <HTTPClient.h>
#include <ArduinoJson.h>
#include "Adafruit_Thermal.h"
#include <time.h>
#include <esp_sleep.h>

// ─── USER CONFIGURATION ──────────────────────────────────────────────────────
#define WIFI_SSID        "YOUR_WIFI_SSID"
#define WIFI_PASSWORD    "YOUR_WIFI_PASSWORD"

// Google Calendar API
// 1. Create a project at console.cloud.google.com
// 2. Enable the Calendar API
// 3. Create an API key (restricted to Calendar API)
// 4. Share your calendar publicly (view only) or use OAuth (beyond MVP scope)
#define GOOGLE_API_KEY   "YOUR_GOOGLE_API_KEY"
#define CALENDAR_ID      "YOUR_CALENDAR_ID"   // e.g. "primary" or full email

// Open-Meteo — Amsterdam coordinates (no API key required)
#define WEATHER_LAT      "52.3676"
#define WEATHER_LON      "4.9041"

// NTP
#define NTP_SERVER       "pool.ntp.org"
#define TZ_OFFSET_SEC    3600   // UTC+1 (Amsterdam CET); adjust for DST manually or use TZ string
#define TZ_DST_SEC       3600   // additional DST offset (1 h in summer)

// Number of to-do lines to print
#define TODO_LINES       5
// ─────────────────────────────────────────────────────────────────────────────

// Pin definitions
#define PRINTER_RX_PIN   4    // ESP32 TX → Printer RX
#define PRINTER_TX_PIN   5    // Printer TX → ESP32 RX (optional / status)

// Printer baud rate
#define PRINTER_BAUD     9600

// Hoisted type definitions
struct WeatherData {
    float tempMin;
    float tempMax;
    float precipitation;
    int   weatherCode;
    bool  valid;
};

struct CalEvent {
    char summary[64];
    char timeStr[32];   // "HH:MM" or "All day"
};

HardwareSerial printerSerial(2);
Adafruit_Thermal printer(&printerSerial);

// ─── WMO weather code → short description ────────────────────────────────────
const char* weatherDescription(int code) {
    if (code == 0)              return "Clear sky";
    if (code == 1)              return "Mainly clear";
    if (code == 2)              return "Partly cloudy";
    if (code == 3)              return "Overcast";
    if (code >= 45 && code <= 48) return "Foggy";
    if (code >= 51 && code <= 55) return "Drizzle";
    if (code >= 61 && code <= 65) return "Rain";
    if (code >= 71 && code <= 75) return "Snow";
    if (code >= 80 && code <= 82) return "Rain showers";
    if (code >= 95 && code <= 99) return "Thunderstorm";
    return "Unknown";
}

bool connectWifi() {
    Serial.printf("Connecting to %s", WIFI_SSID);
    WiFi.begin(WIFI_SSID, WIFI_PASSWORD);
    int attempts = 0;
    while (WiFi.status() != WL_CONNECTED && attempts < 30) {
        delay(500);
        Serial.print(".");
        attempts++;
    }
    if (WiFi.status() == WL_CONNECTED) {
        Serial.println(" connected!");
        return true;
    }
    Serial.println(" FAILED.");
    return false;
}

void syncTime() {
    configTime(TZ_OFFSET_SEC, TZ_DST_SEC, NTP_SERVER);
    Serial.print("Syncing time");
    struct tm ti;
    int attempts = 0;
    while (!getLocalTime(&ti) && attempts < 20) {
        delay(500);
        Serial.print(".");
        attempts++;
    }
    Serial.println(getLocalTime(&ti) ? " done." : " FAILED.");
}

WeatherData fetchWeather() {
    WeatherData wd = {0, 0, 0, 0, false};
    if (WiFi.status() != WL_CONNECTED) return wd;

    HTTPClient http;
    String url = "https://api.open-meteo.com/v1/forecast"
                 "?latitude=" WEATHER_LAT
                 "&longitude=" WEATHER_LON
                 "&daily=temperature_2m_max,temperature_2m_min,precipitation_sum,weathercode"
                 "&timezone=Europe%2FAmsterdam"
                 "&forecast_days=1";

    http.begin(url);
    int httpCode = http.GET();
    if (httpCode != 200) {
        Serial.printf("Weather HTTP error: %d\n", httpCode);
        http.end();
        return wd;
    }

    String payload = http.getString();
    http.end();

    DynamicJsonDocument doc(2048);
    DeserializationError err = deserializeJson(doc, payload);
    if (err) {
        Serial.print("Weather JSON error: ");
        Serial.println(err.c_str());
        return wd;
    }

    JsonObject daily = doc["daily"];
    wd.tempMax       = daily["temperature_2m_max"][0].as<float>();
    wd.tempMin       = daily["temperature_2m_min"][0].as<float>();
    wd.precipitation = daily["precipitation_sum"][0].as<float>();
    wd.weatherCode   = daily["weathercode"][0].as<int>();
    wd.valid         = true;

    Serial.printf("Weather: code=%d  min=%.1f  max=%.1f  precip=%.1f\n",
                  wd.weatherCode, wd.tempMin, wd.tempMax, wd.precipitation);
    return wd;
}

#define MAX_EVENTS 10

int fetchCalendarEvents(CalEvent events[], int maxEvents) {
    if (WiFi.status() != WL_CONNECTED) return 0;

    struct tm ti;
    if (!getLocalTime(&ti)) return 0;

    char timeMin[32], timeMax[32];
    struct tm startDay = ti;
    startDay.tm_hour = 0; startDay.tm_min = 0; startDay.tm_sec = 0;
    struct tm endDay   = ti;
    endDay.tm_hour   = 23; endDay.tm_min = 59; endDay.tm_sec = 59;

    strftime(timeMin, sizeof(timeMin), "%Y-%m-%dT00:00:00Z", &startDay);
    strftime(timeMax, sizeof(timeMax), "%Y-%m-%dT23:59:59Z", &endDay);

    String calId = String(CALENDAR_ID);
    calId.replace("@", "%40");

    String url = "https://www.googleapis.com/calendar/v3/calendars/";
    url += calId;
    url += "/events?key=";
    url += GOOGLE_API_KEY;
    url += "&timeMin=";
    url += timeMin;
    url += "&timeMax=";
    url += timeMax;
    url += "&singleEvents=true&orderBy=startTime&maxResults=";
    url += maxEvents;

    HTTPClient http;
    http.begin(url);
    int httpCode = http.GET();
    if (httpCode != 200) {
        Serial.printf("Calendar HTTP error: %d\n", httpCode);
        http.end();
        return 0;
    }

    String payload = http.getString();
    http.end();

    DynamicJsonDocument doc(8192);
    DeserializationError err = deserializeJson(doc, payload);
    if (err) {
        Serial.print("Calendar JSON error: ");
        Serial.println(err.c_str());
        return 0;
    }

    JsonArray items = doc["items"].as<JsonArray>();
    int count = 0;
    for (JsonObject item : items) {
        if (count >= maxEvents) break;

        const char* sum = item["summary"] | "(No title)";
        strncpy(events[count].summary, sum, 63);
        events[count].summary[63] = '\0';

        const char* startDt = item["start"]["dateTime"] | "";
        if (strlen(startDt) >= 16) {
            snprintf(events[count].timeStr, sizeof(events[count].timeStr),
                     "%c%c:%c%c", startDt[11], startDt[12], startDt[14], startDt[15]);
        } else {
            strncpy(events[count].timeStr, "All day", sizeof(events[count].timeStr));
        }
        count++;
    }

    Serial.printf("Calendar: %d event(s) fetched.\n", count);
    return count;
}

void printBriefing(WeatherData& wd, CalEvent events[], int eventCount) {
    struct tm ti;
    getLocalTime(&ti);
    char dateStr[32];
    strftime(dateStr, sizeof(dateStr), "%A, %d %B %Y", &ti);

    printer.wake();
    printer.setDefault();

    printer.justify('C');
    printer.boldOn();
    printer.setSize('L');
    printer.println("DAILY BRIEFING");
    printer.setSize('S');
    printer.boldOff();
    printer.println(dateStr);
    printer.feed(1);

    printer.justify('L');
    printer.boldOn();
    printer.println("--- WEATHER (Amsterdam) ---");
    printer.boldOff();

    if (wd.valid) {
        char line[64];
        printer.println(weatherDescription(wd.weatherCode));
        snprintf(line, sizeof(line), "High: %.1f C   Low: %.1f C", wd.tempMax, wd.tempMin);
        printer.println(line);
        snprintf(line, sizeof(line), "Precipitation: %.1f mm", wd.precipitation);
        printer.println(line);
    } else {
        printer.println("(Weather unavailable)");
    }
    printer.feed(1);

    printer.boldOn();
    printer.println("--- TODAY'S APPOINTMENTS ---");
    printer.boldOff();

    if (eventCount == 0) {
        printer.println("No events today.");
    } else {
        for (int i = 0; i < eventCount; i++) {
            char line[80];
            snprintf(line, sizeof(line), "[%s] %s", events[i].timeStr, events[i].summary);
            if (strlen(line) <= 32) {
                printer.println(line);
            } else {
                char l1[48], l2[48];
                snprintf(l1, sizeof(l1), "[%s]", events[i].timeStr);
                snprintf(l2, sizeof(l2), "  %s", events[i].summary);
                printer.println(l1);
                printer.println(l2);
            }
        }
    }
    printer.feed(1);

    printer.boldOn();
    printer.println("--- TO DO TODAY ---");
    printer.boldOff();

    for (int i = 0; i < TODO_LINES; i++) {
        printer.println("[ ] ____________________________");
    }
    printer.feed(3);

    printer.sleep();
    Serial.println("Print complete.");
}

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

    Serial.println("=== Daily Briefing Printer ===");

    printerSerial.begin(PRINTER_BAUD, SERIAL_8N1, PRINTER_TX_PIN, PRINTER_RX_PIN);
    printer.begin();

    if (!connectWifi()) {
        printer.wake();
        printer.println("Wi-Fi connection failed!");
        printer.feed(2);
        printer.sleep();
        Serial.println("Halting.");
        while (true) delay(1000);
    }

    syncTime();

    WeatherData wd = fetchWeather();

    CalEvent events[MAX_EVENTS];
    int eventCount = fetchCalendarEvents(events, MAX_EVENTS);

    printBriefing(wd, events, eventCount);

    WiFi.disconnect(true);
    WiFi.mode(WIFI_OFF);
    Serial.println("Done. Deep sleeping...");
    esp_deep_sleep_start();
}

void loop() {
}

// Run this and build other cool things at schematik.io
Libraries: Adafruit Thermal Printer Library, ArduinoJson