How to build a f**king printer
How to build a f**king printer
如何打造一台该死的打印机
I 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.
我原本打算做一个电子墨水屏显示器,却在网上偶然发现了 Xteink X3。在细则的某个角落里,写着这玩意儿是完全可编程的。我心动了。当它到货时,我很喜欢它轻薄的外观,拿在手里感觉很扎实。我开始往 CrossPoint 固件里添加东西:不同的开机动画、可以通过摇晃阅读器来投掷的骰子、用于旧金山社交活动的 LinkedIn 二维码。但要把东西传上去实在太麻烦了:我必须连接它的热点,然后在浏览器里打开一个简陋的上传网页。真糟糕。
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.
在寻找更优雅的传输方式时,一个念头闪过:如果它看起来像纸,那它就应该像纸一样工作。我应该能直接打印到它上面。什么才叫打印机?我想在 MacBook 上打开文件,按下“打印”,然后选择 Xteink。这意味着我得搞清楚电脑在另一端期待看到什么。最终我研究了互联网打印协议(IPP)。它允许电脑询问打印机支持什么功能、提交文档,并查询任务状态。这些消息通过 HTTP 传输。例如,Get-Printer-Attributes 操作用于查询功能,而 Print-Job 则用于发送任务。
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.
我声明了单色输出、300 dpi、单份打印和单面打印。在文档格式方面,我接受 Apple raster 和 PWG raster。这意味着 Mac 在发送前必须先将文档转换为像素。我的“企鹅”(penguin)设备会将结果缩小以适配屏幕。我声明了 A5 和 Letter 纸张、文具介质类型,以及一个名为“正面朝上”的出纸盒。我把它命名为“penguin”,因为它配色很像。我使用 Bonjour 广播了一个同名的 _ipp._tcp 服务。广播信息包含了支持的格式和打印任务的接收地址。为了在 macOS 上实现无驱动发现,我还必须添加 _universal 子类型。这意味着我得直接调用 ESP-IDF 的 mDNS API,因为 Arduino 的封装库并没有暴露这个接口。
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?
让电脑发送页面只是工作的一部分,我还得在设备上接收它。页面放哪儿呢?一张 300 dpi 的 Letter 纸大小为 2,550 × 3,300 像素。按每个灰度像素 1 字节计算,未压缩时约为 8.4 MB。X3 只有 400 KB 的内存,其中 16 KB 预留给缓存。我需要运行 Wi-Fi、运行打印服务器,还要想办法接收整整一页该死的数据。在 Wi-Fi 运行且分配了打印页面图像后,我只剩下 6.8 KB 的堆内存。我想起了 Linux 上的 mmap,能不能对 SD 卡做类似的操作,假装我有更多内存?C3 的内存映射支持仅限于闪存,不支持 SD 卡文件。等等,我能不能把显示屏当作存储器?
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.
如果我把传入的页面通过一个转换流水线,直接把结果写到显示屏上呢?解码像素、缩小适配、抖动处理成黑白。一旦一行准备好,就把它放到显示屏的对应位置,然后重用工作空间。一直循环直到完成整页。显示屏已经预留了屏幕图像的内存,我可以在数据到达时直接在那里构建页面。在此之前,我一直在组装第二个完整的图像,然后再把它复制过去。我的解码器本来就是逐行工作的,我修改了缩放器,让它也能逐行输出,并直接连接到显示屏的屏幕图像中。起初,我让页面像打印机吐纸一样分段显示。每次中间刷新大约需要半秒,所以我改成了等页面全部完成后一次性显示。我使用现有的截图写入功能,将完成的页面以 BMP 格式保存在 SD 卡上。
| Image-buffer RAM | Before | After |
|---|---|---|
| Out of the chip’s 400 KB | ~113 KB | ~62 KB |
| 图像缓冲区内存 | 优化前 | 优化后 |
|---|---|---|
| 芯片 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.
这为网络栈的套接字缓冲区腾出了空间。在“预览”应用里,真的出现了“penguin”。我 MacBook 里不知为何存了一张《无职转生》的漫画样张。我用“预览”打开并点击打印,打印机列表里赫然出现了“penguin”。天哪。我选中它并打印。漫画页面在 Xteink 上看起来效果非常好。凭记忆,它大约一秒钟就显示出来了,而且一直保留着。我花了一个晚上实现了第一次打印。打印服务器直接运行在阅读器上。我可以让它加入 Wi-Fi 网络,或者开启自己的热点“literate-penguin”。我的“企鹅”现在能阅读了。代码在我的 CrossPoint 分支里,包含了打印机实现。保存的打印件会留在 SD 卡上,我可以在阅读器上浏览它们。我的打印机终究还是有一个出纸托盘的,它就是一个文件夹。