Arduino Thermal Printer

I recently had the idea of constructing my own printer. It wanted it to be simple, small and without the need for ink cartridges. After a quick search online I discovered thermal printers. Unbeknownst to me, these are capable of more than just printing receipts and I thought it would be nice to use one to print graphics.

To my delight, I found a nice selection of hackable printers by Adafruit. I decided to buy the Nano Thermal Receipt Printer as that seemed like a nice compact size for my project. Now I just needed to figure out how to control the printer.

A while back I purchased, on a whim, a HUZZAH ESP8266 breakout (also from Adafruit). With no particular use case in mind, I just thought it was a sweet breakout board capable of connecting to a Wifi network or even serving as an access point.

I played around with it for a short while to get the hang of the Arduino development environment. I then put it away in a box for a year or so.

Idea

When my printer arrived in the mail, I decided to hook it up to my ESP8266 – The idea being that the printer could then be accessed via Wifi. The printer could sit on my home network and any device could use it.

Method

I thought about how a computer on the network could actually interface with the printer. I researched a little about how home network printers use CUPS (Common UNIX Printing System) but that seemed a little difficult to work with.

Instead I decided to use the ESP8266 to host a little web server which could be accessed by a browser on any device on the network.

Hardware

  • Adafruit Thermal Printer
  • Adafruit HUZZAH ESP8266 breakout
  • Panel Mount 2.1mm DC barrel jack
  • DC 5V 2A Power Supply Adapter

I was concerned about the two devices operating on different power/logic levels. On the Adafruit website they have this message:

“The ESP8266 runs on 3.3V power and logic, and unless otherwise specified, GPIO pins are not 5V safe! The analog pin is also 1.0V max!”

The printer needs a minimum 5V to operate and anything under 2A will result in faded prints. So I thought that I would need to regulate the power to each device. It turns out that the ESP8266 can be powered 5V (regulated internally) just as the printer. The logic circuit then uses 3.3V to TX and RX.

References

Printer manual – https://cdn-learn.adafruit.com/downloads/pdf/mini-thermal-receipt-printer.pdf

Code

As I mentioned above, I had previously experimented with the Huzzah breakout. This meant getting my hands dirty with the Arduino IDE. The breakout is surprisingly easy to get working using the helpful example projects and libraries.

All the code can be found on my GitHub repo

Arduino

The main function of my project was to serve an “index” endpoint and a “print” endpoint.
The index is basically a static html string containing the client-side UI and functionality. This is a Vue JS application. The client-side libraries are hosted on CDNs but the main Vue application is stored on the breakout.

Due to the limited storage capacity of the ESP8266, the html string needs to be stored in the flash memory. This is done as follows:

static const char app_html[] PROGMEM = "<!DOCTYPE html><html>...</html>";

Then in the main function

server.on("/", handleRoot);
server.on("/print", handlePrint);

When a client connects to the ip address of the ESP8266, the ‘handleRoot’ function is called.

void handleRoot() {
  String content = ""; // create a response string
  content += FPSTR(app_html); //append the html to the string
  server.send(200, "text/html", content); // send the html to the client
}

The print endpoint allows the application to POST form data to print.

A few points about the printer.

  • The maximum width of the graphic is 384 pixels
  • It is very limited on buffer space
  • The printer’s printBitmap function takes an unsigned integer array of bytes

The format of the graphic data can be described using the following example:

Take an image which has 8 pixels wide and 1 pixel high. The pixels are white, black, white, black, white, black, white, black.

This can be represented in binary as 10101010. It can also be represented in decimal as 170.

If we create an unsigned integer array containing this value:

uint8_t graphic[1]; //Create array of size 1
graphic[0] = (uint8_t)170; //Add the value 170

Then, send this to the printer’s printBitmap function and it will print the 8 pixels. Here is how it comes out on my printer.

It doesn’t exactly have the best definition but you can see the little 8 pixel line on the page

Although we are restricted to 384 pixels horizontally, we can make the height as long as we want. The main caveat here being the size of the printer buffer. We need to send the graphic data to the printer in chunks. I found the best chunk size is 384 by 10 pixels.

void handlePrint(){
  if (server.hasArg("data") == false){ // check if content received
    server.send(400, "text/plain", "{\"code\": 400}");
  } else {
    String content = server.arg("data");
  
    /* expect a data payload of uniform size each time */
    const size_t bufferSize = JSON_ARRAY_SIZE(480) +
                              JSON_OBJECT_SIZE(1)  + 1850;

    DynamicJsonBuffer jsonBuffer(bufferSize);
    JsonObject&  parsed = jsonBuffer.parseObject(content); // parse the JSON string

    int size = 480; // define the size of the graphic - 384 x 10 pixels / 8
    uint8_t graphic_data[size]; // create an unsigned integer array
    
    for(int i=0;i<size;i++){
      /* copy the response data to the new unsigned array */
      graphic_data[i] = (uint8_t)parsed["data"][i];
    }

    if( printer.hasPaper() ) {
      /* print the 384 x 10 pixel graphic */
      printer.printBitmap(384, 10, graphic_data, false);
      delay(200);

      /* send success response to client */
      server.send(200, "text/plain", "{\"code\": 200}");
    } else {
      /*send 503 if paper tray is empty*/
      server.send(503, "text/plain", "{\"code\": 503}");
    }
  }
}

Vue & Fabric

The client-side application is built with Vue.js and Fabric.js. Vue takes care of the state management. Element, a Vue 2.0 based component library, is used for the control panel interface.
Fabric is used to render the graphic elements on the page. Fabric is suitable because it has touch support and it uses the JS canvas API.
The components in the control panel are used to configure the Fabric objects. One can create an image or text object and adjust the size and position.

Printing

Once the canvas is ready to be printed, the canvas data needs to be changed to one-bit bi-tonal black-and-white before sent to the printer. The following JS function is used to remove the colour from the canvas data.

function applyGreyScale(data, threshold) {
  var buffer = data.data,
  len = buffer.length,
  i = 0,
  lum;
    for (i; i < len; i += 4) {
      /* determine the luminosity of the pixel */
      lum = buffer[i] * 0.3 + buffer[i + 1] * 0.59 + buffer[i + 2] * 0.11;
          
      /* shift the luminosity value to black or white based on the threshold */
      lum = lum < threshold ? 0 : 256;

      /* change the rgb colour value of the pixel */
      buffer[i] = lum; 
      buffer[i + 1] = lum;
      buffer[i + 2] = lum;
    }
    return data;
}

After the canvas data is converted to bi-tonal black-and-white it needs to be converted to the byte array format mentioned earlier. Currently the canvas data array is four times too large. We need to trim off the red, green, blue, and alpha values and reduce it to one value.

// Example - white, black, white, black, white, black, white, black

// canvas data format (32 bytes)
// [255,255,255,255,0,0,0,255,255,255,255,255,0,0,0,255,255,255,255,255,0,0,0,255,255,255,255,0,0,0,255]

// trim off rgba (8 bytes)
// [255,0,255,0,255,0,255,0]

// represent as bits (8 bytes)
// [1,0,1,0,1,0,1,0]

// represent as decimal (1 byte)
// [127]
function getByteArrayChunks(data){
  var buffer = data.data,
  len = buffer.length,
  i = 0,
  buffer8pixelsByte = new Uint8ClampedArray(Math.ceil(len / 32));

  for (i; i < len; i += 32) { /* trim off rgba values from pixel */ var arr = [ buffer[i], buffer[i + 4], buffer[i + 8], buffer[i + 12], buffer[i + 16], buffer[i + 20], buffer[i + 24], buffer[i + 28] ]; /* represent pixel as bits */ var binStr = arr.map(bit => bit > 0 ? 0 : 1).join('');

    /* represent pixel as decimal (1 byte) */
    buffer8pixelsByte[i / 32] = parseInt(binStr, 2);
  }

  /* Split array into chunks of 480 bytes */
  var chunks = [],
      j, temparray, chunk = 480;
  for (j = 0; j < buffer8pixelsByte.length; j += chunk) {
    temparray = buffer8pixelsByte.slice(j, j + chunk);
    chunks.push(temparray);
  }
  return chunks;
}

Now we can send the chunks to the /print endpoint piece by piece. As each chunk is sent, we must wait for a 200 response from the server before sending the next chunk.

function sendData(data) {
  return new Promise(function(resolve, reject) {
    var formData = new FormData();
    formData.append("data", "{'data':" + JSON.stringify(data) + "}");
    fetch('/print', {
        method: 'post',
        body: formData
    }).then(response=>{
        resolve(xhr.response)
    }).catch(e=>{
        reject(e)
    });
  });
}

function print(){
  /* get canvas data */
  var data = ctx.getImageData(0, 0, width, height);

  /* convert array to print format */
  var chunks = getByteArrayChunks(data);
  const printChunk = async(i) => {
    if (i > chunks.length - 1) return;
    try {
      /* wait for 200 response*/
      var res = await sendData(Array.from(chunks[i]));

      /* print next chunk */
      printChunk(i + 1);
    } catch (e) {
      // handle error
    } 
  };
  printChunk(0);
}

User Interface

I wanted the UI to be as simple as possible. A canvas which shows how the print will appear and a control panel to configure the canvas elements. The interface must also work on any screen size.

The control panel allows the user to choose either an image or text to add to the canvas. Once an element is added, the user can then use the anchor points to increase the size, or move the element a suitable position. The element properties can also be configured from the control panel.

By default, the canvas shows a white page element which represents the boundaries of the thermal page. As the user places new elements over the page element, the canvas applies the selected monochrome filter over the page boundary.

The user has a choice of filter to use.

  1. Floyd Steinberg
  2. Bayer Matrix
  3. Threshold

Each of these filters remove the colour from the canvas data in different ways. The Floyd Steinberg algorithm is an error diffusion dithering algorithm used commonly for gif encoding.
Bayer Matrix uses an ‘ordered dithering’ algorithm which requires a predefined matrix with a specific size.
Threshold is most basic monochrome filter which shifts the colour of each pixel to either black or white based on the threshold value the user decides.

The control panel also allows the user to configure the printer’s heat time, heat interval, print density, and print break time. These values can adjust the intensity of the black colour and the visibility of horizontal print scan lines.

The values of the printer settings, threshold, zoom, and page height are all saved in a cookie for reuse later.

Prints

The result of each print can vary in clarity depending on the amount of detail. The Floyd Steinberg and the 2×2 Bayer Matrix algorithms do a pretty good job on busy photos but you lose some definition. Images with very clear light and dark tones are picked up easily with the threshold filter and produces sharp edges and nice contrast.

Comments

  1. Mohammad Hossein Salari says:

    hi, mind if I ask you to send the arduino code of your project for me.
    This is my E.mail:
    mohammad.hossein.salari[@]gamil.com

    1. Ian Jackson says:

      You can find the code on my Github repo mentioned in the post

  2. Andrew says:

    Hi Ian,
    hope you can help me.. I’ve an huzzah esp8266 (like your) and an adafruit thermal printer guts. I would ask you first if you made some fixes/changes to “softwareSerial” or “adafruit_thermal” libraries in order to work with the esp8266. Then i’ve read from adafruit thermal printer’s product page that the ttl connection works on 5v and the ttl connection of the huzzah esp8266 works on 3.3v. So I’ve purchased a 74AHCT125 in order to convert the 3.3v ttl output from esp8266 to 5v for the thermal printer input. I don’t see it in your “wiring photos”, so my question is if I really need it.
    The result is that nothing is working. All works great with arduino, but not with the huzzah esp8266. Please help me!! Thank you so much!

    1. Ian Jackson says:

      Hi Andrew. I did not need to change anything in the Arduino libraries.

      The printer logic will work with 3.3V TTL serial so you do not need the level shifter for the RX and TX. You
      will need to power the printer using a 5V 2A power supply though. Take a look at the printer manual from Adafruit – I have added to the ‘References’ section.

      In my project I am using the 5V 2A to power both the Huzzah and the printer. The RX and TX are then connected between both devices. The TTL on both devices use 3.3V logic levels so make sure you don’t touch the 5V on these

  3. Ivan Hristovski says:

    Hello Ian , nice work with the project , i wanted to ask you , how did you rotate the images ? the bottom ones , what is the commands ?

    1. Ian Jackson says:

      Hey Ivan. The images are rendered using the Fabric JS framework. Rotating the image can be done using something like this – fabricImgObj.set({angle: 90})

  4. Anonymous says:

    MY PRINTER DONT HAVE DTR ONLY TX RX IT DOESNT WORK

  5. SirChoeg says:

    Hi Ian!
    I tried your code on a cheap lolin esp8266. It works just fine, but if I put a text or a picture on the canvas that does not fill the full width of the canvas, then the background is printed as black. It’s a bit annoying because it messes with the contrast of the whole line. Any thoughts?
    Nice project by the way.
    SirChoeg

Leave a Reply

Your email address will not be published.