LmCast :: Stay tuned in

How to Build a Printer

Recorded: Sept. 8, 2026, 11:09 p.m.

Original Summarized

How to build a f**king printer · Nishant JoshiWriting/How to build a f**king printerHow to build a f**king printersep 7, 2026800 wordspublicI was planning to build an e-ink display when I came across the Xteink X3 online. Somewhere in the fine print, it said the thing was fully programmable.
I was sold.
When it arrived, I liked how flat it was. It felt solid in my hand. I started adding things to the CrossPoint firmware: a different boot animation, dice I could roll by shaking the reader, a LinkedIn QR code for SF networking events.
But getting stuff onto it was tedious. I had to join its hotspot and open a little upload website in my browser.
Yuck.
While looking for a nicer way to send things to it, a thought struck me. If it looks like paper, it should act like paper.
I should be able to print on it.

What makes a printer a printer?
I wanted to open something on my MacBook, press Print, and pick the Xteink. That meant finding out what my computer expected to find at the other end.
I ended up in the Internet Printing Protocol, or IPP. It lets a computer ask a printer what it supports, submit a document, and ask what happened to the job. The messages travel over HTTP. An operation such as Get-Printer-Attributes asks about capabilities; Print-Job sends the work.
I advertised monochrome output, 300 dpi, one copy, and one-sided printing. For document formats, I accepted Apple raster and PWG raster. That meant the Mac had to turn the document into pixels before sending it. penguin would shrink the result to fit its screen.
I declared A5 and Letter paper, media type stationery, and an output bin called face-up.
I called it penguin. It had the right color scheme.
I used Bonjour to announce an _ipp._tcp service under that name. The advertisement included the formats I accepted and the address where print jobs should go.
To get driverless discovery on macOS, I also had to add the _universal subtype. That meant calling ESP-IDF’s mDNS API directly, because the Arduino wrapper didn’t expose it.
Getting the computer to send a page was only part of the job. I still had to receive it on this thing.
Where do I put the page?
A Letter page at 300 dpi is 2,550 × 3,300 pixels. At one byte per grayscale pixel, that’s about 8.4 MB uncompressed.
The X3 has 400 KB of RAM, with 16 KB reserved for cache. I needed to run Wi-Fi, run a printer server, and somehow receive an entire fucking page.
With Wi-Fi running and the printer’s page image allocated, I had 6.8 KB of heap left.
I remembered mmap on Linux. Could I do something like that with the SD card and pretend I had more RAM? The C3’s memory-mapping support was for flash, not files on the SD card.
But wait. Could I make the display my storage?
What if I passed the incoming page through a transformation pipeline and wrote the result straight to the display? Decode the pixels, shrink them to fit, dither them into black and white. As soon as a row was ready, put it in its place on the display and reuse the working space. Keep going until I have a page.
The display already had RAM reserved for its screen image. I could build the page right there as it arrived. Until then, I had been assembling a whole second image just to copy it over.
My decoder already worked row by row. I changed the scaler to hand over finished rows too and wired those into the display’s screen image. At first, I let the page appear in bands, like paper feeding out of a printer. Each intermediate refresh took roughly half a second, so I switched to showing the finished page all at once.
I saved the finished page as a BMP on the SD card using the existing screenshot writer.

Image-buffer RAM
Before
After

Out of the chip’s 400 KB
~113 KB
~62 KB

That gave the network stack room for its socket buffers.
There was a penguin in Preview
I had a sample manga image from Mushoku Tensei on my MacBook for some reason. I opened it in Preview and went to print it.
There was penguin in the printer list.
Holy shit.
I selected it and printed. The manga page looked really good on the Xteink. From memory, it took about a second to appear. It’s still there.

I spent an evening getting to that first print.
The printer server runs on the reader itself. I can have it join a Wi-Fi network or start its own hotspot, literate-penguin.
My penguin can read now.
The code is in my CrossPoint fork, including the printer implementation.
Saved printouts stay on the SD card, and I can browse them on the reader. My printer has an output tray after all. It’s a folder.

The process of building a functional printer capability on an e-ink display, as described by Nishant Joshi, stems from the desire to make the display function physically like paper rather than just an e-ink reader. The initial exploration involved taking an existing programmable e-ink display, such as the Xteink X3, and attempting to implement printing functionality. This endeavor led to investigating established standards to define how a computer would interact with the hardware for printing.

Joshi discovered the relevance of the Internet Printing Protocol or IPP, which facilitates communication between a computer and a printer. The protocol allows a computer to inquire about the printer's capabilities, submit print jobs, and receive status updates via HTTP messaging. To implement this functionality on an e-ink device, the author defined specific attributes: advertising monochrome output, 300 dpi resolution for printing, one copy, one-sided printing, and supported document formats like Apple raster and PWG raster. Furthermore, settings were established for paper media types, such as A5 and Letter sizes, along with an output bin configuration called face-up.

To ensure that macOS systems could discover this new device driverlessly, the author utilized Bonjour to announce an ipp/tcp service under a defined name. Essential for broad compatibility with the macOS environment was adding the universal subtype, which required interfacing directly with the mDNS API from ESP-IDF because the standard Arduino wrapper did not expose this functionality.

A significant technical challenge arose in managing the data transfer, specifically how to receive and display a full page. A single Letter page at 300 dpi translates to approximately 2,550 by 3,300 pixels, requiring roughly 8.4 megabytes of uncompressed data. Given the limited memory resources of the X3, which possesses only 400 KB of RAM, allocating space for Wi-Fi operations and a printer server left very little room for the page image itself. Joshi explored using memory mapping techniques on the SD card but found limitations concerning file system access.

The proposed solution involved treating the display's existing RAM as the processing buffer. The strategy was to implement a transformation pipeline where incoming pixel data from the print job would be decoded, scaled down to fit the screen dimensions, dithered into black and white format, and immediately written to the display image row by row. This process allowed the system to reuse working space efficiently. By modifying the decoder to handle finished rows as well, the author managed the flow of data to display the page correctly, initially showing it in bands before switching to displaying the complete image at once. The final processed image was saved onto the SD card in the BMP format using existing screenshot writer methods.

This method successfully demonstrated the viability of printing on the e-ink display; an example was achieved when a sample manga page from Mushoku Tensei, printed via Preview on a MacBook, appeared correctly on the Xteink. The overall system relies on a printer server running on the reader itself, implemented within a fork of the CrossPoint firmware, which manages Wi-Fi connectivity and handles the printing input and output storage on the SD card.