⏱️ Lectura: 12 min
An e-ink reader with barely 400 KB of total RAM can now receive real print jobs from the Print dialog on a Mac, without installing any driver. Nishant Joshi, a developer and owner of a Xteink X3, implemented the IPP protocol (Internet Printing Protocol) directly in the device’s firmware to pull it off.
📑 En este artículo
- TL;DR
- Introduction
- What happened: the IPP protocol enters the firmware
- Context and history
- Technical details and performance
- How to start testing it
- Impact and analysis
- What’s next
- Frequently Asked Questions
- What is the IPP protocol and how does it differ from printing over USB?
- Do I need to install a driver to use an IPP printer?
- Why use Bonjour/mDNS instead of setting up a fixed IP?
- What is the _universal subtype and why does it matter on macOS?
- Can I replicate this on a regular ESP32 without an e-ink screen?
- What limitations does a homemade IPP server like this one have?
- References
The result behaves like a real printer: it shows up on its own in the system’s printer list and even printed a manga from Preview in about a second, according to the author’s own account. What’s interesting isn’t so much the protocol itself, but how he managed to fit an 8.4 MB page onto a chip that barely has enough RAM for a fraction of that.
TL;DR
- Nishant Joshi turned a Xteink X3 e-reader into a real printer using the IPP protocol (Internet Printing Protocol).
- The e-reader announces itself over Bonjour/mDNS as an _ipp._tcp service with the _universal subtype so it works driverless on macOS.
- It accepts Apple Raster and PWG Raster at 300 dpi, monochrome, one copy, A5 or Letter paper, and a face-up output tray.
- A Letter-size page at 300 dpi weighs around 8.4 MB uncompressed (2,550×3,300 px), but the Xteink X3 only has 400 KB of RAM.
- With WiFi and the IPP server running, only 6.8 KB of free heap remained to receive the entire page.
- The solution was to decode, scale, and dither row by row directly onto the screen buffer, with no extra copy.
- That technique brought the image buffer down from about 113 KB to 62 KB out of the chip’s total 400 KB.
- The complete code was published in the author’s CrossPoint fork on GitHub.
Introduction
Most developers never think about how a page gets from their computer to paper. You press Cmd+P or Ctrl+P and something, somewhere, takes care of the rest. That “something” almost always speaks IPP, a protocol that runs over HTTP and that your Mac, your office printer, and even your phone speak without you noticing.
Joshi decided to open that black box. He had a Xteink X3, a programmable e-ink reader running third-party firmware called CrossPoint, and wanted a more convenient way to send it content than connecting to its hotspot and uploading files through a homemade web page. A simple question occurred to him: if the device looks like paper, why can’t it behave like paper? In other words, why not print to it directly.
What happened: the IPP protocol enters the firmware
Joshi implemented a full IPP protocol server inside the Xteink X3’s firmware. IPP lets a computer ask a printer what it supports through the Get-Printer-Attributes operation, send it a document with Print-Job, and check the job’s status. Everything travels as ordinary HTTP messages.
He declared that his printer, named penguin, supports monochrome output, 300 dpi, one copy, and single-sided printing. As document formats, he accepted Apple Raster and PWG Raster, which means the Mac itself converts the document into pixels before sending it: the Xteink X3 only has to receive and display an already-rasterized image. He also declared A5 and Letter paper sizes, a stationery media type, and an output tray called face-up.
So that it would show up automatically in the printer list without any configuration, he announced the service via Bonjour under the _ipp._tcp type, including in the announcement the accepted formats and the address where jobs should be sent. To achieve driverless discovery on macOS, he also had to add the _universal subtype, which forced him to call the ESP-IDF mDNS API directly, since the Arduino wrapper didn’t expose it.
Context and history
The name “penguin” isn’t random: it refers to the device’s color scheme. The Xteink X3 landed on Joshi’s radar because, according to the product’s marketing, it was “fully programmable.” That led him to install CrossPoint, an alternative firmware for this type of reader, on which he started experimenting with a different boot animation, a dice you roll by shaking the reader, and a LinkedIn QR code for networking events in San Francisco.
The problem showed up in the normal flow for loading content: you had to connect to the reader’s own hotspot and open a small upload page in the browser. Functional, but tedious for something used every day. That’s where the idea of using IPP instead of a manual upload flow came from: taking advantage of a protocol that any modern operating system already knows how to speak, instead of inventing a new one from scratch.
Technical details and performance
The project’s real limit wasn’t the protocol, it was memory. A Letter-size page at 300 dpi measures 2,550 by 3,300 pixels. At one byte per pixel in grayscale, that comes out to around 8.4 MB uncompressed.
The Xteink X3 has 400 KB of RAM total, with 16 KB reserved for cache. With WiFi running, the print server active, and the page image already allocated in memory, the program had just 6.8 KB of free heap left before receiving a single byte of the page.
⚠️ Heads up: Joshi’s first idea was something similar to Linux’s mmap, treating the SD card as if it were extra RAM. It didn’t work: the ESP32-C3’s memory-mapping support is for internal flash, not for files on the SD card. A classic gotcha of porting ideas from large operating systems to a microcontroller.
The solution was different: instead of simulating more RAM, reuse the RAM that already existed for something else. The Xteink X3’s screen already reserves memory for the image it displays at any given moment. Joshi built the page right there, as it arrived, instead of assembling a full separate copy and transferring it afterward.
His decoder already processed the image row by row. He modified the scaler so it would also output finished rows, and connected them directly to the device’s screen image. At first the page appeared in bands, like paper coming out of a real printer; each partial update took about half a second, so he ended up showing the full page all at once instead. The final result is saved as a BMP on the SD card using the same component he already used for screenshots.
| Image buffer | RAM used | % of total (400 KB) |
|---|---|---|
| Before (separate full copy) | ~113 KB | ~28% |
| After (direct write to screen) | ~62 KB | ~15.5% |
flowchart TD
A["Mac prints document"] --> B["Rasterizes to Apple Raster or PWG Raster"]
B --> C["HTTP POST Print-Job to the Xteink X3"]
C --> D["Decodes the image row by row"]
D --> E["Scales and applies dithering"]
E --> F["Writes the row to the screen buffer"]
F --> G[("Saves the final page as BMP on the SD card")]
The two raster formats it accepts aren’t interchangeable in practice: each one is generated by a different operating system.
| Format | When it’s used | Advantage | Limitation |
|---|---|---|---|
| Apple Raster (URF) | Printing from macOS or iOS | Native to Apple’s Print dialog, no driver required | Proprietary format, poorly documented outside the Apple ecosystem |
| PWG Raster | Printing from Linux/CUPS or generic IPP clients | Open standard from the Printer Working Group | The client has to support it explicitly |
How to start testing it
You don’t need an e-reader to experiment with IPP. Any laptop can announce and discover _ipp._tcp services on the local network using mDNS/Bonjour tools that either ship with the operating system or install in a minute.
Discovering IPP printers on your network:
# macOS (dns-sd comes included with the system)
dns-sd -B _ipp._tcp
# Linux (Debian/Ubuntu)
sudo apt install avahi-utils
avahi-browse -r _ipp._tcp
# Windows (requires Bonjour Print Services, included with iTunes or downloadable separately)
dns-sd.exe -B _ipp._tcp
This command lists any IPP printer (real or homemade) visible on the network, along with the address where it’s listening.
To announce your own toy service, without writing a full firmware, Python and the zeroconf library are enough:
# Linux / macOS / Windows (Python 3)
pip install zeroconf
# ipp_announce.py
from zeroconf import ServiceInfo, Zeroconf
import socket
info = ServiceInfo(
"_ipp._tcp.local.",
"my-home-printer._ipp._tcp.local.",
addresses=[socket.inet_aton("192.168.1.50")],
port=631,
properties={"rp": "ipp/print", "ty": "Test printer"},
)
zeroconf = Zeroconf()
zeroconf.register_service(info)
print("_ipp._tcp service announced, press Ctrl+C to exit")
try:
input()
finally:
zeroconf.unregister_service(info)
zeroconf.close()
When you run it, the service shows up in dns-sd -B _ipp._tcp or in avahi-browse without you having written a single line of printing code: that’s exactly what Joshi took advantage of so macOS would detect “penguin” without drivers.
To send a real print job to an IPP server from Node.js, the ipp package acts as the client:
// Linux / macOS / Windows (Node.js)
npm install ipp
// send-job.js
const ipp = require("ipp");
const fs = require("fs");
const printer = ipp.Printer("http://192.168.1.50:631/ipp/print");
const datos = fs.readFileSync("page.pwg");
const mensaje = {
"operation-attributes-tag": {
"requesting-user-name": "developer",
"job-name": "test-from-node",
"document-format": "image/pwg-raster",
},
data: datos,
};
printer.execute("Print-Job", mensaje, (error, respuesta) => {
if (error) throw error;
console.log("Job sent:", respuesta["job-attributes-tag"]);
});
This script builds a minimal IPP message with the job name and document format, and sends it over HTTP to port 631, the same one CUPS uses and that Joshi declared in his firmware.
Impact and analysis
What makes this project interesting isn’t that someone “hacked” an e-reader, but that it shows how much real protocol fits into very few kilobytes when implemented by hand, without the full CUPS stack or a general-purpose operating system underneath. IPP runs over HTTP, a protocol that any WiFi-enabled microcontroller can already speak; what’s usually missing isn’t the network layer, but the memory for the data that network carries.
💭 Key point: the bottleneck wasn’t in the protocol (HTTP plus a handful of IPP attributes) but in the data itself: a rasterized image. Rewriting the pipeline so it wouldn’t duplicate that image in memory was what made the whole project viable.
There’s an honest limit worth pointing out: a homemade IPP protocol server like this one implements just two operations (Get-Printer-Attributes and Print-Job). It doesn’t handle job queues, authentication, cancellation, or concurrent printing from multiple clients, things that CUPS does handle on a shared office print server. It works perfectly for a single-user personal device, not as a replacement for a shared network printer.
What’s next
The code was published in the author’s own CrossPoint fork, opening the door for other owners of readers with the same firmware to replicate the print server or extend it with support for more paper sizes, color printing on devices that allow it, or more detailed job status reporting via IPP. Joshi himself already uses the device to browse the printed files saved on the SD card, which works as a kind of permanent output tray.
📖 Summary on Telegram: View summary
If you have an ESP32 with ESP-IDF support and want to experiment, try announcing your own _ipp._tcp service with the Python script above this afternoon and confirm it shows up in dns-sd -B _ipp._tcp or avahi-browse -r _ipp._tcp.
Frequently Asked Questions
What is the IPP protocol and how does it differ from printing over USB?
IPP (Internet Printing Protocol) is a protocol that runs over HTTP and lets a computer query a printer’s capabilities, send it a document, and check the job’s status, all over the network. Unlike USB, it doesn’t require a cable or a manufacturer-specific driver: it’s enough for both ends to speak the same protocol.
Do I need to install a driver to use an IPP printer?
In most cases, no. If the printer correctly announces its capabilities and supported formats, systems like macOS can automatically generate a generic driver from that information, without downloading anything from the manufacturer.
Why use Bonjour/mDNS instead of setting up a fixed IP?
Bonjour lets the device announce itself on the local network on its own, with its name, address, and capabilities, so computers can detect it without the user having to type in an IP or install additional software.
What is the _universal subtype and why does it matter on macOS?
It’s an additional subtype of the _ipp._tcp service that macOS uses specifically to offer driverless discovery and printing (AirPrint-style). Without declaring it, the device can still be visible on the network but won’t qualify for that no-configuration flow.
Can I replicate this on a regular ESP32 without an e-ink screen?
Yes, the protocol part (HTTP server plus IPP attributes plus mDNS announcement) doesn’t depend on having a screen. What changes is where you save or display the received page: without a screen, you’d have to write it directly to the SD card or to flash instead of to the screen buffer.
What limitations does a homemade IPP server like this one have?
It doesn’t handle job queues, authentication, or simultaneous printing from multiple clients. It’s suitable for a single-user personal device, not as a replacement for a shared network printer in an office.
References
- Nishant Joshi’s blog: the original post where he documents the complete implementation of the IPP server on the Xteink X3.
- RFC 8011 (IETF): specification of the model and semantics of the Internet Printing Protocol.
- Printer Working Group: the organization that maintains the PWG Raster standard used by IPP printers.
- Bonjour (Apple Developer): documentation for the service discovery protocol used for the _ipp._tcp announcement.
- Internet Printing Protocol (Wikipedia): general context and history of the IPP protocol.
📱 Do you like this content? Follow @programacion on Telegram for daily tech content in Spanish: quick summaries, fresh content every day.
Imagen destacada: Foto de Nicolas Thomas en Unsplash
0 Comments