Reverse engineering a Cat Printer with the help of an LLM to make it print long cats
By sending the printing commands directly by code over bluetooth
Okay, I’ve got cat printer:
It’s cheap, it’s wireless(-only), it prints well enough and looks like a cat. The problem is, you can’t just plug it and use it as a printer - you have to use it’s own app “Fun Print“. The app is neat but I need to print programmatically, through my own code. No documentation, the printer has many versions and the libraries others created don’t work with the version I have. The plan is to see what Fun Print does and then do it myself. However, I have no technical understanding of Bluetooth works so I will do it with the help of an LLM.
Listening to the communications between the cat printer and the Fun Print app
ChatGPT suggested to use Wireshark or dedicated sniffing hardware. This was a bad advice, you don’t need to buy hardware or try to make Wireshark sniff on macOS. Apple provides its own PackageLogger tool which will show you all the bluetooth packages both on macOS and on iOS when you connect your iPhone over USB.
There’s just one setup step: You need to install the appropriate profile on the device you’ll sniff. Go ahead and download it from Apple and install.

Once you’ve done that, open the PackageLogger which you can get from Apple’s developer portal. It’s in the Additional Tools for Xcode package. Here.
Under the file menu, there you have the options to trace macOS and iOS bluetooth packages.
And here it is, all the Bluetooth communications in all of its glory.

As ChatGPT explained to me, the HCI is about the communications between your computer and the Bluetooth chip in it and L2CAP is a logical protocol that keeps things tidy . The packages that go to the printer are the ATT ones and that’s where the meat is.
Sending data to the bluetooth printer
Apple provides the CoreBluetooth library which you can use on all Apple platforms to do high level bluetooth stuff.
With ChatGPT it was quite easy to bootstrap the app. Using the delegate pattern you tell the CoreBluetooth to start scanning and then you collect the list of the discovered devices. Connecting to the device is a single line of code. Tell you LLM to create you a class for discovering and connecting to Bluetooth using CoreBluetooth.
But how do you send and receive data from the device? You do it by writing, reading or get notified through a device “characteristics“. In essence, the device can have many of those characteristics and they each can support different functions(some are read only, others allow you to write and give you a response etc). CoreBluetooth can handle the discovery of those characteristics and their properties as well. Tell your LLM to add characteristics discovery function.

Let’s observe the packages. Before sending data is sending this
"2221 A900 0400 2B00 3000 FFFF"
When I change the prints side, this changes "2221 A900 0400 2B00 B000 FFFF"
Yep this is line numbers * 2^8
When changing the darkness settings its sending a write command "2221 A100 0100 (0000) FF" . This is setting the number from 1-100 * 2^8
Now that I can send header and footer commands as well as print commands and I can set how dark should it print, the rest must be easy. I should be able to send values from 0 to 255 for each pixel on each line and be able to print whatever I want. Right?
Wrong.
When I send 384 values as a repeating gradient I get this.
This is a repeating pattern but its definitely sliding between lines, so it must be overflowing. Hmm First let’s try to get a consistent pattern when multiple lines are sent. After some trial and error, I found that the correct number is 48 values. Then I get this:
Okay, this looks like some kind of a gradient but not what I was expecting. I’m starting to think that this printer can’t actually print in different shades of gray, instead it must have some some patterns for numbers. 0 is empty FF is full black but it is not for a single pixel. As it appears, there are 48 “pixels“ that can produce some pattern to choose from. But Wait a minute, 48 is very suspicious number! Those are not pixels, those are bytes.
yep: 384/48 = 8
You can actually pack eight 0’s and 1’s in every byte and since there are 48 bytes per line, the math checks out. I better ask my LLM to write me a workflow.
I told my LLM to create this pipeline for me: image->grayscale image ->resize->dither->turn into array of pixel stings for lines->packBitsToData
It worked.
More to explore: How to print a long paper: It appears that for consistent printing you should avoid saturating or starving the flow of data; If you do that, you can keep printing until the paper roll is over.
But can it print softer colors? Grayscale vs Dithered? The official app can, it has HD mode where dark and light areas are created by having different shades of gray instead of different concentration of dots.
However It appears that when in HD mode it is encoded differently. I will need to figure out how that works.