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
Updated

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
Components needed
| Component | Type | Qty | Buy |
|---|---|---|---|
| ESP32 development board | board | 1 | |
| 5 V power supply (2 A) | other | 1 | €1.65 |
| Male-to-male jumper wires | other | 6 | |
| USB data cable for ESP32 | other | 1 | |
| CSN-A2 Thermal Printer | actuator | 1 |
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
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.
- A 5 V / 2 A phone-style wall adapter with a barrel jack or screw terminals is the most common choice. A 5 V buck module from a USB wall brick also works on the bench.
- Do not power VH from the ESP32 3.3 V or 5 V regulator — the inrush current will brown out the MCU.
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).
- Using UART2 keeps GPIO1/GPIO3 free for USB serial upload and Serial Monitor debugging.
- This sketch uses 9600 baud; confirm with the printer self-test slip if yours was reconfigured.
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.
- The sketch uses SNTP after Wi-Fi connects; allow a few seconds after boot before the first print on cold start.
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.
- If the printer only buzzes or feeds blank paper, power VH closer to 9 V often improves contrast.
Pin assignments
| Pin | Connection | Type |
|---|---|---|
| 5V | 5v-printer-power-supply_0 +5V → Printer VH (pin 3) | POWER |
| GND | 5v-printer-power-supply_0 GND → Printer GND and ESP32 GND | GROUND |
| 5V | csn-a2-thermal-printer_0 VH | POWER |
| GND | csn-a2-thermal-printer_0 GND | GROUND |
| GPIO 4 | csn-a2-thermal-printer_0 RX | UART |
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.ioReady to build this?
Open this project in Schematik to get the full wiring diagram, pin assignments, and deployable code for the Morning Thermal Printer.
Open in Schematik →