Skip to content

Instantly share code, notes, and snippets.

@stecman
Last active February 7, 2023 22:29
Embed
What would you like to do?
Brother P-Touch PT-P300BT bluetooth driver python

Controlling the Brother P-Touch Cube label maker from a computer

The Brother PTP300BT label maker is intended to be controlled using the official Brother P-Touch Design & Print iOS/Android app. The app has arbitrary limits on what you can print (1 text object and up to 3 preset icons), so I thought it would be a fun challenge to reverse engineer the protocol to print whatever I wanted.

Python code at the bottom if you want to skip the fine details.

Process

Intitially I had a quick peek at the Android APK to see if there was any useful information inside. The code that handles the communication with the printer in Print&Design turned out to be a native library, but the app clearly prepares a bitmap image and passes it to this native library for printing. Bitmaps are definitely something we can work with.

Next I used the bluetooth sniffing capability of stock Android to capture a few label prints from the official app. Inspecting these packet captures in Wireshark, it was apparent that all of the communication used bluetooth's serial port profile (SPP). Interestingly, the the printer shows up as "Fujitsu" in packet captures, and since Brother has a lot of label maker products I figured there was a good chance they were using some existing label maker hardware and firmware with a bluetooth to serial adapter bolted on.

After a little Googling, this hunch paid off - a bunch of developer documentation for some of Brother's higher-end/business label maker products matched up with the bytes being sent over the bluetooth serial connection. Mainly:

At first I found similarities in a manual for Brother's ESC/P protocol, which has the same command format, initialisation command and 32 byte status format, but the P-Touch Cube doesn't appear to support this (based on trying the ESC/P commands on the device).

Serial protocol

From Brother's developer docs for a different device, the packet captures could be broken down as:

// 64 bytes of 0x0 (to clear print buffer?)
...

// Initialize/reset settings registers
1B 40

// Enter raster mode (aka. PTCBP)
1B 69 61 01

// Set media and quality (most importantly, the number of 128px lines to expect)
// Found docs for this last by searching for the first three bytes (command)
// See http://www.undocprint.org/formats/page_description_languages/brother_p-touch
1B 69 7A C4 01 0C 00 08 01 00 00 00 00

// Set expanded mode bits (print chaining: off)
1B 69 4B 08

// Set mode bits (mirror print: no, auto tape cut: no)
1B 69 4D 00

// Set margin (feed) size
1B 69 64 1C 00

// Set compression mode: TIFF (packbits)
4D 02

// Transfer n1 + n2*256 bytes of raster data
// The official app transfers one line of data at a time (16 bytes = 128 pixels)
47 n1 n2 [data]
...

// Print and feed
1A

Image data

Image data is sent to the printer as a 1-bit-per-pixel bitmap. The Brother app sends a 128 pixel wide image (regardless of tape width), oriented as lines across the print head. For a horizontal label (printing along the length of tape), the input image needs to be rotated by 90 degrees before sending.

Once in the correct orientation, image data needs to be mirrored horizontally (with the settings above at least). It looks like the command 1B 69 4D can be used to enable mirroring by the printer, but I haven't tested this.

The outer edges of a 12mm label do not appear to be printable (print head too narrow?). The outer 30 pixels of each side (length-wise) are not printed. I haven't tested with narrower labels.

Python code

The code here is what I had at the point I got this working - it's a bit hacked together. It prints images, but the status messages printed aren't complete and the main script needs some tidying up. The printer sometimes goes to an error state after printing (haven't figured out why yet), which can be cleared by pressing the power button once.

This needs a few modules installed to run:

pyserial
pypng
packbits

Then it can be used as:

# Existing image formated to spec above
./labelmaker.py monochrome-128px-wide-image.png

# Using imagemagick to get a usable input image from any horizontal oriented image
# -resize 128x can be used instead of -crop 128x as needed
# -rotate 90 can be removed if the image is portrait already
convert inputimage.png -monochrome -gravity center -crop 128x -rotate 90 -flop out.png

I was working on Linux, so the serial device is currently hard-coded as /dev/rfcomm0. On OSX, a /dev/tty.* device will show up once the printer is paired.

To pair the printer with my Linux machine, I used:

# Pair device
$ bluetoothctl
> scan on
... (turn printer on and wait for it to show up: PT-P300BT8894)
> pair [address]

# Setup serial port
$ sudo modprobe rfcomm
$ sudo rfcomm bind rfcomm0 [address of printer]
#!/usr/bin/env python
from labelmaker_encode import encode_raster_transfer, read_png, unsigned_char
import binascii
import packbits
import serial
import sys
import time
STATUS_OFFSET_BATTERY = 6
STATUS_OFFSET_EXTENDED_ERROR = 7
STATUS_OFFSET_ERROR_INFO_1 = 8
STATUS_OFFSET_ERROR_INFO_2 = 9
STATUS_OFFSET_STATUS_TYPE = 18
STATUS_OFFSET_PHASE_TYPE = 19
STATUS_OFFSET_NOTIFICATION = 22
STATUS_TYPE = [
"Reply to status request",
"Printing completed",
"Error occured",
"IF mode finished",
"Power off",
"Notification",
"Phase change",
]
STATUS_BATTERY = [
"Full",
"Half",
"Low",
"Change batteries",
"AC adapter in use"
]
def print_status(raw):
if len(raw) != 32:
print "Error: status must be 32 bytes. Got " + len(raw)
return
if raw[STATUS_OFFSET_STATUS_TYPE] < len(STATUS_TYPE):
print "Status: " + STATUS_TYPE[raw[STATUS_OFFSET_STATUS_TYPE]]
else:
print "Status: 0x" + binascii.hexlify(raw[STATUS_OFFSET_STATUS_TYPE])
if raw[STATUS_OFFSET_BATTERY] < len(STATUS_BATTERY):
print "Battery: " + STATUS_BATTERY[raw[STATUS_OFFSET_BATTERY]]
else:
print "Battery: 0x" + binascii.hexlify(raw[STATUS_OFFSET_BATTERY])
print "Error info 1: 0x" + binascii.hexlify(raw[STATUS_OFFSET_ERROR_INFO_1])
print "Error info 2: 0x" + binascii.hexlify(raw[STATUS_OFFSET_ERROR_INFO_2])
print "Extended error: 0x" + binascii.hexlify(raw[STATUS_OFFSET_EXTENDED_ERROR])
print
# Check for input image
if len(sys.argv) < 2:
print "Usage: %s <path-to-image>" % sys.argv[0]
sys.exit(1)
# Get serial device
ser = serial.Serial(
'/dev/rfcomm0',
baudrate=9600,
stopbits=serial.STOPBITS_ONE,
parity=serial.PARITY_NONE,
bytesize=8,
dsrdtr=True
)
print(ser.name)
# Read input image into memory
data = read_png(sys.argv[1])
# Enter raster graphics (PTCBP) mode
ser.write(b"\x1b\x69\x61\x01")
# Initialize
ser.write(b"\x1b\x40")
# Dump status
ser.write(b"\x1b\x69\x53")
print_status( ser.read(size=32) )
# Flush print buffer
for i in range(64):
ser.write(b"\x00")
# Initialize
ser.write(b"\x1b\x40")
# Enter raster graphics (PTCBP) mode
ser.write(b"\x1b\x69\x61\x01")
# Found docs on http://www.undocprint.org/formats/page_description_languages/brother_p-touch
ser.write(b"\x1B\x69\x7A") # Set media & quality
ser.write(b"\xC4\x01") # print quality, continuous roll
ser.write(b"\x0C") # Tape width in mm
ser.write(b"\x00") # Label height in mm (0 for continuous roll)
# Number of raster lines in image data
raster_lines = len(data) / 16
print raster_lines, raster_lines % 256, int(raster_lines / 256)
ser.write( unsigned_char.pack( raster_lines % 256 ) )
ser.write( unsigned_char.pack( int(raster_lines / 256) ) )
# Unused data bytes in the "set media and quality" command
ser.write(b"\x00\x00\x00\x00")
# Set print chaining off (0x8) or on (0x0)
ser.write(b"\x1B\x69\x4B\x08")
# Set no mirror, no auto tape cut
ser.write(b"\x1B\x69\x4D\x00")
# Set margin amount (feed amount)
ser.write(b"\x1B\x69\x64\x00\x00")
# Set compression mode: TIFF
ser.write(b"\x4D\x02")
# Send image data
print("Sending image data")
ser.write( encode_raster_transfer(data) )
print "Done"
# Print and feed
ser.write(b"\x1A")
# Dump status that the printer returns
print_status( ser.read(size=32) )
# Initialize
ser.write(b"\x1b\x40")
ser.close()
import packbits
import png
import struct
# "Raster graphics transfer" serial command
TRANSFER_COMMAND = b"\x47"
unsigned_char = struct.Struct('B');
def as_unsigned_char(byte):
""" Interpret a byte as an unsigned int """
return unsigned_char.unpack(byte)[0]
def encode_raster_transfer(data):
""" Encode 1 bit per pixel image data for transfer over serial to the printer """
buf = bytearray()
# Send in chunks of 1 line (128px @ 1bpp = 16 bytes)
# This mirrors the official app from Brother. Other values haven't been tested.
chunk_size = 16
for i in xrange(0, len(data), chunk_size):
chunk = data[i : i + chunk_size]
# Encode as tiff
packed_chunk = packbits.encode(chunk)
# Write header
buf.append(TRANSFER_COMMAND)
# Write number of bytes to transfer (n1 + n2*256)
length = len(packed_chunk)
buf.append(unsigned_char.pack( int(length % 256) ))
buf.append(unsigned_char.pack( int(length / 256) ))
# Write data
buf.extend(packed_chunk)
return buf
def decode_raster_transfer(data):
""" Read data encoded as T encoded as TIFF with transfer headers """
buf = bytearray()
i = 0
while i < len(data):
if data[i] == TRANSFER_COMMAND:
# Decode number of bytes to transfer
n1 = as_unsigned_char(data[i+1])
n2 = as_unsigned_char(data[i+2])
num_bytes = n1 + n2*256
# Copy contents of transfer to output buffer
transferedData = data[i + 3 : i + 3 + num_bytes]
buf.extend(transferedData)
# Confirm
if len(transferedData) != num_bytes:
raise Exception("Failed to read %d bytes at index %s: end of input data reached." % (num_bytes, i))
# Shift to the next position after these command and data bytes
i = i + 3 + num_bytes
else:
raise Exception("Unexpected byte %s" % data[i])
return buf
def read_png(path):
""" Read a (monochrome) PNG image and convert to 1bpp raw data
This should work with any 8 bit PNG. To ensure compatibility, the image can
be processed with Imagemagick first using the -monochrome flag.
"""
buf = bytearray()
# State for bit packing
bit_cursor = 8
byte = 0
# Read the PNG image
reader = png.Reader(filename=path)
width, height, rows, metadata = reader.asRGB()
# Loop over image and pack into 1bpp buffer
for row in rows:
for pixel in xrange(0, len(row), 3):
bit_cursor -= 1
if row[pixel] == 0:
byte |= (1 << bit_cursor)
if bit_cursor == 0:
buf.append(unsigned_char.pack(byte))
byte = 0
bit_cursor = 8
return buf
@puzrin
Copy link

puzrin commented Dec 28, 2020

@probonopd can't say anything concrete about p-700 without examples. What exactly is "not quite right"?

To be honest, i have more high priority projects. I can share all i know, if you have concrete questions, but i'd like to avoid active participation.

@probonopd
Copy link

probonopd commented Dec 28, 2020

@puzrin I have something working here... What license is your code under? Would MIT or BSD be OK? I want to upload it to a "real" repo.

@brzd
Copy link

brzd commented Dec 28, 2020

@probonopd That would be great to start with. I was thinking possibility to use cli for bulk printing from i.e. csv If there would be gui to have this possibility then even better. But to start with very simple solution to key in or copy would be fine.

. @brzd what kind of GUI do you need? Would something like this work for you?

@puzrin
Copy link

puzrin commented Dec 28, 2020

@probonopd https://github.com/puzrin/lbprint/blob/master/LICENSE

MIT, + something for embedded fonts. If you copy without fonts => pure MIT.

@probonopd
Copy link

probonopd commented Dec 28, 2020

I realize only now that https://github.com/puzrin/lbprint/ exists. I have used the snippets in this thread so far...

https://github.com/probonopd/ptouch-770/tree/patch-1/pure-python is a version of the GUI that works for me - still very rough around the edges (still Python 2, no proper error handling etc.) but it may be a start for anyone who is interested.

Trying to convert it to Python 3, but stuck with TypeError: 'bytes' object cannot be interpreted as an integer here:

            if bit_cursor == 0:
                buf.append(unsigned_char.pack(byte))
                # TypeError: 'bytes' object cannot be interpreted as an integer
                byte = 0
                bit_cursor = 8

@puzrin
Copy link

puzrin commented Dec 29, 2020

UPD. Issues with image size caused by old batteries. After replace print max height as expected.

The only issue left - P300BT falls into error state several seconds after printing.

@vsigler
Copy link

vsigler commented Sep 22, 2021

Great thread here! Not sure if anyone is still listening here, but after a few changes to work with python3, I got it to print. However, if I feed it a 128px image, only about half is actually printed.

Digging through the other implementations, the images are resized to 128-(2*30) = 68px. The protocol reference says 70pins for printed area. However, if I try to feed it a 68px or 70px image, I get an error. It did print with 64px size, but then the image was super small, corrupted and printed twice.

Generally, with different image sizes, I've got different kinds of errors/crashes:

  1. data transfer error returned by the firmware - recoverable
  2. error state - red led color
  3. printer crash - printer powers down
  4. printer freeze - green led flashing indefinitely, not responding to power button, had to pull out batteries

These can be achieved using different image sizes. However, I have yet to crack the correct image size.

The current implementations are either in python or javascript, but I need a java implementation, so I am trying to understand the protocol properly, so that I can write it.

Edit: Ok, silly me, I missed the info in the text. The 128px is of course including the non-printable area, so the image needs to be zero-padded from the sides. 🤦

@probonopd
Copy link

@vsigler

The 128px is of course including the non-printable area, so the image needs to be zero-padded from the sides.

That is also my conclusion.

after a few changes to work with python3, I got it to print

Can you share those please? Would be great to be able to finally ditch python2.7 for P-Touch printing 👍

@vsigler
Copy link

vsigler commented Sep 26, 2021

@probonopd
Here you go: https://gist.github.com/vsigler/98eafaf8cdf2374669e590328164f5fc
:)

I also added an option to dump the communication, so that I could diff these easily for further development.

@probonopd
Copy link

Thank you very much @vsigler, great to have a python3 version. :+1

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment