Understadning Thermal Printers and How to Use it with ESP32

Published  August 14, 2025   0
ESP32 Thermal Printer - Print Invoice, Receipts, Barcode, QR code, Images etc

Thermal printer technology has revolutionized receipt printing in retail, healthcare, and IoT projects. This comprehensive ESP32 thermal printer tutorial covers how to interface popular thermal printers, such as the PNP-500 with ESP32 and Arduino microcontrollers. Whether you're building an ESP32 receipt printer, barcode printer, or QR code printing system, this guide provides complete circuit diagrams, Arduino code, and step-by-step instructions for your microcontroller projects.

Thermal printers are widely used in point-of-sale systems, medical equipment, and embedded applications because they require no ink, operate silently, and offer fast printing speeds. By interfacing a thermal printer with ESP32, you can create powerful IoT printing solutions for smart home automation, industrial monitoring, or custom receipt printing systems. Previously, we have also covered how to use a thermal printer with Arduino Uno and how to use a thermal printer with Raspberry Pi. You can check them out if you are interested. 

Basics of a Thermal Printer

Thermal printing is a non-impact printing method that uses heat to produce images or text on specially coated heat-sensitive paper, known as thermal paper. It prints by passing the thermal paper over a print head that is specifically designed for this purpose, which has heating elements arranged in a fine line or dot matrix. When specific areas of the thermal paper are heated, a chemical reaction causes those spots to darken, creating the required print.

ESP32 Thermal Printer Closeup

The thermal paper for this printer starts from 57mm, 76mm, 80mm, up to 110mm in width, and ranges from 15m to 70m in length. So, depending on the machine’s capability, specific widths and lengths of paper are used.

Thermal Printer Types

TypeMedia UsedKey Features/Use CasesTypical Applications
Direct ThermalHeat-sensitive paperNo ink/toner, simple, low maintenance, silent, prints may fade over timeReceipts, shipping labels, barcode labels, tickets
Thermal TransferPlain paper + ink ribbonMore durable prints, uses ink ribbon, can print in color, longer-lasting printsProduct labels, ID tags, asset tracking, wristbands, archival labels

Direct thermal printing is most commonly used where the printed information does not need to last long, such as retail POS receipts, event tickets, and short-term barcode labels.

On the other hand, thermal transfer printing is preferred for applications that require more durable and long-lasting prints. This includes labelling for chemicals and outdoor products, as well as patient identification in healthcare settings.

Advantages

  1. No ink or toner required (for direct thermal printers).
  2. Low maintenance due to fewer moving parts.
  3. Silent and fast operation.
  4. Simple electronic interface suitable for microcontroller integration.

Disadvantages

  1. Prints can fade with heat, light, or friction (primarily direct thermal).
  2. Limited to monochrome in most models.
  3. Special thermal paper is required.

PNP-500 Thermal Printer for ESP32 and Arduino Projects

The PNP-500 is a compact, panel-mount thermal printer that's simple to use and highly reliable, making it a favorite for industrial and measurement applications. You'll often find it integrated into diagnostic equipment, control panels, and weighing scales, where space is tight and speed matters.

There is another printer available, the RS203, which is quite similar to the PNP-500. So, this ESP32 thermal printer tutorial also works with the RS203 model.

Thermal Printer Top and Side View

Key Features of PNP 500 Thermal Printer

⇒ Printing Method: Direct thermal line printing. This means it prints by selectively heating thermal paper.
⇒ Paper Width: 57mm, perfect for standard receipts and data logs.
⇒ Print Width: 48mm.
⇒ Print Speed: 50–80mm/sec, offering fast output for most needs.
⇒ Resolution: 8 dots/mm (384 dots/line), ensuring crisp text and barcodes.
⇒ Interfaces: Serial (RS232C, TTL) and USB, so you have options for how to connect.
⇒ Operating Voltage: 5–9V DC, and can handle up to 12V.
⇒ Dimensions: 76.8×77.4×47.6mm (WxDxH)—small enough to fit in tight control panels.
⇒ Max Paper Roll: 58mm wide, 40mm diameter, which covers most long-printing needs.
⇒ Print Head Life: Up to 50km of printing, so it's built to last.

Dimensions of PNP500 Thermal Printer

The physical dimensions of this printer can be found in the image below.

Dimensions Thermal Printer

With all these outlined dimensions, it's easy for you to plan the cutout for the enclosure in your project or 3d printable case, etc.

Hardware Overview: PNP-500 Thermal Printer

Now let’s do a deep dive into the hardware parts and their working. To make this clear, I have disassembled the printer completely so that everything is easy to understand.
Below, you can see the fully disassembled thermal printer ESP32 setup.

ESP32 Thermal Printer Disassembled

Be careful if you also plan to disassemble it for learning purposes. Make sure the plastic hinges are opened correctly, or they might break easily. This requires solid patience.

Now, let’s discuss more about the parts of the printer.
This PNP-500 thermal printer has a total of 6 most important parts to understand. They are,

1. Main control PCB

There is only one PCB in this printer that handles all the functions. So, let’s understand the PCB, starting from the top view.

Thermal Printer with ESP32 Pinout Controller PCB Top View

In the image above with marked parts, you can identify the purpose of each I/O pin for for interfacing thermal printer with the ESP32 pinout. For communication, this module provides two options: RS232 and TTL. If you are using a microcontroller, the TTL port is very easy to integrate.

You can also find the power input pin. This supports a wide voltage range of 5V–9V, or 12V DC, as stated in the paper. However, I didn’t get perfect printing results with 5V. A voltage above 6V gave me the right darkness. Specifically, I used a 2S Li-ion battery pack to power the module.

Understanding the interfacing thermal printer with the ESP32 pinout makes it easier to get a perfect connection, and you can follow the complete wiring setup in the thermal printer ESP32 wiring diagram.

LED and KEY close up of Thermal Printer PCB

Next, there is an LED marked as D2, which is a status LED. Its status indicators are as follows:

  • Blinks once: Working properly

  • Blinks twice: No printer detected

  • Blinks three times: No paper detected

  • Blinks five times: Printer mechanism is overheating

  • Blinks ten times: No Chinese character stock IC detected

Next, there is a button marked as K1. It has two functionalities:

After pressing it while powering on the device, a test print page is printed, which looks like the following

Test Print Page in ESP32 Thermal Printer

In the test page,

  • First, the character code table is printed.

  • Then, demo text for three font sizes is printed (9x24, 9x17, 8x16).

  • Followed by information such as Baud rate, Degree (head temperature), Voltage, Heat Density (darkness of print), Heat Dot (number of heating elements activated simultaneously), ON/OFF timing, Firmware version, and finally, a QR code indicating "QRCode Support!".

ESP32 Thermal Printer Controller PCB Bottom View

In the image above, you can see the bottom view of the PCB. There are no I/O ports here—just the main ICs:

  • BY25D16 – A 16MB NOR-type Flash Memory, probably used to store important configuration data and large functions.

  • PT5139A – A dual H-Bridge motor driver IC used to drive the stepper motor for the paper feed mechanism.

  • TPT3232E – An RS232 transceiver IC used for RS232 communication.

  • ARM-based 32-bit MCU – The exact model or part number is unknown, but it serves as the main processor of the printer, running on a 3.3V supply.

2. Print Head

The print head is special, as it’s the main component responsible for the printing process. It’s a tiny mechanism. Practically, there are 384 individual heating elements, which produce 384 individual dots. You can see them in the image below. We verified the count ourselves to confirm it.

Print Head of Thermal Printer ESP32

The print head already includes the required shift registers, embedded in the black region below the heating area, which is an IC. This helps reduce the number of control pins to just 6, along with additional GND and power pins.

This thermal print head also includes a thermistor for heat measurement. Along with this, there is a paper detection sensor and a servo motor that connect to the main PCB via the same ribbon cable.

Be careful with the ribbon cable; even though it’s flexible, it can tear easily.


3. Spring Suspension Behind the Print Head

In the image below, you can see the two springs connected to the thermal print head.

Back Side Spring Suspension

More than just connecting, the springs are actually used to push the print head against the feeding paper, regardless of its thickness or texture. The goal is to ensure all heating elements make proper contact with the paper for consistent printing.

4. Paper Feeding Mechanism

The paper feeding mechanism consists of five gears, two shafts (connected to the motor and the rubberised roller), a stepper motor, and a rubberised roller.

Below, you can see a close-up of the stepper motor and gear assembly, where a small metal gear is directly connected to the stepper motor. The remaining three gears connect the motor shaft to the main roller shaft.

Stepper Motor and the Gears of Thermal Printer using ESP32

The gear ratios are pre-calculated and stored in the system. Their purpose is to precisely feed the paper line-by-line, dot-by-dot, according to timing, which ensures perfect printing output.

In the image below, you can see the last gear connected to the roller shaft.

Roller and the Paper Feed Path of Thermal Printer using ESP32

Notice that the roller is attached to the open/close cover of the paper roll storage. When closed, it provides the necessary friction to feed the paper from the back, where the texture is a bit rough, which helps feed the paper smoothly without damage.

Moreover, the coated side of the paper is smoother, allowing it to slide easily toward the print head.

5. IR-Based Paper Presence Detection System

This paper presence detection system is straightforward. It uses an IR-based proximity sensor to detect paper.

The concept is simple. IR light gets reflected only from white, glossy surfaces. If the surface is darker or rough, the photodiode doesn’t receive the reflected light, indicating no paper.

Paper Presence Sensor

Above, you can see the location and physical appearance of the sensor in the thermal printer.

The paper storage can support up to a maximum of 40mm diameter rolls, which equals approximately 16 to 20 meters in paper length.

Below, you can see a clear picture representing the paper roll storage area.

Paper Storage of ESP32 Thermal Printer

How to program a Thermal Printer?

To be more specific, operating such modules is simpler than you think. There’s already a whole lot of command instructions built into the module. Based on the model of the printer you have, you can find the list of commands in the manual or datasheet of the module. These commands are sent to the printer using any of the communication ports available on the module.

In our case, the PNP-500 has TTL (UART) and RS232 ports. Some modules may also have a USB. That’s all!

Now, let me show you the list of commands available for the PNP-500 module.

Basic Control Commands for Thermal Printer

FunctionCommandASCIIHex CodeParametersParameter ValuesExample
Print and feed n linesESC d nESC d0x1B 0x64 nn = linesn = 0-255 lines0x1B 0x64 0x03 (feed 3 lines)
Select justificationESC a nESC a0x1B 0x61 nn = alignment0=Left, 1=Center, 2=Right0x1B 0x61 0x01 (center)
Upside-down printingESC { nESC {0x1B 0x7B nn = mode0=Normal, 1=Upside-down0x1B 0x7B 0x01 (enable)
Underline modeESC - nESC -0x1B 0x2D nn = thickness0=Off, 1=1-dot, 2=2-dot0x1B 0x2D 0x02 (2-dot)
Inverse print modeGS B nGS B0x1D 0x42 nn = mode0=Normal, 1=Inverse0x1D 0x42 0x01 (white on black)
Set line spacingESC 3 nESC 30x1B 0x33 nn = dotsn = 0-255 dots0x1B 0x33 0x00 (0 dots)

Thermal Printer - Print Quality Control Commands

FunctionCommandASCIIHex CodeParametersParameter CalculationExample
Print density & heat controlDC2 # nDC2 #0x12 0x23 nn = combinedBits 7-5: breakTime/250μs<br>Bits 4-0: (density%-50)/50x12 0x23 0x9F (high)<br>0x12 0x23 0x58 (reset)

Below, you can refer to the Density Calculation Formula,

densityLevel = (densityPercent - 50) / 5  // Range: 0-31
breakTimeLevel = breakDelayUs / 250       // Range: 0-7
n = (breakTimeLevel << 5) | (densityLevel & 0x1F)

Commands to Print Bitmap Image 

FunctionCommandASCIIHex CodeParametersParameter DetailsData Format
Print raster bitmapGS v 0 m xL xH yL yHGS v 00x1D 0x76 0x30 m xL xH yL yH [data]m=mode, x=width bytes, y=height dotsm=0 (normal density)<br>xL,xH = bytesPerLine (little-endian)<br>yL,yH = height (little-endian)1-bit bitmap, 8 pixels/byte
Print bitmap line-by-lineDC2 * r nDC2 *0x12 0x2A r n [data]r=lines, n=bytesr=1 (single line)<br>n=bytes per lineSend for each image line

Below, you can see the Bitmap Size Calculation that is used in the code.

bytesPerLine = (width + 7) / 8
xL = bytesPerLine & 0xFF
xH = (bytesPerLine >> 8) & 0xFF
yL = height & 0xFF  
yH = (height >> 8) & 0xFF

Now, below you can see the complete sequence used for the Bitmap Image Printing.

1. 0x1B 0x33 0x00                              // Line spacing: 0
2. // For each 24-line chunk:
3. 0x1D 0x76 0x30 0x00 xL xH yL yH [chunk]     // Print chunk
4. 0x1B 0x64 0x03                              // Feed 3 lines at end

Thermal Printer Barcode Configuration Commands

First, you need to do a little configuration before printing the barcode.

FunctionCommandASCIIHex CodeParametersParameter ValuesExample
Set barcode heightGS h nGS h0x1D 0x68 nn = heightn = 1-255 dots0x1D 0x68 0x50 (80 dots)
Set barcode widthGS w nGS w0x1D 0x77 nn = widthn = 2-6 (multiplier)0x1D 0x77 0x02 (2× width)
HRI text positionGS H nGS H0x1D 0x48 nn = position0=None, 1=Above, 2=Below, 3=Both0x1D 0x48 0x02 (below)

After finishing up with the configuration, we can print the barcode,

FunctionCommandASCIIHex CodeParametersBarcode TypesData Requirements
Print barcodeGS k m n [data]GS k0x1D 0x6B m n [data]m=type, n=length, data=content0x41=UPC-A<br>0x43=EAN13<br>0x49=CODE128UPC-A: 11 digits<br>EAN13: 12 digits<br>CODE128: variable

Below you can see the example commands for barcode printing.

1. CODE128: 0x1D 0x6B 0x49 0x06 "123456"
2. UPC-A:   0x1D 0x6B 0x41 0x0B "12345678901"
3. EAN13:   0x1D 0x6B 0x43 0x0C "123456789012"

Now, below you can see the complete sequence used for the barcode.
0x1D 0x68 0x50                    // Height: 80 dots
0x1D 0x77 0x02                    // Width: 2x
0x1D 0x48 0x02                    // HRI below
0x1D 0x6B 0x49 n [data]           // Print barcode (n=data length)

For advanced projects requiring barcode scanning capabilities, you can also consider interfacing USB barcode scanner with Raspberry Pi 4 to read 2D barcodes, which works seamlessly with thermal printer systems.

QR Code Commands for Thermal Printer

StepFunctionCommandASCIIHex CodeParametersParameter Details
1Set QR modelGS ( k 4 0 1 A 2 0GS ( k 4 NUL 1 A 2 NUL0x1D 0x28 0x6B 0x04 0x00 0x31 0x41 0x32 0x00Fixed sequenceModel 2 (standard)
2Set module sizeGS ( k 3 0 1 C nGS ( k 3 NUL 1 C n0x1D 0x28 0x6B 0x03 0x00 0x31 0x43 nn = module sizen = 3-16 pixels
3Set error correctionGS ( k 3 0 1 E nGS ( k 3 NUL 1 E n0x1D 0x28 0x6B 0x03 0x00 0x31 0x45 nn = error level48=L(~7%), 49=M(~15%)<br>50=Q(~25%), 51=H(~30%)
4Store QR dataGS ( k pL pH 1 P 0 [data]GS ( k pL pH 1 P 0 [data]0x1D 0x28 0x6B pL pH 0x31 0x50 0x30 [data]pL,pH = data length+3pL,pH in little-endian
5Print QR codeGS ( k 3 0 1 Q 0GS ( k 3 NUL 1 Q 00x1D 0x28 0x6B 0x03 0x00 0x31 0x51 0x30Fixed sequenceExecute stored QR

Below you can see the QR Code Data Length Calculation,

len = data.length() + 3
pL = len & 0xFF
pH = (len >> 8) & 0xFF

Also, remember, QR codes come with four error correction levels that determine how much of the code can be restored if it becomes dirty or damaged. Level L (hex value 0x30) offers about 7% recovery and is suitable for clean environments. Level M (0x31) provides around 15% recovery and is the default choice for general use. Level Q (0x32) allows for 25% recovery, making it ideal for cases where moderate damage might occur. Finally, Level H (0x33) offers the highest protection with 30% recovery, recommended for scenarios where the QR code is at high risk of damage.

Now below you can see the complete sequence used for the QR Code.

1. 0x1D 0x28 0x6B 0x04 0x00 0x31 0x41 0x32 0x00        // Set model 2
2. 0x1D 0x28 0x6B 0x03 0x00 0x31 0x43 0x06             // Module size 6
3. 0x1D 0x28 0x6B 0x03 0x00 0x31 0x45 0x31             // Error level M
4. 0x1D 0x28 0x6B pL pH 0x31 0x50 0x30 [data]         // Store data
5. 0x1D 0x28 0x6B 0x03 0x00 0x31 0x51 0x30             // Print QR

More than the number of functions listed above, you can still find additional commands in the official datasheet. In the above section, I have covered only the essential commands.
Now, let’s move on to practical testing.

Hardware Required

For our ESP32 Thermal Printer demonstration, we will be using the ESP32 as the main microcontroller, which is the only hardware required. Still, to make the demonstration easier, I added two button inputs, which will help us trigger certain print commands.

  • ESP32 Development Board (Any Model/Brand) – 1
  • Thermal Printer (PNP-500 / RS203) – 1
  • Resistors (1KΩ) – 2
  • Connecting Wires – As Required
  • Breadboard – 1
  • 2S Li-Ion Battery – 1 (For powering the printer)
  • Push Buttons – 2 (Optional)

These components should be enough for the demonstration. As I usually say, you're not limited to just these; feel free to expand or modify based on your needs.

Circuit Diagram for ESP32 Thermal Printer

For interfacing the PNP-500 thermal printer with the ESP32 Dev Module, we’ll be using the TTL communication port of the printer. This allows us to use the UART interface of the ESP32. I suggest using the hardware UART for the best performance.

ESP32 Thermal Printer Circuit Diagram

In the ESP32 thermal printer circuit diagram above, you can see the connections between components. I’ve used two pull-up resistors on the RX and TX data lines. Their main purpose is to reduce EM interference or signal noise, especially if you're using long wires.

I’ve also added two push buttons, which when pressed, will start printing. However, you can also send commands via the Serial Monitor to the ESP32 to trigger printing.

As of now, I’m using two separate power supplies: a 2S Li-ion battery pack for the printer, and USB power for the ESP32. But you can also power both using just the Li-ion battery pack by making a simple modification, connecting the battery’s positive terminal to the 5V (or Vin) pin of the ESP32, and the negative terminal to GND.
This setup is clearly shown in the image below.

Thermal Printer ESP32 Circuit Diagram With Common Power Supply

After assembling everything properly, our ESP32 thermal printer circuit is ready for programming. Below, you can see the fully assembled image of our ESP32 thermal printer.

Completely Assembled Thermal Printer ESP32

Code for Interfacing the Thermal Printer with the ESP32

The code for this interface is fairly straightforward, as I’ve included all the necessary functions in the test code. You can find the full code used in this ESP32 Thermal Printer demo on our GitHub repository linked below.

Code - Thermal Printer with the ESP32Code - Thermal Printer with the ESP32 Zip File

Note: In addition to the code, you’ll need bitmap data if you want to print an image on a thermal printer. This web app is particularly useful for converting graphics, we have already used this in our Arduino OLED Eyes Animation project, and also Space Race Game using Arduino

How to Get the Bitmap Binary Code for Printing an Image?

Bitmap Binary Code for Printing an Image

⇒ Step 1:
Open https://javl.github.io/image2cpp/  in any browser.
This web app will help us convert the image into a byte array.

⇒ Step 2:
Upload your image. Make sure its width does not exceed 384px.

⇒ Step 3:
Remember, in thermal printing, the white areas are what get printed. So, invert the image if required.
Also, set the code output format to “Arduino Code; Single bitmap.”

⇒ Step 4:
Click the "Generate Code" button. The byte array will appear in the text box below.
Copy this code and paste it into a file named data.h in your directory.

With that done, we can now move on to reviewing the main code!

ESP32 Code for Thermal Printer

As mentioned earlier, the complete code for this tutorial is on our GitHub repo and also at the bottom of this article. This section explains the important snippets of our ESP32 thermal printer code so that you can modify and adapt it for your own use case. 

1. Libraries and Headers

#include <Arduino.h>
#include "data.h"
#include <esp_heap_caps.h>

Arduino.h: Core Arduino library providing basic functions like pinMode(), digitalWrite(), Serial, etc.

data.h: Custom header file containing bitmap image data stored in PROGMEM (flash memory). This is mainly for keeping the code organised. Even if you paste the byte array directly into the main code, it will still work.

esp_heap_caps.h: ESP32-specific library for advanced memory management and heap monitoring functions

2. Data Structures and Constants

BitmapImage Structure

struct BitmapImage {
 const char* name;           // Human-readable name for the image
 const unsigned char* data;  // Pointer to image data in PROGMEM
 int width;                  // Image width in pixels
 int height;                 // Image height in pixels
};

This structure organises image metadata, making it easy to manage multiple bitmap images with their names and dimensions.

Available Images Array

const BitmapImage availableImages[] = {
 { "circuitDigestLogo", circuitDigestLogo, 360, 360 },
 { "tony", tony, 380, 528 },
 // ... more images
};

This array creates a catalogue of all available images, stored in flash memory to save RAM. Each entry contains the image name, data pointer, and dimensions.

Hardware Pin Definitions

#define RXD2 16  
#define TXD2 17 
#define GND 5    
#define Vcc 21   
#define Key1 22  
#define Key2 23  

These define the ESP32 GPIO pins used for:

  • UART2 (pins 16,17): Serial communication with the thermal printer
  • Power control (pins 5,21): Managing the power supply line of the data line and the push button.
  • Buttons (pins 22,23): Physical user interface for demo functions

Global Variables

bool globalUpsideDown = true;          
unsigned long lastKey1Press = 0;     
unsigned long lastKey2Press = 0;    
int imageIndex = 0;               
const unsigned long debounceDelay = 200;

These variables maintain the system state and handle user input timing.

3. Setup Function - System Initialization

void setup() {

 // Configure power control pins for printer

 pinMode(GND, OUTPUT);

 pinMode(Vcc, OUTPUT);

 digitalWrite(GND, LOW);   // Ensure ground is always low

 digitalWrite(Vcc, HIGH);  // Ensure power is always high
 // Configure button input pins with internal pullup resistors
 pinMode(Key1, INPUT_PULLUP);
 pinMode(Key2, INPUT_PULLUP);
 // Initialize serial communications
 Serial.begin(115200);                               // Debug monitor at high speed
 printerSerial.begin(9600, SERIAL_8N1, RXD2, TXD2); // Printer at standard thermal printer baud rate
 delay(1000);  // Give printer time to initialize
 // Set initial printer alignment to center
 align(1);
 // Print memory status and set initial upside-down state
 printMemoryStats("setup done");
 setGlobalUpsideDown(true);  // Default: enable upside-down printing
}

Let's know what happens during setup.

  • Power Management: Sets up dedicated pins to control the power supply for the signal line of the printer and push buttons.
  • Button Configuration: Enables internal pullup resistors for button inputs (buttons read LOW when pressed)
  • Serial Communication:
    • USB Serial at 115200 baud for debugging/commands
    • Printer Serial at 9600 baud (standard for thermal printers)
  • Printer Initialisation: Waits 1 second for printer boot, sets centre alignment
  • System State: Checks memory usage and enables upside-down printing by default (Just optional).

4. Loop Function - Main Program Cycle

void loop() {
 // Handle serial command input with static buffer for efficiency
 static String inputBuffer = "";
 inputBuffer.reserve(64);  // Pre-allocate string memory to reduce fragmentation
 
 // Process incoming serial data character by character
 while (Serial.available()) {
   char c = Serial.read();
   if (c == '\n' || c == '\r') {
     // End of command - process if buffer has content
     if (inputBuffer.length() > 0) {
       handleSerialCommand(inputBuffer);
       inputBuffer = "";  // Clear buffer for next command
     }
   } else {
     inputBuffer += c;
   }
 }
 // Button handling with debouncing
 // Key1: Print next image in sequence
 if (digitalRead(Key1) == LOW) {
   if (millis() - lastKey1Press > debounceDelay) {
     demoPrintNextImage();
     lastKey1Press = millis();
   }
 }
 // Key2: Print comprehensive demo page
 if (digitalRead(Key2) == LOW) {
   if (millis() - lastKey2Press > debounceDelay) {
     demoPrintAllFormats();
     lastKey2Press = millis();
   }
 }
}


Loop function responsibilities are,

  1. Serial Command Processing: Builds commands character by character, executes when complete
  2. Button Debouncing: Prevents multiple triggers from mechanical button bounce
  3. Demo Functions: Key1 cycles through images, Key2 prints comprehensive demo

5. Custom Functions

  • handleSerialCommand() - Processes text commands from computer (like "QRCODE Hello")
  • setDarknessAndDelay() - Adjusts how dark the print is and printing speed
  • printQRCode() - Creates QR codes for websites, text, and contacts
  • printBarcode_CODE128/UPCA/EAN13() - Prints barcodes for products and inventory
  • printBitmapGS_Method() - Prints images and logos stored in memory
  • feedLines() - Moves paper forward by specified lines
  • align() - Sets text alignment (left, centre, right)
  • upsideDownPrinting() - Flips all printing upside down
  • underlineMode() - Adds underlines to text
  • inverseMode() - Prints white text on a black background
  • printMemoryStats() - Shows how much memory is being used
  • rotateBitmap1bpp_180() - Rotates images 180 degrees for upside-down mode
  • createTestPattern() - Makes test patterns to check printer quality
  • demoPrintNextImage() - Button 1 function - prints next image in list
  • demoPrintAllFormats() - Button 2 function - prints an example of everything the printer can do.

You can find the full code in the GitHub link below.

 ESP32 Thermal Printer in Action

The working demonstration is quite simple, as I’ve already created all the necessary functions. For this demo, I’m using a separate power supply for the thermal printer. The ESP32 is powered and communicates via serial USB-C cable from my laptop.

Demonstration Setup Of The ESP32 Thermal Printer

Once you’ve successfully uploaded the code, it’s time to test the device. Below, you can see the ESP32 thermal printer printing an image.

Tony Stark Printed Working Demonstration ESP32 Thermal Printer

The print quality is pretty good, but we did face several issues while getting the image to print correctly. Most common problems can be solved by cleaning the print head using a soft cloth and isopropyl alcohol. Remaining issues were resolved through multiple code revisions, so you don't have to worry about that.  Also, make sure the printer has a proper power supply. Supplying only 5V results in poor-quality image prints. However, for text printing, 5V should be sufficient.

Here’s another GIF showing a demonstration of text printing, which also includes the Bar Code and QR Code.

ESP32 Thermal Printer Printing Demo Text


As a bonus, I’ve also provided a code snippet that can print a professional GST invoice using this small ESP32 thermal printer. Feel free to test that out as well! Below you can see the GIF Video of the ESP32 receipt printing.

Invoice Printed ESP32 Thermal Printer

With that, we wrap up this tutorial. You’ve learned how thermal printers work, how to interface them, and even how to print custom images and invoices. If you have any doubts or face issues, feel free to ask in the comment section below or start a discussion on any of our community platforms. 

Projects on Thermal Printer Interfacing

Previously, we have built many thermal printer interfacing projects. If you want to know more about those topics, links are given below.

 Thermal Printer Interfacing with Arduino Uno

Thermal Printer Interfacing with Arduino Uno

In this tutorial, learn how to interface a thermal printer with Arduino Uno, using a tactile switch for a push-to-print feature. This step-by-step tutorial covers wiring and setup.

 Thermal Printer interfacing with PIC16F877A

Thermal Printer interfacing with PIC16F877A

Learn how to interface a CSN A1 thermal printer with the popular PIC16F877A microcontroller. This project connects the printer to the PIC microcontroller and uses a tactile switch to trigger printing.

 Smart Shopping Cart with POS Thermal Printer, Barcode Scanner, 20x4 LCD and Raspberry Pi

Smart Shopping Cart with POS Thermal Printer, Barcode Scanner, 20x4 LCD and Raspberry Pi

Build a Smart Shopping Cart using Raspberry Pi 3 by interfacing a thermal receipt printer, LCD, and USB barcode scanner. Retrieve item details like name and price from a Google Spreadsheet for efficient and automated shopping.

 

Complete Project Code

/*
* Thermal Printer Controller for ESP32
* 
* Author: Rithik Krisna M / CircuitDigest.com
* Date: August/2025
* 
* Serial Commands:
* Type 'HELP' for full list of available commands
*/
#include <Arduino.h>
#include "data.h"
#include <esp_heap_caps.h>
// Struct for bitmap images - contains metadata for each available image
struct BitmapImage {
 const char* name;           // Human-readable name for the image
 const unsigned char* data;  // Pointer to image data in PROGMEM
 int width;                  // Image width in pixels
 int height;                 // Image height in pixels
};
// List of all available images - stored in PROGMEM to save RAM
const BitmapImage availableImages[] = {
 { "circuitDigestLarge", circuitDigestLarge, 360, 360 },
 { "tony", tony, 380, 528 },
 { "naruto", naruto, 380, 615 },
 { "circuitDigestSmall", circuitDigestSmall, 200, 56 },
 { "luffy", luffy, 340, 263 },
 { "luffy2", luffy2, 340, 287 },
 { "SpritedAway", SpritedAway, 380, 395 },
 { "Totoro", Totoro, 360, 500 },
 { "minato", minato, 380, 458 },
 { "itachi", itachi, 380, 733 },
 { "starkIndustriesLogo", starkIndustriesLogo, 380, 61 },
 { "AvengersLogo", AvengersLogo, 380, 456 },
 { "gojo", gojo, 380, 609 }
};
// Calculate number of available images at compile time
const int numAvailableImages = sizeof(availableImages) / sizeof(availableImages[0]);
// Hardware pin definitions for ESP32
#define RXD2 16  // UART2 RX pin for printer communication
#define TXD2 17  // UART2 TX pin for printer communication
// Control pins for printer power management
#define GND 5    // Always LOW - printer ground control
#define DTRD 4   // Not used in current implementation
#define Vcc 21   // Always HIGH - printer power control
// Button input pins with internal pullup
#define Key1 22  // Button 1 - cycles through images
#define Key2 23  // Button 2 - prints demo page
// Serial communication setup
HardwareSerial printerSerial(2);  // Use UART2 for printer communication
// Constants for memory and performance optimization
const int IMAGE_WIDTH = 200;   // Default image width (not currently used)
const int IMAGE_HEIGHT = 56;   // Default image height (not currently used)
const unsigned long debounceDelay = 200;  // Button debounce delay in milliseconds
// Global state variables
bool globalUpsideDown = true;          // Global upside-down printing flag
unsigned long lastKey1Press = 0;      // Last time Key1 was pressed (for debouncing)
unsigned long lastKey2Press = 0;      // Last time Key2 was pressed (for debouncing)
int imageIndex = 0;                   // Current image index for button cycling
// Function prototypes - forward declarations for better organization
void printBitmapGS_Method(const unsigned char* progmemData, int width, int height);
void printTestPattern(int width, int height, int pattern);
void handleSerialCommand(String cmd);
void printMemoryStats(const char* tag);
void setGlobalUpsideDown(bool enable);
// Memory diagnostics helper - prints current heap status for ESP32
void printMemoryStats(const char* tag) {
 Serial.print("[MEM] ");
 Serial.print(tag);
 Serial.print(" | FreeHeap: ");
 Serial.print(ESP.getFreeHeap());
 Serial.print(" | MinFreeHeap: ");
 Serial.print(ESP.getMinFreeHeap());
 Serial.print(" | MaxAllocHeap: ");
 Serial.print(ESP.getMaxAllocHeap());
 Serial.print(" | HeapSize: ");
 Serial.println(ESP.getHeapSize());
}
// Set global upside-down printing mode for all output
void setGlobalUpsideDown(bool enable) {
 globalUpsideDown = enable;
 upsideDownPrinting(enable ? 1 : 0);  // Apply to printer immediately
 Serial.print("Global upside-down set to: ");
 Serial.println(enable ? "ON" : "OFF");
}
// Arduino setup function - runs once at startup
void setup() {
 // Configure power control pins for printer
 pinMode(GND, OUTPUT);
 pinMode(Vcc, OUTPUT);
 digitalWrite(GND, LOW);   // Ensure ground is always low
 digitalWrite(Vcc, HIGH);  // Ensure power is always high
 // Configure button input pins with internal pullup resistors
 pinMode(Key1, INPUT_PULLUP);
 pinMode(Key2, INPUT_PULLUP);
 // Initialize serial communications
 Serial.begin(115200);                               // Debug monitor at high speed
 printerSerial.begin(9600, SERIAL_8N1, RXD2, TXD2); // Printer at standard thermal printer baud rate
 delay(1000);  // Give printer time to initialize
 // Set initial printer alignment to center
 align(1);
 // Print memory status and set initial upside-down state
 printMemoryStats("setup done");
 setGlobalUpsideDown(true);  // Default: enable upside-down printing
}
// Main Arduino loop - handles serial commands and button presses
void loop() {
 // Handle serial command input with static buffer for efficiency
 static String inputBuffer = "";
 inputBuffer.reserve(64);  // Pre-allocate string memory to reduce fragmentation
 
 // Process incoming serial data character by character
 while (Serial.available()) {
   char c = Serial.read();
   if (c == '\n' || c == '\r') {
     // End of command - process if buffer has content
     if (inputBuffer.length() > 0) {
       handleSerialCommand(inputBuffer);
       inputBuffer = "";  // Clear buffer for next command
     }
   } else {
     inputBuffer += c;
   }
 }
 // Button handling with debouncing
 // Key1: Print next image in sequence
 if (digitalRead(Key1) == LOW) {
   if (millis() - lastKey1Press > debounceDelay) {
     demoPrintNextImage();
     lastKey1Press = millis();
   }
 }
 
 // Key2: Print comprehensive demo page
 if (digitalRead(Key2) == LOW) {
   if (millis() - lastKey2Press > debounceDelay) {
     demoPrintAllFormats();
     lastKey2Press = millis();
   }
 }
}
// Parse and execute serial commands
void handleSerialCommand(String cmd) {
 cmd.trim();  // Remove leading/trailing whitespace
 if (cmd.length() == 0) return;
 // Split command and parameters for processing
 int spaceIdx = cmd.indexOf(' ');
 String command = (spaceIdx == -1) ? cmd : cmd.substring(0, spaceIdx);
 String params = (spaceIdx == -1) ? "" : cmd.substring(spaceIdx + 1);
 command.toUpperCase();  // Make command case-insensitive
 // Command processing - organized alphabetically for maintainability
 if (command == "HELP") {
   // Display all available commands and their usage
   Serial.println("Available commands:");
   Serial.println("QRCODE <data>");
   Serial.println("BARCODE <type> <data> (CODE128, UPCA, EAN13)");
   Serial.println("ALIGN <0|1|2>");
   Serial.println("DARKNESS <percent 50-205> <delay_us 0-1750>");
   Serial.print("BITMAP <name> (Available:");
   for (int i = 0; i < numAvailableImages; ++i) {
     Serial.print(" ");
     Serial.print(availableImages[i].name);
     if (i < numAvailableImages - 1) Serial.print(",");
   }
   Serial.println(")");
   Serial.println("TESTPATTERN <pattern 0-3>");
   Serial.println("TEXT <text> (bitmap text)");
   Serial.println("TEXTMODE <text> (native text)");
   Serial.println("UPSIDEDOWN <0|1>");
   Serial.println("UNDERLINE <0|1|2>");
   Serial.println("INVERSE <0|1>");
   Serial.println("FEED <n>");
   Serial.println("MEM");
   Serial.println("GLOBALUPSIDEDOWN <0|1> (all output upside down)");
   Serial.println("---");
   return;
 }
 
 if (command == "QRCODE") {
   printQRCode(params);
   return;
 }
 
 if (command == "BARCODE") {
   parseBarcode(params);
   return;
 }
 
 if (command == "ALIGN") {
   int n = params.toInt();
   align(n);
   Serial.println("Align set to: " + String(n));
   return;
 }
 
 if (command == "DARKNESS") {
   // Parse darkness parameters: percentage and delay
   int sp = params.indexOf(' ');
   if (sp == -1) {
     Serial.println("Usage: DARKNESS <percent> <delay_us>");
     return;
   }
   int percent = params.substring(0, sp).toInt();
   int delayus = params.substring(sp + 1).toInt();
   setDarknessAndDelay(percent, delayus);
   Serial.println("Darkness set to: " + String(percent) + "% Delay: " + String(delayus) + "us");
   return;
 }
 
 if (command == "BITMAP") {
   if (params.length() == 0) {
     // Show usage and available bitmaps
     Serial.println("Usage: BITMAP <name>");
     Serial.print("Available:");
     for (int i = 0; i < numAvailableImages; ++i) {
       Serial.print(" ");
       Serial.print(availableImages[i].name);
       if (i < numAvailableImages - 1) Serial.print(",");
     }
     Serial.println();
     return;
   }
   printNamedBitmap(params);
   return;
 }
 
 if (command == "TESTPATTERN") {
   int pattern = params.toInt();
   printTestPattern(100, 50, pattern);
   return;
 }
 
 
 if (command == "TEXTMODE") {
   printerSerial.println(params);
   Serial.println("Printed native text: " + params);
   feedLines(2);
   return;
 }
 
 if (command == "UPSIDEDOWN") {
   int n = params.toInt();
   upsideDownPrinting(n);
   Serial.println("UpsideDown: " + String(n));
   printerSerial.println("Sample: UpsideDown mode " + String(n));
   feedLines(2);
   return;
 }
 
 if (command == "UNDERLINE") {
   int n = params.toInt();
   underlineMode(n);
   Serial.println("Underline: " + String(n));
   printerSerial.println("Sample: Underline mode " + String(n));
   feedLines(2);
   return;
 }
 
 if (command == "INVERSE") {
   int n = params.toInt();
   inverseMode(n);
   Serial.println("Inverse: " + String(n));
   printerSerial.println("Sample: Inverse mode " + String(n));
   feedLines(2);
   return;
 }
 
 if (command == "FEED") {
   int n = params.toInt();
   feedLines(n);
   Serial.println("Feed lines: " + String(n));
   return;
 }
 
 if (command == "MEM") {
   printMemoryStats("on demand");
   return;
 }
 
 if (command == "GLOBALUPSIDEDOWN") {
   int n = params.toInt();
   setGlobalUpsideDown(n != 0);
   return;
 }
 
 // Unknown command
 Serial.println("Unknown command. Type HELP for list.");
}
// Set print darkness (% from 50 to 205%) and break delay (µs)
// This controls how dark the print appears and timing between print operations
void setDarknessAndDelay(uint8_t densityPercent, uint16_t breakDelayUs) {
 // Clamp density between 50% and 205% in steps of 5%
 if (densityPercent < 50) densityPercent = 50;
 if (densityPercent > 205) densityPercent = 205;
 // Convert % to value (0–31) where 50% + 5*x = density
 uint8_t densityLevel = (densityPercent - 50) / 5;
 // Clamp delay to nearest 250us step (max = 7 * 250 = 1750us)
 if (breakDelayUs > 1750) breakDelayUs = 1750;
 uint8_t breakTimeLevel = breakDelayUs / 250;
 // Merge both into single byte: D7–D5 = breakTimeLevel, D4–D0 = densityLevel
 uint8_t n = (breakTimeLevel << 5) | (densityLevel & 0x1F);
 // Send command: DC2 '#' n
 printerSerial.write(0x12);  // DC2
 printerSerial.write(0x23);  // '#'
 printerSerial.write(n);
}
// Print QR Code with specified data
// Uses the printer's built-in QR code generation capability
void printQRCode(String data) {
 upsideDownPrinting(globalUpsideDown ? 1 : 0);
 if (data.length() == 0) {
   Serial.println("Usage: QRCODE <data>");
   return;
 }
 // [1] Set QR code model (Model 2 is standard)
 printerSerial.write(0x1D);  // GS
 printerSerial.write(0x28);  // (
 printerSerial.write(0x6B);  // k
 printerSerial.write(0x04);  // pL
 printerSerial.write(0x00);  // pH
 printerSerial.write(0x31);  // cn
 printerSerial.write(0x41);  // fn
 printerSerial.write(0x32);  // Model 2
 printerSerial.write(0x00);  // n
 // [2] Set size of module (3–16 dots per module)
 printerSerial.write(0x1D);
 printerSerial.write(0x28);
 printerSerial.write(0x6B);
 printerSerial.write(0x03);
 printerSerial.write(0x00);
 printerSerial.write(0x31);
 printerSerial.write(0x43);
 printerSerial.write(0x06);  // module size = 6
 // [3] Set error correction level (L=48, M=49, Q=50, H=51)
 printerSerial.write(0x1D);
 printerSerial.write(0x28);
 printerSerial.write(0x6B);
 printerSerial.write(0x03);
 printerSerial.write(0x00);
 printerSerial.write(0x31);
 printerSerial.write(0x45);
 printerSerial.write(0x31);  // 'M' level (medium error correction)
 // [4] Store data in QR code buffer
 int len = data.length() + 3;  // +3 for command overhead
 byte pL = len & 0xFF;         // Low byte of length
 byte pH = (len >> 8) & 0xFF;  // High byte of length
 printerSerial.write(0x1D);
 printerSerial.write(0x28);
 printerSerial.write(0x6B);
 printerSerial.write(pL);
 printerSerial.write(pH);
 printerSerial.write(0x31);
 printerSerial.write(0x50);
 printerSerial.write(0x30);
 printerSerial.print(data);  // Actual QR code data
 // [5] Print the stored QR code
 printerSerial.write(0x1D);
 printerSerial.write(0x28);
 printerSerial.write(0x6B);
 printerSerial.write(0x03);
 printerSerial.write(0x00);
 printerSerial.write(0x31);
 printerSerial.write(0x51);
 printerSerial.write(0x30);
 Serial.println("Printed QR Code: " + data);
 feedLines(3);  // Add some space after QR code
}
// Parse barcode command and call appropriate printing function
void parseBarcode(String params) {
 int spaceIndex = params.indexOf(' ');
 if (spaceIndex == -1) {
   Serial.println("Usage: BARCODE <type> <data>");
   Serial.println("Types: CODE128, UPCA, EAN13");
   return;
 }
 String type = params.substring(0, spaceIndex);
 String data = params.substring(spaceIndex + 1);
 // Route to appropriate barcode printing function
 if (type == "CODE128") {
   printBarcode_CODE128(data);
 } else if (type == "UPCA") {
   printBarcode_UPCA(data);
 } else if (type == "EAN13") {
   printBarcode_EAN13(data);
 } else {
   Serial.println("Unknown barcode type: " + type);
 }
}
// Print CODE128 barcode - variable length alphanumeric
void printBarcode_CODE128(String data) {
 upsideDownPrinting(globalUpsideDown ? 1 : 0);
 
 // Set barcode height (in dots)
 printerSerial.write(0x1D);  // GS
 printerSerial.write(0x68);  // h
 printerSerial.write(0x50);  // Height = 80 dots
 delay(100);
 // Set barcode width (module width)
 printerSerial.write(0x1D);  // GS
 printerSerial.write(0x77);  // w
 printerSerial.write(0x02);  // Width = 2 dots per module
 delay(100);
 // Set HRI (Human Readable Interpretation) position
 printerSerial.write(0x1D);  // GS
 printerSerial.write(0x48);  // H
 printerSerial.write(0x02);  // HRI below barcode
 delay(100);
 // Print the barcode
 printerSerial.write(0x1D);           // GS
 printerSerial.write(0x6B);           // k
 printerSerial.write(0x49);           // CODE128 type
 printerSerial.write(data.length());  // Data length
 printerSerial.print(data);           // Barcode data
 Serial.println("Printed CODE128: " + data);
 feedLines(2);
}
// Print UPC-A barcode - requires exactly 11 digits
void printBarcode_UPCA(String data) {
 upsideDownPrinting(globalUpsideDown ? 1 : 0);
 
 // Validate data length for UPC-A
 if (data.length() != 11) {
   Serial.println("UPC-A requires 11 digits");
   return;
 }
 // Set barcode parameters (same as CODE128)
 printerSerial.write(0x1D);  // GS
 printerSerial.write(0x68);  // h
 printerSerial.write(0x50);  // Height = 80 dots
 delay(100);
 printerSerial.write(0x1D);  // GS
 printerSerial.write(0x77);  // w
 printerSerial.write(0x02);  // Width = 2
 delay(100);
 printerSerial.write(0x1D);  // GS
 printerSerial.write(0x48);  // H
 printerSerial.write(0x02);  // HRI below
 delay(100);
 // Print UPC-A barcode
 printerSerial.write(0x1D);           // GS
 printerSerial.write(0x6B);           // k
 printerSerial.write(0x41);           // UPC-A type
 printerSerial.write(data.length());  // Data length
 printerSerial.print(data);
 Serial.println("Printed UPC-A: " + data);
 feedLines(2);
}
// Print EAN13 barcode - requires exactly 12 digits
void printBarcode_EAN13(String data) {
 upsideDownPrinting(globalUpsideDown ? 1 : 0);
 
 // Validate data length for EAN13
 if (data.length() != 12) {
   Serial.println("EAN13 requires 12 digits");
   return;
 }
 // Set barcode parameters (same as others)
 printerSerial.write(0x1D);  // GS
 printerSerial.write(0x68);  // h
 printerSerial.write(0x50);  // Height = 80 dots
 delay(100);
 printerSerial.write(0x1D);  // GS
 printerSerial.write(0x77);  // w
 printerSerial.write(0x02);  // Width = 2
 delay(100);
 printerSerial.write(0x1D);  // GS
 printerSerial.write(0x48);  // H
 printerSerial.write(0x02);  // HRI below
 delay(100);
 // Print EAN13 barcode
 printerSerial.write(0x1D);           // GS
 printerSerial.write(0x6B);           // k
 printerSerial.write(0x43);           // EAN13 type
 printerSerial.write(data.length());  // Data length
 printerSerial.print(data);
 Serial.println("Printed EAN13: " + data);
 feedLines(2);
}
// Printer control functions - these send ESC/POS commands to control printer behavior
// Print and feed n lines - creates vertical spacing
void feedLines(int n) {
 printerSerial.write(0x1B);  // ESC
 printerSerial.write(0x64);  // d
 printerSerial.write(n);     // 0≤n≤255
 delay(500);  // Allow time for paper feed
}
// Turn upside-down printing mode on/off
void upsideDownPrinting(int n) {
 printerSerial.write(0x1B);  // ESC
 printerSerial.write(0x7B);  // {
 printerSerial.write(n);     // 0/1 - OFF/ON
 delay(500);
}
// Set underline mode - 0=off, 1=1dot thick, 2=2dots thick
void underlineMode(int n) {
 printerSerial.write(0x1B);  // ESC
 printerSerial.write(0x2D);  // -
 printerSerial.write(n);     // 0/1/2
 delay(500);
}
// Set text justification - 0=left, 1=center, 2=right
void align(int n) {
 printerSerial.write(0x1B);  // ESC
 printerSerial.write(0x61);  // a
 printerSerial.write(n);     // 0/1/2
 delay(500);
}
// Turn inverse (white on black) mode on/off
void inverseMode(int n) {
 printerSerial.write(0x1D);  // GS
 printerSerial.write(0x42);  // B
 printerSerial.write(n);     // 0/1 - OFF/ON
 delay(500);
}
// Print bitmap image using standard GS v command - optimized for memory usage
void printBitmap(uint8_t* imageData, int width, int height) {
 // Validate dimensions to prevent printer damage or memory issues
 if (width <= 0 || height <= 0 || width > 512 || height > 1662) {
   Serial.println("Invalid image dimensions");
   return;
 }
 // Calculate bytes per line (width must be multiple of 8 for 1-bit images)
 int bytesPerLine = (width + 7) / 8;
 // Use normal mode (m=0) for standard density printing
 uint8_t m = 0;
 // Calculate command parameters for image size
 uint8_t xL = bytesPerLine & 0xFF;        // Low byte of horizontal bytes
 uint8_t xH = (bytesPerLine >> 8) & 0xFF; // High byte of horizontal bytes
 uint8_t yL = height & 0xFF;              // Low byte of vertical dots
 uint8_t yH = (height >> 8) & 0xFF;       // High byte of vertical dots
 // Send GS v command for bitmap printing
 printerSerial.write(0x1D);  // GS
 printerSerial.write(0x76);  // v
 printerSerial.write(0x30);  // 0
 printerSerial.write(m);     // mode
 printerSerial.write(xL);    // xL
 printerSerial.write(xH);    // xH
 printerSerial.write(yL);    // yL
 printerSerial.write(yH);    // yH
 // Send image data efficiently
 int totalBytes = bytesPerLine * height;
 for (int i = 0; i < totalBytes; i++) {
   printerSerial.write(imageData[i]);
 }
 Serial.println("Bitmap printed: " + String(width) + "x" + String(height));
 feedLines(2);
}
// Create test pattern in bitmap buffer - useful for printer testing
void createTestPattern(uint8_t* buffer, int width, int height, int pattern) {
 int bytesPerLine = (width + 7) / 8;
 // Generate different test patterns
 for (int y = 0; y < height; y++) {
   for (int x = 0; x < bytesPerLine; x++) {
     uint8_t byte_val = 0;
     // Process 8 pixels at a time (1 byte)
     for (int bit = 0; bit < 8; bit++) {
       int pixel_x = x * 8 + bit;
       if (pixel_x >= width) break; // Don't exceed image width
       bool pixel = false;
       // Generate different patterns based on pattern parameter
       switch (pattern) {
         case 0:  // Checkerboard pattern
           pixel = ((pixel_x / 8) + (y / 8)) % 2 == 0;
           break;
         case 1:  // Horizontal lines
           pixel = (y % 4) < 2;
           break;
         case 2:  // Vertical lines
           pixel = (pixel_x % 4) < 2;
           break;
         case 3:  // Diagonal lines
           pixel = ((pixel_x + y) % 8) < 4;
           break;
         default: // Default diagonal pattern
           pixel = (pixel_x + y) % 2 == 0;
       }
       // Set bit in byte if pixel should be printed
       if (pixel) {
         byte_val |= (0x80 >> bit);
       }
     }
     buffer[y * bytesPerLine + x] = byte_val;
   }
 }
}
// Print a test pattern - useful for testing printer functionality
void printTestPattern(int width, int height, int pattern) {
 // Limit size to prevent memory issues on ESP32
 if (width > 384 || height > 400) {
   Serial.println("Image too large for ESP32 memory");
   return;
 }
 printMemoryStats("before test pattern alloc");
 int bytesPerLine = (width + 7) / 8;
 int totalBytes = bytesPerLine * height;
 // Allocate memory for image data
 uint8_t* imageData = (uint8_t*)malloc(totalBytes);
 if (imageData == NULL) {
   Serial.println("Failed to allocate memory for image");
   printMemoryStats("alloc fail");
   return;
 }
 // Create the test pattern
 createTestPattern(imageData, width, height, pattern);
 // Print the bitmap
 printBitmap(imageData, width, height);
 printMemoryStats("before free test pattern");
 // Free allocated memory
 free(imageData);
 printMemoryStats("after free test pattern");
}
// Alternative bitmap printing method using DC2 * command
// This method prints line by line to avoid spacing issues
void printBitmapDC2_Method(const unsigned char* progmemData, int width, int height) {
 upsideDownPrinting(globalUpsideDown ? 1 : 0);
 // Set printer to maximum darkness for bitmap printing
 printerSerial.write(0x12);  // DC2
 printerSerial.write(0x23);  // #
 printerSerial.write(0x9F);  // Maximum darkness setting
 delay(50);
 Serial.println("Using DC2 * method (Method 1)...");
 int bytesPerLine = (width + 7) / 8;
 // Print line by line to avoid spacing issues
 for (int line = 0; line < height; line++) {
   // Send DC2 * command for each line
   printerSerial.write(0x12);                 // DC2
   printerSerial.write(0x2A);                 // *
   printerSerial.write(0x01);                 // r = 1 line height
   printerSerial.write(bytesPerLine & 0xFF);  // n = bytes per line
   // Send one line of data from PROGMEM
   for (int i = 0; i < bytesPerLine; i++) {
     uint8_t byte_val = pgm_read_byte(progmemData + (line * bytesPerLine) + i);
     printerSerial.write(byte_val);
   }
   // Small delay between lines for printer processing
   delayMicroseconds(10);
 }
 Serial.println("DC2 * method completed!");
 feedLines(2);
 
 // Reset printer darkness to normal
 printerSerial.write(0x12);  // DC2
 printerSerial.write(0x23);  // #
 printerSerial.write(0x58);  // Normal darkness setting
 delay(50);
}
// Helper function: Rotate a 1bpp bitmap by 180 degrees
// Returns a newly allocated buffer that caller must free()
// Used for upside-down printing when globalUpsideDown is enabled
uint8_t* rotateBitmap1bpp_180(const uint8_t* src, int width, int height) {
 int bytesPerLine = (width + 7) / 8;
 int totalBytes = bytesPerLine * height;
 
 // Allocate destination buffer and clear it
 uint8_t* dst = (uint8_t*)calloc(totalBytes, 1);
 if (!dst) return nullptr;
 
 // Rotate each pixel by 180 degrees
 for (int y = 0; y < height; ++y) {
   for (int x = 0; x < width; ++x) {
     // Extract pixel from source
     int srcByte = y * bytesPerLine + (x / 8);
     int srcBit = 7 - (x % 8);
     bool pixel = (src[srcByte] >> srcBit) & 1;
     
     // Calculate rotated position
     int nx = width - 1 - x;
     int ny = height - 1 - y;
     
     // Set pixel in destination
     int dstByte = ny * bytesPerLine + (nx / 8);
     int dstBit = 7 - (nx % 8);
     if (pixel) dst[dstByte] |= (1 << dstBit);
   }
 }
 return dst;
}
// Main bitmap printing method using GS v command with chunking
// This is the primary method for printing large images efficiently
void printBitmapGS_Method(const unsigned char* progmemData, int width, int height) {
 upsideDownPrinting(globalUpsideDown ? 1 : 0);
 align(1);  // Center align for images
 
 // Set printer to maximum darkness for better image quality
 printerSerial.write(0x12);  // DC2
 printerSerial.write(0x23);  // #
 printerSerial.write(0x9F);  // Maximum darkness
 delay(50);
 const unsigned char* dataToPrint = progmemData;
 int w = width, h = height;
 uint8_t* rotated = nullptr;
 // Handle global upside-down printing by rotating the entire image
 if (globalUpsideDown) {
   // Copy from PROGMEM to RAM, then rotate 180°
   int bytesPerLine = (width + 7) / 8;
   int totalBytes = bytesPerLine * height;
   
   uint8_t* ramCopy = (uint8_t*)malloc(totalBytes);
   if (ramCopy) {
     // Copy image data from PROGMEM to RAM for rotation
     for (int i = 0; i < totalBytes; ++i)
       ramCopy[i] = pgm_read_byte(progmemData + i);
     
     // Rotate the image 180 degrees
     rotated = rotateBitmap1bpp_180(ramCopy, width, height);
     free(ramCopy);
     
     if (rotated) {
       dataToPrint = rotated;
     }
   }
 }
 // Validate image dimensions
 if (w <= 0 || h <= 0) {
   Serial.println("Invalid image size");
   if (rotated) free(rotated);
   return;
 }
 int bytesPerLine = (w + 7) / 8;
 int totalChunks = (h + 23) / 24;  // Process in 24-line chunks for memory efficiency
 // Set line spacing to 0 for seamless image printing
 printerSerial.write(0x1B);  // ESC
 printerSerial.write(0x33);  // '3'
 printerSerial.write(0x00);  // 0 dots line spacing
 // Process image in chunks to manage memory usage
 for (int chunk = 0; chunk < totalChunks; chunk++) {
   int linesInChunk = min(24, h - chunk * 24);  // Last chunk may be smaller
   
   // Calculate chunk parameters
   int yL = linesInChunk & 0xFF;
   int yH = (linesInChunk >> 8) & 0xFF;
   int xL = bytesPerLine & 0xFF;
   int xH = (bytesPerLine >> 8) & 0xFF;
   // Send GS v command for this chunk
   printerSerial.write(0x1D);  // GS
   printerSerial.write(0x76);  // 'v'
   printerSerial.write(0x30);  // '0'
   printerSerial.write(0x00);  // m = 0 (normal density)
   printerSerial.write(xL);
   printerSerial.write(xH);
   printerSerial.write(yL);
   printerSerial.write(yH);
   // Send chunk data
   for (int row = 0; row < linesInChunk; row++) {
     int baseIndex = (chunk * 24 + row) * bytesPerLine;
     for (int col = 0; col < bytesPerLine; col++) {
       printerSerial.write(dataToPrint[baseIndex + col]);
     }
   }
   
   delay(30);  // Allow printer to process chunk
 }
 // Feed paper and restore normal settings
 printerSerial.write(0x1B);  // ESC
 printerSerial.write(0x64);  // d
 printerSerial.write(3);     // Feed 3 lines
 Serial.println("Finished printing image in chunks.");
 
 // Reset printer darkness to normal
 printerSerial.write(0x12);  // DC2
 printerSerial.write(0x23);  // #
 printerSerial.write(0x58);  // Normal darkness
 delay(50);
 
 // Clean up rotated image memory if allocated
 if (rotated) free(rotated);
}
// Helper function to print a named bitmap from the availableImages array
void printNamedBitmap(String imageName) {
 imageName.trim();
 imageName.toLowerCase();  // Make search case-insensitive
 
 // Search for matching image name
 for (int i = 0; i < numAvailableImages; ++i) {
   String candidate = String(availableImages[i].name);
   candidate.toLowerCase();
   if (imageName == candidate) {
     printBitmapGS_Method(availableImages[i].data, availableImages[i].width, availableImages[i].height);
     return;
   }
 }
 
 Serial.println("Unknown image name. Type HELP for list.");
}
// Demo function: Print next image in sequence (triggered by Key1)
void demoPrintNextImage() {
 static int currentImageIndex = 0;
 
 const BitmapImage& img = availableImages[currentImageIndex];
 Serial.print("[Key1] Printing image: ");
 Serial.println(img.name);
 
 printBitmapGS_Method(img.data, img.width, img.height);
 
 // Advance to next image, wrap around to beginning
 currentImageIndex = (currentImageIndex + 1) % numAvailableImages;
}
// Demo function: Print comprehensive demo page (triggered by Key2)
// Shows all printer capabilities in one printout
void demoPrintAllFormats() {
 Serial.println("[Key2] Printing all barcodes, QR, and formatted text...");
 
 // Print demo header
 align(1);  // Center align
 printerSerial.println("--- DEMO PAGE ---");
 feedLines(1);
 // Demonstrate different barcode types
 align(0);  // Left align for labels
 printerSerial.println("CODE128: 123456");
 printBarcode_CODE128("123456");
 
 printerSerial.println("UPCA: 12345678901");
 printBarcode_UPCA("12345678901");
 
 printerSerial.println("EAN13: 123456789012");
 printBarcode_EAN13("123456789012");
 feedLines(1);
 // Demonstrate QR Code
 align(1);  // Center align for QR
 printerSerial.println("QR Code: HelloWorld");
 printQRCode("HelloWorld");
 feedLines(1);
 // Demonstrate text alignment
 align(0);
 printerSerial.println("Align Left");
 align(1);
 printerSerial.println("Align Center");
 align(2);
 printerSerial.println("Align Right");
 feedLines(1);
 // Demonstrate underline modes
 underlineMode(1);
 printerSerial.println("Underline 1");
 underlineMode(2);
 printerSerial.println("Underline 2");
 underlineMode(0);  // Turn off underline
 feedLines(1);
 // Demonstrate inverse printing
 inverseMode(1);
 printerSerial.println("Inverse ON");
 inverseMode(0);
 printerSerial.println("Inverse OFF");
 feedLines(1);
 // Demonstrate upside down printing
 upsideDownPrinting(1);
 printerSerial.println("Upside Down ON");
 upsideDownPrinting(0);
 printerSerial.println("Upside Down OFF");
 feedLines(2);
 // Print normal footer text
 printerSerial.println("Normal text");
 feedLines(2);
 
 Serial.println("[Key2] Demo page printed.");
}
Have any question related to this Article?

Add New Comment

Login to Comment Sign in with Google Log in with Facebook Sign in with GitHub