133 lines
5.6 KiB
Markdown
133 lines
5.6 KiB
Markdown
# thermal-printer
|
|
A project to reverse engineer the protocol of my thermal printer - Vyzio B15 (X6) and [implement it myself](#Usage), so I can print from my laptop.
|
|
|
|
This project was inspired by [WerWolv's Cat printer blogpost](https://werwolv.net/blog/cat_printer)
|
|
, but I avoided looking at their code and peaking at the blogpost as much as i could.
|
|
|
|
## Protocol
|
|
### Command structure
|
|
Consists of commands. Each command has the following structure:
|
|
`0x51` - Magic value
|
|
`0x78` - Magic value
|
|
`0x..` - Command byte
|
|
`0x00` or `0x01` - Direction (0 for Phone->Printer, 1 for reverse)
|
|
`0x..` - Data length, low byte
|
|
`0x00` - Data length, high byte (always zero)
|
|
`data` * N - Data
|
|
`crc8` - crc8 of only the data
|
|
`0xFF` - End magic
|
|
|
|
### Printing an image
|
|
And the app sends the following commands to print an image (refer to the [commands](#getcommandcodecmdname)):
|
|
1. Blackening (quality) - always 51 (3) for my printer. It is from 1 to 5 (49 to 53).
|
|
2. Energy - only if the print type isn't TEXT: corresponds to the _Print depth_ set in the app, with the formula `energy = 7500 + (PD - 4)*0.15*7500`. The Print depth is from 1 to 7.
|
|
3. PrintType - Image(`0x00`), Text(`0x01`) of Label(`0x03`). Still can't find the differences between them, but I think that Image allows for darker images, but I'm not sure.
|
|
4. FeedPaper(speed) - I don't know what it does, but the speed is 10 for text and 30 for images and labels.
|
|
5. Drawing - commands with either run-length encoded or bit-packed lines
|
|
6. FeedPaper(25), Paper(0x30), Paper(0x30), FeedPaper(25)
|
|
I don't know what the FeedPaper calls do, but the Paper command feeds the paper so the printed image emerges.
|
|
|
|
## Usage
|
|
In the `./src` directory you can find python files with functions to use the protocol and example usage in `./src/main.py`.
|
|
|
|
### commands.py
|
|
Here you can find functions to create the bytes to command the printer.
|
|
|
|
#### checkImage(imgPath)
|
|
Checks if the image at this path is usable for the printer (384 px wide and 1bpp)
|
|
|
|
#### calcCrc8(data)
|
|
Calculates the crc8 checksum
|
|
|
|
#### commandBytes(cmd, data)
|
|
Generates the byte array for a command
|
|
|
|
#### getCommandCode(cmdName)
|
|
Returns the byte corresponding to the command. Accepts:
|
|
| Name(s) | Description | Byte |
|
|
| - | - | - |
|
|
| BLACKENING _(QUALITY)_ | Sets the quality level for the printer. I recommend keeping at default value 51. | `0xA4` |
|
|
| ENER(A)GY | Sets the energy (print depth) - the strength of heating of the black pixels. Use the `getEnergy()` function. Default value is 7500 for my model. | `0xAF` |
|
|
| PRINT_TYPE | Sets the print type. | `0xBE` |
|
|
| FEED_PAPER | Idk what, **DOES NOT** feed the paper. | `0xBD` |
|
|
| DRAW_COMPRESSED | Draws a row of pixels, encoded with run-length encoding. The highest bit of each byte is the value and the other 7 encode the run length. | `0xBF` |
|
|
| DRAW_PACKED | Draws a row of pixels where each pixel corresponds to one bit in the command's data. | `0xA2` |
|
|
| PAPER | Actually feeds the paper. Takes number of lines to move, with data size 2 (potentially 16 bite, little endian). | `0xA1` |
|
|
|
|
#### runLenght(line)
|
|
Creates a byte array with the line compresses as explained above (see DRAW_COMPRESSED).
|
|
|
|
#### bitPack(line)
|
|
Creates a byte array with each pixel corresponding to a bit.
|
|
|
|
#### toArrayLE(num)
|
|
Converts 16-bit number to little-endian
|
|
|
|
#### getEnergy(printDepth)
|
|
Calculates the energy corresponding to a certain value of "Print depth" in the app
|
|
|
|
#### getTypeByte(typeStr)
|
|
Returns byte code for the print type. Supports
|
|
| Type | Byte |
|
|
| - | - |
|
|
| IMAGE | `0x00` |
|
|
| TEXT | `0x01` |
|
|
| LABEL | `0x03` |
|
|
|
|
#### getSpeed(typeStr)
|
|
Returns the app's default "printSpeed" for types. Supports "IMAGE" (30) and "TEXT" (10). This is used with FEED_PAPER, idk how.
|
|
|
|
#### readImage(imgPath)
|
|
Reads image from the path specified.
|
|
|
|
#### compressImageToCmds(img)
|
|
For each line applies both run-length encoding and bit packing and chooses the shorter. Packages each line's compressed data into the appropriate command.
|
|
|
|
#### saveCommandsToFile(commands, saveToFile=None, saveToFileHuman=None)
|
|
Saves a list of commands to a binary file and/or a human-readable hex file.
|
|
|
|
#### genMoveUp(lines=0x60)
|
|
Generates the paper feeding commands at the end of the app's routine for printing an image.
|
|
|
|
#### genCommands( img, ... _(see source code)_ )
|
|
Generates the commands for printing an image with the specified parameters.
|
|
|
|
### connection.py and class Connection
|
|
#### async connect()
|
|
Connects to the device and awaits OKAY status.
|
|
|
|
#### async disconnect()
|
|
Disconnects from the device.
|
|
|
|
#### \_\_init\_\_(macAddress)
|
|
Sets up the Connection for the .connect method.
|
|
|
|
#### send(commands, delay=0)
|
|
Writes each command individually to the appropriate characteristic, waiting _delay_ seconds between each one. In the end asks device for status and awaits OKAY.
|
|
|
|
#### Context managers
|
|
The Connection class supports the context manager protocol. It automatically connects and disconnects to the device.
|
|
```python
|
|
async with Connection("AA:BB:CC:DD:EE:FF") as conn:
|
|
...
|
|
```
|
|
|
|
#### FakeConnection
|
|
This class mirrors the methods of Connection, but its constructor takes paths to log files, where the sent data gets dumped.
|
|
|
|
### decode.py
|
|
#### readHumanFile(fileName)
|
|
Reads a human-readable file of hex values into an array of bytes.
|
|
|
|
#### readFile(fileName)
|
|
Reads a binary file into an array of bytes.
|
|
|
|
#### decodeCommands(cmdBytes, saveFilename=None)
|
|
Decodes the bytes given into the image they convey and returns it. If _saveFilename_ is set, then it saves the image there.
|
|
|
|
### textToImage.py
|
|
#### textToImage(text, ... _(see source code)_ )
|
|
Generates a PIL image from the given text.
|
|
|
|
## Write up
|
|
You can read about my process of reverse engineering the protocol [here](WRITE_UP.md). |