Skip to main content
BlogERP

Epson TM-T82 ESC/POS Printing From a Custom POS App

The TM-T82 does not print HTML. Here is the byte-level path that actually works: transports, ESC/POS commands, 48-column arithmetic and code pages.

Matthews Wong

Software Engineer

August 202612 min read
Epson TM-T82 ESC/POS Printing From a Custom POS App

Key Takeaway

An Epson TM-T82 prints ESC/POS byte streams, not web pages. A reliable custom POS builds the receipt as bytes, sends them straight to the printer over USB, serial or TCP port 9100, and lays text out in fixed 48-column lines rather than trusting a browser print dialog.

The first receipt I ever sent to an Epson TM-T82 came out as a page of Courier text with a two-centimetre margin, cut in the middle of the total, and it took the printer eleven seconds to produce. It was correct HTML. It was a useless receipt. That gap between what a web developer thinks printing is and what a POS printer thinks printing is cost me the better part of a week, and everything below is what I wish someone had written down before I started.

This is the byte-level view: what the hardware is, how the command language works, why column arithmetic replaces CSS, and where Indonesian text quietly breaks. Every claim here is either from Epson's own technical references or from a printer sitting on my desk.

Why the browser print dialog is the wrong tool

The tempting path is to render the receipt as HTML, call window.print, and pick the TM-T82 from the dialog. It works in a demo and fails in a shop, for four reasons that are all structural rather than fixable.

  • It goes through the graphics pipeline. The driver rasterises your page into a bitmap the width of the paper and sends it dot by dot, so a receipt that should take under a second takes many, and the printer sounds like it is struggling because it is.
  • It cannot open the cash drawer or cut where you want. Both are ESC/POS commands, and a rendered page has nowhere to put them.
  • It requires a human. The print dialog is a modal that a cashier has to dismiss, on every sale, forever, and the one time they pick the wrong printer nobody notices until the end of the shift.
  • Its layout is a lie. The browser lays out proportional text; the printer lays out a fixed grid of character cells. Anything you centre or align in CSS lands somewhere else on paper.

The alternative is not harder, only less familiar: you build an array of bytes and write it to a device. There is no rendering step at all, which is exactly why it is fast and exactly why it is predictable.

A small thermal receipt printer on a desk with a printed receipt curling out of its paper slot
A thermal receipt printer mid-job. Paper feeds under a fixed head and out over the tear bar; nothing about the mechanism has a concept of a page, a margin or a font file. Photo: Till Dettmering / Wikimedia Commons, CC BY 2.0

What a TM-T82 actually is

Before writing any code, it helps to know the numbers you are designing against. Epson's own specifications for the T82 family are unusually concrete, and they explain nearly every layout constraint you will hit later.

PropertyValue on the TM-T82 family
Print methodThermal line printing — one horizontal line of dots at a time, no ink, no ribbon
SpeedUp to 150 mm per second on the TM-T82, which is roughly a full receipt per second
Resolution203 x 203 dpi, i.e. 8 dots per mm, with a 72 mm print width on 80 mm paper
Character gridFont A is 12 x 24 dots giving 48 columns; Font B is 9 x 17 dots giving 64 columns
EnduranceAround 60 million lines MCBF and an auto cutter rated near 1.5 million cuts

Read the character grid row twice, because it is the single most useful fact in this article. The printer has no proportional fonts and no arbitrary positioning: every line is a sequence of identical cells, and your entire layout budget is 48 of them. Design in that grid and everything lines up; fight it and nothing does.

Close-up of a thermal print head module showing the heating element strip and its orange flexible ribbon cable
The whole imaging system: a strip of tiny resistors that heat and cool once per printed line. On a TM-class printer that strip carries 8 dots per millimetre, which is where the 203 dpi and the fixed 48-column grid both come from. Photo: Raimond Spekking / Wikimedia Commons, CC BY-SA 4.0

The smallest receipt that works

ESC/POS is a byte protocol Epson introduced for its TM printers and which most of the industry now imitates. Commands begin with ESC, hex 1B, or GS, hex 1D, followed by a letter and its parameters; anything that is not a command is printed literally. That is the whole mental model.

// The whole "hello receipt" in raw ESC/POS. No driver, no dialog.
const ESC = 0x1b, GS = 0x1d, LF = 0x0a;

const bytes = Buffer.concat([
  Buffer.from([ESC, 0x40]),               // ESC @   initialize
  Buffer.from([ESC, 0x74, 0x00]),         // ESC t 0 code page 437
  Buffer.from([ESC, 0x61, 0x01]),         // ESC a 1 centre
  Buffer.from([GS, 0x21, 0x11]),          // GS !    double height + width
  Buffer.from([ESC, 0x45, 0x01]),         // ESC E 1 emphasized on
  Buffer.from("TOKO SUMBER REJEKI\n", "ascii"),
  Buffer.from([ESC, 0x45, 0x00]),         // ESC E 0 emphasized off
  Buffer.from([GS, 0x21, 0x00]),          // GS ! 0  back to normal size
  Buffer.from([ESC, 0x61, 0x00]),         // ESC a 0 left
  Buffer.from("Jl. Diponegoro 12, Semarang\n", "ascii"),
  Buffer.from("-".repeat(48) + "\n", "ascii"),
  Buffer.from([LF, LF, LF, LF]),          // feed past the tear bar
  Buffer.from([GS, 0x56, 0x42, 0x00]),    // GS V B 0 partial cut with feed
]);

Notice what is absent: there is no page setup, no margins, no font loading and no confirmation. You initialise, you set a mode, you send text, you feed, you cut. The printer executes commands in the order they arrive in its buffer, which is also why a cut command placed too early cuts through the total.

Keep a hexdump of one known-good receipt in your repository as a fixture. When a receipt goes wrong in production, diffing the bytes you sent against that fixture finds the bug in seconds, and it survives every refactor of the code that generates them.

Column arithmetic is the layout engine

With 48 fixed cells per line, right-aligning money is not a style, it is subtraction. Every layout helper in a POS printing library is some version of the two functions below, and writing them yourself takes ten minutes and removes a dependency.

const COLUMNS_FONT_A = 48;   // 80 mm paper, 72 mm print width, 12x24 font

// One item line: name on the left, money hard against the right margin.
function itemLine(name: string, amount: string): string {
  const room = COLUMNS_FONT_A - amount.length - 1;
  const label = name.length > room ? name.slice(0, room - 1) + "." : name;
  return label.padEnd(room + 1, " ") + amount;
}

itemLine("Kopi Susu Gula Aren", "25.000");
// "Kopi Susu Gula Aren                       25.000"

// Wrap, never truncate silently, when the product name is genuinely long.
function wrapName(name: string, width = COLUMNS_FONT_A - 10): string[] {
  const words = name.split(" ");
  return words.reduce<string[]>((lines, word) => {
    const last = lines[lines.length - 1];
    if (last && (last + " " + word).length <= width) {
      lines[lines.length - 1] = last + " " + word;
    } else {
      lines.push(word);
    }
    return lines;
  }, []);
}

Two rules save most of the pain. First, never let a product name silently truncate a price off the end of the line: reserve the amount's width first and give the name whatever remains. Second, decide once whether long names wrap or ellipsise and apply it everywhere, because a receipt where some lines wrap and others do not reads as broken even when the arithmetic is right.

Code pages: where Indonesian receipts break

The printer does not speak UTF-8. It holds a set of single-byte character tables and prints whichever one is currently selected, chosen with ESC t. Send UTF-8 bytes without thinking and accented characters arrive as pairs of symbols, because each byte of the multi-byte sequence is printed as its own glyph.

  • Default to code page 0, PC437, and encode your text to it before sending. Plain Indonesian is ASCII, so ordinary item names and rupiah amounts pass through untouched.
  • Strip or transliterate before printing rather than hoping. A product name pasted from a supplier catalogue can carry a curly apostrophe or a non-breaking space that has no cell in any code page.
  • If you genuinely need a wider character set, select the matching table explicitly per block of text and switch back afterwards, the way open-source stacks such as python-escpos do with their code page mapping.

The currency symbol deserves its own decision. I print amounts as bare grouped digits and put the word Rp in a header line, which avoids depending on any code page having a rupiah glyph and keeps the digits aligned in the same cells on every line.

Rupiah amounts are the one place where a rendering bug becomes a financial dispute. Format the number once, in one function, with an explicit grouping separator, and print the result of that function everywhere — receipt, screen and report — so a customer holding paper and a manager reading a dashboard can never see different totals.

A print path that survives a busy Saturday

Printing is I/O against a physical device that jams, runs out of paper and gets unplugged by whoever is mopping. The path below is what I settled on after the first month of complaints.

  1. Persist the receipt first. Write the payload and the payment id to a queue table inside the same transaction that closes the sale, so a crash between payment and print leaves evidence rather than a mystery.
  2. Send from a single worker. One process owns the printer, drains the queue in order and never interleaves two receipts, which matters because interleaved ESC/POS is indistinguishable from corruption.
  3. Bound every wait. A connect timeout, a write timeout and a maximum attempt count turn an unplugged printer into a visible queued job instead of a hung cashier screen.
  4. Make reprints boring. A reprint is a new attempt against the same payment id, prints a clearly marked duplicate, and is available to the cashier without a manager password. If reprinting is hard, staff will re-run the sale.

That structure also gives you the metric that matters operationally: how many receipts are queued and how old the oldest one is. A printer that has been offline for four minutes shows up as a number before it shows up as a queue of annoyed customers.

What I would do differently

I spent too long trying to make one code path serve both an 80 mm counter printer and a 58 mm mobile printer. The command language is the same, but 48 columns and 32 columns are different layouts, not the same layout at different sizes, and pretending otherwise produced receipts that were technically correct and visually wrong on both.

I would also have tested with cheap paper from day one. Every receipt looks good on the sample roll in the box. The rolls a shop actually buys are thinner and less sensitive, and they are the ones that reveal whether your contrast and emphasis choices hold up.

Printing to a TM-T82 stops being difficult the moment you stop treating it as a printer in the desktop sense and start treating it as a serial device that accepts a small command language. Build the bytes, own the transport, respect the 48-column grid, and the hardware becomes the most reliable part of the whole point-of-sale system.

FAQ

Frequently Asked Questions

Short answers to what readers ask most about this topic.

Can an Epson TM-T82 print HTML or a PDF directly?

Not natively. The printer executes ESC/POS commands, which are byte sequences beginning with ESC (hex 1B) or GS (hex 1D). A driver can rasterise an HTML page or PDF into a bitmap and stream it as dots, but that path is far slower and gives you no control over the cutter or the cash drawer. For point of sale, generate ESC/POS bytes directly.

How many characters fit on one line of a TM-T82 receipt?

On 80 mm paper the print width is 72 mm at 203 dpi. Font A is a 12 by 24 dot cell, which gives 48 characters per line, and Font B is 9 by 17 dots, giving 64. On 58 mm paper the same Font A yields roughly 32 columns. Because the fonts are monospaced, your layout is literally a character count.

Why does Indonesian or accented text print as strange symbols?

The printer does not understand UTF-8. It prints from single-byte character tables selected with the ESC t command, so each byte of a multi-byte UTF-8 sequence becomes its own glyph. Encode your text to the selected code page — PC437 is the usual default — or select a table that contains the characters you need before printing that block.

What is the fastest way to send bytes to a TM-T82?

Whichever transport is physically attached: a USB or serial device node, or a TCP connection to port 9100 on Ethernet models. On Windows you can also push raw bytes through the print queue using the RAW datatype, which keeps queue management while skipping rasterisation. Open one connection per receipt and close it when done.

Should receipts be printed synchronously during checkout?

No. Persist the receipt payload and payment id in a queue table inside the same transaction that closes the sale, then let a single worker drain that queue. A printer that is offline, jammed or out of paper then becomes a visible queued job rather than a hung checkout screen, and reprints become a retry against the same payment id.